Skip to content

Commit 7ce3b4e

Browse files
committed
cgosqlite,sqlite: expose incremental API and add documentation
It is likely useful to report progress information and offer abort for large databases, so expose this in the high level API in a safe way.
1 parent 7a5c4d2 commit 7ce3b4e

File tree

4 files changed

+84
-33
lines changed

4 files changed

+84
-33
lines changed

cgosqlite/cgosqlite.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -182,30 +182,31 @@ type backup struct {
182182
backup *C.sqlite3_backup
183183
}
184184

185-
func (b *backup) Step(numPages int) (more bool, err error) {
185+
func (b *backup) Step(numPages int) (more bool, remaining, pageCount int, err error) {
186186
res := C.sqlite3_backup_step(b.backup, C.int(numPages))
187+
188+
// It is not safe to call remaining and pagecount concurrently with step, so
189+
// instead just return them each time.
190+
remaining = int(C.sqlite3_backup_remaining(b.backup))
191+
pageCount = int(C.sqlite3_backup_pagecount(b.backup))
192+
187193
if res == C.SQLITE_DONE {
188-
return false, nil
194+
return false, remaining, pageCount, nil
189195
}
190196
if res == C.SQLITE_OK {
191-
return true, nil
197+
return true, remaining, pageCount, nil
192198
}
193-
return false, errCode(res)
199+
return false, remaining, pageCount, errCode(res)
194200
}
195201

196202
func (b *backup) Finish() error {
197203
res := C.sqlite3_backup_finish(b.backup)
204+
if res == C.SQLITE_OK {
205+
return nil
206+
}
198207
return errCode(res)
199208
}
200209

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-
209210
func (db *DB) Prepare(query string, prepFlags sqliteh.PrepareFlags) (stmt sqliteh.Stmt, remainingQuery string, err error) {
210211
csql := C.CString(query)
211212
defer C.free(unsafe.Pointer(csql))

sqlite.go

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -855,23 +855,52 @@ func DB(sqlconn SQLConn, fn func(sqliteh.DB) error) error {
855855
})
856856
}
857857

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 {
858+
type Backup struct {
859+
backup sqliteh.Backup
860+
src sqliteh.DB
861+
dst sqliteh.DB
862+
}
863+
864+
// NewBackup starts a new backup operation that will read from the SQLite
865+
// database srcConn, schema srcSchema, and write to the database dstConn, schema
866+
// dstSchema. SQLite backups can be efficient as long as `srcConn` is the only
867+
// writer to the database, if there are other writers the backup process will
868+
// restart on the next Step() after a write. Finish must be called on the
869+
// returned backup object in order to free resources consumed by the backup
870+
// operation, even if errors occur during steps of the backup process.
871+
func NewBackup(dstConn SQLConn, dstSchema string, srcConn SQLConn, srcSchema string) (*Backup, error) {
872+
var b Backup
873+
err := DB(dstConn, func(dst sqliteh.DB) error {
861874
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+
var err error
876+
b.src = src
877+
b.dst = dst
878+
b.backup, err = dst.BackupInit(dstSchema, src, srcSchema)
879+
return errWithMsg(dst, err, "Backup")
875880
})
876881
})
882+
return &b, err
883+
}
884+
885+
// Step must be called repeatedly until it returns more=false. It may return
886+
// non-fatal busy or locked errors, which the applicaiton may wish to record for
887+
// tracking purposes. The SQLite docs suggest sleeping between calls to Step in
888+
// order to allow the application to make forward progress on other work.
889+
// numPages controls the desired amount of work performed per step. `remaining`
890+
// is the number of outstanding pages to be copied, and `pageCount` is the total
891+
// number of pages in the source database.
892+
func (b *Backup) Step(numPages int) (more bool, remaining, pageCount int, err error) {
893+
more, remaining, pageCount, err = b.backup.Step(numPages)
894+
err = errWithMsg(b.dst, err, "Step")
895+
return
896+
}
897+
898+
// Finish frees up the backup object and any resources it consumes. It must be
899+
// called even if errors occured calling Step. If Step reports a fatal error,
900+
// Finish will also return the same error. Finish can be called at any time to
901+
// abort a backup operation early.
902+
func (b *Backup) Finish() error {
903+
err := b.backup.Finish()
904+
err = errWithMsg(b.dst, err, "Finish")
905+
return err
877906
}

sqlite_test.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,30 @@ func TestBackup(t *testing.T) {
813813
t.Fatal(err)
814814
}
815815
defer dstConn.Close()
816-
if err := Backup(dstConn, "main", srcConn, "main"); err != nil {
816+
817+
var backup = func(dstConn *sql.Conn, dstName string, srcConn *sql.Conn, srcName string) error {
818+
t.Helper()
819+
b, err := NewBackup(dstConn, dstName, srcConn, srcName)
820+
if err != nil {
821+
return err
822+
}
823+
var (
824+
more = true
825+
remaining int
826+
pageCount int
827+
)
828+
for more {
829+
more, remaining, pageCount, err = b.Step(1024)
830+
t.Logf("backup step: more=%v, remaining=%d, pageCount=%d", more, remaining, pageCount)
831+
if err != nil {
832+
t.Errorf("backup step: %v", err)
833+
more = false
834+
}
835+
}
836+
return b.Finish()
837+
}
838+
839+
if err := backup(dstConn, "main", srcConn, "main"); err != nil {
817840
t.Fatal(err)
818841
}
819842
if _, err := dstConn.ExecContext(ctx, "ATTACH 'file:dst2?mode=memory' AS dst2;"); err != nil {
@@ -823,7 +846,7 @@ func TestBackup(t *testing.T) {
823846
if err := dstConn.QueryRowContext(ctx, "SELECT count(*) FROM t1").Scan(&count); err != nil || count != 1 {
824847
t.Fatalf("err=%v, count=%d", err, count)
825848
}
826-
if err := Backup(dstConn, "dst2", srcConn, "src2"); err != nil {
849+
if err := backup(dstConn, "dst2", srcConn, "src2"); err != nil {
827850
t.Fatal(err)
828851
}
829852
count = 0

sqliteh/sqliteh.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,9 @@ type Stmt interface {
172172
// https://www.sqlite.org/c3ref/backup_finish.html
173173
type Backup interface {
174174
// Step is called repeatedly to transfer data between the two DBs.
175-
Step(numPages int) (more bool, err error)
175+
Step(numPages int) (more bool, remaining, pageCount int, err error)
176176
// Finish releases all resources associated with the Backup.
177177
Finish() error
178-
Remaining() int
179-
PageCount() int
180178
}
181179

182180
// ColumnType are constants for each of the SQLite datatypes.

0 commit comments

Comments
 (0)