88 "log"
99 "os"
1010 "os/exec"
11+ "path/filepath"
1112 "strconv"
1213 "strings"
1314 "time"
@@ -340,11 +341,110 @@ func runQuickCheck(file string) error {
340341 return nil
341342}
342343
344+ // checkpointWALBeforeUpload folds any pending WAL frames into the main
345+ // database file and verifies no data is left behind in sidecar files. The
346+ // upload ships only the main database file, so frames still sitting in
347+ // <file>-wal (or a hot rollback journal) would silently be missing from the
348+ // imported database.
349+ func checkpointWALBeforeUpload (file string ) error {
350+ if out , err := exec .Command ("sqlite3" , "-list" , file , "PRAGMA wal_checkpoint(TRUNCATE);" ).CombinedOutput (); err != nil {
351+ return fmt .Errorf ("could not checkpoint database %s: %w: %s" , file , err , out )
352+ }
353+ for _ , sidecar := range []struct { suffix , hint string }{
354+ {"-wal" , "close all connections to the database and retry the import" },
355+ {"-journal" , "the database has a leftover rollback journal; open and cleanly close it with sqlite3 first" },
356+ } {
357+ if err := checkSidecarEmpty (file + sidecar .suffix , sidecar .hint ); err != nil {
358+ return err
359+ }
360+ }
361+ return nil
362+ }
363+
364+ func checkSidecarEmpty (sidecarPath , hint string ) error {
365+ info , err := os .Stat (sidecarPath )
366+ if errors .Is (err , os .ErrNotExist ) {
367+ return nil
368+ }
369+ if err != nil {
370+ return fmt .Errorf ("could not check %s: %w" , sidecarPath , err )
371+ }
372+ if info .Size () > 0 {
373+ return fmt .Errorf ("%s is not empty, importing would lose the data it holds: %s" , sidecarPath , hint )
374+ }
375+ return nil
376+ }
377+
378+ // tursodbLogPath returns the logical-log sidecar path tursodb uses for a
379+ // database file, mirroring turso_core's `with_extension("db-log")`: the
380+ // file's extension (if any) is replaced with "db-log".
381+ func tursodbLogPath (file string ) string {
382+ return strings .TrimSuffix (file , filepath .Ext (file )) + ".db-log"
383+ }
384+
343385func handleDBFileAWS (file string , cipher string ) (* turso.DBSeed , error ) {
344- if err := sqliteFileIntegrityChecks (file , cipher ); err != nil {
386+ format , err := sniffSQLiteFileFormat (file )
387+ if err != nil {
345388 return nil , err
346389 }
347390
391+ if format == fileFormatRollback {
392+ // The server only accepts WAL or MVCC format files. Converting to WAL
393+ // is exactly the remediation the error message used to instruct users
394+ // to run themselves, and sqlite3 is already a hard requirement here.
395+ fmt .Printf ("File %s uses a rollback journal; converting it to WAL mode for import.\n " , file )
396+ if out , err := exec .Command ("sqlite3" , file , "PRAGMA journal_mode=WAL;" ).CombinedOutput (); err != nil {
397+ return nil , fmt .Errorf ("could not convert %s to WAL mode: %w: %s" , file , err , out )
398+ }
399+ if format , err = sniffSQLiteFileFormat (file ); err != nil {
400+ return nil , err
401+ }
402+ }
403+
404+ switch format {
405+ case fileFormatWAL :
406+ if err := checkpointWALBeforeUpload (file ); err != nil {
407+ return nil , err
408+ }
409+ if err := sqliteFileIntegrityChecks (file , cipher ); err != nil {
410+ return nil , err
411+ }
412+ case fileFormatMVCC :
413+ // sqlite3-based checks can't run on MVCC (tursodb format) files: the
414+ // sqlite3 binary reports them as not-a-database. The server verifies
415+ // the file with the tursodb engine after upload.
416+ if ! tursoDBFlag {
417+ return nil , fmt .Errorf ("%s is in tursodb (MVCC) format and can only be imported into a tursodb database" , file )
418+ }
419+ if cipher != "" {
420+ return nil , errors .New ("remote encryption is not supported when importing tursodb (MVCC) format files" )
421+ }
422+ // The upload ships only the main database file: a non-empty tursodb
423+ // logical log next to it means the file is not a fully checkpointed
424+ // snapshot and importing it would lose the log's data.
425+ if err := checkSidecarEmpty (tursodbLogPath (file ), "checkpoint the database with tursodb before importing" ); err != nil {
426+ return nil , err
427+ }
428+ fileInfo , err := os .Stat (file )
429+ if err != nil {
430+ return nil , fmt .Errorf ("failed to get file info: %w" , err )
431+ }
432+ if fileInfo .Size () > MaxAWSDBSizeBytes {
433+ return nil , errors .New ("database file size exceeds maximum allowed size of 20 GB" )
434+ }
435+ case fileFormatNotSQLite :
436+ isDump , err := checkIfDump (file )
437+ if err != nil {
438+ return nil , fmt .Errorf ("failed to get file header: %w" , err )
439+ }
440+ if isDump {
441+ return nil , fmt .Errorf ("%s is a sqlite3 dump, not a sqlite3 database. Please import a sqlite database" , file )
442+ }
443+ return nil , fmt .Errorf ("file %s is not a valid SQLite database file" , file )
444+ default :
445+ return nil , fmt .Errorf ("file %s has an unsupported SQLite file format" , file )
446+ }
447+
348448 seed := & turso.DBSeed {
349449 Type : "database_upload" ,
350450 Filepath : file ,
@@ -365,6 +465,16 @@ func handleDBFile(client *turso.Client, file string, isAWS bool, cipher string)
365465 return handleDBFileAWS (file , cipher )
366466 }
367467
468+ format , err := sniffSQLiteFileFormat (file )
469+ if err != nil {
470+ return nil , err
471+ }
472+ if format == fileFormatMVCC {
473+ // non-AWS groups are seeded by replaying a .dump, which sqlite3 cannot
474+ // produce from an MVCC (tursodb format) file
475+ return nil , fmt .Errorf ("%s is in tursodb (MVCC) format and can only be imported into AWS groups" , file )
476+ }
477+
368478 if err := checkSQLiteFile (file ); err != nil {
369479 return nil , err
370480 }
0 commit comments