Skip to content

Commit b85861a

Browse files
tianzhouclaude
andauthored
fix: preserve INCLUDE columns on CREATE INDEX (#385) (#390)
The index inspector was using indnatts (total columns) for both key and include columns, causing INCLUDE columns to be merged into regular indexed columns. Now uses indnkeyatts to extract only key columns, and separately extracts include columns from positions indnkeyatts+1 through indnatts. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 659f7f4 commit b85861a

11 files changed

Lines changed: 82 additions & 25 deletions

File tree

internal/diff/index.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ func generateIndexSQLWithName(index *ir.Index, indexName string, targetSchema st
121121
}
122122
builder.WriteString(")")
123123

124+
// INCLUDE columns (non-key columns stored in the index)
125+
if len(index.IncludeColumns) > 0 {
126+
builder.WriteString(" INCLUDE (")
127+
for i, col := range index.IncludeColumns {
128+
if i > 0 {
129+
builder.WriteString(", ")
130+
}
131+
builder.WriteString(col)
132+
}
133+
builder.WriteString(")")
134+
}
135+
124136
// NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
125137
if index.NullsNotDistinct && index.Type == ir.IndexTypeUnique {
126138
builder.WriteString(" NULLS NOT DISTINCT")

internal/diff/table.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,16 @@ func indexesStructurallyEqual(oldIndex, newIndex *ir.Index) bool {
15111511
}
15121512
}
15131513

1514+
// Compare INCLUDE columns
1515+
if len(oldIndex.IncludeColumns) != len(newIndex.IncludeColumns) {
1516+
return false
1517+
}
1518+
for i, oldCol := range oldIndex.IncludeColumns {
1519+
if oldCol != newIndex.IncludeColumns[i] {
1520+
return false
1521+
}
1522+
}
1523+
15141524
return true
15151525
}
15161526

ir/inspector.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -746,16 +746,17 @@ func (i *Inspector) buildIndexes(ctx context.Context, schema *IR, targetSchema s
746746
}
747747

748748
index := &Index{
749-
Schema: schemaName,
750-
Table: tableName,
751-
Name: indexName,
752-
Type: indexType,
753-
Method: method,
754-
IsPartial: isPartial,
755-
IsExpression: hasExpressions,
756-
Where: "",
757-
Comment: comment,
758-
Columns: []*IndexColumn{},
749+
Schema: schemaName,
750+
Table: tableName,
751+
Name: indexName,
752+
Type: indexType,
753+
Method: method,
754+
IncludeColumns: indexRow.IncludeColumns,
755+
IsPartial: isPartial,
756+
IsExpression: hasExpressions,
757+
Where: "",
758+
Comment: comment,
759+
Columns: []*IndexColumn{},
759760
}
760761

761762
// Check for NULLS NOT DISTINCT (PostgreSQL 15+)

ir/ir.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,12 @@ type Index struct {
251251
Type IndexType `json:"type"`
252252
Method string `json:"method"` // btree, hash, gin, gist, etc.
253253
Columns []*IndexColumn `json:"columns"`
254-
IsPartial bool `json:"is_partial"` // has a WHERE clause
255-
IsExpression bool `json:"is_expression"` // functional/expression index
256-
Where string `json:"where,omitempty"` // partial index condition
257-
NullsNotDistinct bool `json:"nulls_not_distinct,omitempty"` // NULLS NOT DISTINCT (PG15+)
258-
Comment string `json:"comment,omitempty"`
254+
IncludeColumns []string `json:"include_columns,omitempty"` // INCLUDE columns (non-key)
255+
IsPartial bool `json:"is_partial"` // has a WHERE clause
256+
IsExpression bool `json:"is_expression"` // functional/expression index
257+
Where string `json:"where,omitempty"` // partial index condition
258+
NullsNotDistinct bool `json:"nulls_not_distinct,omitempty"` // NULLS NOT DISTINCT (PG15+)
259+
Comment string `json:"comment,omitempty"`
259260
}
260261

261262
// IndexColumn represents a column within an index

