Compare commits

..

2 Commits

Author SHA1 Message Date
72200ea72e chore(release): update package version to 1.0.55
All checks were successful
Release / test (push) Successful in -32m1s
Release / release (push) Successful in -31m13s
Release / pkg-aur (push) Successful in -32m13s
Release / pkg-deb (push) Successful in -31m12s
Release / pkg-rpm (push) Successful in -29m45s
2026-05-05 11:36:29 +02:00
608893a3d6 feat(index): implement GIN index support for quoted text columns and enhance index column resolution 2026-05-05 11:32:15 +02:00
7 changed files with 107 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
# Maintainer: Hein (Warky Devs) <hein@warky.dev> # Maintainer: Hein (Warky Devs) <hein@warky.dev>
pkgname=relspec pkgname=relspec
pkgver=1.0.54 pkgver=1.0.55
pkgrel=1 pkgrel=1
pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs." pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs."
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')

View File

@@ -1,5 +1,5 @@
Name: relspec Name: relspec
Version: 1.0.54 Version: 1.0.55
Release: 1%{?dist} Release: 1%{?dist}
Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs. Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs.

View File

@@ -604,7 +604,7 @@ func buildIndexColumnExpressions(table *models.Table, index *models.Index, index
for _, colName := range index.Columns { for _, colName := range index.Columns {
colExpr := colName colExpr := colName
if table != nil { if table != nil {
if col, ok := table.Columns[colName]; ok && col != nil { if col, ok := resolveIndexColumn(table, colName); ok && col != nil {
colExpr = col.SQLName() colExpr = col.SQLName()
if strings.EqualFold(indexType, "gin") && isTextType(col.Type) { if strings.EqualFold(indexType, "gin") && isTextType(col.Type) {
opClass := extractOperatorClass(index.Comment) opClass := extractOperatorClass(index.Comment)

View File

@@ -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) { func TestWriteMigration_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb") current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public") currentSchema := models.InitSchema("public")

View File

@@ -6,7 +6,7 @@ BEGIN
SELECT tc.constraint_name, SELECT tc.constraint_name,
COALESCE( COALESCE(
ARRAY( ARRAY(
SELECT a.attname SELECT a.attname::text
FROM pg_constraint c FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace JOIN pg_namespace n ON n.oid = t.relnamespace

View File

@@ -261,7 +261,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
columnExprs := make([]string, 0, len(index.Columns)) columnExprs := make([]string, 0, len(index.Columns))
for _, colName := range index.Columns { for _, colName := range index.Columns {
colExpr := colName 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 // For GIN indexes on text columns, add operator class
if strings.EqualFold(indexType, "gin") && isTextType(col.Type) { if strings.EqualFold(indexType, "gin") && isTextType(col.Type) {
opClass := extractOperatorClass(index.Comment) 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 // Build column list with operator class support for GIN indexes
columnExprs := make([]string, 0, len(index.Columns)) columnExprs := make([]string, 0, len(index.Columns))
for _, colName := range index.Columns { for _, colName := range index.Columns {
if col, ok := table.Columns[colName]; ok { if col, ok := resolveIndexColumn(table, colName); ok {
colExpr := col.SQLName() colExpr := col.SQLName()
// For GIN indexes on text columns, add operator class // For GIN indexes on text columns, add operator class
if strings.EqualFold(index.Type, "gin") && isTextType(col.Type) { if strings.EqualFold(index.Type, "gin") && isTextType(col.Type) {
@@ -1269,6 +1269,33 @@ func isTextTypeWithoutLength(colType string) bool {
return strings.EqualFold(colType, "text") 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 // formatStringList formats a list of strings as a SQL-safe comma-separated quoted list
func formatStringList(items []string) string { func formatStringList(items []string) string {
quoted := make([]string, len(items)) quoted := make([]string, len(items))

View File

@@ -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) { func TestWriteForeignKeys(t *testing.T) {
// Create a test database with two related tables // Create a test database with two related tables
db := models.InitDatabase("testdb") db := models.InitDatabase("testdb")