Implements tests/_plans and previously deferred packages; updates plan README with new coverage numbers.
206 lines
7.8 KiB
Go
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)
|
|
}
|
|
}
|