Implements tests/_plans and previously deferred packages; updates plan README with new coverage numbers.
251 lines
8.5 KiB
Go
251 lines
8.5 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
|
rdbml "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
func shopDB() *models.Database {
|
|
s := models.InitSchema("public")
|
|
users := models.InitTable("users", "public")
|
|
id := models.InitColumn("id", "users", "public")
|
|
id.Type, id.IsPrimaryKey, id.NotNull, id.AutoIncrement, id.Sequence = "integer", true, true, true, 1
|
|
email := models.InitColumn("email", "users", "public")
|
|
email.Type, email.NotNull, email.Sequence = "text", true, 2
|
|
age := models.InitColumn("age", "users", "public")
|
|
age.Type, age.Sequence, age.Default = "integer", 3, 18
|
|
users.Columns["id"], users.Columns["email"], users.Columns["age"] = id, email, age
|
|
|
|
uq := models.InitConstraint("uq_users_email", models.UniqueConstraint)
|
|
uq.Columns = []string{"email"}
|
|
ck := models.InitConstraint("ck_age", models.CheckConstraint)
|
|
ck.Expression = "age >= 0"
|
|
users.Constraints["uq_users_email"], users.Constraints["ck_age"] = uq, ck
|
|
ix := models.InitIndex("idx_users_age", "users", "public")
|
|
ix.Columns = []string{"age"}
|
|
uix := models.InitIndex("uidx_users_nick", "users", "public")
|
|
uix.Columns, uix.Unique = []string{"age", "email"}, true
|
|
pkIx := models.InitIndex("users_pkey", "users", "public")
|
|
pkIx.Columns = []string{"id"}
|
|
users.Indexes["idx_users_age"], users.Indexes["uidx_users_nick"], users.Indexes["users_pkey"] = ix, uix, pkIx
|
|
|
|
orders := models.InitTable("orders", "public")
|
|
oid := models.InitColumn("id", "orders", "public")
|
|
oid.Type, oid.IsPrimaryKey, oid.NotNull = "integer", true, true
|
|
uid := models.InitColumn("user_id", "orders", "public")
|
|
uid.Type, uid.NotNull = "integer", true
|
|
orders.Columns["id"], orders.Columns["user_id"] = oid, uid
|
|
fk := models.InitConstraint("fk_orders_users", models.ForeignKeyConstraint)
|
|
fk.Columns, fk.ReferencedTable, fk.ReferencedColumns = []string{"user_id"}, "users", []string{"id"}
|
|
orders.Constraints["fk_orders_users"] = fk
|
|
|
|
s.Tables = append(s.Tables, users, orders)
|
|
db := models.InitDatabase("shop")
|
|
db.Schemas = append(db.Schemas, s)
|
|
return db
|
|
}
|
|
|
|
func scriptFor(t *testing.T, db *models.Database) string {
|
|
t.Helper()
|
|
out := filepath.Join(t.TempDir(), "out.sql")
|
|
if err := NewWriter(&writers.WriterOptions{OutputPath: out}).WriteDatabase(db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := os.ReadFile(out)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func TestWriteDatabase_Script(t *testing.T) {
|
|
got := scriptFor(t, shopDB())
|
|
for _, want := range []string{
|
|
"-- SQLite Database Schema", "-- Database: shop", "PRAGMA foreign_keys",
|
|
"CREATE TABLE", "users", "orders", "CREATE INDEX", "idx_users_age", "CREATE UNIQUE INDEX",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing %q\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, "users_pkey") {
|
|
t.Errorf("pkey index must be skipped:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "-- Schema: public") {
|
|
t.Errorf("default schema must not be announced:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteDatabase_Deterministic(t *testing.T) {
|
|
first := scriptFor(t, shopDB())
|
|
for i := 0; i < 15; i++ {
|
|
if got := scriptFor(t, shopDB()); got != first {
|
|
t.Fatalf("output differs on run %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriter_ReusableAfterFileOutput(t *testing.T) {
|
|
out := filepath.Join(t.TempDir(), "o.sql")
|
|
w := NewWriter(&writers.WriterOptions{OutputPath: out})
|
|
for i := 0; i < 2; i++ {
|
|
if err := w.WriteDatabase(shopDB()); err != nil {
|
|
t.Fatalf("write %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteSchemaAndTable_UseOutputPath(t *testing.T) {
|
|
db := shopDB()
|
|
dir := t.TempDir()
|
|
|
|
sOut := filepath.Join(dir, "s.sql")
|
|
if err := NewWriter(&writers.WriterOptions{OutputPath: sOut}).WriteSchema(db.Schemas[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b, _ := os.ReadFile(sOut); !strings.Contains(string(b), "CREATE TABLE") {
|
|
t.Errorf("schema output:\n%s", b)
|
|
}
|
|
|
|
tOut := filepath.Join(dir, "t.sql")
|
|
if err := NewWriter(&writers.WriterOptions{OutputPath: tOut}).WriteTable(db.Schemas[0].Tables[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b, _ := os.ReadFile(tOut); !strings.Contains(string(b), "CREATE TABLE") {
|
|
t.Errorf("table output:\n%s", b)
|
|
}
|
|
}
|
|
|
|
func TestWriteDatabase_BadOutputPath(t *testing.T) {
|
|
bad := filepath.Join(t.TempDir(), "missing", "x.sql")
|
|
if err := NewWriter(&writers.WriterOptions{OutputPath: bad}).WriteDatabase(shopDB()); err == nil || !strings.Contains(err.Error(), "failed to create output file") {
|
|
t.Errorf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteAgainstSQLiteFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "shop.db")
|
|
opts := &writers.WriterOptions{Metadata: map[string]any{"connection_string": path}}
|
|
if err := NewWriter(opts).WriteDatabase(shopDB()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if opts.Metadata["execution_failed"] != 0 || opts.Metadata["execution_success"].(int) == 0 {
|
|
t.Errorf("metadata: %+v", opts.Metadata)
|
|
}
|
|
|
|
conn, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
for _, tbl := range []string{"users", "orders"} {
|
|
var n string
|
|
if err := conn.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, tbl).Scan(&n); err != nil {
|
|
t.Errorf("table %s not created: %v", tbl, err)
|
|
}
|
|
}
|
|
var idx int
|
|
if err := conn.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='index' AND name IN ('idx_users_age','uidx_users_nick','uq_users_email')`).Scan(&idx); err != nil || idx != 3 {
|
|
t.Errorf("indexes created: %d (%v)", idx, err)
|
|
}
|
|
if _, err := conn.Exec(`INSERT INTO users(email) VALUES('a@x')`); err != nil {
|
|
t.Errorf("insert: %v", err)
|
|
}
|
|
if _, err := conn.Exec(`INSERT INTO users(email) VALUES('a@x')`); err == nil {
|
|
t.Error("unique constraint on email must be enforced")
|
|
}
|
|
}
|
|
|
|
func TestExecute_StopsOnErrorUnlessIgnored(t *testing.T) {
|
|
// Pre-create "users" so the first CREATE TABLE fails.
|
|
prepare := func(t *testing.T) string {
|
|
path := filepath.Join(t.TempDir(), "pre.db")
|
|
conn, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
if _, err := conn.Exec(`CREATE TABLE users (x int)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
path := prepare(t)
|
|
opts := &writers.WriterOptions{Metadata: map[string]any{"connection_string": path}}
|
|
err := NewWriter(opts).WriteDatabase(shopDB())
|
|
if err == nil || !strings.Contains(err.Error(), "failed to execute") {
|
|
t.Fatalf("expected failure, got %v", err)
|
|
}
|
|
if opts.Metadata["execution_failed"] != 1 {
|
|
t.Errorf("must stop at first failure: %+v", opts.Metadata)
|
|
}
|
|
|
|
path = prepare(t)
|
|
opts = &writers.WriterOptions{Metadata: map[string]any{"connection_string": path, "ignore_errors": true}}
|
|
err = NewWriter(opts).WriteDatabase(shopDB())
|
|
if err == nil {
|
|
t.Fatal("errors are still reported when ignored")
|
|
}
|
|
if opts.Metadata["execution_success"].(int) == 0 || opts.Metadata["execution_failed"].(int) == 0 {
|
|
t.Errorf("ignore_errors must continue past failures: %+v", opts.Metadata)
|
|
}
|
|
conn, _ := sql.Open("sqlite", path)
|
|
defer conn.Close()
|
|
var n string
|
|
if err := conn.QueryRow(`SELECT name FROM sqlite_master WHERE name='orders'`).Scan(&n); err != nil {
|
|
t.Errorf("orders must still be created: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTruncateStatement(t *testing.T) {
|
|
if got := truncateStatement("CREATE TABLE\n x"); got != "CREATE TABLE x" {
|
|
t.Errorf("collapse: %q", got)
|
|
}
|
|
long := strings.Repeat("a", 200)
|
|
if got := truncateStatement(long); len(got) != 83 || !strings.HasSuffix(got, "...") {
|
|
t.Errorf("truncate: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestTableSchemaName(t *testing.T) {
|
|
for in, want := range map[string]string{"public": "", "PUBLIC": "", "main": "", "auth": "auth", "": ""} {
|
|
if got := tableSchemaName(in); got != want {
|
|
t.Errorf("tableSchemaName(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckConstraintsWrittenAsComments(t *testing.T) {
|
|
w := NewWriter(&writers.WriterOptions{})
|
|
var sb strings.Builder
|
|
w.writer = &sb
|
|
if err := w.writeCheckConstraints("", shopDB().Schemas[0].Tables[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := sb.String(); !strings.Contains(got, "ck_age") || !strings.Contains(got, "age >= 0") {
|
|
t.Errorf("check output: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDBMLFixtureExecutes(t *testing.T) {
|
|
db, err := rdbml.NewReader(&readers.ReaderOptions{FilePath: "../../../tests/assets/dbml/complex.dbml"}).ReadDatabase()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "complex.db")
|
|
opts := &writers.WriterOptions{Metadata: map[string]any{"connection_string": path, "ignore_errors": true}}
|
|
_ = NewWriter(opts).WriteDatabase(db)
|
|
if opts.Metadata["execution_success"].(int) == 0 {
|
|
t.Errorf("nothing executed: %+v", opts.Metadata)
|
|
}
|
|
}
|