Files
relspecgo/pkg/writers/pgsql/writer_test.go
T

1585 lines
49 KiB
Go

package pgsql
import (
"bytes"
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
func TestWriteDatabase(t *testing.T) {
// Create a test database
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create a test table
table := models.InitTable("users", "public")
table.Description = "User accounts table"
// Add columns
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
idCol.Description = "Primary key"
idCol.Default = "nextval('\"public\".\"identity_users_id\"'::regclass)"
table.Columns["id"] = idCol
nameCol := models.InitColumn("name", "users", "public")
nameCol.Type = "text"
nameCol.Description = "User name"
table.Columns["name"] = nameCol
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "text"
emailCol.Description = "Email address"
table.Columns["email"] = emailCol
// Add primary key constraint
pkConstraint := &models.Constraint{
Name: "pk_users",
Type: models.PrimaryKeyConstraint,
Columns: []string{"id"},
}
table.Constraints["pk_users"] = pkConstraint
// Add unique index
uniqueEmailIndex := &models.Index{
Name: "uidx_users_email",
Unique: true,
Columns: []string{"email"},
}
table.Indexes["uidx_users_email"] = uniqueEmailIndex
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the database
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Print output for debugging
t.Logf("Generated SQL:\n%s", output)
// Verify output contains expected elements
expectedStrings := []string{
"CREATE TABLE",
"PRIMARY KEY",
"UNIQUE INDEX",
"COMMENT ON TABLE",
"COMMENT ON COLUMN",
}
for _, expected := range expectedStrings {
if !strings.Contains(output, expected) {
t.Errorf("Output missing expected string: %s\nFull output:\n%s", expected, output)
}
}
}
func TestWriteDatabase_ConcurrentIndex(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("users", "public")
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "text"
table.Columns["email"] = emailCol
concurrentIndex := &models.Index{
Name: "idx_users_email",
Columns: []string{"email"},
Concurrent: true,
}
table.Indexes["idx_users_email"] = concurrentIndex
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "CREATE INDEX CONCURRENTLY IF NOT EXISTS \"idx_users_email\"") {
t.Errorf("Output missing CONCURRENTLY index creation:\n%s", output)
}
}
func TestWriteDatabase_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("plans", "public")
tagsCol := models.InitColumn("tags", "plans", "public")
tagsCol.Type = "text[]"
table.Columns["tags"] = tagsCol
index := &models.Index{
Name: "idx_plans_tags",
Type: "gin",
Columns: []string{"tags"},
}
table.Indexes[index.Name] = index
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, `USING gin ("tags" array_ops)`) {
t.Fatalf("expected GIN index on array column with array_ops, got:\n%s", output)
}
if strings.Contains(output, "gin_trgm_ops") {
t.Fatalf("did not expect gin_trgm_ops for text[] column, got:\n%s", output)
}
}
func TestWriteDatabase_GinIndexOnQuotedTextColumnUsesTrigramOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_personas", "public")
nameCol := models.InitColumn("name", "agent_personas", "public")
nameCol.Type = "text"
table.Columns["name"] = nameCol
index := &models.Index{
Name: "idx_agent_personas_name_gin",
Type: "gin",
Columns: []string{"name"},
}
table.Indexes[index.Name] = index
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, `CREATE EXTENSION IF NOT EXISTS pg_trgm`) {
t.Fatalf("expected trigram extension for text GIN index, got:\n%s", output)
}
if !strings.Contains(output, `USING gin ("name" gin_trgm_ops)`) {
t.Fatalf("expected quoted text GIN index to include gin_trgm_ops, got:\n%s", output)
}
}
func TestWriteDatabase_GinIndexOnJSONBUsesJSONBOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
detailsCol := models.InitColumn("details", "learnings", "public")
detailsCol.Type = "jsonb"
table.Columns["details"] = detailsCol
index := &models.Index{
Name: "idx_learnings_details",
Type: "gin",
Columns: []string{"details"},
}
table.Indexes[index.Name] = index
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, `USING gin ("details" jsonb_ops)`) {
t.Fatalf("expected GIN jsonb index to include jsonb_ops, got:\n%s", output)
}
if strings.Contains(output, "gin_trgm_ops") {
t.Fatalf("did not expect gin_trgm_ops for jsonb column, got:\n%s", output)
}
}
func TestWriteForeignKeys(t *testing.T) {
// Create a test database with two related tables
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create parent table (users)
usersTable := models.InitTable("users", "public")
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
usersTable.Columns["id"] = idCol
// Create child table (posts)
postsTable := models.InitTable("posts", "public")
postIdCol := models.InitColumn("id", "posts", "public")
postIdCol.Type = "integer"
postsTable.Columns["id"] = postIdCol
userIdCol := models.InitColumn("user_id", "posts", "public")
userIdCol.Type = "integer"
postsTable.Columns["user_id"] = userIdCol
// Add foreign key constraint
fkConstraint := &models.Constraint{
Name: "fk_posts_users",
Type: models.ForeignKeyConstraint,
Columns: []string{"user_id"},
ReferencedTable: "users",
ReferencedSchema: "public",
ReferencedColumns: []string{"id"},
OnDelete: "CASCADE",
OnUpdate: "CASCADE",
}
postsTable.Constraints["fk_posts_users"] = fkConstraint
// Add relationship
relationship := &models.Relationship{
Name: "fk_posts_users",
FromTable: "posts",
FromSchema: "public",
ToTable: "users",
ToSchema: "public",
ForeignKey: "fk_posts_users",
}
postsTable.Relationships["fk_posts_users"] = relationship
schema.Tables = append(schema.Tables, usersTable, postsTable)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the database
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Print output for debugging
t.Logf("Generated SQL:\n%s", output)
// Verify foreign key is present
if !strings.Contains(output, "FOREIGN KEY") {
t.Errorf("Output missing FOREIGN KEY statement\nFull output:\n%s", output)
}
if !strings.Contains(output, "ON DELETE CASCADE") {
t.Errorf("Output missing ON DELETE CASCADE\nFull output:\n%s", output)
}
if !strings.Contains(output, "ON UPDATE CASCADE") {
t.Errorf("Output missing ON UPDATE CASCADE\nFull output:\n%s", output)
}
}
func TestWriteUniqueConstraints(t *testing.T) {
// Create a test database with unique constraints
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create table with unique constraints
table := models.InitTable("users", "public")
// Add columns
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "varchar(255)"
emailCol.NotNull = true
table.Columns["email"] = emailCol
guidCol := models.InitColumn("guid", "users", "public")
guidCol.Type = "uuid"
guidCol.NotNull = true
table.Columns["guid"] = guidCol
// Add unique constraints
emailConstraint := &models.Constraint{
Name: "uq_email",
Type: models.UniqueConstraint,
Schema: "public",
Table: "users",
Columns: []string{"email"},
}
table.Constraints["uq_email"] = emailConstraint
guidConstraint := &models.Constraint{
Name: "uq_guid",
Type: models.UniqueConstraint,
Schema: "public",
Table: "users",
Columns: []string{"guid"},
}
table.Constraints["uq_guid"] = guidConstraint
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the database
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Print output for debugging
t.Logf("Generated SQL:\n%s", output)
// Verify unique constraints are present
if !strings.Contains(output, "-- Unique constraints for schema: public") {
t.Errorf("Output missing unique constraints header")
}
if !strings.Contains(output, "ADD CONSTRAINT \"uq_email\" UNIQUE (\"email\")") {
t.Errorf("Output missing uq_email unique constraint\nFull output:\n%s", output)
}
if !strings.Contains(output, "ADD CONSTRAINT \"uq_guid\" UNIQUE (\"guid\")") {
t.Errorf("Output missing uq_guid unique constraint\nFull output:\n%s", output)
}
}
func TestWriteCheckConstraints(t *testing.T) {
// Create a test database with check constraints
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create table with check constraints
table := models.InitTable("products", "public")
// Add columns
priceCol := models.InitColumn("price", "products", "public")
priceCol.Type = "numeric(10,2)"
table.Columns["price"] = priceCol
statusCol := models.InitColumn("status", "products", "public")
statusCol.Type = "varchar(20)"
table.Columns["status"] = statusCol
quantityCol := models.InitColumn("quantity", "products", "public")
quantityCol.Type = "integer"
table.Columns["quantity"] = quantityCol
// Add check constraints
priceConstraint := &models.Constraint{
Name: "ck_price_positive",
Type: models.CheckConstraint,
Schema: "public",
Table: "products",
Expression: "price >= 0",
}
table.Constraints["ck_price_positive"] = priceConstraint
statusConstraint := &models.Constraint{
Name: "ck_status_valid",
Type: models.CheckConstraint,
Schema: "public",
Table: "products",
Expression: "status IN ('active', 'inactive', 'discontinued')",
}
table.Constraints["ck_status_valid"] = statusConstraint
quantityConstraint := &models.Constraint{
Name: "ck_quantity_nonnegative",
Type: models.CheckConstraint,
Schema: "public",
Table: "products",
Expression: "quantity >= 0",
}
table.Constraints["ck_quantity_nonnegative"] = quantityConstraint
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the database
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Print output for debugging
t.Logf("Generated SQL:\n%s", output)
// Verify check constraints are present
if !strings.Contains(output, "-- Check constraints for schema: public") {
t.Errorf("Output missing check constraints header")
}
if !strings.Contains(output, "ADD CONSTRAINT \"ck_price_positive\" CHECK (price >= 0)") {
t.Errorf("Output missing ck_price_positive check constraint\nFull output:\n%s", output)
}
if !strings.Contains(output, "ADD CONSTRAINT \"ck_status_valid\" CHECK (status IN ('active', 'inactive', 'discontinued'))") {
t.Errorf("Output missing ck_status_valid check constraint\nFull output:\n%s", output)
}
if !strings.Contains(output, "ADD CONSTRAINT \"ck_quantity_nonnegative\" CHECK (quantity >= 0)") {
t.Errorf("Output missing ck_quantity_nonnegative check constraint\nFull output:\n%s", output)
}
}
func TestWriteAllConstraintTypes(t *testing.T) {
// Create a comprehensive test with all constraint types
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create orders table
ordersTable := models.InitTable("orders", "public")
// Add columns
idCol := models.InitColumn("id", "orders", "public")
idCol.Type = "integer"
idCol.IsPrimaryKey = true
ordersTable.Columns["id"] = idCol
userIDCol := models.InitColumn("user_id", "orders", "public")
userIDCol.Type = "integer"
userIDCol.NotNull = true
ordersTable.Columns["user_id"] = userIDCol
orderNumberCol := models.InitColumn("order_number", "orders", "public")
orderNumberCol.Type = "varchar(50)"
orderNumberCol.NotNull = true
ordersTable.Columns["order_number"] = orderNumberCol
totalCol := models.InitColumn("total", "orders", "public")
totalCol.Type = "numeric(10,2)"
ordersTable.Columns["total"] = totalCol
statusCol := models.InitColumn("status", "orders", "public")
statusCol.Type = "varchar(20)"
ordersTable.Columns["status"] = statusCol
// Add primary key constraint
pkConstraint := &models.Constraint{
Name: "pk_orders",
Type: models.PrimaryKeyConstraint,
Schema: "public",
Table: "orders",
Columns: []string{"id"},
}
ordersTable.Constraints["pk_orders"] = pkConstraint
// Add unique constraint
uniqueConstraint := &models.Constraint{
Name: "uq_order_number",
Type: models.UniqueConstraint,
Schema: "public",
Table: "orders",
Columns: []string{"order_number"},
}
ordersTable.Constraints["uq_order_number"] = uniqueConstraint
// Add check constraint
checkConstraint := &models.Constraint{
Name: "ck_total_positive",
Type: models.CheckConstraint,
Schema: "public",
Table: "orders",
Expression: "total > 0",
}
ordersTable.Constraints["ck_total_positive"] = checkConstraint
statusCheckConstraint := &models.Constraint{
Name: "ck_status_valid",
Type: models.CheckConstraint,
Schema: "public",
Table: "orders",
Expression: "status IN ('pending', 'completed', 'cancelled')",
}
ordersTable.Constraints["ck_status_valid"] = statusCheckConstraint
// Add foreign key constraint (referencing a users table)
fkConstraint := &models.Constraint{
Name: "fk_orders_user",
Type: models.ForeignKeyConstraint,
Schema: "public",
Table: "orders",
Columns: []string{"user_id"},
ReferencedSchema: "public",
ReferencedTable: "users",
ReferencedColumns: []string{"id"},
OnDelete: "CASCADE",
OnUpdate: "CASCADE",
}
ordersTable.Constraints["fk_orders_user"] = fkConstraint
schema.Tables = append(schema.Tables, ordersTable)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the database
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Print output for debugging
t.Logf("Generated SQL:\n%s", output)
// Verify all constraint types are present
expectedConstraints := map[string]string{
"Primary Key": "PRIMARY KEY",
"Unique": "ADD CONSTRAINT \"uq_order_number\" UNIQUE (\"order_number\")",
"Check (total)": "ADD CONSTRAINT \"ck_total_positive\" CHECK (total > 0)",
"Check (status)": "ADD CONSTRAINT \"ck_status_valid\" CHECK (status IN ('pending', 'completed', 'cancelled'))",
"Foreign Key": "FOREIGN KEY",
}
for name, expected := range expectedConstraints {
if !strings.Contains(output, expected) {
t.Errorf("Output missing %s constraint: %s\nFull output:\n%s", name, expected, output)
}
}
// Verify section headers
sections := []string{
"-- Primary keys for schema: public",
"-- Unique constraints for schema: public",
"-- Check constraints for schema: public",
"-- Foreign keys for schema: public",
}
for _, section := range sections {
if !strings.Contains(output, section) {
t.Errorf("Output missing section header: %s", section)
}
}
}
func TestWriteTable(t *testing.T) {
// Create a single table
table := models.InitTable("products", "public")
idCol := models.InitColumn("id", "products", "public")
idCol.Type = "integer"
table.Columns["id"] = idCol
nameCol := models.InitColumn("name", "products", "public")
nameCol.Type = "text"
table.Columns["name"] = nameCol
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the table
err := writer.WriteTable(table)
if err != nil {
t.Fatalf("WriteTable failed: %v", err)
}
output := buf.String()
// Verify output contains table creation
if !strings.Contains(output, "CREATE TABLE") {
t.Error("Output missing CREATE TABLE statement")
}
if !strings.Contains(output, "products") {
t.Error("Output missing table name 'products'")
}
}
func TestEscapeQuote(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"\"simple\" text", "\"simple\" text"},
{"text with 'quote'", "text with ''quote''"},
{"multiple 'quotes' here", "multiple ''quotes'' here"},
{"", ""},
}
for _, tt := range tests {
result := escapeQuote(tt.input)
if result != tt.expected {
t.Errorf("escapeQuote(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestIsIntegerType(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"integer", true},
{"INTEGER", true},
{"bigint", true},
{"smallint", true},
{"serial", true},
{"bigserial", true},
{"text", false},
{"varchar", false},
{"uuid", false},
}
for _, tt := range tests {
result := isIntegerType(tt.input)
if result != tt.expected {
t.Errorf("isIntegerType(%q) = %v, want %v", tt.input, result, tt.expected)
}
}
}
func TestTypeConversion(t *testing.T) {
// Test that invalid Go types are converted to valid PostgreSQL types
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create a test table with Go types instead of SQL types
table := models.InitTable("test_types", "public")
// Add columns with Go types (invalid for PostgreSQL)
stringCol := models.InitColumn("name", "test_types", "public")
stringCol.Type = "string" // Should be converted to "text"
table.Columns["name"] = stringCol
int64Col := models.InitColumn("big_id", "test_types", "public")
int64Col.Type = "int64" // Should be converted to "bigint"
table.Columns["big_id"] = int64Col
int16Col := models.InitColumn("small_id", "test_types", "public")
int16Col.Type = "int16" // Should be converted to "smallint"
table.Columns["small_id"] = int16Col
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write the database
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Print output for debugging
t.Logf("Generated SQL:\n%s", output)
// Verify that Go types were converted to PostgreSQL types
if strings.Contains(output, "string") {
t.Errorf("Output contains 'string' type - should be converted to 'text'\nFull output:\n%s", output)
}
if strings.Contains(output, "int64") {
t.Errorf("Output contains 'int64' type - should be converted to 'bigint'\nFull output:\n%s", output)
}
if strings.Contains(output, "int16") {
t.Errorf("Output contains 'int16' type - should be converted to 'smallint'\nFull output:\n%s", output)
}
// Verify correct PostgreSQL types are present
if !strings.Contains(output, "text") {
t.Errorf("Output missing 'text' type (converted from 'string')\nFull output:\n%s", output)
}
if !strings.Contains(output, "bigint") {
t.Errorf("Output missing 'bigint' type (converted from 'int64')\nFull output:\n%s", output)
}
if !strings.Contains(output, "smallint") {
t.Errorf("Output missing 'smallint' type (converted from 'int16')\nFull output:\n%s", output)
}
}
func TestPrimaryKeyExistenceCheck(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("products", "public")
idCol := models.InitColumn("id", "products", "public")
idCol.Type = "integer"
idCol.IsPrimaryKey = true
table.Columns["id"] = idCol
nameCol := models.InitColumn("name", "products", "public")
nameCol.Type = "text"
table.Columns["name"] = nameCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
t.Logf("Generated SQL:\n%s", output)
// Verify our naming convention is used
if !strings.Contains(output, "pk_public_products") {
t.Errorf("Output missing expected primary key name 'pk_public_products'\nFull output:\n%s", output)
}
// Verify it drops auto-generated primary keys
if !strings.Contains(output, "products_pkey") || !strings.Contains(output, "DROP CONSTRAINT") {
t.Errorf("Output missing logic to drop auto-generated primary key\nFull output:\n%s", output)
}
// Verify it compares the current primary key columns before dropping/recreating
if !strings.Contains(output, "current_pk_matches") || !strings.Contains(output, "ARRAY['id']") {
t.Errorf("Output missing safe primary key comparison logic\nFull output:\n%s", output)
}
// Verify it only adds the desired key when no PK exists or an auto-generated mismatch was dropped
if !strings.Contains(output, "current_pk_name IS NULL") || !strings.Contains(output, "current_pk_name IN ('products_pkey', 'public_products_pkey')") {
t.Errorf("Output missing guarded primary key creation logic\nFull output:\n%s", output)
}
}
func TestColumnSizeSpecifiers(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("test_sizes", "public")
// Integer with invalid size specifier - should ignore size
integerCol := models.InitColumn("int_col", "test_sizes", "public")
integerCol.Type = "integer"
integerCol.Length = 32
table.Columns["int_col"] = integerCol
// Bigint with invalid size specifier - should ignore size
bigintCol := models.InitColumn("bigint_col", "test_sizes", "public")
bigintCol.Type = "bigint"
bigintCol.Length = 64
table.Columns["bigint_col"] = bigintCol
// Smallint with invalid size specifier - should ignore size
smallintCol := models.InitColumn("smallint_col", "test_sizes", "public")
smallintCol.Type = "smallint"
smallintCol.Length = 16
table.Columns["smallint_col"] = smallintCol
// Text with length - should convert to varchar
textCol := models.InitColumn("text_col", "test_sizes", "public")
textCol.Type = "text"
textCol.Length = 100
table.Columns["text_col"] = textCol
// Varchar with length - should keep varchar with length
varcharCol := models.InitColumn("varchar_col", "test_sizes", "public")
varcharCol.Type = "varchar"
varcharCol.Length = 50
table.Columns["varchar_col"] = varcharCol
// Decimal with precision and scale - should keep them
decimalCol := models.InitColumn("decimal_col", "test_sizes", "public")
decimalCol.Type = "decimal"
decimalCol.Precision = 19
decimalCol.Scale = 4
table.Columns["decimal_col"] = decimalCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
t.Logf("Generated SQL:\n%s", output)
// Verify invalid size specifiers are NOT present
invalidPatterns := []string{
"integer(32)",
"bigint(64)",
"smallint(16)",
"text(100)",
}
for _, pattern := range invalidPatterns {
if strings.Contains(output, pattern) {
t.Errorf("Output contains invalid pattern '%s' - PostgreSQL doesn't support this\nFull output:\n%s", pattern, output)
}
}
// Verify valid patterns ARE present
validPatterns := []string{
"integer", // without size
"bigint", // without size
"smallint", // without size
"varchar(100)", // text converted to varchar with length
"varchar(50)", // varchar with length
"decimal(19,4)", // decimal with precision and scale
}
for _, pattern := range validPatterns {
if !strings.Contains(output, pattern) {
t.Errorf("Output missing expected pattern '%s'\nFull output:\n%s", pattern, output)
}
}
}
func TestWriteDatabase_PrimaryKeyTemplateDoesNotDropMatchingAutoPrimaryKey(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
idCol := models.InitColumn("id", "learnings", "public")
idCol.Type = "bigint"
idCol.IsPrimaryKey = true
table.Columns["id"] = idCol
parentCol := models.InitColumn("duplicate_of_learning_id", "learnings", "public")
parentCol.Type = "bigint"
table.Columns["duplicate_of_learning_id"] = parentCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "current_pk_matches") {
t.Fatalf("expected generated SQL to compare current PK columns, got:\n%s", output)
}
if !strings.Contains(output, "ARRAY['id']") {
t.Fatalf("expected generated SQL to compare against desired PK columns, got:\n%s", output)
}
if !strings.Contains(output, "NOT current_pk_matches") {
t.Fatalf("expected generated SQL to avoid dropping matching PKs, got:\n%s", output)
}
}
func TestGenerateColumnDefinition_PreservesExplicitTypeModifiers(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{})
cases := []struct {
name string
colType string
length int
precision int
scale int
wantType string
}{
{
name: "character varying already includes length",
colType: "character varying(50)",
length: 50,
wantType: "character varying(50)",
},
{
name: "numeric already includes precision",
colType: "numeric(10,2)",
precision: 10,
scale: 2,
wantType: "numeric(10,2)",
},
{
name: "custom vector modifier preserved",
colType: "vector(1536)",
wantType: "vector(1536)",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
col := models.InitColumn("sample", "events", "public")
col.Type = tc.colType
col.Length = tc.length
col.Precision = tc.precision
col.Scale = tc.scale
def := writer.generateColumnDefinition(col)
if !strings.Contains(def, " "+tc.wantType+" ") && !strings.HasSuffix(def, " "+tc.wantType) {
t.Fatalf("generated definition %q does not contain expected type %q", def, tc.wantType)
}
if strings.Contains(def, ")(") {
t.Fatalf("generated definition %q appears to duplicate modifiers", def)
}
})
}
}
func TestGenerateAddColumnStatements(t *testing.T) {
// Create a test database with tables that have new columns
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create a table with columns
table := models.InitTable("users", "public")
// Existing column
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
idCol.NotNull = true
idCol.Sequence = 1
table.Columns["id"] = idCol
// New column to be added
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "varchar"
emailCol.Length = 255
emailCol.NotNull = true
emailCol.Sequence = 2
table.Columns["email"] = emailCol
// New column with default
statusCol := models.InitColumn("status", "users", "public")
statusCol.Type = "text"
statusCol.Default = "active"
statusCol.Sequence = 3
table.Columns["status"] = statusCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
// Create writer
options := &writers.WriterOptions{}
writer := NewWriter(options)
// Generate ADD COLUMN statements
statements, err := writer.GenerateAddColumnsForDatabase(db)
if err != nil {
t.Fatalf("GenerateAddColumnsForDatabase failed: %v", err)
}
// Join all statements to verify content
output := strings.Join(statements, "\n")
t.Logf("Generated ADD COLUMN statements:\n%s", output)
// Verify expected elements
expectedStrings := []string{
"ALTER TABLE \"public\".\"users\" ADD COLUMN IF NOT EXISTS \"id\" integer NOT NULL",
"ALTER TABLE \"public\".\"users\" ADD COLUMN IF NOT EXISTS \"email\" varchar(255) NOT NULL",
"ALTER TABLE \"public\".\"users\" ADD COLUMN IF NOT EXISTS \"status\" text DEFAULT 'active'",
}
for _, expected := range expectedStrings {
if !strings.Contains(output, expected) {
t.Errorf("Output missing expected string: %s\nFull output:\n%s", expected, output)
}
}
// Verify IF NOT EXISTS logic
ifNotExistsCount := strings.Count(output, "IF NOT EXISTS")
if ifNotExistsCount < 3 {
t.Errorf("Expected at least 3 IF NOT EXISTS checks (one per column), got %d", ifNotExistsCount)
}
}
func TestWriteAddColumnStatements(t *testing.T) {
// Create a test database
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
// Create a table with a new column to be added
table := models.InitTable("products", "public")
idCol := models.InitColumn("id", "products", "public")
idCol.Type = "integer"
table.Columns["id"] = idCol
// New column with various properties
descCol := models.InitColumn("description", "products", "public")
descCol.Type = "text"
descCol.NotNull = false
table.Columns["description"] = descCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
// Create writer with output to buffer
var buf bytes.Buffer
options := &writers.WriterOptions{}
writer := NewWriter(options)
writer.writer = &buf
// Write ADD COLUMN statements
err := writer.WriteAddColumnStatements(db)
if err != nil {
t.Fatalf("WriteAddColumnStatements failed: %v", err)
}
output := buf.String()
t.Logf("Generated output:\n%s", output)
// Verify output contains expected elements
if !strings.Contains(output, "ALTER TABLE \"public\".\"products\" ADD COLUMN IF NOT EXISTS \"id\" integer") {
t.Errorf("Output missing ADD COLUMN for id\nFull output:\n%s", output)
}
if !strings.Contains(output, "ALTER TABLE \"public\".\"products\" ADD COLUMN IF NOT EXISTS \"description\" text") {
t.Errorf("Output missing ADD COLUMN for description\nFull output:\n%s", output)
}
if strings.Contains(output, "information_schema.columns") {
t.Errorf("column additions should use native IF NOT EXISTS\nFull output:\n%s", output)
}
}
func TestWriteSchema_EmitsGuardedAlterColumnTypeStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_skills", "public")
nameCol := models.InitColumn("name", "agent_skills", "public")
nameCol.Type = "character varying"
nameCol.Length = 255
table.Columns["name"] = nameCol
tagsCol := models.InitColumn("tags", "agent_skills", "public")
tagsCol.Type = "text[]"
table.Columns["tags"] = tagsCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "-- Alter column types for schema: public") {
t.Fatalf("expected alter column type section, got:\n%s", output)
}
if !strings.Contains(output, "pg_catalog.format_type") {
t.Fatalf("expected guarded live-type check, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN \"name\" TYPE character varying(255)") {
t.Fatalf("expected guarded alter for character varying(255), got:\n%s", output)
}
if !strings.Contains(output, "ARRAY['varchar(255)', 'character varying(255)']") {
t.Fatalf("expected equivalent type spellings for varchar guard, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN \"tags\" TYPE text[]") {
t.Fatalf("expected guarded alter for array type, got:\n%s", output)
}
if !strings.Contains(output, `ALTER COLUMN "tags" TYPE text[] USING "tags"::text[];`) {
t.Fatalf("expected guarded alter for array type to include USING cast, got:\n%s", output)
}
}
func TestWriteSchema_EmitsGuardedAlterColumnDefaultStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_skills", "public")
statusCol := models.InitColumn("status", "agent_skills", "public")
statusCol.Type = "text"
statusCol.Default = "active"
table.Columns["status"] = statusCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "-- Alter column defaults for schema: public") {
t.Fatalf("expected alter column default section, got:\n%s", output)
}
if !strings.Contains(output, "pg_get_expr(d.adbin, d.adrelid)") {
t.Fatalf("expected guarded live-default check, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN \"status\" SET DEFAULT 'active'") {
t.Fatalf("expected guarded SET DEFAULT for status column, got:\n%s", output)
}
}
func TestWriteSchema_AlterColumnDefaultStripsBackticksFromFunctionExpression(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_skills", "public")
updatedAtCol := models.InitColumn("updatedat", "agent_skills", "public")
updatedAtCol.Type = "timestamp"
updatedAtCol.Default = "`now()`"
table.Columns["updatedat"] = updatedAtCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if strings.Contains(output, "`") {
t.Fatalf("expected no backticks in generated SQL, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN \"updatedat\" SET DEFAULT now()") {
t.Fatalf("expected guarded SET DEFAULT now() without backticks, got:\n%s", output)
}
}
func TestWriteSchema_GuardedAlterColumnTypePreservesConversionFailure(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_skills", "public")
nameCol := models.InitColumn("name", "agent_skills", "public")
nameCol.Type = "integer"
table.Columns["name"] = nameCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "ALTER COLUMN \"name\" TYPE integer USING \"name\"::integer;") {
t.Fatalf("expected explicit type conversion, got:\n%s", output)
}
for _, unsafe := range []string{"EXCEPTION WHEN OTHERS", "renamed_column", "RENAME COLUMN"} {
if strings.Contains(output, unsafe) {
t.Fatalf("type conversion must propagate errors without replacing the column (%s):\n%s", unsafe, output)
}
}
}
func TestWriteSchema_EmitsGuardedAlterColumnNullabilityStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("origin")
table := models.InitTable("service_instance", "origin")
typeCol := models.InitColumn("rid_service_instance_type", "service_instance", "origin")
typeCol.Type = "text"
typeCol.NotNull = false
table.Columns["rid_service_instance_type"] = typeCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "-- Alter column nullability for schema: origin") {
t.Fatalf("expected alter column nullability section, got:\n%s", output)
}
if !strings.Contains(output, "a.attnotnull") {
t.Fatalf("expected guarded live-nullability check, got:\n%s", output)
}
if !strings.Contains(output, "current_not_null IS DISTINCT FROM false") {
t.Fatalf("expected guard comparing live nullability against desired value, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN \"rid_service_instance_type\" DROP NOT NULL") {
t.Fatalf("expected guarded DROP NOT NULL for nullable column, got:\n%s", output)
}
}
func TestWriteSchema_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
idCol := models.InitColumn("id", "learnings", "public")
idCol.Type = "bigserial"
table.Columns["id"] = idCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "ALTER COLUMN \"id\" TYPE bigint") {
t.Fatalf("expected serial alter to use bigint storage type, got:\n%s", output)
}
if strings.Contains(output, "ALTER COLUMN \"id\" TYPE bigserial;") {
t.Fatalf("did not expect invalid bigserial alter statement, got:\n%s", output)
}
if !strings.Contains(output, `ALTER COLUMN "id" TYPE bigint USING "id"::bigint;`) {
t.Fatalf("expected serial alter to include USING cast, got:\n%s", output)
}
}
// buildVectorSpatialSchema returns a database with a pgvector column and a PostGIS column.
func buildVectorSpatialSchema(indexType, indexComment string) *models.Database {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("documents", "public")
embedding := models.InitColumn("embedding", "documents", "public")
embedding.Type = "vector(1536)"
table.Columns["embedding"] = embedding
location := models.InitColumn("location", "documents", "public")
location.Type = "geometry(Point,4326)"
table.Columns["location"] = location
if indexType != "" {
index := &models.Index{
Name: "idx_documents_embedding",
Type: indexType,
Columns: []string{"embedding"},
Comment: indexComment,
}
table.Indexes[index.Name] = index
}
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
return db
}
func writeDatabaseOutput(t *testing.T, db *models.Database) string {
t.Helper()
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
return buf.String()
}
func TestWriteDatabase_VectorAndPostGISColumnsCreateExtensions(t *testing.T) {
output := writeDatabaseOutput(t, buildVectorSpatialSchema("", ""))
for _, want := range []string{
"CREATE EXTENSION IF NOT EXISTS postgis;",
"CREATE EXTENSION IF NOT EXISTS vector;",
"vector(1536)",
"geometry(Point,4326)",
} {
if !strings.Contains(output, want) {
t.Fatalf("expected output to contain %q, got:\n%s", want, output)
}
}
// postgis must be created before postgis-dependent extensions and stay deterministic
if strings.Index(output, "EXISTS postgis;") > strings.Index(output, "EXISTS vector;") {
t.Fatalf("expected extensions to be emitted in sorted order, got:\n%s", output)
}
}
func TestWriteDatabase_HNSWIndexUsesDefaultVectorOperatorClass(t *testing.T) {
output := writeDatabaseOutput(t, buildVectorSpatialSchema("hnsw", ""))
if !strings.Contains(output, "USING hnsw (\"embedding\" vector_cosine_ops)") {
t.Fatalf("expected hnsw index with default vector operator class, got:\n%s", output)
}
if !strings.Contains(output, "CREATE EXTENSION IF NOT EXISTS vector;") {
t.Fatalf("expected pgvector extension, got:\n%s", output)
}
}
func TestWriteDatabase_VectorIndexHonoursRequestedOperatorClassAndStorageParameters(t *testing.T) {
output := writeDatabaseOutput(t, buildVectorSpatialSchema("ivfflat", "opclass=vector_l2_ops; with (lists=100)"))
if !strings.Contains(output, "USING ivfflat (\"embedding\" vector_l2_ops) WITH (lists = 100)") {
t.Fatalf("expected ivfflat index with requested opclass and storage parameters, got:\n%s", output)
}
}
func TestWriteDatabase_VectorIndexIgnoresIncompatibleOperatorClass(t *testing.T) {
output := writeDatabaseOutput(t, buildVectorSpatialSchema("hnsw", "opclass=halfvec_l2_ops"))
if !strings.Contains(output, "USING hnsw (\"embedding\" vector_cosine_ops)") {
t.Fatalf("expected halfvec operator class to be rejected for a vector column, got:\n%s", output)
}
}
func TestWriteDatabase_VectorIndexIgnoresCommentProseInStorageParameters(t *testing.T) {
output := writeDatabaseOutput(t, buildVectorSpatialSchema("hnsw", "tuned with (m=16, ef_construction=64, drop table foo)"))
if !strings.Contains(output, "WITH (m = 16, ef_construction = 64)") {
t.Fatalf("expected only well-formed storage parameters, got:\n%s", output)
}
if strings.Contains(output, "drop table") {
t.Fatalf("expected prose to be dropped from storage parameters, got:\n%s", output)
}
}
func TestWriteDatabase_GistIndexOnGeometryUsesDefaultOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("places", "public")
geom := models.InitColumn("geom", "places", "public")
geom.Type = "geometry(Point,4326)"
table.Columns["geom"] = geom
table.Indexes["idx_places_geom"] = &models.Index{
Name: "idx_places_geom",
Type: "gist",
Columns: []string{"geom"},
}
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
output := writeDatabaseOutput(t, db)
if !strings.Contains(output, "USING gist (\"geom\")") {
t.Fatalf("expected gist index to rely on the PostGIS default operator class, got:\n%s", output)
}
}
func TestWriteDatabase_GistIndexHonoursRequestedSpatialOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("places", "public")
geom := models.InitColumn("geom", "places", "public")
geom.Type = "geometry(PointZ,4326)"
table.Columns["geom"] = geom
table.Indexes["idx_places_geom_nd"] = &models.Index{
Name: "idx_places_geom_nd",
Type: "gist",
Columns: []string{"geom"},
Comment: "opclass=gist_geometry_ops_nd",
}
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
output := writeDatabaseOutput(t, db)
if !strings.Contains(output, "USING gist (\"geom\" gist_geometry_ops_nd)") {
t.Fatalf("expected requested spatial operator class, got:\n%s", output)
}
}
func TestGenerateDatabaseStatements_VectorIndexIncludesOperatorClassAndParameters(t *testing.T) {
db := buildVectorSpatialSchema("hnsw", "opclass=vector_ip_ops; with (m=16)")
writer := NewWriter(&writers.WriterOptions{})
statements, err := writer.GenerateDatabaseStatements(db)
if err != nil {
t.Fatalf("GenerateDatabaseStatements failed: %v", err)
}
joined := strings.Join(statements, "\n")
for _, want := range []string{
"CREATE EXTENSION IF NOT EXISTS vector",
"CREATE EXTENSION IF NOT EXISTS postgis",
"USING hnsw (\"embedding\" vector_ip_ops) WITH (m = 16)",
} {
if !strings.Contains(joined, want) {
t.Fatalf("expected statements to contain %q, got:\n%s", want, joined)
}
}
}
func TestIndexStorageParameters(t *testing.T) {
tests := []struct {
name string
comment string
want string
}{
{"empty", "", ""},
{"no with clause", "opclass=vector_cosine_ops", ""},
{"single parameter", "with (lists=100)", "lists = 100"},
{"multiple parameters", "WITH (m = 16, ef_construction = 64)", "m = 16, ef_construction = 64"},
{"quoted value kept", "with (fillfactor='90')", "fillfactor = '90'"},
{"bm25 key field", "with (key_field='id')", "key_field = 'id'"},
{"dollar quoted value", "with (options = $$[build.internal]\nlists = [4096]$$)", "options = $$[build.internal]\nlists = [4096]$$"},
{"dollar quoted value with parens", "with (options = $$f(x)$$, m = 16)", "options = $$f(x)$$, m = 16"},
{"prose dropped", "with (lists=100, please drop everything)", "lists = 100"},
{"unterminated quote dropped", "with (key_field='id)", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := indexStorageParameters(tt.comment); got != tt.want {
t.Errorf("indexStorageParameters(%q) = %q, want %q", tt.comment, got, tt.want)
}
})
}
}
func TestGenerateColumnDefinition_GeneratedColumnEmitsGeneratedClauseNotDefault(t *testing.T) {
w := &Writer{}
col := models.InitColumn("geom", "info_city", "public")
col.Type = "text"
col.NotNull = true
col.Generated = true
col.GenerationExpression = "st_makepoint(lon, lat)"
// A generated column's introspected "default" is actually its generation
// expression surfaced via pg_attrdef; it must never be rendered as DEFAULT.
col.Default = "st_makepoint(lon, lat)"
got := w.generateColumnDefinition(col)
want := "\"geom\" text GENERATED ALWAYS AS (st_makepoint(lon, lat)) STORED NOT NULL"
if got != want {
t.Fatalf("generateColumnDefinition() = %q, want %q", got, want)
}
}
func TestGenerateColumnDefinition_IdentityColumnEmitsIdentityClauseNotDefault(t *testing.T) {
w := &Writer{}
col := models.InitColumn("id", "users", "public")
col.Type = "bigint"
col.NotNull = true
col.Identity = true
col.IdentityGeneration = "ALWAYS"
// Identity columns never carry a literal default; this must never surface.
col.Default = "some-stray-value"
got := w.generateColumnDefinition(col)
want := "\"id\" bigint GENERATED ALWAYS AS IDENTITY NOT NULL"
if got != want {
t.Fatalf("generateColumnDefinition() = %q, want %q", got, want)
}
}
// Two FKs to the same table: each relationship must emit its own FK columns,
// not whichever FK map iteration returns first.
func TestWriteDatabase_MultipleForeignKeysToSameTable(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
dept := models.InitTable("department", "public")
id := models.InitColumn("id_department", "department", "public")
id.Type = "bigint"
id.IsPrimaryKey = true
dept.Columns["id_department"] = id
emp := models.InitTable("employee", "public")
for _, name := range []string{"rid_department", "rid_manager"} {
col := models.InitColumn(name, "employee", "public")
col.Type = "bigint"
emp.Columns[name] = col
fkName := "fk_employee_" + name
fk := models.InitConstraint(fkName, models.ForeignKeyConstraint)
fk.Schema, fk.Table, fk.Columns = "public", "employee", []string{name}
fk.ReferencedSchema, fk.ReferencedTable, fk.ReferencedColumns = "public", "department", []string{"id_department"}
emp.Constraints[fkName] = fk
rel := models.InitRelationship("employee_to_department_"+name, models.OneToMany)
rel.FromTable, rel.ToTable, rel.ToSchema, rel.ForeignKey = "employee", "department", "public", fkName
emp.Relationships[rel.Name] = rel
}
schema.Tables = append(schema.Tables, dept, emp)
db.Schemas = append(db.Schemas, schema)
for i := 0; i < 20; i++ {
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
for _, name := range []string{"rid_department", "rid_manager"} {
want := "ADD CONSTRAINT " + quoteIdent("fk_employee_"+name) + "\n FOREIGN KEY (" + quoteIdent(name) + ")"
if !strings.Contains(output, want) {
t.Fatalf("run %d: missing %q in output:\n%s", i, want, output)
}
}
}
}