Skip to content

Commit 208ec67

Browse files
committed
sqlite: implement backup api
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
1 parent f8c7ee1 commit 208ec67

File tree

4 files changed

+161
-10
lines changed

4 files changed

+161
-10
lines changed

cgosqlite/cgosqlite.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,53 @@ func (db *DB) TxnState(schema string) sqliteh.TxnState {
145145
return sqliteh.TxnState(C.sqlite3_txn_state(db.db, cSchema))
146146
}
147147

148+
func (db *DB) BackupInit(dstSchema string, src sqliteh.DB, srcSchema string) (sqliteh.Backup, error) {
149+
var cDstSchema, cSrcSchema *C.char
150+
if dstSchema != "" {
151+
cDstSchema = C.CString(dstSchema)
152+
defer C.free(unsafe.Pointer(cDstSchema))
153+
}
154+
if srcSchema != "" {
155+
cSrcSchema = C.CString(srcSchema)
156+
defer C.free(unsafe.Pointer(cSrcSchema))
157+
}
158+
159+
b := C.sqlite3_backup_init(db.db, cDstSchema, src.(*DB).db, cSrcSchema)
160+
if b == nil {
161+
// sqlite3_backup_init docs tell us the error is on the dst DB.
162+
return nil, sqliteh.ErrCode(db.ExtendedErrCode())
163+
}
164+
return &backup{backup: b}, nil
165+
}
166+
167+
type backup struct {
168+
backup *C.sqlite3_backup
169+
}
170+
171+
func (b *backup) Step(numPages int) (more bool, err error) {
172+
res := C.sqlite3_backup_step(b.backup, C.int(numPages))
173+
if res == C.SQLITE_DONE {
174+
return false, nil
175+
}
176+
if res == C.SQLITE_OK {
177+
return true, nil
178+
}
179+
return false, errCode(res)
180+
}
181+
182+
func (b *backup) Finish() error {
183+
res := C.sqlite3_backup_finish(b.backup)
184+
return errCode(res)
185+
}
186+
187+
func (b *backup) Remaining() int {
188+
return int(C.sqlite3_backup_remaining(b.backup))
189+
}
190+
191+
func (b *backup) PageCount() int {
192+
return int(C.sqlite3_backup_pagecount(b.backup))
193+
}
194+
148195
func (db *DB) Prepare(query string, prepFlags sqliteh.PrepareFlags) (stmt sqliteh.Stmt, remainingQuery string, err error) {
149196
csql := C.CString(query)
150197
defer C.free(unsafe.Pointer(csql))

sqlite.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,20 +139,25 @@ type connector struct {
139139
connInitFunc ConnInitFunc
140140
}
141141

142+
func errWithMsg(db sqliteh.DB, err error, loc string) error {
143+
if ec, ok := err.(sqliteh.ErrCode); ok {
144+
e := &Error{
145+
Code: sqliteh.Code(ec),
146+
Loc: loc,
147+
}
148+
if db != nil {
149+
e.Msg = db.ErrMsg()
150+
}
151+
return e
152+
}
153+
return err
154+
}
155+
142156
func (p *connector) Driver() driver.Driver { return drv{} }
143157
func (p *connector) Connect(ctx context.Context) (driver.Conn, error) {
144158
db, err := Open(p.name, sqliteh.OpenFlagsDefault, "")
145159
if err != nil {
146-
if ec, ok := err.(sqliteh.ErrCode); ok {
147-
e := &Error{
148-
Code: sqliteh.Code(ec),
149-
Loc: "Open",
150-
}
151-
if db != nil {
152-
e.Msg = db.ErrMsg()
153-
}
154-
err = e
155-
}
160+
err = errWithMsg(db, err, "Open")
156161
if db != nil {
157162
db.Close()
158163
}
@@ -781,3 +786,35 @@ func WithPersist(ctx context.Context) context.Context {
781786

782787
// persistQuery is used as a context value.
783788
type persistQuery struct{}
789+
790+
// DB executes fn with the sqliteh.DB underlying sqlconn.
791+
func DB(sqlconn SQLConn, fn func(sqliteh.DB) error) error {
792+
return sqlconn.Raw(func(driverConn interface{}) error {
793+
c, ok := driverConn.(*conn)
794+
if !ok {
795+
return fmt.Errorf("sqlite.Checkpoint: sql.Conn is not the sqlite driver: %T", driverConn)
796+
}
797+
return fn(c.db)
798+
})
799+
}
800+
801+
// Backup backups the specified database from srcConn to dstConn.
802+
func Backup(dstConn SQLConn, dstSchema string, srcConn SQLConn, srcSchema string) error {
803+
return DB(dstConn, func(dst sqliteh.DB) error {
804+
return DB(srcConn, func(src sqliteh.DB) error {
805+
b, err := dst.BackupInit(dstSchema, src, srcSchema)
806+
if err != nil {
807+
return errWithMsg(dst, err, "Backup")
808+
}
809+
more := true
810+
for more {
811+
more, err = b.Step(1024)
812+
if err != nil {
813+
b.Finish()
814+
return errWithMsg(dst, err, "Step")
815+
}
816+
}
817+
return errWithMsg(dst, b.Finish(), "Finish")
818+
})
819+
})
820+
}

sqlite_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,59 @@ func TestConnInit(t *testing.T) {
631631
db.Close()
632632
}
633633

634+
func TestBackup(t *testing.T) {
635+
src, err := sql.Open("sqlite3", "file:src?mode=memory")
636+
if err != nil {
637+
t.Fatal(err)
638+
}
639+
defer src.Close()
640+
dst, err := sql.Open("sqlite3", "file:dst?mode=memory")
641+
if err != nil {
642+
t.Fatal(err)
643+
}
644+
defer dst.Close()
645+
ctx := context.Background()
646+
647+
srcConn, err := src.Conn(ctx)
648+
if err != nil {
649+
t.Fatal(err)
650+
}
651+
defer srcConn.Close()
652+
err = ExecScript(srcConn, `
653+
ATTACH 'file:src2?mode=memory' AS src2;
654+
CREATE TABLE t1 (c);
655+
INSERT INTO t1 VALUES ('a');
656+
CREATE TABLE src2.t2 (c);
657+
INSERT INTO src2.t2 VALUES ('b');
658+
`)
659+
if err != nil {
660+
t.Fatal(err)
661+
}
662+
663+
dstConn, err := src.Conn(ctx)
664+
if err != nil {
665+
t.Fatal(err)
666+
}
667+
defer dstConn.Close()
668+
if err := Backup(dstConn, "main", srcConn, "main"); err != nil {
669+
t.Fatal(err)
670+
}
671+
if _, err := dstConn.ExecContext(ctx, "ATTACH 'file:dst2?mode=memory' AS dst2;"); err != nil {
672+
t.Fatal(err)
673+
}
674+
var count int
675+
if err := dstConn.QueryRowContext(ctx, "SELECT count(*) FROM t1").Scan(&count); err != nil || count != 1 {
676+
t.Fatalf("err=%v, count=%d", err, count)
677+
}
678+
if err := Backup(dstConn, "dst2", srcConn, "src2"); err != nil {
679+
t.Fatal(err)
680+
}
681+
count = 0
682+
if err := dstConn.QueryRowContext(ctx, "SELECT count(*) FROM dst2.t2").Scan(&count); err != nil || count != 1 {
683+
t.Fatalf("err=%v, count=%d", err, count)
684+
}
685+
}
686+
634687
func BenchmarkPersist(b *testing.B) {
635688
ctx := context.Background()
636689
db := openTestDB(b)

sqliteh/sqliteh.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ type DB interface {
5252
AutoCheckpoint(n int) error
5353
// TxnState is sqlite3_txn_state.
5454
TxnState(schema string) TxnState
55+
// BackupInit is sqlite3_backup_init, this DB is the destination.
56+
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
57+
BackupInit(dstSchema string, src DB, srcSchema string) (Backup, error)
5558
}
5659

5760
// Stmt is an sqlite3_stmt* database connection object.
@@ -164,6 +167,17 @@ type Stmt interface {
164167
ColumnTableName(col int) string
165168
}
166169

170+
// Backup is an sqlite3_backup object.
171+
// https://www.sqlite.org/c3ref/backup_finish.html
172+
type Backup interface {
173+
// Step is called repeatedly to transfer data between the two DBs.
174+
Step(numPages int) (more bool, err error)
175+
// Finish releases all resources associated with the Backup.
176+
Finish() error
177+
Remaining() int
178+
PageCount() int
179+
}
180+
167181
// ColumnType are constants for each of the SQLite datatypes.
168182
// https://www.sqlite.org/c3ref/c_blob.html
169183
type ColumnType int

0 commit comments

Comments
 (0)