Skip to content

Commit 7a5c4d2

Browse files
crawshawraggi
authored andcommitted
sqlite: implement backup api
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
1 parent 0716cf4 commit 7a5c4d2

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
@@ -159,6 +159,53 @@ func (db *DB) TxnState(schema string) sqliteh.TxnState {
159159
return sqliteh.TxnState(C.sqlite3_txn_state(db.db, cSchema))
160160
}
161161

162+
func (db *DB) BackupInit(dstSchema string, src sqliteh.DB, srcSchema string) (sqliteh.Backup, error) {
163+
var cDstSchema, cSrcSchema *C.char
164+
if dstSchema != "" {
165+
cDstSchema = C.CString(dstSchema)
166+
defer C.free(unsafe.Pointer(cDstSchema))
167+
}
168+
if srcSchema != "" {
169+
cSrcSchema = C.CString(srcSchema)
170+
defer C.free(unsafe.Pointer(cSrcSchema))
171+
}
172+
173+
b := C.sqlite3_backup_init(db.db, cDstSchema, src.(*DB).db, cSrcSchema)
174+
if b == nil {
175+
// sqlite3_backup_init docs tell us the error is on the dst DB.
176+
return nil, sqliteh.ErrCode(db.ExtendedErrCode())
177+
}
178+
return &backup{backup: b}, nil
179+
}
180+
181+
type backup struct {
182+
backup *C.sqlite3_backup
183+
}
184+
185+
func (b *backup) Step(numPages int) (more bool, err error) {
186+
res := C.sqlite3_backup_step(b.backup, C.int(numPages))
187+
if res == C.SQLITE_DONE {
188+
return false, nil
189+
}
190+
if res == C.SQLITE_OK {
191+
return true, nil
192+
}
193+
return false, errCode(res)
194+
}
195+
196+
func (b *backup) Finish() error {
197+
res := C.sqlite3_backup_finish(b.backup)
198+
return errCode(res)
199+
}
200+
201+
func (b *backup) Remaining() int {
202+
return int(C.sqlite3_backup_remaining(b.backup))
203+
}
204+
205+
func (b *backup) PageCount() int {
206+
return int(C.sqlite3_backup_pagecount(b.backup))
207+
}
208+
162209
func (db *DB) Prepare(query string, prepFlags sqliteh.PrepareFlags) (stmt sqliteh.Stmt, remainingQuery string, err error) {
163210
csql := C.CString(query)
164211
defer C.free(unsafe.Pointer(csql))

sqlite.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,20 +123,25 @@ type connector struct {
123123
connInitFunc ConnInitFunc
124124
}
125125

126+
func errWithMsg(db sqliteh.DB, err error, loc string) error {
127+
if ec, ok := err.(sqliteh.ErrCode); ok {
128+
e := &Error{
129+
Code: sqliteh.Code(ec),
130+
Loc: loc,
131+
}
132+
if db != nil {
133+
e.Msg = db.ErrMsg()
134+
}
135+
return e
136+
}
137+
return err
138+
}
139+
126140
func (p *connector) Driver() driver.Driver { return drv{} }
127141
func (p *connector) Connect(ctx context.Context) (driver.Conn, error) {
128142
db, err := Open(p.name, sqliteh.OpenFlagsDefault, "")
129143
if err != nil {
130-
if ec, ok := err.(sqliteh.ErrCode); ok {
131-
e := &Error{
132-
Code: sqliteh.Code(ec),
133-
Loc: "Open",
134-
}
135-
if db != nil {
136-
e.Msg = db.ErrMsg()
137-
}
138-
err = e
139-
}
144+
err = errWithMsg(db, err, "Open")
140145
if db != nil {
141146
db.Close()
142147
}
@@ -838,3 +843,35 @@ func WithPersist(ctx context.Context) context.Context {
838843

839844
// persistQuery is used as a context value.
840845
type persistQuery struct{}
846+
847+
// DB executes fn with the sqliteh.DB underlying sqlconn.
848+
func DB(sqlconn SQLConn, fn func(sqliteh.DB) error) error {
849+
return sqlconn.Raw(func(driverConn interface{}) error {
850+
c, ok := driverConn.(*conn)
851+
if !ok {
852+
return fmt.Errorf("sqlite.Checkpoint: sql.Conn is not the sqlite driver: %T", driverConn)
853+
}
854+
return fn(c.db)
855+
})
856+
}
857+
858+
// Backup backups the specified database from srcConn to dstConn.
859+
func Backup(dstConn SQLConn, dstSchema string, srcConn SQLConn, srcSchema string) error {
860+
return DB(dstConn, func(dst sqliteh.DB) error {
861+
return DB(srcConn, func(src sqliteh.DB) error {
862+
b, err := dst.BackupInit(dstSchema, src, srcSchema)
863+
if err != nil {
864+
return errWithMsg(dst, err, "Backup")
865+
}
866+
more := true
867+
for more {
868+
more, err = b.Step(1024)
869+
if err != nil {
870+
b.Finish()
871+
return errWithMsg(dst, err, "Step")
872+
}
873+
}
874+
return errWithMsg(dst, b.Finish(), "Finish")
875+
})
876+
})
877+
}

sqlite_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,59 @@ func TestAttachOrderingDeadlock(t *testing.T) {
779779
}
780780
}
781781

782+
func TestBackup(t *testing.T) {
783+
src, err := sql.Open("sqlite3", "file:src?mode=memory")
784+
if err != nil {
785+
t.Fatal(err)
786+
}
787+
defer src.Close()
788+
dst, err := sql.Open("sqlite3", "file:dst?mode=memory")
789+
if err != nil {
790+
t.Fatal(err)
791+
}
792+
defer dst.Close()
793+
ctx := context.Background()
794+
795+
srcConn, err := src.Conn(ctx)
796+
if err != nil {
797+
t.Fatal(err)
798+
}
799+
defer srcConn.Close()
800+
err = ExecScript(srcConn, `
801+
ATTACH 'file:src2?mode=memory' AS src2;
802+
CREATE TABLE t1 (c);
803+
INSERT INTO t1 VALUES ('a');
804+
CREATE TABLE src2.t2 (c);
805+
INSERT INTO src2.t2 VALUES ('b');
806+
`)
807+
if err != nil {
808+
t.Fatal(err)
809+
}
810+
811+
dstConn, err := src.Conn(ctx)
812+
if err != nil {
813+
t.Fatal(err)
814+
}
815+
defer dstConn.Close()
816+
if err := Backup(dstConn, "main", srcConn, "main"); err != nil {
817+
t.Fatal(err)
818+
}
819+
if _, err := dstConn.ExecContext(ctx, "ATTACH 'file:dst2?mode=memory' AS dst2;"); err != nil {
820+
t.Fatal(err)
821+
}
822+
var count int
823+
if err := dstConn.QueryRowContext(ctx, "SELECT count(*) FROM t1").Scan(&count); err != nil || count != 1 {
824+
t.Fatalf("err=%v, count=%d", err, count)
825+
}
826+
if err := Backup(dstConn, "dst2", srcConn, "src2"); err != nil {
827+
t.Fatal(err)
828+
}
829+
count = 0
830+
if err := dstConn.QueryRowContext(ctx, "SELECT count(*) FROM dst2.t2").Scan(&count); err != nil || count != 1 {
831+
t.Fatalf("err=%v, count=%d", err, count)
832+
}
833+
}
834+
782835
func BenchmarkPersist(b *testing.B) {
783836
ctx := context.Background()
784837
db := openTestDB(b)

sqliteh/sqliteh.go

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

5861
// Stmt is an sqlite3_stmt* database connection object.
@@ -165,6 +168,17 @@ type Stmt interface {
165168
ColumnTableName(col int) string
166169
}
167170

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

0 commit comments

Comments
 (0)