You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1088 lines
32 KiB
Go
1088 lines
32 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"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 := os.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")
|
|
}
|
|
}
|
|
|
|
func Test_DBPaths(t *testing.T) {
|
|
dbWAL, pathWAL := mustCreateOnDiskDatabaseWAL()
|
|
defer dbWAL.Close()
|
|
defer os.Remove(pathWAL)
|
|
if exp, got := pathWAL, dbWAL.Path(); exp != got {
|
|
t.Fatalf("expected path %s, got %s", exp, got)
|
|
}
|
|
if exp, got := pathWAL+"-wal", dbWAL.WALPath(); exp != got {
|
|
t.Fatalf("expected WAL path %s, got %s", exp, got)
|
|
}
|
|
|
|
db, path := mustCreateOnDiskDatabase()
|
|
defer db.Close()
|
|
defer os.Remove(path)
|
|
if exp, got := path, db.Path(); exp != got {
|
|
t.Fatalf("expected path %s, got %s", exp, got)
|
|
}
|
|
if exp, got := "", db.WALPath(); exp != got {
|
|
t.Fatalf("expected WAL path %s, got %s", exp, got)
|
|
}
|
|
}
|
|
|
|
// Test_TableCreation tests basic operation of an 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)
|
|
}
|
|
|
|
testQ := func() {
|
|
t.Helper()
|
|
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)
|
|
}
|
|
}
|
|
|
|
_, err = db.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("fiona")`)
|
|
if err != nil {
|
|
t.Fatalf("error executing insertion into table: %s", err.Error())
|
|
}
|
|
testQ()
|
|
|
|
// Confirm checkpoint works without error.
|
|
if err := db.Checkpoint(); err != nil {
|
|
t.Fatalf("failed to checkpoint database: %s", err.Error())
|
|
}
|
|
testQ()
|
|
|
|
// Check that VACUUM returns without error.
|
|
if err := db.Vacuum(); err != nil {
|
|
t.Fatalf("failed to VACUUM database: %s", err.Error())
|
|
}
|
|
testQ()
|
|
}
|
|
|
|
// 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(); err != nil {
|
|
t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error())
|
|
}
|
|
// Checkpoint a second time, to ensure it's idempotent.
|
|
if err := db.Checkpoint(); err != nil {
|
|
t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// Test_WALDatabaseCheckpointOKNoWAL tests that a checkpoint succeeds
|
|
// even when no WAL file exists.
|
|
func Test_WALDatabaseCheckpointOKNoWAL(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())
|
|
}
|
|
if !db.WALEnabled() {
|
|
t.Fatalf("WAL mode not enabled")
|
|
}
|
|
if fileExists(db.WALPath()) {
|
|
t.Fatalf("WAL file exists when no writes have happened")
|
|
}
|
|
defer db.Close()
|
|
if err := db.Checkpoint(); err != nil {
|
|
t.Fatalf("failed to checkpoint database in WAL mode with non-existent WAL: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// Test_WALDatabaseCheckpointOKDelete tests that a checkpoint returns no error
|
|
// even when the database is opened in DELETE mode.
|
|
func Test_WALDatabaseCheckpointOKDelete(t *testing.T) {
|
|
path := mustTempFile()
|
|
defer os.Remove(path)
|
|
|
|
db, err := Open(path, false, false)
|
|
if err != nil {
|
|
t.Fatalf("failed to open database in DELETE mode: %s", err.Error())
|
|
}
|
|
if db.WALEnabled() {
|
|
t.Fatalf("WAL mode enabled")
|
|
}
|
|
defer db.Close()
|
|
if err := db.Checkpoint(); err != nil {
|
|
t.Fatalf("failed to checkpoint database in DELETE 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)
|
|
}
|
|
}
|
|
|
|
func Test_WALDisableCheckpointing(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")
|
|
}
|
|
|
|
// Test that databases open with checkpoint disabled by default.
|
|
// This is critical.
|
|
n, err := db.GetCheckpointing()
|
|
if err != nil {
|
|
t.Fatalf("failed to get checkpoint value: %s", err.Error())
|
|
}
|
|
if exp, got := 0, n; exp != got {
|
|
t.Fatalf("unexpected checkpoint value, expected %d, got %d", exp, got)
|
|
}
|
|
|
|
if err := db.EnableCheckpointing(); err != nil {
|
|
t.Fatalf("failed to disable checkpointing: %s", err.Error())
|
|
}
|
|
n, err = db.GetCheckpointing()
|
|
if err != nil {
|
|
t.Fatalf("failed to get checkpoint value: %s", err.Error())
|
|
}
|
|
if exp, got := 1000, n; exp != got {
|
|
t.Fatalf("unexpected checkpoint value, expected %d, got %d", 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()
|
|
|
|
n, err := db.GetCheckpointing()
|
|
if err != nil {
|
|
t.Fatalf("failed to get checkpoint value: %s", err.Error())
|
|
}
|
|
if exp, got := 0, n; exp != got {
|
|
t.Fatalf("unexpected checkpoint value, expected %d, got %d", exp, got)
|
|
}
|
|
|
|
dbFile := filepath.Base(dbPath)
|
|
walPath := dbPath + "-wal"
|
|
walFile := filepath.Base(walPath)
|
|
|
|
replayDir := mustTempDir()
|
|
defer os.RemoveAll(replayDir)
|
|
replayDBPath := filepath.Join(replayDir, dbFile)
|
|
|
|
// Create and 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(); err != nil {
|
|
t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error())
|
|
}
|
|
|
|
// Create and 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(); err != nil {
|
|
t.Fatalf("failed to checkpoint database in WAL mode: %s", err.Error())
|
|
}
|
|
|
|
// Create and 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(); 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 := os.CreateTemp("", "rqlite-db-test")
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
tmpfile.Close()
|
|
return tmpfile.Name()
|
|
}
|
|
|
|
func mustTempDir() string {
|
|
tmpdir, err := os.MkdirTemp("", "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 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()
|
|
}
|