From 16a30c19168daca18dd6b989c7b352c1a553f6b5 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Tue, 7 Jul 2026 19:09:09 -0700 Subject: [PATCH 1/2] fix: preserve INCLUDE columns in CREATE INDEX CONCURRENTLY (#508) The duplicate generateIndexSQL in rewrite.go (used for concurrent index creation) was missing the INCLUDE clause entirely, causing covering indexes to lose their non-key columns. Also fix operator class vs direction ordering to match the canonical version in index.go. Co-Authored-By: Claude Opus 4.6 --- internal/plan/rewrite.go | 12 +++++-- .../diff.sql | 1 + .../new.sql | 8 +++++ .../old.sql | 6 ++++ .../plan.json | 34 +++++++++++++++++++ .../plan.sql | 13 +++++++ .../plan.txt | 27 +++++++++++++++ 7 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql create mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql create mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql create mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json create mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql create mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt diff --git a/internal/plan/rewrite.go b/internal/plan/rewrite.go index ae299858..6efd0b26 100644 --- a/internal/plan/rewrite.go +++ b/internal/plan/rewrite.go @@ -440,18 +440,24 @@ func generateIndexSQL(index *ir.Index, isConcurrent bool) string { var columnParts []string for _, col := range index.Columns { part := col.Name - if col.Direction != "" && col.Direction != "ASC" { - part += " " + col.Direction - } if col.Operator != "" { part += " " + col.Operator } + if col.Direction != "" && col.Direction != "ASC" { + part += " " + col.Direction + } columnParts = append(columnParts, part) } sql.WriteString(joinStrings(columnParts, ", ")) sql.WriteString(")") + if len(index.IncludeColumns) > 0 { + sql.WriteString(" INCLUDE (") + sql.WriteString(strings.Join(index.IncludeColumns, ", ")) + sql.WriteString(")") + } + if index.NullsNotDistinct && index.Type == ir.IndexTypeUnique { sql.WriteString(" NULLS NOT DISTINCT") } diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql new file mode 100644 index 00000000..db8e67cb --- /dev/null +++ b/testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS t_cov ON t (a) INCLUDE (b); diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql new file mode 100644 index 00000000..815fa706 --- /dev/null +++ b/testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql @@ -0,0 +1,8 @@ +CREATE TABLE t ( + id integer NOT NULL, + a integer, + b integer, + CONSTRAINT t_pk PRIMARY KEY (id) +); + +CREATE INDEX t_cov ON t (a) INCLUDE (b); diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql new file mode 100644 index 00000000..ae3927f0 --- /dev/null +++ b/testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql @@ -0,0 +1,6 @@ +CREATE TABLE t ( + id integer NOT NULL, + a integer, + b integer, + CONSTRAINT t_pk PRIMARY KEY (id) +); diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json new file mode 100644 index 00000000..78e2cf1e --- /dev/null +++ b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json @@ -0,0 +1,34 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.12.0", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "e8233fa598bbe617be7a28d4e34418c8060d315ab8357d4168d2a228f40ed06f" + }, + "groups": [ + { + "steps": [ + { + "sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS t_cov ON t (a) INCLUDE (b);", + "type": "table.index", + "operation": "create", + "path": "public.t.t_cov" + } + ] + }, + { + "steps": [ + { + "sql": "SELECT \n COALESCE(i.indisvalid, false) as done,\n CASE \n WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total\n ELSE 0\n END as progress\nFROM pg_class c\nLEFT JOIN pg_index i ON c.oid = i.indexrelid\nLEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid\nWHERE c.relname = 't_cov';", + "directive": { + "type": "wait", + "message": "Creating index t_cov" + }, + "type": "table.index", + "operation": "create", + "path": "public.t.t_cov" + } + ] + } + ] +} diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql new file mode 100644 index 00000000..3020a0d7 --- /dev/null +++ b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql @@ -0,0 +1,13 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS t_cov ON t (a) INCLUDE (b); + +-- pgschema:wait +SELECT + COALESCE(i.indisvalid, false) as done, + CASE + WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total + ELSE 0 + END as progress +FROM pg_class c +LEFT JOIN pg_index i ON c.oid = i.indexrelid +LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid +WHERE c.relname = 't_cov'; diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt new file mode 100644 index 00000000..8e9bf2fc --- /dev/null +++ b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt @@ -0,0 +1,27 @@ +Plan: 1 to modify. + +Summary by type: + tables: 1 to modify + +Tables: + ~ t + + t_cov (index) + +DDL to be executed: +-------------------------------------------------- + +-- Transaction Group #1 +CREATE INDEX CONCURRENTLY IF NOT EXISTS t_cov ON t (a) INCLUDE (b); + +-- Transaction Group #2 +-- pgschema:wait +SELECT + COALESCE(i.indisvalid, false) as done, + CASE + WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total + ELSE 0 + END as progress +FROM pg_class c +LEFT JOIN pg_index i ON c.oid = i.indexrelid +LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid +WHERE c.relname = 't_cov'; \ No newline at end of file From c3830b84ebf52786e69bb9a2b34e80668586292b Mon Sep 17 00:00:00 2001 From: tianzhou Date: Tue, 7 Jul 2026 19:48:42 -0700 Subject: [PATCH 2/2] test: consolidate INCLUDE columns test into add_composite_index fixture Move the INCLUDE columns concurrent index test case from standalone issue_508_include_columns_concurrent_index fixture into the existing add_composite_index fixture. Co-Authored-By: Claude Opus 4.6 --- .../diff/online/add_composite_index/diff.sql | 2 ++ .../diff/online/add_composite_index/new.sql | 4 ++- .../diff/online/add_composite_index/plan.json | 24 +++++++++++++ .../diff/online/add_composite_index/plan.sql | 14 ++++++++ .../diff/online/add_composite_index/plan.txt | 21 ++++++++++-- .../diff.sql | 1 - .../new.sql | 8 ----- .../old.sql | 6 ---- .../plan.json | 34 ------------------- .../plan.sql | 13 ------- .../plan.txt | 27 --------------- 11 files changed, 62 insertions(+), 92 deletions(-) delete mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql delete mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql delete mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql delete mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json delete mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql delete mode 100644 testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt diff --git a/testdata/diff/online/add_composite_index/diff.sql b/testdata/diff/online/add_composite_index/diff.sql index 2419bade..68155862 100644 --- a/testdata/diff/online/add_composite_index/diff.sql +++ b/testdata/diff/online/add_composite_index/diff.sql @@ -1 +1,3 @@ +CREATE INDEX IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (username); + CREATE INDEX IF NOT EXISTS idx_users_email_status ON users (email, status DESC); diff --git a/testdata/diff/online/add_composite_index/new.sql b/testdata/diff/online/add_composite_index/new.sql index e2dd8fdb..1882697a 100644 --- a/testdata/diff/online/add_composite_index/new.sql +++ b/testdata/diff/online/add_composite_index/new.sql @@ -6,4 +6,6 @@ CREATE TABLE public.users ( status text ); -CREATE INDEX idx_users_email_status ON public.users (email, status DESC); \ No newline at end of file +CREATE INDEX idx_users_email_status ON public.users (email, status DESC); + +CREATE INDEX idx_users_email_include ON public.users (email) INCLUDE (username); \ No newline at end of file diff --git a/testdata/diff/online/add_composite_index/plan.json b/testdata/diff/online/add_composite_index/plan.json index aff9eabc..bdff1f5c 100644 --- a/testdata/diff/online/add_composite_index/plan.json +++ b/testdata/diff/online/add_composite_index/plan.json @@ -6,6 +6,30 @@ "hash": "4174f5cda975543aad9cf30ba2b3a2bd5ace1e79185c119db587df0795195c33" }, "groups": [ + { + "steps": [ + { + "sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (username);", + "type": "table.index", + "operation": "create", + "path": "public.users.idx_users_email_include" + } + ] + }, + { + "steps": [ + { + "sql": "SELECT \n COALESCE(i.indisvalid, false) as done,\n CASE \n WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total\n ELSE 0\n END as progress\nFROM pg_class c\nLEFT JOIN pg_index i ON c.oid = i.indexrelid\nLEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid\nWHERE c.relname = 'idx_users_email_include';", + "directive": { + "type": "wait", + "message": "Creating index idx_users_email_include" + }, + "type": "table.index", + "operation": "create", + "path": "public.users.idx_users_email_include" + } + ] + }, { "steps": [ { diff --git a/testdata/diff/online/add_composite_index/plan.sql b/testdata/diff/online/add_composite_index/plan.sql index 037529e6..9d42d6f1 100644 --- a/testdata/diff/online/add_composite_index/plan.sql +++ b/testdata/diff/online/add_composite_index/plan.sql @@ -1,3 +1,17 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (username); + +-- pgschema:wait +SELECT + COALESCE(i.indisvalid, false) as done, + CASE + WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total + ELSE 0 + END as progress +FROM pg_class c +LEFT JOIN pg_index i ON c.oid = i.indexrelid +LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid +WHERE c.relname = 'idx_users_email_include'; + CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_status ON users (email, status DESC); -- pgschema:wait diff --git a/testdata/diff/online/add_composite_index/plan.txt b/testdata/diff/online/add_composite_index/plan.txt index 2b0d7b11..0f82758e 100644 --- a/testdata/diff/online/add_composite_index/plan.txt +++ b/testdata/diff/online/add_composite_index/plan.txt @@ -5,13 +5,14 @@ Summary by type: Tables: ~ users + + idx_users_email_include (index) + idx_users_email_status (index) DDL to be executed: -------------------------------------------------- -- Transaction Group #1 -CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_status ON users (email, status DESC); +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_include ON users (email) INCLUDE (username); -- Transaction Group #2 -- pgschema:wait @@ -24,4 +25,20 @@ SELECT FROM pg_class c LEFT JOIN pg_index i ON c.oid = i.indexrelid LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid -WHERE c.relname = 'idx_users_email_status'; +WHERE c.relname = 'idx_users_email_include'; + +-- Transaction Group #3 +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_status ON users (email, status DESC); + +-- Transaction Group #4 +-- pgschema:wait +SELECT + COALESCE(i.indisvalid, false) as done, + CASE + WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total + ELSE 0 + END as progress +FROM pg_class c +LEFT JOIN pg_index i ON c.oid = i.indexrelid +LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid +WHERE c.relname = 'idx_users_email_status'; \ No newline at end of file diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql deleted file mode 100644 index db8e67cb..00000000 --- a/testdata/diff/online/issue_508_include_columns_concurrent_index/diff.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX IF NOT EXISTS t_cov ON t (a) INCLUDE (b); diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql deleted file mode 100644 index 815fa706..00000000 --- a/testdata/diff/online/issue_508_include_columns_concurrent_index/new.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE t ( - id integer NOT NULL, - a integer, - b integer, - CONSTRAINT t_pk PRIMARY KEY (id) -); - -CREATE INDEX t_cov ON t (a) INCLUDE (b); diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql deleted file mode 100644 index ae3927f0..00000000 --- a/testdata/diff/online/issue_508_include_columns_concurrent_index/old.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE t ( - id integer NOT NULL, - a integer, - b integer, - CONSTRAINT t_pk PRIMARY KEY (id) -); diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json deleted file mode 100644 index 78e2cf1e..00000000 --- a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "1.0.0", - "pgschema_version": "1.12.0", - "created_at": "1970-01-01T00:00:00Z", - "source_fingerprint": { - "hash": "e8233fa598bbe617be7a28d4e34418c8060d315ab8357d4168d2a228f40ed06f" - }, - "groups": [ - { - "steps": [ - { - "sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS t_cov ON t (a) INCLUDE (b);", - "type": "table.index", - "operation": "create", - "path": "public.t.t_cov" - } - ] - }, - { - "steps": [ - { - "sql": "SELECT \n COALESCE(i.indisvalid, false) as done,\n CASE \n WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total\n ELSE 0\n END as progress\nFROM pg_class c\nLEFT JOIN pg_index i ON c.oid = i.indexrelid\nLEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid\nWHERE c.relname = 't_cov';", - "directive": { - "type": "wait", - "message": "Creating index t_cov" - }, - "type": "table.index", - "operation": "create", - "path": "public.t.t_cov" - } - ] - } - ] -} diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql deleted file mode 100644 index 3020a0d7..00000000 --- a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS t_cov ON t (a) INCLUDE (b); - --- pgschema:wait -SELECT - COALESCE(i.indisvalid, false) as done, - CASE - WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total - ELSE 0 - END as progress -FROM pg_class c -LEFT JOIN pg_index i ON c.oid = i.indexrelid -LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid -WHERE c.relname = 't_cov'; diff --git a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt b/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt deleted file mode 100644 index 8e9bf2fc..00000000 --- a/testdata/diff/online/issue_508_include_columns_concurrent_index/plan.txt +++ /dev/null @@ -1,27 +0,0 @@ -Plan: 1 to modify. - -Summary by type: - tables: 1 to modify - -Tables: - ~ t - + t_cov (index) - -DDL to be executed: --------------------------------------------------- - --- Transaction Group #1 -CREATE INDEX CONCURRENTLY IF NOT EXISTS t_cov ON t (a) INCLUDE (b); - --- Transaction Group #2 --- pgschema:wait -SELECT - COALESCE(i.indisvalid, false) as done, - CASE - WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total - ELSE 0 - END as progress -FROM pg_class c -LEFT JOIN pg_index i ON c.oid = i.indexrelid -LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid -WHERE c.relname = 't_cov'; \ No newline at end of file