From a54594e49b3995cdf515373c218b8ffe6ece11d3 Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 31 Jan 2026 20:33:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(writer):=20=F0=9F=8E=89=20Add=20support=20?= =?UTF-8?q?for=20unique=20constraints=20in=20schema=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement unique constraint handling in GenerateSchemaStatements * Add writeUniqueConstraints method for generating SQL statements * Create unit test for unique constraints in writer_test.go --- pkg/writers/pgsql/writer.go | 79 ++++++++++++++++++++++++++++++++ pkg/writers/pgsql/writer_test.go | 70 ++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/pkg/writers/pgsql/writer.go b/pkg/writers/pgsql/writer.go index 0ef7a8c..f527083 100644 --- a/pkg/writers/pgsql/writer.go +++ b/pkg/writers/pgsql/writer.go @@ -295,6 +295,31 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro } } + // Phase 5.5: Unique constraints + for _, table := range schema.Tables { + for _, constraint := range table.Constraints { + if constraint.Type != models.UniqueConstraint { + continue + } + + // Wrap in DO block to check for existing constraint + stmt := fmt.Sprintf("DO $$\nBEGIN\n"+ + " IF NOT EXISTS (\n"+ + " SELECT 1 FROM information_schema.table_constraints\n"+ + " WHERE table_schema = '%s'\n"+ + " AND table_name = '%s'\n"+ + " AND constraint_name = '%s'\n"+ + " ) THEN\n"+ + " ALTER TABLE %s.%s ADD CONSTRAINT %s UNIQUE (%s);\n"+ + " END IF;\n"+ + "END;\n$$", + schema.Name, table.Name, constraint.Name, + schema.SQLName(), table.SQLName(), constraint.Name, + strings.Join(constraint.Columns, ", ")) + statements = append(statements, stmt) + } + } + // Phase 6: Foreign keys for _, table := range schema.Tables { for _, constraint := range table.Constraints { @@ -542,6 +567,11 @@ func (w *Writer) WriteSchema(schema *models.Schema) error { return err } + // Phase 5.5: Create unique constraints (priority 185) + if err := w.writeUniqueConstraints(schema); err != nil { + return err + } + // Phase 6: Create foreign key constraints (priority 195) if err := w.writeForeignKeys(schema); err != nil { return err @@ -865,6 +895,55 @@ func (w *Writer) writeIndexes(schema *models.Schema) error { return nil } +// writeUniqueConstraints generates ALTER TABLE statements for unique constraints +func (w *Writer) writeUniqueConstraints(schema *models.Schema) error { + fmt.Fprintf(w.writer, "-- Unique constraints for schema: %s\n", schema.Name) + + for _, table := range schema.Tables { + // Sort constraints by name for consistent output + constraintNames := make([]string, 0, len(table.Constraints)) + for name, constraint := range table.Constraints { + if constraint.Type == models.UniqueConstraint { + constraintNames = append(constraintNames, name) + } + } + sort.Strings(constraintNames) + + for _, name := range constraintNames { + constraint := table.Constraints[name] + + // Build column list + columnExprs := make([]string, 0, len(constraint.Columns)) + for _, colName := range constraint.Columns { + if col, ok := table.Columns[colName]; ok { + columnExprs = append(columnExprs, col.SQLName()) + } + } + + if len(columnExprs) == 0 { + continue + } + + // Wrap in DO block to check for existing constraint + fmt.Fprintf(w.writer, "DO $$\n") + fmt.Fprintf(w.writer, "BEGIN\n") + fmt.Fprintf(w.writer, " IF NOT EXISTS (\n") + fmt.Fprintf(w.writer, " SELECT 1 FROM information_schema.table_constraints\n") + fmt.Fprintf(w.writer, " WHERE table_schema = '%s'\n", schema.Name) + fmt.Fprintf(w.writer, " AND table_name = '%s'\n", table.Name) + fmt.Fprintf(w.writer, " AND constraint_name = '%s'\n", constraint.Name) + fmt.Fprintf(w.writer, " ) THEN\n") + fmt.Fprintf(w.writer, " ALTER TABLE %s.%s ADD CONSTRAINT %s UNIQUE (%s);\n", + schema.SQLName(), table.SQLName(), constraint.Name, strings.Join(columnExprs, ", ")) + fmt.Fprintf(w.writer, " END IF;\n") + fmt.Fprintf(w.writer, "END;\n") + fmt.Fprintf(w.writer, "$$;\n\n") + } + } + + return nil +} + // writeForeignKeys generates ALTER TABLE statements for foreign keys func (w *Writer) writeForeignKeys(schema *models.Schema) error { fmt.Fprintf(w.writer, "-- Foreign keys for schema: %s\n", schema.Name) diff --git a/pkg/writers/pgsql/writer_test.go b/pkg/writers/pgsql/writer_test.go index 5a857e0..eb3f9b7 100644 --- a/pkg/writers/pgsql/writer_test.go +++ b/pkg/writers/pgsql/writer_test.go @@ -164,6 +164,76 @@ func TestWriteForeignKeys(t *testing.T) { } } +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 TestWriteTable(t *testing.T) { // Create a single table table := models.InitTable("products", "public")