Skip to content

Commit a6ec13c

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 somewhat safe way. A lock was not introduced into the driver directly at this time because doing so is much more complex, particularly as Stmt.DBHandle would need to recover that lock - essnetially requiring global state with further synchronization.
1 parent 7a5c4d2 commit a6ec13c

File tree

4 files changed

+110
-36
lines changed

4 files changed

+110
-36
lines changed

cgosqlite/cgosqlite.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,30 +182,33 @@ 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-
if res == C.SQLITE_DONE {
188-
return false, nil
189-
}
190-
if res == C.SQLITE_OK {
191-
return true, nil
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+
193+
more = true
194+
switch res {
195+
case C.SQLITE_OK, C.SQLITE_BUSY, C.SQLITE_LOCKED:
196+
more = true
197+
default:
198+
more = false
192199
}
193-
return false, errCode(res)
200+
201+
return more, remaining, pageCount, errCode(res)
194202
}
195203

196204
func (b *backup) Finish() error {
197205
res := C.sqlite3_backup_finish(b.backup)
206+
if res == C.SQLITE_OK {
207+
return nil
208+
}
198209
return errCode(res)
199210
}
200211

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

sqlite.go

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -855,23 +855,73 @@ 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+
// Backup holds an in-progress backup context.
859+
type Backup struct {
860+
backup sqliteh.Backup
861+
src sqliteh.DB
862+
dst sqliteh.DB
863+
}
864+
865+
// NewBackup starts a new backup operation that will read from the SQLite
866+
// database srcConn, schema srcSchema, and write to the database dstConn, schema
867+
// dstSchema.
868+
// The database owned by dstConn will be locked for the duration, and must not
869+
// be modified by other connections or processes.
870+
// The database owned by srcConn will be read-locked during each call to Step,
871+
// but can otherwise be used normally. Applications must arrange to ensure that
872+
// there is mutual exclusion between queries and step calls on the source
873+
// connection.
874+
// If a different connection alters the source database during the backup, Step
875+
// will restart the backup process from the beginning, however if the source
876+
// connection alters the database, the backup can continue and will include
877+
// pages affected by the concurrent transactions.
878+
// Finish must be called on the returned backup object in order to free
879+
// resources consumed by the backup operation, even if errors occur during steps
880+
// of the backup process. Finish can also be called any time that Step is not
881+
// running in order to abort the backup.
882+
func NewBackup(dstConn SQLConn, dstSchema string, srcConn SQLConn, srcSchema string) (*Backup, error) {
883+
var b Backup
884+
err := DB(dstConn, func(dst sqliteh.DB) error {
861885
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")
886+
var err error
887+
b.src = src
888+
b.dst = dst
889+
b.backup, err = dst.BackupInit(dstSchema, src, srcSchema)
890+
return errWithMsg(dst, err, "Backup")
875891
})
876892
})
893+
return &b, err
894+
}
895+
896+
// Step makes incremental progress toward a complete online backup. It performs
897+
// at most numPages of copies from the source database to the target database.
898+
//
899+
// Step may be called in between other queries on the source connection, so as
900+
// to concurrently to service traffic, however Step must not be called in
901+
// parallel with other queries on the source connection.
902+
//
903+
// Step may return more=true and non-fatal errors of either SQLITE_BUSY or
904+
// SQLITE_LOCKED, however either of these errors being returned likely indicate
905+
// that an external writer has modified the source database and there will be a
906+
// side effect that the backup will restart from the beginning on the next call
907+
// to Step.
908+
//
909+
// Progress is reported by the `remaining` and `pageCount` values. remaining is
910+
// the number of pages left to copy, and pageCount is the current number of
911+
// pages in total that must be copied. pageCount may change in size due to
912+
// writes that occur during the backup process.
913+
func (b *Backup) Step(numPages int) (more bool, remaining, pageCount int, err error) {
914+
more, remaining, pageCount, err = b.backup.Step(numPages)
915+
err = errWithMsg(b.dst, err, "Step")
916+
return
917+
}
918+
919+
// Finish frees up the backup object and any resources it consumes. It must be
920+
// called even if errors occured calling Step. If Step reports a fatal error,
921+
// Finish will also return the same error. Finish can be called at any time to
922+
// abort a backup operation early.
923+
func (b *Backup) Finish() error {
924+
err := b.backup.Finish()
925+
err = errWithMsg(b.dst, err, "Finish")
926+
return err
877927
}

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)