From 608893a3d65f52e6dc9a20176336a18cb25a39af Mon Sep 17 00:00:00 2001 From: "Hein (Warky)" Date: Tue, 5 May 2026 11:32:15 +0200 Subject: [PATCH] feat(index): implement GIN index support for quoted text columns and enhance index column resolution --- pkg/writers/pgsql/migration_writer.go | 2 +- pkg/writers/pgsql/migration_writer_test.go | 40 +++++++++++++++++++ ...create_primary_key_with_autogen_check.tmpl | 2 +- pkg/writers/pgsql/writer.go | 31 +++++++++++++- pkg/writers/pgsql/writer_test.go | 34 ++++++++++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/pkg/writers/pgsql/migration_writer.go b/pkg/writers/pgsql/migration_writer.go index 7732f37..7ebfab6 100644 --- a/pkg/writers/pgsql/migration_writer.go +++ b/pkg/writers/pgsql/migration_writer.go @@ -604,7 +604,7 @@ func buildIndexColumnExpressions(table *models.Table, index *models.Index, index for _, colName := range index.Columns { colExpr := colName if table != nil { - if col, ok := table.Columns[colName]; ok && col != nil { + if col, ok := resolveIndexColumn(table, colName); ok && col != nil { colExpr = col.SQLName() if strings.EqualFold(indexType, "gin") && isTextType(col.Type) { opClass := extractOperatorClass(index.Comment) diff --git a/pkg/writers/pgsql/migration_writer_test.go b/pkg/writers/pgsql/migration_writer_test.go index 0aaa154..f63a6b2 100644 --- a/pkg/writers/pgsql/migration_writer_test.go +++ b/pkg/writers/pgsql/migration_writer_test.go @@ -137,6 +137,46 @@ func TestWriteMigration_GinIndexOnTextUsesTrigramOperatorClass(t *testing.T) { } } +func TestWriteMigration_GinIndexOnQuotedTextColumnUsesTrigramOperatorClass(t *testing.T) { + current := models.InitDatabase("testdb") + currentSchema := models.InitSchema("public") + current.Schemas = append(current.Schemas, currentSchema) + + model := models.InitDatabase("testdb") + modelSchema := 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 + + modelSchema.Tables = append(modelSchema.Tables, table) + model.Schemas = append(model.Schemas, modelSchema) + + var buf bytes.Buffer + writer, err := NewMigrationWriter(&writers.WriterOptions{}) + if err != nil { + t.Fatalf("Failed to create writer: %v", err) + } + writer.writer = &buf + + if err := writer.WriteMigration(model, current); err != nil { + t.Fatalf("WriteMigration failed: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "USING gin (name gin_trgm_ops)") { + t.Fatalf("expected quoted text column GIN index to include gin_trgm_ops, got:\n%s", output) + } +} + func TestWriteMigration_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *testing.T) { current := models.InitDatabase("testdb") currentSchema := models.InitSchema("public") diff --git a/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl b/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl index cb12d1e..da70960 100644 --- a/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl +++ b/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl @@ -6,7 +6,7 @@ BEGIN SELECT tc.constraint_name, COALESCE( ARRAY( - SELECT a.attname + SELECT a.attname::text FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace diff --git a/pkg/writers/pgsql/writer.go b/pkg/writers/pgsql/writer.go index c7aa92c..a4caac5 100644 --- a/pkg/writers/pgsql/writer.go +++ b/pkg/writers/pgsql/writer.go @@ -261,7 +261,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro columnExprs := make([]string, 0, len(index.Columns)) for _, colName := range index.Columns { colExpr := colName - if col, ok := table.Columns[colName]; ok { + if col, ok := resolveIndexColumn(table, colName); ok { // For GIN indexes on text columns, add operator class if strings.EqualFold(indexType, "gin") && isTextType(col.Type) { opClass := extractOperatorClass(index.Comment) @@ -855,7 +855,7 @@ func (w *Writer) writeIndexes(schema *models.Schema) error { // Build column list with operator class support for GIN indexes columnExprs := make([]string, 0, len(index.Columns)) for _, colName := range index.Columns { - if col, ok := table.Columns[colName]; ok { + if col, ok := resolveIndexColumn(table, colName); ok { colExpr := col.SQLName() // For GIN indexes on text columns, add operator class if strings.EqualFold(index.Type, "gin") && isTextType(col.Type) { @@ -1269,6 +1269,33 @@ func isTextTypeWithoutLength(colType string) bool { return strings.EqualFold(colType, "text") } +func resolveIndexColumn(table *models.Table, colName string) (*models.Column, bool) { + if table == nil { + return nil, false + } + if col, ok := table.Columns[colName]; ok && col != nil { + return col, true + } + + normalized := strings.ToLower(strings.Trim(colName, `"`)) + for key, col := range table.Columns { + if col == nil { + continue + } + if strings.ToLower(strings.Trim(key, `"`)) == normalized { + return col, true + } + if strings.ToLower(strings.Trim(col.Name, `"`)) == normalized { + return col, true + } + if strings.ToLower(strings.Trim(col.SQLName(), `"`)) == normalized { + return col, true + } + } + + return nil, false +} + // formatStringList formats a list of strings as a SQL-safe comma-separated quoted list func formatStringList(items []string) string { quoted := make([]string, len(items)) diff --git a/pkg/writers/pgsql/writer_test.go b/pkg/writers/pgsql/writer_test.go index 60b17a3..47eaa52 100644 --- a/pkg/writers/pgsql/writer_test.go +++ b/pkg/writers/pgsql/writer_test.go @@ -124,6 +124,40 @@ func TestWriteDatabase_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *test } } +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, `USING gin (name gin_trgm_ops)`) { + t.Fatalf("expected quoted text GIN index to include gin_trgm_ops, got:\n%s", output) + } +} + func TestWriteForeignKeys(t *testing.T) { // Create a test database with two related tables db := models.InitDatabase("testdb")