Skip to content

Commit 2dbcc48

Browse files
committed
Initial pragma support.
1 parent 0f0716c commit 2dbcc48

File tree

5 files changed

+152
-11
lines changed

5 files changed

+152
-11
lines changed

litestream/api.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type ReplicaOptions struct {
4949
}
5050

5151
// NewReplica creates a read-replica from a Litestream client.
52-
func NewReplica(name string, client litestream.ReplicaClient, options ReplicaOptions) {
52+
func NewReplica(name string, client ReplicaClient, options ReplicaOptions) {
5353
if options.Logger != nil {
5454
options.Logger = options.Logger.With("name", name)
5555
} else {
@@ -61,6 +61,7 @@ func NewReplica(name string, client litestream.ReplicaClient, options ReplicaOpt
6161
if options.CacheSize == 0 {
6262
options.CacheSize = DefaultCacheSize
6363
}
64+
options.MinLevel = max(0, min(options.MinLevel, litestream.SnapshotLevel))
6465

6566
liteMtx.Lock()
6667
defer liteMtx.Unlock()
@@ -78,9 +79,9 @@ func RemoveReplica(name string) {
7879
delete(liteDBs, name)
7980
}
8081

81-
// NewPrimary creates a new primary that replicates through a client.
82+
// NewPrimary creates a new primary that replicates through client.
8283
// If restore is not nil, the database is first restored.
83-
func NewPrimary(ctx context.Context, path string, client litestream.ReplicaClient, restore *litestream.RestoreOptions) (*litestream.DB, error) {
84+
func NewPrimary(ctx context.Context, path string, client ReplicaClient, restore *RestoreOptions) (*litestream.DB, error) {
8485
lsdb := litestream.NewDB(path)
8586
lsdb.Replica = litestream.NewReplicaWithClient(lsdb, client)
8687

@@ -97,3 +98,8 @@ func NewPrimary(ctx context.Context, path string, client litestream.ReplicaClien
9798
}
9899
return lsdb, nil
99100
}
101+
102+
type (
103+
ReplicaClient = litestream.ReplicaClient
104+
RestoreOptions = litestream.RestoreOptions
105+
)

litestream/api_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package litestream
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
"time"
7+
8+
"github.com/benbjohnson/litestream"
9+
"github.com/benbjohnson/litestream/file"
10+
"github.com/ncruces/go-sqlite3/driver"
11+
_ "github.com/ncruces/go-sqlite3/embed"
12+
)
13+
14+
func Test_integration(t *testing.T) {
15+
dir := t.TempDir()
16+
dbpath := filepath.Join(dir, "test.db")
17+
backup := filepath.Join(dir, "backup", "test.db")
18+
19+
db, err := driver.Open(dbpath)
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
defer db.Close()
24+
25+
client := file.NewReplicaClient(backup)
26+
NewReplica("test.db", client, ReplicaOptions{})
27+
28+
if err := setupPrimary(t, dbpath, client); err != nil {
29+
t.Fatal(err)
30+
}
31+
32+
replica, err := driver.Open("file:test.db?vfs=litestream")
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
defer replica.Close()
37+
38+
_, err = db.ExecContext(t.Context(), `CREATE TABLE users (id INT, name VARCHAR(10))`)
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
43+
_, err = db.ExecContext(t.Context(),
44+
`INSERT INTO users (id, name) VALUES (0, 'go'), (1, 'zig'), (2, 'whatever')`)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
time.Sleep(DefaultPollInterval + litestream.DefaultMonitorInterval)
50+
51+
rows, err := replica.QueryContext(t.Context(), `SELECT id, name FROM users`)
52+
if err != nil {
53+
t.Fatal(err)
54+
}
55+
defer rows.Close()
56+
57+
row := 0
58+
ids := []int{0, 1, 2}
59+
names := []string{"go", "zig", "whatever"}
60+
for ; rows.Next(); row++ {
61+
var id int
62+
var name string
63+
err := rows.Scan(&id, &name)
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
68+
if id != ids[row] {
69+
t.Errorf("got %d, want %d", id, ids[row])
70+
}
71+
if name != names[row] {
72+
t.Errorf("got %q, want %q", name, names[row])
73+
}
74+
}
75+
if row != 3 {
76+
t.Errorf("got %d, want %d", row, len(ids))
77+
}
78+
79+
var lag int
80+
err = replica.QueryRowContext(t.Context(), `PRAGMA litestream_lag`).Scan(&lag)
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
if lag < 0 || lag > 2 {
85+
t.Errorf("got %d", lag)
86+
}
87+
88+
var txid string
89+
err = replica.QueryRowContext(t.Context(), `PRAGMA litestream_txid`).Scan(&txid)
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
if txid != "0000000000000001" {
94+
t.Errorf("got %q", txid)
95+
}
96+
}
97+
98+
func setupPrimary(tb testing.TB, path string, client ReplicaClient) error {
99+
db, err := NewPrimary(tb.Context(), path, client, nil)
100+
if err == nil {
101+
tb.Cleanup(func() { db.Close(tb.Context()) })
102+
}
103+
return err
104+
}

litestream/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/ncruces/go-sqlite3/litestream
33
go 1.24.4
44

55
require (
6-
github.com/benbjohnson/litestream v0.5.2
7-
github.com/ncruces/go-sqlite3 v0.30.1
6+
github.com/benbjohnson/litestream v0.5.3-0.20251109214555-48dc960260f0
7+
github.com/ncruces/go-sqlite3 v0.30.2
88
github.com/ncruces/wbt v0.2.0
99
github.com/superfly/ltx v0.5.0
1010
golang.org/x/sync v0.18.0

litestream/go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.35.1 h1:iF4Xxkc0H9c/K2dS0zZw3SCkj0Z7
6060
github.com/aws/aws-sdk-go-v2/service/sts v1.35.1/go.mod h1:0bxIatfN0aLq4mjoLDeBpOjOke68OsFlXPDFJ7V0MYw=
6161
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
6262
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
63-
github.com/benbjohnson/litestream v0.5.2 h1:uD9I17n6RgUgyCwPM/Sw2YXNmMGixecUB5kmJ4FL08o=
64-
github.com/benbjohnson/litestream v0.5.2/go.mod h1:jSW6AGqbxmJnEXGjMHchlZclGphzbJ6jGrGo5fYIDhU=
63+
github.com/benbjohnson/litestream v0.5.3-0.20251109214555-48dc960260f0 h1:190qM2axbs1p7NyRm5/ygqrDdcIYhc5QXPTK04Cgyno=
64+
github.com/benbjohnson/litestream v0.5.3-0.20251109214555-48dc960260f0/go.mod h1:0gjSLi7Qm5INdtdo6YJMFMsS5e/KM2s3CFYF3OtLmY8=
6565
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
6666
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
6767
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -113,8 +113,8 @@ github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
113113
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
114114
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
115115
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
116-
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
117-
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
116+
github.com/ncruces/go-sqlite3 v0.30.2 h1:1GVbHAkKAOwjJd3JYl8ldrYROudfZUOah7oXPD7VZbQ=
117+
github.com/ncruces/go-sqlite3 v0.30.2/go.mod h1:AxKu9sRxkludimFocbktlY6LiYSkxiI5gTA8r+os/Nw=
118118
github.com/ncruces/go-sqlite3/litestream/modernc v0.30.1 h1:3SNAOrm+qmLprkZybcvBrVNHyt0QYHljUGxmOXnL+K0=
119119
github.com/ncruces/go-sqlite3/litestream/modernc v0.30.1/go.mod h1:GSM2gXEOb9HIFFtsl0IUtnpvpDmVi7Kbp8z5GzwA0Tw=
120120
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
@@ -143,6 +143,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
143143
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
144144
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
145145
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
146+
github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
147+
github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
146148
github.com/superfly/ltx v0.5.0 h1:dXNrcT3ZtMb6iKZopIV7z5UBscnapg0b0F02loQsk5o=
147149
github.com/superfly/ltx v0.5.0/go.mod h1:Nf50QAIXU/ET4ua3AuQ2fh31MbgNQZA7r/DYx6Os77s=
148150
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=

litestream/vfs.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"strconv"
910
"sync"
1011
"time"
1112

@@ -160,6 +161,34 @@ func (f *liteFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
160161
return 0
161162
}
162163

164+
func (f *liteFile) Pragma(name, value string) (string, error) {
165+
switch name {
166+
case "litestream_txid":
167+
txid := f.txid
168+
if txid == 0 {
169+
// Outside transaction.
170+
f.db.mtx.Lock()
171+
txid = f.db.txids[f.db.opts.MinLevel]
172+
f.db.mtx.Unlock()
173+
}
174+
return txid.String(), nil
175+
176+
case "litestream_lag":
177+
f.db.mtx.Lock()
178+
lastPoll := f.db.lastPoll
179+
f.db.mtx.Unlock()
180+
181+
if lastPoll.IsZero() {
182+
// Never polled successfully.
183+
return "-1", nil
184+
}
185+
lag := time.Since(lastPoll) / time.Second
186+
return strconv.FormatInt(int64(lag), 10), nil
187+
}
188+
189+
return "", sqlite3.NOTFOUND
190+
}
191+
163192
func (f *liteFile) SetDB(conn any) {
164193
f.conn = conn.(*sqlite3.Conn)
165194
}
@@ -216,7 +245,7 @@ func (f *liteDB) pollReplica(ctx context.Context) (*pageIndex, ltx.TXID, error)
216245

217246
// Limit polling interval.
218247
if time.Since(f.lastPoll) < f.opts.PollInterval {
219-
return f.pages, f.txids[0], nil
248+
return f.pages, f.txids[f.opts.MinLevel], nil
220249
}
221250

222251
for level := range pollLevels(f.opts.MinLevel) {
@@ -227,7 +256,7 @@ func (f *liteDB) pollReplica(ctx context.Context) (*pageIndex, ltx.TXID, error)
227256
}
228257

229258
f.lastPoll = time.Now()
230-
return f.pages, f.txids[0], nil
259+
return f.pages, f.txids[f.opts.MinLevel], nil
231260
}
232261

233262
// +checklocks:f.mtx

0 commit comments

Comments
 (0)