Files
relspecgo/pkg/writers/sqlite/writer_full_test.go
T
warkanum 495a21b67b test: expand coverage across readers, writers, cmd, ui, diff and merge
Implements tests/_plans and previously deferred packages; updates plan
README with new coverage numbers.
2026-10-03 21:33:59 +02:00

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)
}
}