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.
361 lines
9.4 KiB
Go
361 lines
9.4 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"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_TableCreationInMemoryFK ensures foreign key constraints work
|
|
func Test_TableCreationInMemoryFK(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 := mustCreateInMemoryDatabase()
|
|
defer db.Close()
|
|
|
|
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 := mustCreateInMemoryDatabaseFK()
|
|
defer dbFK.Close()
|
|
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_ConcurrentQueriesInMemory(t *testing.T) {
|
|
db := mustCreateInMemoryDatabase()
|
|
defer db.Close()
|
|
|
|
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 := mustCreateOnDiskDatabase()
|
|
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 := mustCreateOnDiskDatabase()
|
|
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 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 mustCreateInMemoryDatabase() *DB {
|
|
db, err := OpenInMemory(false)
|
|
if err != nil {
|
|
panic("failed to open in-memory database")
|
|
}
|
|
return db
|
|
}
|
|
|
|
func mustCreateInMemoryDatabaseFK() *DB {
|
|
db, err := OpenInMemory(true)
|
|
if err != nil {
|
|
panic("failed to open in-memory database with foreign key constraints")
|
|
}
|
|
return db
|
|
}
|
|
|
|
// 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)
|
|
}
|