feat(pgsql): add support for concurrent index creation

* Implemented `Concurrent` field in index model
* Updated index creation template to support `CREATE INDEX CONCURRENTLY`
* Added tests for concurrent index creation in migration writer
This commit is contained in:
Hein
2026-08-24 12:03:30 +02:00
parent 2b6bb7f948
commit 76b8321065
7 changed files with 86 additions and 3 deletions
+1
View File
@@ -169,6 +169,7 @@ When `include_audit` is enabled, adds:
- Constraint actions (CASCADE, RESTRICT, SET NULL) - Constraint actions (CASCADE, RESTRICT, SET NULL)
- Partial indexes - Partial indexes
- Function-based indexes - Function-based indexes
- Concurrent index creation (`CREATE INDEX CONCURRENTLY`) via `Index.Concurrent`
- Check constraints with expressions - Check constraints with expressions
## Data Types ## Data Types
+1
View File
@@ -652,6 +652,7 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
IndexType: indexType, IndexType: indexType,
Columns: strings.Join(columnExprs, ", "), Columns: strings.Join(columnExprs, ", "),
Unique: modelIndex.Unique, Unique: modelIndex.Unique,
Concurrent: modelIndex.Concurrent,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -334,6 +334,46 @@ func TestWriteMigration_DoesNotAlterEquivalentNormalizedColumnType(t *testing.T)
} }
} }
func TestWriteMigration_ConcurrentIndex(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("articles", "public")
titleCol := models.InitColumn("title", "articles", "public")
titleCol.Type = "text"
table.Columns["title"] = titleCol
index := &models.Index{
Name: "idx_articles_title",
Columns: []string{"title"},
Concurrent: true,
}
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, "CREATE INDEX CONCURRENTLY IF NOT EXISTS") {
t.Fatalf("expected CONCURRENTLY create index statement, got:\n%s", output)
}
}
func TestWriteMigration_GinIndexOnTextUsesTrigramOperatorClass(t *testing.T) { func TestWriteMigration_GinIndexOnTextUsesTrigramOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb") current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public") currentSchema := models.InitSchema("public")
+1
View File
@@ -139,6 +139,7 @@ type CreateIndexData struct {
IndexType string IndexType string
Columns string Columns string
Unique bool Unique bool
Concurrent bool
} }
// CreateForeignKeyData contains data for create foreign key template // CreateForeignKeyData contains data for create foreign key template
@@ -1,2 +1,2 @@
CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{quote_ident .IndexName}} CREATE {{if .Unique}}UNIQUE {{end}}INDEX {{if .Concurrent}}CONCURRENTLY {{end}}IF NOT EXISTS {{quote_ident .IndexName}}
ON {{qual_table .SchemaName .TableName}} USING {{.IndexType}} ({{.Columns}}); ON {{qual_table .SchemaName .TableName}} USING {{.IndexType}} ({{.Columns}});
+7 -2
View File
@@ -1097,8 +1097,13 @@ func (w *Writer) writeIndexes(schema *models.Schema) error {
whereClause = fmt.Sprintf(" WHERE %s", index.Where) whereClause = fmt.Sprintf(" WHERE %s", index.Where)
} }
fmt.Fprintf(w.writer, "CREATE %sINDEX IF NOT EXISTS %s\n", concurrently := ""
unique, indexName) if index.Concurrent {
concurrently = "CONCURRENTLY "
}
fmt.Fprintf(w.writer, "CREATE %sINDEX %sIF NOT EXISTS %s\n",
unique, concurrently, indexName)
fmt.Fprintf(w.writer, " ON %s USING %s (%s)%s;\n\n", fmt.Fprintf(w.writer, " ON %s USING %s (%s)%s;\n\n",
w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), whereClause) w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), whereClause)
} }
+35
View File
@@ -87,6 +87,41 @@ func TestWriteDatabase(t *testing.T) {
} }
} }
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) { func TestWriteDatabase_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb") db := models.InitDatabase("testdb")
schema := models.InitSchema("public") schema := models.InitSchema("public")