diff --git a/pkg/writers/pgsql/README.md b/pkg/writers/pgsql/README.md index d4d1334..5377b66 100644 --- a/pkg/writers/pgsql/README.md +++ b/pkg/writers/pgsql/README.md @@ -169,6 +169,7 @@ When `include_audit` is enabled, adds: - Constraint actions (CASCADE, RESTRICT, SET NULL) - Partial indexes - Function-based indexes +- Concurrent index creation (`CREATE INDEX CONCURRENTLY`) via `Index.Concurrent` - Check constraints with expressions ## Data Types diff --git a/pkg/writers/pgsql/migration_writer.go b/pkg/writers/pgsql/migration_writer.go index 879c79a..c8256d3 100644 --- a/pkg/writers/pgsql/migration_writer.go +++ b/pkg/writers/pgsql/migration_writer.go @@ -652,6 +652,7 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo IndexType: indexType, Columns: strings.Join(columnExprs, ", "), Unique: modelIndex.Unique, + Concurrent: modelIndex.Concurrent, }) if err != nil { return nil, err diff --git a/pkg/writers/pgsql/migration_writer_test.go b/pkg/writers/pgsql/migration_writer_test.go index bfe630a..d88cb2c 100644 --- a/pkg/writers/pgsql/migration_writer_test.go +++ b/pkg/writers/pgsql/migration_writer_test.go @@ -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) { current := models.InitDatabase("testdb") currentSchema := models.InitSchema("public") diff --git a/pkg/writers/pgsql/templates.go b/pkg/writers/pgsql/templates.go index 12a2b97..16ec99f 100644 --- a/pkg/writers/pgsql/templates.go +++ b/pkg/writers/pgsql/templates.go @@ -139,6 +139,7 @@ type CreateIndexData struct { IndexType string Columns string Unique bool + Concurrent bool } // CreateForeignKeyData contains data for create foreign key template diff --git a/pkg/writers/pgsql/templates/create_index.tmpl b/pkg/writers/pgsql/templates/create_index.tmpl index 55fe127..dda3b01 100644 --- a/pkg/writers/pgsql/templates/create_index.tmpl +++ b/pkg/writers/pgsql/templates/create_index.tmpl @@ -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}}); \ No newline at end of file diff --git a/pkg/writers/pgsql/writer.go b/pkg/writers/pgsql/writer.go index 8ad8f32..969a411 100644 --- a/pkg/writers/pgsql/writer.go +++ b/pkg/writers/pgsql/writer.go @@ -1097,8 +1097,13 @@ func (w *Writer) writeIndexes(schema *models.Schema) error { whereClause = fmt.Sprintf(" WHERE %s", index.Where) } - fmt.Fprintf(w.writer, "CREATE %sINDEX IF NOT EXISTS %s\n", - unique, indexName) + concurrently := "" + 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", w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), whereClause) } diff --git a/pkg/writers/pgsql/writer_test.go b/pkg/writers/pgsql/writer_test.go index 7c4dc14..d217ca7 100644 --- a/pkg/writers/pgsql/writer_test.go +++ b/pkg/writers/pgsql/writer_test.go @@ -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) { db := models.InitDatabase("testdb") schema := models.InitSchema("public")