Files
relspecgo/pkg/writers/mssql/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

206 lines
7.8 KiB
Go

package mssql
import (
"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("sales")
users := models.InitTable("users", "sales")
users.Description = "Registered users"
id := models.InitColumn("id", "users", "sales")
id.Type, id.IsPrimaryKey, id.NotNull, id.AutoIncrement, id.Sequence = "int", true, true, true, 1
email := models.InitColumn("email", "users", "sales")
email.Type, email.Length, email.NotNull, email.Sequence, email.Description = "string", 255, true, 2, "Login e-mail"
age := models.InitColumn("age", "users", "sales")
age.Type, age.Sequence, age.Default = "int", 3, 18
users.Columns["id"], users.Columns["email"], users.Columns["age"] = id, email, age
pk := models.InitConstraint("PK_users", models.PrimaryKeyConstraint)
pk.Columns = []string{"id"}
uq := models.InitConstraint("UQ_users_email", models.UniqueConstraint)
uq.Columns = []string{"email"}
ck := models.InitConstraint("CK_users_age", models.CheckConstraint)
ck.Expression = "[age] >= 0"
emptyCk := models.InitConstraint("CK_empty", models.CheckConstraint)
users.Constraints["PK_users"], users.Constraints["UQ_users_email"], users.Constraints["CK_users_age"], users.Constraints["CK_empty"] = pk, uq, ck, emptyCk
ix := models.InitIndex("IX_users_age", "users", "sales")
ix.Columns, ix.Unique = []string{"age"}, true
pkIx := models.InitIndex("pk_users_idx", "users", "sales")
pkIx.Columns = []string{"id"}
noCols := models.InitIndex("IX_nocols", "users", "sales")
users.Indexes["IX_users_age"], users.Indexes["pk_users_idx"], users.Indexes["IX_nocols"] = ix, pkIx, noCols
orders := models.InitTable("orders", "sales")
oid := models.InitColumn("id", "orders", "sales")
oid.Type, oid.IsPrimaryKey, oid.NotNull = "int", true, true
uid := models.InitColumn("user_id", "orders", "sales")
uid.Type, uid.NotNull = "int", 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"}
fk.OnDelete = "cascade"
badFk := models.InitConstraint("FK_bad", models.ForeignKeyConstraint)
orders.Constraints["FK_orders_users"], orders.Constraints["FK_bad"] = fk, badFk
s.Tables = append(s.Tables, users, orders)
db := models.InitDatabase("shop")
db.Schemas = append(db.Schemas, s)
return db
}
func writeToFile(t *testing.T, opts *writers.WriterOptions, db *models.Database) string {
t.Helper()
out := filepath.Join(t.TempDir(), "out.sql")
opts.OutputPath = out
if err := NewWriter(opts).WriteDatabase(db); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(out)
if err != nil {
t.Fatal(err)
}
return string(b)
}
func TestWriteDatabase_FullScript(t *testing.T) {
got := writeToFile(t, &writers.WriterOptions{}, shopDB())
for _, want := range []string{
"-- Database: shop", "CREATE SCHEMA [sales];",
"CREATE TABLE [sales].[users]", "[email] NVARCHAR(255) NOT NULL", "DEFAULT 18",
"ALTER TABLE [sales].[users] ADD CONSTRAINT [PK_users] PRIMARY KEY ([id]);",
"ALTER TABLE [sales].[orders] ADD CONSTRAINT [PK_sales_orders] PRIMARY KEY ([id]);", // generated PK name from IsPrimaryKey
"CREATE UNIQUE INDEX [IX_users_age] ON [sales].[users] ([age]);",
"ADD CONSTRAINT [UQ_users_email] UNIQUE ([email]);",
"ADD CONSTRAINT [CK_users_age] CHECK ([age] >= 0);",
"ADD CONSTRAINT [FK_orders_users] FOREIGN KEY ([user_id])",
"REFERENCES [sales].[users] ([id])", "ON DELETE CASCADE ON UPDATE NO ACTION;",
"@value = 'Registered users'", "@level2type = 'COLUMN', @level2name = 'email';",
} {
if !strings.Contains(got, want) {
t.Errorf("missing %q\n%s", want, got)
}
}
for _, unwanted := range []string{"pk_users_idx", "IX_nocols", "CK_empty", "FK_bad"} {
if strings.Contains(got, unwanted) {
t.Errorf("%q must be skipped\n%s", unwanted, got)
}
}
}
func TestWriteDatabase_PhaseOrder(t *testing.T) {
got := writeToFile(t, &writers.WriterOptions{}, shopDB())
last := -1
for _, marker := range []string{"-- Schema: sales", "-- Tables for", "-- Primary keys", "-- Indexes", "-- Unique constraints", "-- Check constraints", "-- Foreign keys", "-- Comments"} {
i := strings.Index(got, marker)
if i < 0 || i < last {
t.Fatalf("marker %q out of order (index %d after %d)", marker, i, last)
}
last = i
}
}
func TestWriteDatabase_Deterministic(t *testing.T) {
first := writeToFile(t, &writers.WriterOptions{}, shopDB())
for i := 0; i < 15; i++ {
if got := writeToFile(t, &writers.WriterOptions{}, shopDB()); got != first {
t.Fatalf("output differs on run %d", i)
}
}
}
func TestWriteDatabase_FlattenAndDbo(t *testing.T) {
flat := writeToFile(t, &writers.WriterOptions{FlattenSchema: true}, shopDB())
if strings.Contains(flat, "CREATE SCHEMA") || !strings.Contains(flat, "CREATE TABLE [users]") || strings.Contains(flat, "[sales].") {
t.Errorf("flatten:\n%s", flat)
}
db := shopDB()
db.Schemas[0].Name = "dbo"
dbo := writeToFile(t, &writers.WriterOptions{}, db)
if strings.Contains(dbo, "CREATE SCHEMA") {
t.Errorf("dbo schema must not be created:\n%s", dbo)
}
}
func TestWriteTableAndSchema(t *testing.T) {
db := shopDB()
out := filepath.Join(t.TempDir(), "t.sql")
if err := NewWriter(&writers.WriterOptions{OutputPath: out}).WriteTable(db.Schemas[0].Tables[0]); err != nil {
t.Fatal(err)
}
b, _ := os.ReadFile(out)
if !strings.Contains(string(b), "CREATE TABLE [sales].[users]") || strings.Contains(string(b), "CREATE TABLE [sales].[orders]") {
t.Errorf("WriteTable output:\n%s", b)
}
}
func TestWriteDatabase_OutputErrors(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 TestWriteDatabase_ConnectionFailure(t *testing.T) {
opts := &writers.WriterOptions{Metadata: map[string]any{
"connection_string": "sqlserver://u:p@127.0.0.1:1?database=none&connection+timeout=1",
}}
if err := NewWriter(opts).WriteDatabase(shopDB()); err == nil || !strings.Contains(err.Error(), "ping database") {
t.Errorf("got %v", err)
}
}
func TestGenerateStatements_CoversFullSchema(t *testing.T) {
w := NewWriter(&writers.WriterOptions{})
stmts, err := w.generateStatements(shopDB())
if err != nil {
t.Fatal(err)
}
joined := strings.Join(stmts, "\n---\n")
for _, want := range []string{
"CREATE SCHEMA [sales]", "CREATE TABLE [sales].[users]",
"PRIMARY KEY ([id])", "CREATE UNIQUE INDEX [IX_users_age]", "UNIQUE ([email])",
"CHECK ([age] >= 0)", "FOREIGN KEY ([user_id])", "EXEC sp_addextendedproperty",
} {
if !strings.Contains(joined, want) {
t.Errorf("missing %q in:\n%s", want, joined)
}
}
for _, stmt := range stmts {
if strings.HasPrefix(stmt, "--") || strings.HasSuffix(stmt, ";") || stmt == "" {
t.Errorf("statement not clean: %q", stmt)
}
}
if w.writer != nil {
t.Error("generateStatements must restore the writer")
}
}
func TestDBMLFixtureProducesScript(t *testing.T) {
db, err := rdbml.NewReader(&readers.ReaderOptions{FilePath: "../../../tests/assets/dbml/complex.dbml"}).ReadDatabase()
if err != nil {
t.Fatal(err)
}
got := writeToFile(t, &writers.WriterOptions{}, db)
if !strings.Contains(got, "CREATE TABLE") || !strings.Contains(got, "-- Foreign keys") {
t.Errorf("script:\n%s", got)
}
}
func TestColumnsOrderedBySequence(t *testing.T) {
got := writeToFile(t, &writers.WriterOptions{}, shopDB())
id, email, age := strings.Index(got, "[id] INT"), strings.Index(got, "[email] NVARCHAR"), strings.Index(got, "[age] INT")
if !(id < email && email < age) {
t.Errorf("columns must follow Sequence (id, email, age): %d %d %d\n%s", id, email, age, got)
}
}