package db import ( "database/sql" "fmt" "io" "io/ioutil" "math/rand" "os" "path/filepath" "sync" "testing" "time" "github.com/rqlite/rqlite/command" "github.com/rqlite/rqlite/command/encoding" ) func Test_RemoveFiles(t *testing.T) { d := t.TempDir() mustCreateClosedFile(fmt.Sprintf("%s/foo", d)) mustCreateClosedFile(fmt.Sprintf("%s/foo-wal", d)) if err := RemoveFiles(fmt.Sprintf("%s/foo", d)); err != nil { t.Fatalf("failed to remove files: %s", err.Error()) } files, err := ioutil.ReadDir(d) if err != nil { t.Fatalf("failed to read directory: %s", err.Error()) } if len(files) != 0 { t.Fatalf("expected directory to be empty, but wasn't") } } // Test_TableCreation tests basic operation of an in-memory database, // ensuring that using different connection objects (as the Execute and Query // will do) works properly i.e. that the connections object work on the same // in-memory database. func Test_TableCreation(t *testing.T) { db, path := mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) r, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } q, err := db.QueryStringStmt("SELECT * FROM foo") if err != nil { t.Fatalf("failed to query empty table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"]}]`, asJSON(q); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } // Confirm checkpoint works without error on an in-memory database. It should just be ignored. if err := db.Checkpoint(5 * time.Second); err != nil { t.Fatalf("failed to checkpoint in-memory database: %s", err.Error()) } } // Test_TableCreationFK ensures foreign key constraints work func Test_TableCreationFK(t *testing.T) { createTableFoo := "CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)" createTableBar := "CREATE TABLE bar (fooid INTEGER NOT NULL PRIMARY KEY, FOREIGN KEY(fooid) REFERENCES foo(id))" insertIntoBar := "INSERT INTO bar(fooid) VALUES(1)" db, path := mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) r, err := db.ExecuteStringStmt(createTableFoo) if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } r, err = db.ExecuteStringStmt(createTableBar) if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } r, err = db.ExecuteStringStmt(insertIntoBar) if err != nil { t.Fatalf("failed to insert record: %s", err.Error()) } if exp, got := `[{"last_insert_id":1,"rows_affected":1}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } // Now, do same testing with FK constraints enabled. dbFK, path := mustCreateOnDiskDatabaseWALFK() defer dbFK.Close() defer os.Remove(path) if !dbFK.FKEnabled() { t.Fatal("FK constraints not marked as enabled") } r, err = dbFK.ExecuteStringStmt(createTableFoo) if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } r, err = dbFK.ExecuteStringStmt(createTableBar) if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } r, err = dbFK.ExecuteStringStmt(insertIntoBar) if err != nil { t.Fatalf("failed to insert record: %s", err.Error()) } if exp, got := `[{"error":"FOREIGN KEY constraint failed"}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } } func Test_ConcurrentQueries(t *testing.T) { db, path := mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) r, err := db.ExecuteStringStmt(`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`) if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } for i := 0; i < 5000; i++ { r, err = db.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("fiona")`) if err != nil { t.Fatalf("failed to insert record: %s", err.Error()) } if exp, got := fmt.Sprintf(`[{"last_insert_id":%d,"rows_affected":1}]`, i+1), asJSON(r); exp != got { t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } } var wg sync.WaitGroup for i := 0; i < 32; i++ { wg.Add(1) go func() { defer wg.Done() ro, err := db.QueryStringStmt(`SELECT COUNT(*) FROM foo`) if err != nil { t.Logf("failed to query table: %s", err.Error()) } if exp, got := `[{"columns":["COUNT(*)"],"types":["integer"],"values":[[5000]]}]`, asJSON(ro); exp != got { t.Logf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } }() } wg.Wait() } func Test_SimpleTransaction(t *testing.T) { db, path := mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) _, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } req := &command.Request{ Transaction: true, Statements: []*command.Statement{ { Sql: `INSERT INTO foo(id, name) VALUES(1, "fiona")`, }, { Sql: `INSERT INTO foo(id, name) VALUES(2, "fiona")`, }, { Sql: `INSERT INTO foo(id, name) VALUES(3, "fiona")`, }, { Sql: `INSERT INTO foo(id, name) VALUES(4, "fiona")`, }, }, } r, err := db.Execute(req, false) if err != nil { t.Fatalf("failed to insert records: %s", err.Error()) } if exp, got := `[{"last_insert_id":1,"rows_affected":1},{"last_insert_id":2,"rows_affected":1},{"last_insert_id":3,"rows_affected":1},{"last_insert_id":4,"rows_affected":1}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } ro, err := db.QueryStringStmt(`SELECT * FROM foo`) if err != nil { t.Fatalf("failed to query table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"fiona"],[2,"fiona"],[3,"fiona"],[4,"fiona"]]}]`, asJSON(ro); exp != got { t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } } func Test_PartialFailTransaction(t *testing.T) { db, path := mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) _, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } req := &command.Request{ Transaction: true, Statements: []*command.Statement{ { Sql: `INSERT INTO foo(id, name) VALUES(1, "fiona")`, }, { Sql: `INSERT INTO foo(id, name) VALUES(2, "fiona")`, }, { Sql: `INSERT INTO foo(id, name) VALUES(1, "fiona")`, }, { Sql: `INSERT INTO foo(id, name) VALUES(4, "fiona")`, }, }, } r, err := db.Execute(req, false) if err != nil { t.Fatalf("failed to insert records: %s", err.Error()) } if exp, got := `[{"last_insert_id":1,"rows_affected":1},{"last_insert_id":2,"rows_affected":1},{"error":"UNIQUE constraint failed: foo.id"}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } ro, err := db.QueryStringStmt(`SELECT * FROM foo`) if err != nil { t.Fatalf("failed to query table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"]}]`, asJSON(ro); exp != got { t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got) } } func Test_IsValidSQLiteOnDisk(t *testing.T) { path := mustTempFile() defer os.Remove(path) dsn := fmt.Sprintf("file:%s", path) db, err := sql.Open("sqlite3", dsn) if err != nil { t.Fatalf("failed to create SQLite database: %s", err.Error()) } _, err = db.Exec("CREATE TABLE foo (name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if err := db.Close(); err != nil { t.Fatalf("failed to close database: %s", err.Error()) } if !IsValidSQLiteFile(path) { t.Fatalf("good SQLite file marked as invalid") } data, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read SQLite file: %s", err.Error()) } if !IsValidSQLiteData(data) { t.Fatalf("good SQLite data marked as invalid") } } func Test_CheckIntegrityOnDisk(t *testing.T) { path := mustTempFile() defer os.Remove(path) dsn := fmt.Sprintf("file:%s", path) db, err := sql.Open("sqlite3", dsn) if err != nil { t.Fatalf("failed to create SQLite database: %s", err.Error()) } _, err = db.Exec("CREATE TABLE foo (name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if err := db.Close(); err != nil { t.Fatalf("failed to close database: %s", err.Error()) } for _, check := range []bool{true, false} { ok, err := CheckIntegrity(path, check) if err != nil { t.Fatalf("failed to check integrity of database (full=%t): %s", check, err.Error()) } if !ok { t.Fatalf("database failed integrity check (full=%t)", check) } } sz := int(mustFileSize(path)) b, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read SQLite file: %s", err.Error()) } // Chop the file in half, integrity check shouldn't even be error-free if err := os.WriteFile(path, b[:len(b)-sz/2], 0644); err != nil { t.Fatalf("failed to write SQLite file: %s", err.Error()) } for _, check := range []bool{true, false} { _, err := CheckIntegrity(path, check) if err == nil { t.Fatalf("succeeded checking integrity of database (full=%t): %s", check, err.Error()) } } // Unable to create a data set that actually fails integrity check. } func Test_IsWALModeEnabledOnDiskDELETE(t *testing.T) { path := mustTempFile() defer os.Remove(path) dsn := fmt.Sprintf("file:%s", path) db, err := sql.Open("sqlite3", dsn) if err != nil { t.Fatalf("failed to create SQLite database: %s", err.Error()) } _, err = db.Exec("CREATE TABLE foo (name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if err := db.Close(); err != nil { t.Fatalf("failed to close database: %s", err.Error()) } if !IsDELETEModeEnabledSQLiteFile(path) { t.Fatalf("DELETE file marked as non-DELETE") } if IsWALModeEnabledSQLiteFile(path) { t.Fatalf("non WAL file marked as WAL") } data, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read SQLite file: %s", err.Error()) } if IsWALModeEnabled(data) { t.Fatalf("non WAL data marked as WAL") } if !IsDELETEModeEnabled(data) { t.Fatalf("data marked as non-DELETE") } } func Test_IsWALModeEnabledOnDiskWAL(t *testing.T) { path := mustTempFile() defer os.Remove(path) dsn := fmt.Sprintf("file:%s", path) db, err := sql.Open("sqlite3", dsn) if err != nil { t.Fatalf("failed to create SQLite database: %s", err.Error()) } _, err = db.Exec("CREATE TABLE foo (name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } _, err = db.Exec("PRAGMA journal_mode=WAL") if err != nil { t.Fatalf("failed to enable WAL mode: %s", err.Error()) } _, err = db.Exec(`INSERT INTO foo(name) VALUES("fiona")`) if err != nil { t.Fatalf("error inserting record into table: %s", err.Error()) } if !IsValidSQLiteWALFile(path + "-wal") { t.Fatalf("WAL file marked not marked as valid") } if err := db.Close(); err != nil { t.Fatalf("failed to close database: %s", err.Error()) } if !IsWALModeEnabledSQLiteFile(path) { t.Fatalf("WAL file marked as non-WAL") } if IsDELETEModeEnabledSQLiteFile(path) { t.Fatalf("WAL file marked as DELETE") } data, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read SQLite file: %s", err.Error()) } if !IsWALModeEnabled(data) { t.Fatalf("WAL data marked as non-WAL") } if IsDELETEModeEnabled(data) { t.Fatalf("WAL data marked as DELETE") } } // Test_WALDatabaseCreatedOK tests that a WAL file is created, and that // a checkpoint succeeds func Test_WALDatabaseCreatedOK(t *testing.T) { path := mustTempFile() defer os.Remove(path) db, err := Open(path, false, true) if err != nil { t.Fatalf("failed to open database in WAL mode: %s", err.Error()) } defer db.Close() if !db.WALEnabled() { t.Fatalf("WAL mode not enabled") } if _, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if !IsWALModeEnabledSQLiteFile(path) { t.Fatalf("SQLite file not marked as WAL") } walPath := path + "-wal" if _, err := os.Stat(walPath); os.IsNotExist(err) { t.Fatalf("WAL file does not exist") } if err := db.Checkpoint(5 * time.Second); err != nil { t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error()) } } // Test_WALDatabaseCreatedOKFromDELETE tests that a WAL database is created properly, // even when supplied with a DELETE-mode database. func Test_WALDatabaseCreatedOKFromDELETE(t *testing.T) { deletePath := mustTempFile() defer os.Remove(deletePath) deleteDB, err := Open(deletePath, false, false) if err != nil { t.Fatalf("failed to open database in WAL mode: %s", err.Error()) } defer deleteDB.Close() if _, err := deleteDB.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } _, err = deleteDB.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("fiona")`) if err != nil { t.Fatalf("error executing insertion into table: %s", err.Error()) } walDB, err := Open(deletePath, false, true) if err != nil { t.Fatalf("failed to open database in WAL mode: %s", err.Error()) } defer walDB.Close() if !IsWALModeEnabledSQLiteFile(deletePath) { t.Fatalf("SQLite file not marked as WAL") } rows, err := walDB.QueryStringStmt("SELECT * FROM foo") if err != nil { t.Fatalf("failed to query WAL table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"fiona"]]}]`, asJSON(rows); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } } // Test_DELETEDatabaseCreatedOKFromWAL tests that a DELETE database is created properly, // even when supplied with a WAL-mode database. func Test_DELETEDatabaseCreatedOKFromWAL(t *testing.T) { walPath := mustTempFile() defer os.Remove(walPath) walDB, err := Open(walPath, false, true) if err != nil { t.Fatalf("failed to open database in WAL mode: %s", err.Error()) } defer walDB.Close() if _, err := walDB.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } _, err = walDB.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("fiona")`) if err != nil { t.Fatalf("error executing insertion into table: %s", err.Error()) } if err := walDB.Close(); err != nil { // Closing the WAL database is required if it's to be opened in DELETE mode. t.Fatalf("failed to close database: %s", err.Error()) } deleteDB, err2 := Open(walPath, false, false) if err2 != nil { t.Fatalf("failed to open database in DELETE mode: %s", err2.Error()) } defer deleteDB.Close() if !IsDELETEModeEnabledSQLiteFile(walPath) { t.Fatalf("SQLite file not marked as WAL") } rows, err := deleteDB.QueryStringStmt("SELECT * FROM foo") if err != nil { t.Fatalf("failed to query WAL table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"fiona"]]}]`, asJSON(rows); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } } // Test_WALReplayOK tests that WAL files are replayed as expected. func Test_WALReplayOK(t *testing.T) { testFunc := func(t *testing.T, replayIntoDelete bool) { dbPath := mustTempFile() defer os.Remove(dbPath) db, err := Open(dbPath, false, true) if err != nil { t.Fatalf("failed to open database in WAL mode: %s", err.Error()) } defer db.Close() dbFile := filepath.Base(dbPath) walPath := dbPath + "-wal" walFile := filepath.Base(walPath) replayDir := mustTempDir() defer os.RemoveAll(replayDir) replayDBPath := filepath.Join(replayDir, dbFile) // Take over control of checkpointing if err := db.DisableCheckpointing(); err != nil { t.Fatalf("failed to disable checkpointing: %s", err.Error()) } // Copy the SQLite file and WAL #1 if _, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if !fileExists(walPath) { t.Fatalf("WAL file at %s does not exist", walPath) } mustCopyFile(replayDBPath, dbPath) mustCopyFile(filepath.Join(replayDir, walFile+"_001"), walPath) if err := db.Checkpoint(5 * time.Second); err != nil { t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error()) } // Copy WAL #2 _, err = db.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("fiona")`) if err != nil { t.Fatalf("error executing insertion into table: %s", err.Error()) } if !fileExists(walPath) { t.Fatalf("WAL file at %s does not exist", walPath) } mustCopyFile(filepath.Join(replayDir, walFile+"_002"), walPath) if err := db.Checkpoint(5 * time.Second); err != nil { t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error()) } // Copy WAL #3 _, err = db.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("declan")`) if err != nil { t.Fatalf("error executing insertion into table: %s", err.Error()) } if !fileExists(walPath) { t.Fatalf("WAL file at %s does not exist", walPath) } mustCopyFile(filepath.Join(replayDir, walFile+"_003"), walPath) if err := db.Close(); err != nil { t.Fatalf("failed to close database: %s", err.Error()) } wals := []string{ filepath.Join(replayDir, walFile+"_001"), filepath.Join(replayDir, walFile+"_002"), filepath.Join(replayDir, walFile+"_003"), } if err := ReplayWAL(replayDBPath, wals, replayIntoDelete); err != nil { t.Fatalf("failed to replay WAL files: %s", err.Error()) } if replayIntoDelete { if !IsDELETEModeEnabledSQLiteFile(replayDBPath) { t.Fatal("replayed database not marked as DELETE mode") } } else { if !IsWALModeEnabledSQLiteFile(replayDBPath) { t.Fatal("replayed database not marked as WAL mode") } } // Check that there are no files related to WALs in the replay directory // Both the copied WAL files should be gone, and there should be no // "real" WAL file either. walFiles, err := filepath.Glob(filepath.Join(replayDir, "*-wal*")) if err != nil { t.Fatalf("failed to glob replay directory: %s", err.Error()) } if len(walFiles) != 0 { t.Fatalf("replay directory contains WAL files: %s", walFiles) } replayedDB, err := Open(replayDBPath, false, true) if err != nil { t.Fatalf("failed to open replayed database: %s", err.Error()) } rows, err := replayedDB.QueryStringStmt("SELECT * FROM foo") if err != nil { t.Fatalf("failed to query WAL table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"fiona"],[2,"declan"]]}]`, asJSON(rows); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } } t.Run("replayIntoWAL", func(t *testing.T) { testFunc(t, false) }) t.Run("replayIntoDELETE", func(t *testing.T) { testFunc(t, true) }) } func Test_WALReplayFailures(t *testing.T) { dbDir := mustTempDir() defer os.RemoveAll(dbDir) walDir := mustTempDir() defer os.RemoveAll(walDir) err := ReplayWAL(filepath.Join(dbDir, "foo.db"), []string{filepath.Join(walDir, "foo.db-wal")}, false) if err != ErrWALReplayDirectoryMismatch { t.Fatalf("expected %s, got %s", ErrWALReplayDirectoryMismatch, err.Error()) } } func test_FileCreationOnDisk(t *testing.T, db *DB) { defer db.Close() if db.FKEnabled() { t.Fatal("FK constraints marked as enabled") } // Confirm checkpoint works on all types of on-disk databases. Worst case, this // should be ignored. if err := db.Checkpoint(5 * time.Second); err != nil { t.Fatalf("failed to checkpoint database in DELETE mode: %s", err.Error()) } } // test_ConnectionIsolationOnDisk test that ISOLATION behavior of on-disk databases doesn't // change unexpectedly. func test_ConnectionIsolationOnDisk(t *testing.T, db *DB) { r, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } r, err = db.ExecuteStringStmt("BEGIN") if err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if exp, got := `[{}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } r, err = db.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("fiona")`) if err != nil { t.Fatalf("error executing insertion into table: %s", err.Error()) } if exp, got := `[{"last_insert_id":1,"rows_affected":1}]`, asJSON(r); exp != got { t.Fatalf("unexpected results for execute, expected %s, got %s", exp, got) } q, err := db.QueryStringStmt("SELECT * FROM foo") if err != nil { t.Fatalf("failed to query empty table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"]}]`, asJSON(q); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } _, err = db.ExecuteStringStmt("COMMIT") if err != nil { t.Fatalf("error executing insertion into table: %s", err.Error()) } q, err = db.QueryStringStmt("SELECT * FROM foo") if err != nil { t.Fatalf("failed to query empty table: %s", err.Error()) } if exp, got := `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"fiona"]]}]`, asJSON(q); exp != got { t.Fatalf("unexpected results for query, expected %s, got %s", exp, got) } } func Test_DatabaseCommonOnDiskOperations(t *testing.T) { testCases := []struct { name string testFunc func(*testing.T, *DB) }{ {"FileCreationOnDisk", test_FileCreationOnDisk}, {"ConnectionIsolationOnDisk", test_ConnectionIsolationOnDisk}, } for _, tc := range testCases { db, path := mustCreateOnDiskDatabase() defer db.Close() defer os.Remove(path) t.Run(tc.name+":disk", func(t *testing.T) { tc.testFunc(t, db) }) db, path = mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) t.Run(tc.name+":wal", func(t *testing.T) { tc.testFunc(t, db) }) } } // Test_ParallelOperationsInMemory runs multiple accesses concurrently, ensuring // that correct results are returned in every goroutine. It's not 100% that this // test would bring out a bug, but it's almost 100%. // // See https://github.com/mattn/go-sqlite3/issues/959#issuecomment-890283264 func Test_ParallelOperationsInMemory(t *testing.T) { db, path := mustCreateOnDiskDatabaseWAL() defer db.Close() defer os.Remove(path) if _, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if _, err := db.ExecuteStringStmt("CREATE TABLE bar (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } if _, err := db.ExecuteStringStmt("CREATE TABLE qux (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"); err != nil { t.Fatalf("failed to create table: %s", err.Error()) } // Confirm schema is as expected, when checked from same goroutine. if rows, err := db.QueryStringStmt(`SELECT sql FROM sqlite_master`); err != nil { t.Fatalf("failed to query for schema after creation: %s", err.Error()) } else { if exp, got := `[{"columns":["sql"],"types":["text"],"values":[["CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"],["CREATE TABLE bar (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"],["CREATE TABLE qux (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"]]}]`, asJSON(rows); exp != got { t.Fatalf("schema not as expected during after creation, exp %s, got %s", exp, got) } } var exWg sync.WaitGroup exWg.Add(3) foo := make(chan time.Time) bar := make(chan time.Time) qux := make(chan time.Time) done := make(chan bool) ticker := time.NewTicker(1 * time.Millisecond) go func() { for { select { case t := <-ticker.C: foo <- t bar <- t qux <- t case <-done: close(foo) close(bar) close(qux) return } } }() go func() { defer exWg.Done() for range foo { if _, err := db.ExecuteStringStmt(`INSERT INTO foo(id, name) VALUES(1, "fiona")`); err != nil { t.Logf("failed to insert records into foo: %s", err.Error()) } } }() go func() { defer exWg.Done() for range bar { if _, err := db.ExecuteStringStmt(`INSERT INTO bar(id, name) VALUES(1, "fiona")`); err != nil { t.Logf("failed to insert records into bar: %s", err.Error()) } } }() go func() { defer exWg.Done() for range qux { if _, err := db.ExecuteStringStmt(`INSERT INTO qux(id, name) VALUES(1, "fiona")`); err != nil { t.Logf("failed to insert records into qux: %s", err.Error()) } } }() var qWg sync.WaitGroup qWg.Add(3) for i := 0; i < 3; i++ { go func(j int) { defer qWg.Done() var n int for { if rows, err := db.QueryStringStmt(`SELECT sql FROM sqlite_master`); err != nil { t.Logf("failed to query for schema during goroutine %d execution: %s", j, err.Error()) } else { n++ if exp, got := `[{"columns":["sql"],"types":["text"],"values":[["CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"],["CREATE TABLE bar (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"],["CREATE TABLE qux (id INTEGER NOT NULL PRIMARY KEY, name TEXT)"]]}]`, asJSON(rows); exp != got { t.Logf("schema not as expected during goroutine execution, exp %s, got %s, after %d queries", exp, got, n) } } if n == 500000 { break } } }(i) } qWg.Wait() close(done) exWg.Wait() } func mustCreateOnDiskDatabase() (*DB, string) { var err error f := mustTempFile() db, err := Open(f, false, false) if err != nil { panic("failed to open database in DELETE mode") } return db, f } func mustCreateOnDiskDatabaseWAL() (*DB, string) { var err error f := mustTempFile() db, err := Open(f, false, true) if err != nil { panic("failed to open database in WAL mode") } return db, f } func mustCreateOnDiskDatabaseWALFK() (*DB, string) { var err error f := mustTempFile() db, err := Open(f, true, true) if err != nil { panic("failed to open database in WAL mode") } return db, f } // mustExecute executes a statement, and panics on failure. Used for statements // that should never fail, even taking into account test setup. func mustExecute(db *DB, stmt string) { r, err := db.ExecuteStringStmt(stmt) if err != nil { panic(fmt.Sprintf("failed to execute statement: %s", err.Error())) } if r[0].Error != "" { panic(fmt.Sprintf("failed to execute statement: %s", r[0].Error)) } } func asJSON(v interface{}) string { enc := encoding.Encoder{} b, err := enc.JSONMarshal(v) if err != nil { panic(fmt.Sprintf("failed to JSON marshal value: %s", err.Error())) } return string(b) } // mustTempFile returns a path to a temporary file in directory dir. It is up to the // caller to remove the file once it is no longer needed. func mustTempFile() string { tmpfile, err := ioutil.TempFile("", "rqlite-db-test") if err != nil { panic(err.Error()) } tmpfile.Close() return tmpfile.Name() } func mustTempDir() string { tmpdir, err := ioutil.TempDir("", "rqlite-db-test") if err != nil { panic(err.Error()) } return tmpdir } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } // function which copies a src file to a dst file, panics if any error func mustCopyFile(dst, src string) { srcFile, err := os.Open(src) if err != nil { panic(err) } defer srcFile.Close() dstFile, err := os.Create(dst) if err != nil { panic(err) } defer dstFile.Close() _, err = io.Copy(dstFile, srcFile) if err != nil { panic(err) } } func mustRenameFile(oldpath, newpath string) { if err := os.Rename(oldpath, newpath); err != nil { panic(err) } } func mustCreateClosedFile(path string) { f, err := os.Create(path) if err != nil { panic("failed to create file") } if err := f.Close(); err != nil { panic("failed to close file") } } func mustStat(path string) os.FileInfo { fi, err := os.Stat(path) if err != nil { panic("failed to stat file") } return fi } func mustFileSize(path string) int64 { fi := mustStat(path) return fi.Size() } func mustRandomInt(n int) int { rand.Seed(time.Now().UnixNano()) return rand.Intn(n) }