feat: add sync checkpoint restore for last successful sync
Capture rolling online DB snapshots after push/drain/reconcile and expose SyncPage 数据恢复 plus checkpoint/restore APIs; mark Z12h and restore done in coop docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
89
platform/internal/dbsync/checkpoint_test.go
Normal file
89
platform/internal/dbsync/checkpoint_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package dbsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheckpointSaveRestoreRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := NewFileStore(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteDSN := "file:" + filepath.ToSlash(filepath.Join(dir, "online.db")) + "?_pragma=busy_timeout(5000)"
|
||||
ch := &Channel{
|
||||
ID: "ch-cp-1",
|
||||
TenantID: 1,
|
||||
Name: "test",
|
||||
Remote: Endpoint{
|
||||
Driver: DriverSQLite,
|
||||
DSN: remoteDSN,
|
||||
},
|
||||
PKColumns: map[string]string{"demo": "id"},
|
||||
}
|
||||
ctx := context.Background()
|
||||
db, err := Open(DriverSQLite, remoteDSN)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = db.Close()
|
||||
InvalidateRemote(DriverSQLite, remoteDSN)
|
||||
})
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE demo (id TEXT PRIMARY KEY, name TEXT)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO demo(id,name) VALUES('a','one'),('b','two')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := EnsureMeta(ctx, db, DriverSQLite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := SaveCheckpoint(ctx, store, ch, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meta, err := LoadCheckpointMeta(store, ch.ID)
|
||||
if err != nil || meta.Latest == nil || meta.Latest.RowCount != 2 {
|
||||
t.Fatalf("meta=%+v err=%v", meta, err)
|
||||
}
|
||||
|
||||
// mutate then restore
|
||||
if _, err := db.ExecContext(ctx, `DELETE FROM demo WHERE id='a'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO demo(id,name) VALUES('c','three')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := RestoreCheckpoint(ctx, store, ch, "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.OK || res.Upserted < 2 {
|
||||
t.Fatalf("restore=%+v", res)
|
||||
}
|
||||
var n int
|
||||
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM demo`).Scan(&n); err != nil || n != 2 {
|
||||
t.Fatalf("count=%d err=%v", n, err)
|
||||
}
|
||||
var name string
|
||||
if err := db.QueryRowContext(ctx, `SELECT name FROM demo WHERE id='a'`).Scan(&name); err != nil || name != "one" {
|
||||
t.Fatalf("row a name=%q err=%v", name, err)
|
||||
}
|
||||
|
||||
// second save rotates
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
if _, err := db.ExecContext(ctx, `UPDATE demo SET name='one2' WHERE id='a'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SaveCheckpoint(ctx, store, ch, "test2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meta2, _ := LoadCheckpointMeta(store, ch.ID)
|
||||
if meta2.Previous == nil || meta2.Previous.RowCount != 2 {
|
||||
t.Fatalf("expected previous after rotate: %+v", meta2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user