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