ir/queries/queries.sql

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -436,27 +436,32 @@ WITH index_base AS (
436436
ELSE false
437437
END as has_expressions,
438438
COALESCE(d.description, '') AS index_comment,
439+
idx.indnkeyatts as num_key_columns,
439440
idx.indnatts as num_columns,
440441
ARRAY(
441442
SELECT pg_get_indexdef(idx.indexrelid, k::int, true)
442-
FROM generate_series(1, idx.indnatts) k
443+
FROM generate_series(1, idx.indnkeyatts) k
443444
) as column_definitions,
444445
ARRAY(
445446
SELECT
446447
CASE
447448
WHEN (idx.indoption[k-1] & 1) = 1 THEN 'DESC'
448449
ELSE 'ASC'
449450
END
450-
FROM generate_series(1, idx.indnatts) k
451+
FROM generate_series(1, idx.indnkeyatts) k
451452
) as column_directions,
452453
ARRAY(
453454
SELECT CASE
454455
WHEN opc.opcdefault THEN '' -- Omit default operator classes
455456
ELSE COALESCE(opc.opcname, '')
456457
END
457-
FROM generate_series(1, idx.indnatts) k
458+
FROM generate_series(1, idx.indnkeyatts) k
458459
LEFT JOIN pg_opclass opc ON opc.oid = idx.indclass[k-1]
459-
) as column_opclasses
460+
) as column_opclasses,
461+
ARRAY(
462+
SELECT pg_get_indexdef(idx.indexrelid, k::int, true)
463+
FROM generate_series(idx.indnkeyatts + 1, idx.indnatts) k
464+
) as include_columns
460465
FROM pg_index idx
461466
JOIN pg_class i ON i.oid = idx.indexrelid
462467
JOIN pg_class t ON t.oid = idx.indrelid
@@ -488,10 +493,12 @@ SELECT
488493
sp.partial_predicate,
489494
ib.has_expressions,
490495
ib.index_comment,
496+
ib.num_key_columns,
491497
ib.num_columns,
492498
ib.column_definitions,
493499
ib.column_directions,
494-
ib.column_opclasses
500+
ib.column_opclasses,
501+
ib.include_columns
495502
FROM index_base ib
496503
CROSS JOIN LATERAL (
497504
SELECT

ir/queries/queries.sql.go

Lines changed: 16 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testdata/diff/create_index/add_index/diff.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS users (
77

88
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email varchar_pattern_ops);
99

10+
CREATE INDEX IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (name);
11+
1012
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique ON users (email) NULLS NOT DISTINCT;
1113

1214
CREATE INDEX IF NOT EXISTS idx_users_id ON users (id);

testdata/diff/create_index/add_index/new.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ CREATE INDEX idx_users_id ON public.users (id);
1212
CREATE INDEX "public.idx_users" ON public.users (email, name);
1313
-- Test NULLS NOT DISTINCT (issue #355)
1414
CREATE UNIQUE INDEX idx_users_email_unique ON public.users (email) NULLS NOT DISTINCT;
15+
-- Test INCLUDE columns (issue #385)
16+
CREATE INDEX idx_users_email_include ON public.users (email) INCLUDE (name);

testdata/diff/create_index/add_index/plan.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
"operation": "create",
2121
"path": "public.users.idx_users_email"
2222
},
23+
{
24+
"sql": "CREATE INDEX IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (name);",
25+
"type": "table.index",
26+
"operation": "create",
27+
"path": "public.users.idx_users_email_include"
28+
},
2329
{
2430
"sql": "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique ON users (email) NULLS NOT DISTINCT;",
2531
"type": "table.index",

testdata/diff/create_index/add_index/plan.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS users (
77

88
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email varchar_pattern_ops);
99

10+
CREATE INDEX IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (name);
11+
1012
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique ON users (email) NULLS NOT DISTINCT;
1113

1214
CREATE INDEX IF NOT EXISTS idx_users_id ON users (id);

0 commit comments

Comments
 (0)