Skip to content

Commit ca93c49

Browse files
committed
Relative time, fixes.
1 parent 15e9087 commit ca93c49

4 files changed

Lines changed: 191 additions & 16 deletions

File tree

litestream/vfs.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/superfly/ltx"
1616

1717
"github.com/ncruces/go-sqlite3"
18+
"github.com/ncruces/go-sqlite3/util/sql3util"
1819
"github.com/ncruces/go-sqlite3/util/vfsutil"
1920
"github.com/ncruces/go-sqlite3/vfs"
2021
"github.com/ncruces/wbt"
@@ -212,7 +213,7 @@ func (f *liteFile) Pragma(name, value string) (string, error) {
212213
return syncTime.Format(time.RFC3339Nano), nil
213214
}
214215

215-
if !f.locked {
216+
if f.locked {
216217
return "", sqlite3.MISUSE
217218
}
218219

@@ -222,12 +223,17 @@ func (f *liteFile) Pragma(name, value string) (string, error) {
222223
return "", nil
223224
}
224225

225-
syncTime, err := sqlite3.TimeFormatAuto.Decode(value)
226-
if err != nil {
227-
return "", err
226+
var syncTime time.Time
227+
if years, months, days, duration, ok := sql3util.ParseTimeShift(value); ok {
228+
syncTime = time.Now().AddDate(years, months, days).Add(duration)
229+
} else {
230+
syncTime, _ = sqlite3.TimeFormatAuto.Decode(value)
231+
}
232+
if syncTime.IsZero() {
233+
return "", sqlite3.MISUSE
228234
}
229235

230-
err = f.buildIndex(f.context(), syncTime)
236+
err := f.buildIndex(f.context(), syncTime)
231237
if err != nil {
232238
f.db.opts.Logger.Error("build index", "error", err)
233239
}
@@ -251,11 +257,8 @@ func (f *liteFile) context() context.Context {
251257
func (f *liteFile) buildIndex(ctx context.Context, syncTime time.Time) error {
252258
// Build the index from scratch from a Litestream restore plan.
253259
infos, err := litestream.CalcRestorePlan(ctx, f.db.client, 0, syncTime, f.db.opts.Logger)
254-
if err != nil {
255-
if !errors.Is(err, litestream.ErrTxNotAvailable) {
256-
return fmt.Errorf("calc restore plan: %w", err)
257-
}
258-
return nil
260+
if err != nil && !errors.Is(err, litestream.ErrTxNotAvailable) {
261+
return fmt.Errorf("calc restore plan: %w", err)
259262
}
260263

261264
var txid ltx.TXID
@@ -295,11 +298,8 @@ func (d *liteDB) buildIndex(ctx context.Context) error {
295298

296299
// Build the index from scratch from a Litestream restore plan.
297300
infos, err := litestream.CalcRestorePlan(ctx, d.client, 0, time.Time{}, d.opts.Logger)
298-
if err != nil {
299-
if !errors.Is(err, litestream.ErrTxNotAvailable) {
300-
return fmt.Errorf("calc restore plan: %w", err)
301-
}
302-
return nil
301+
if err != nil && !errors.Is(err, litestream.ErrTxNotAvailable) {
302+
return fmt.Errorf("calc restore plan: %w", err)
303303
}
304304

305305
for _, info := range infos {

litestream/vfs_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ func Test_integration(t *testing.T) {
9393
if txid != "0000000000000001" {
9494
t.Errorf("got %q", txid)
9595
}
96+
97+
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='00:01'`)
98+
if err != nil {
99+
t.Fatal(err)
100+
}
101+
102+
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='1970-01-01'`)
103+
if err != nil {
104+
t.Fatal(err)
105+
}
106+
107+
var sync time.Time
108+
err = replica.QueryRowContext(t.Context(), `PRAGMA litestream_time`).Scan(&sync)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
if !sync.Equal(time.Unix(0, 0)) {
113+
t.Errorf("got %v", sync)
114+
}
96115
}
97116

98117
func setupPrimary(tb testing.TB, path string, client ReplicaClient) error {

util/sql3util/arg.go

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package sql3util
22

3-
import "strings"
3+
import (
4+
"strings"
5+
"time"
6+
)
47

58
// NamedArg splits an named arg into a key and value,
69
// around an equals sign.
@@ -63,3 +66,115 @@ func ParseBool(s string) (b, ok bool) {
6366
}
6467
return false, false
6568
}
69+
70+
// ParseTimeShift parses a time shift modifier,
71+
// also the output of timediff.
72+
//
73+
// https://sqlite.org/lang_datefunc.html
74+
func ParseTimeShift(s string) (years, months, days int, duration time.Duration, ok bool) {
75+
// Sign part: ±
76+
neg := strings.HasPrefix(s, "-")
77+
sign := neg || strings.HasPrefix(s, "+")
78+
if sign {
79+
s = s[1:]
80+
}
81+
82+
if ok = len(s) >= 5; !ok {
83+
return // !ok
84+
}
85+
86+
defer func() {
87+
if neg {
88+
years = -years
89+
months = -months
90+
days = -days
91+
duration = -duration
92+
}
93+
}()
94+
95+
// Date part: YYYY-MM-DD
96+
if s[4] == '-' {
97+
if ok = sign && len(s) >= 10 && s[7] == '-'; !ok {
98+
return // !ok
99+
}
100+
if years, ok = parseInt(s[0:4]); !ok {
101+
return // !ok
102+
}
103+
if months, ok = parseInt(s[5:7]); !ok {
104+
return // !ok
105+
}
106+
if days, ok = parseInt(s[8:10]); !ok {
107+
return // !ok
108+
}
109+
if len(s) == 10 {
110+
return
111+
}
112+
if ok = s[10] == ' '; !ok {
113+
return // !ok
114+
}
115+
s = s[11:]
116+
}
117+
118+
// Time part: HH:MM
119+
if ok = len(s) >= 5 && s[2] == ':'; !ok {
120+
return // !ok
121+
}
122+
123+
var hours, minutes int
124+
if hours, ok = parseInt(s[0:2]); !ok {
125+
return
126+
}
127+
if minutes, ok = parseInt(s[3:5]); !ok {
128+
return
129+
}
130+
duration = time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute
131+
132+
if len(s) == 5 {
133+
return
134+
}
135+
if ok = len(s) >= 8 && s[5] == ':'; !ok {
136+
return // !ok
137+
}
138+
139+
// Seconds part: HH:MM:SS
140+
var seconds int
141+
if seconds, ok = parseInt(s[6:8]); !ok {
142+
return
143+
}
144+
duration += time.Duration(seconds) * time.Second
145+
146+
if len(s) == 8 {
147+
return
148+
}
149+
if ok = len(s) >= 10 && s[8] == '.'; !ok {
150+
return // !ok
151+
}
152+
s = s[9:]
153+
154+
// Nanosecond part: HH:MM:SS.SSS
155+
var nanos int
156+
if nanos, ok = parseInt(s[0:min(9, len(s))]); !ok {
157+
return
158+
}
159+
for i := len(s); i < 9; i++ {
160+
nanos *= 10
161+
}
162+
duration += time.Duration(nanos)
163+
164+
// Subnanosecond part.
165+
if len(s) > 9 {
166+
_, ok = parseInt(s[9:])
167+
}
168+
return
169+
}
170+
171+
func parseInt(s string) (i int, _ bool) {
172+
for _, r := range []byte(s) {
173+
r -= '0'
174+
if r > 9 {
175+
return
176+
}
177+
i = i*10 + int(r)
178+
}
179+
return i, true
180+
}

util/sql3util/arg_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sql3util_test
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/ncruces/go-sqlite3/util/sql3util"
78
)
@@ -53,3 +54,43 @@ func TestParseBool(t *testing.T) {
5354
})
5455
}
5556
}
57+
58+
func TestParseTimeShift(t *testing.T) {
59+
epoch := time.Unix(0, 0)
60+
tests := []struct {
61+
str string
62+
val time.Time
63+
ok bool
64+
}{
65+
{"", epoch, false},
66+
{"0001-12-30", epoch, false},
67+
{"+_001-12-30", epoch, false},
68+
{"+0001-_2-30", epoch.AddDate(1, 0, 0), false},
69+
{"+0001-12-_0", epoch.AddDate(1, 12, 0), false},
70+
{"+0001-12-30", epoch.AddDate(1, 12, 30), true},
71+
{"-0001-12-30", epoch.AddDate(-1, -12, -30), true},
72+
{"+0001-12-30T", epoch.AddDate(1, 12, 30), false},
73+
{"+0001-12-30 12", epoch.AddDate(1, 12, 30), false},
74+
{"+0001-12-30 _2:30", epoch.AddDate(1, 12, 30), false},
75+
{"+0001-12-30 12:_0", epoch.AddDate(1, 12, 30), false},
76+
{"+0001-12-30 12:30", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 30*time.Minute), true},
77+
{"+0001-12-30 12:30:", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 30*time.Minute), false},
78+
{"+0001-12-30 12:30:_0", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 30*time.Minute), false},
79+
{"+0001-12-30 12:30:60", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute), true},
80+
{"+0001-12-30 12:30:60.", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute), false},
81+
{"+0001-12-30 12:30:60._", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute), false},
82+
{"+0001-12-30 12:30:60.1", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute + 100*time.Millisecond), true},
83+
{"+0001-12-30 12:30:60.123456789_", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute + 123456789), false},
84+
{"+0001-12-30 12:30:60.1234567890", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute + 123456789), true},
85+
{"-12:30:60.1234567890", epoch.Add(-12*time.Hour - 31*time.Minute - 123456789), true},
86+
}
87+
for _, tt := range tests {
88+
t.Run(tt.str, func(t *testing.T) {
89+
years, months, days, duration, gotOK := sql3util.ParseTimeShift(tt.str)
90+
gotVal := epoch.AddDate(years, months, days).Add(duration)
91+
if !gotVal.Equal(tt.val) || gotOK != tt.ok {
92+
t.Errorf("ParseTimeShift(%q) = (%v, %v) want (%v, %v)", tt.str, gotVal, gotOK, tt.val, tt.ok)
93+
}
94+
})
95+
}
96+
}

0 commit comments

Comments
 (0)