Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,16 @@ func applyLegacyCheckNormalizations(clause string) string {
}

// convertAnyArrayToIn converts PostgreSQL's "column = ANY (ARRAY[...])" format
// to the more readable "column IN (...)" format
// to the more readable "column IN (...)" format.
//
// Type casts are always preserved to ensure:
// - Custom types (enums, domains) are properly qualified (e.g., 'value'::public.my_enum)
// - Output matches pg_dump's format exactly
// - Comparison between desired and current states is accurate
//
// Example transformations:
// - "status = ANY (ARRAY['active'::public.status_type])" → "status IN ('active'::public.status_type)"
// - "gender = ANY (ARRAY['M'::text, 'F'::text])" → "gender IN ('M'::text, 'F'::text)"
func convertAnyArrayToIn(expr string) string {
if !strings.Contains(expr, "= ANY (ARRAY[") {
return expr
Expand All @@ -917,19 +926,15 @@ func convertAnyArrayToIn(expr string) string {
valuesPart = strings.TrimSuffix(valuesPart, "]))")
valuesPart = strings.TrimSuffix(valuesPart, "])")

// Split the values and clean them up
// Split values and preserve them as-is, including all type casts
values := strings.Split(valuesPart, ", ")
var cleanValues []string
for _, val := range values {
val = strings.TrimSpace(val)
// Remove type casts like ::text, ::varchar, etc.
if idx := strings.Index(val, "::"); idx != -1 {
val = val[:idx]
}
cleanValues = append(cleanValues, val)
}

// Return converted format: "column IN ('val1', 'val2')"
// Return converted format: "column IN ('val1'::type, 'val2'::type)"
return fmt.Sprintf("%s IN (%s)", columnName, strings.Join(cleanValues, ", "))
}

231 changes: 145 additions & 86 deletions ir/queries/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -296,97 +296,156 @@ WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
ORDER BY n.nspname, cl.relname, c.contype, c.conname, a.attnum;

-- GetIndexes retrieves all indexes including regular and unique indexes created with CREATE INDEX
-- IMPORTANT: Uses LATERAL join with set_config to temporarily set search_path to empty
-- This ensures pg_get_expr() includes schema qualifiers for types in partial index predicates,
-- matching pg_dump's behavior and preventing false positives when comparing schemas
-- name: GetIndexes :many
SELECT
n.nspname as schemaname,
t.relname as tablename,
i.relname as indexname,
idx.indisunique as is_unique,
idx.indisprimary as is_primary,
(idx.indpred IS NOT NULL) as is_partial,
am.amname as method,
pg_get_indexdef(idx.indexrelid) as indexdef,
CASE
WHEN idx.indpred IS NOT NULL THEN pg_get_expr(idx.indpred, idx.indrelid)
ELSE NULL
END as partial_predicate,
CASE
WHEN idx.indexprs IS NOT NULL THEN true
ELSE false
END as has_expressions
FROM pg_index idx
JOIN pg_class i ON i.oid = idx.indexrelid
JOIN pg_class t ON t.oid = idx.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = i.relam
WHERE
NOT idx.indisprimary
AND NOT EXISTS (
SELECT 1 FROM pg_constraint c
WHERE c.conindid = idx.indexrelid
AND c.contype IN ('u', 'p')
)
AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
AND n.nspname NOT LIKE 'pg_temp_%'
AND n.nspname NOT LIKE 'pg_toast_temp_%'
ORDER BY n.nspname, t.relname, i.relname;
WITH index_base AS (
SELECT
n.nspname as schemaname,
t.relname as tablename,
i.relname as indexname,
idx.indisunique as is_unique,
idx.indisprimary as is_primary,
(idx.indpred IS NOT NULL) as is_partial,
am.amname as method,
pg_get_indexdef(idx.indexrelid) as indexdef,
idx.indpred,
idx.indrelid,
CASE
WHEN idx.indexprs IS NOT NULL THEN true
ELSE false
END as has_expressions
FROM pg_index idx
JOIN pg_class i ON i.oid = idx.indexrelid
JOIN pg_class t ON t.oid = idx.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = i.relam
WHERE
NOT idx.indisprimary
AND NOT EXISTS (
SELECT 1 FROM pg_constraint c
WHERE c.conindid = idx.indexrelid
AND c.contype IN ('u', 'p')
)
AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
AND n.nspname NOT LIKE 'pg_temp_%'
AND n.nspname NOT LIKE 'pg_toast_temp_%'
)
SELECT
ib.schemaname,
ib.tablename,
ib.indexname,
ib.is_unique,
ib.is_primary,
ib.is_partial,
ib.method,
ib.indexdef,
-- Use LATERAL join to guarantee execution order:
-- 1. set_config sets search_path to empty (like pg_dump does)
-- 2. pg_get_expr then uses that search_path
-- This ensures type references are schema-qualified (e.g., 'value'::public.my_enum)
sp.partial_predicate,
ib.has_expressions
FROM index_base ib
CROSS JOIN LATERAL (
SELECT
set_config('search_path', '', true) as dummy,
CASE
WHEN ib.indpred IS NOT NULL THEN pg_get_expr(ib.indpred, ib.indrelid)
ELSE NULL
END as partial_predicate
) sp
ORDER BY ib.schemaname, ib.tablename, ib.indexname;

-- GetIndexesForSchema retrieves all indexes for a specific schema
-- IMPORTANT: Uses LATERAL join with set_config to temporarily set search_path to empty
-- This ensures pg_get_expr() includes schema qualifiers for types in partial index predicates,
-- matching pg_dump's behavior and preventing false positives when comparing schemas
-- name: GetIndexesForSchema :many
SELECT
n.nspname as schemaname,
t.relname as tablename,
i.relname as indexname,
idx.indisunique as is_unique,
idx.indisprimary as is_primary,
(idx.indpred IS NOT NULL) as is_partial,
am.amname as method,
pg_get_indexdef(idx.indexrelid) as indexdef,
CASE
WHEN idx.indpred IS NOT NULL THEN pg_get_expr(idx.indpred, idx.indrelid)
ELSE NULL
END as partial_predicate,
CASE
WHEN idx.indexprs IS NOT NULL THEN true
ELSE false
END as has_expressions,
COALESCE(d.description, '') AS index_comment,
idx.indnatts as num_columns,
ARRAY(
SELECT pg_get_indexdef(idx.indexrelid, k::int, true)
FROM generate_series(1, idx.indnatts) k
) as column_definitions,
ARRAY(
SELECT
CASE
WHEN (idx.indoption[k-1] & 1) = 1 THEN 'DESC'
ELSE 'ASC'
WITH index_base AS (
SELECT
n.nspname as schemaname,
t.relname as tablename,
i.relname as indexname,
idx.indisunique as is_unique,
idx.indisprimary as is_primary,
(idx.indpred IS NOT NULL) as is_partial,
am.amname as method,
pg_get_indexdef(idx.indexrelid) as indexdef,
idx.indpred,
idx.indrelid,
CASE
WHEN idx.indexprs IS NOT NULL THEN true
ELSE false
END as has_expressions,
COALESCE(d.description, '') AS index_comment,
idx.indnatts as num_columns,
ARRAY(
SELECT pg_get_indexdef(idx.indexrelid, k::int, true)
FROM generate_series(1, idx.indnatts) k
) as column_definitions,
ARRAY(
SELECT
CASE
WHEN (idx.indoption[k-1] & 1) = 1 THEN 'DESC'
ELSE 'ASC'
END
FROM generate_series(1, idx.indnatts) k
) as column_directions,
ARRAY(
SELECT CASE
WHEN opc.opcdefault THEN '' -- Omit default operator classes
ELSE COALESCE(opc.opcname, '')
END
FROM generate_series(1, idx.indnatts) k
) as column_directions,
ARRAY(
SELECT CASE
WHEN opc.opcdefault THEN '' -- Omit default operator classes
ELSE COALESCE(opc.opcname, '')
END
FROM generate_series(1, idx.indnatts) k
LEFT JOIN pg_opclass opc ON opc.oid = idx.indclass[k-1]
) as column_opclasses
FROM pg_index idx
JOIN pg_class i ON i.oid = idx.indexrelid
JOIN pg_class t ON t.oid = idx.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = i.relam
LEFT JOIN pg_description d ON d.objoid = i.oid AND d.objsubid = 0
WHERE
NOT idx.indisprimary
AND NOT EXISTS (
SELECT 1 FROM pg_constraint c
WHERE c.conindid = idx.indexrelid
AND c.contype IN ('u', 'p')
)
AND n.nspname = $1
ORDER BY n.nspname, t.relname, i.relname;
FROM generate_series(1, idx.indnatts) k
LEFT JOIN pg_opclass opc ON opc.oid = idx.indclass[k-1]
) as column_opclasses
FROM pg_index idx
JOIN pg_class i ON i.oid = idx.indexrelid
JOIN pg_class t ON t.oid = idx.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = i.relam
LEFT JOIN pg_description d ON d.objoid = i.oid AND d.objsubid = 0
WHERE
NOT idx.indisprimary
AND NOT EXISTS (
SELECT 1 FROM pg_constraint c
WHERE c.conindid = idx.indexrelid
AND c.contype IN ('u', 'p')
)
AND n.nspname = $1
)
SELECT
ib.schemaname,
ib.tablename,
ib.indexname,
ib.is_unique,
ib.is_primary,
ib.is_partial,
ib.method,
ib.indexdef,
-- Use LATERAL join to guarantee execution order:
-- 1. set_config sets search_path to empty (like pg_dump does)
-- 2. pg_get_expr then uses that search_path
-- This ensures type references are schema-qualified (e.g., 'value'::public.my_enum)
sp.partial_predicate,
ib.has_expressions,
ib.index_comment,
ib.num_columns,
ib.column_definitions,
ib.column_directions,
ib.column_opclasses
FROM index_base ib
CROSS JOIN LATERAL (
SELECT
set_config('search_path', '', true) as dummy,
CASE
WHEN ib.indpred IS NOT NULL THEN pg_get_expr(ib.indpred, ib.indrelid)
ELSE NULL
END as partial_predicate
) sp
ORDER BY ib.schemaname, ib.tablename, ib.indexname;

-- GetSequences retrieves all sequences
-- name: GetSequences :many
Expand Down
Loading