Skip to content

Commit c0dc021

Browse files
authored
Add exclusion constraints to analyze/read_schema (#658)
This PR adds support for displaying exclusion constraints in the output of `analyze`/`read_schema`. Attributes: * `name`: name of the constraint * `method`: selected index method (`exclude.index_method` option) * `columns`: the list of columns included in the constraint * `predicate`: conditional expression if partials index * `definition`: constraint definition ``` go run . analyze { ... "excludeConstraints": { "forbid_duplicated_titles": { "name": "forbid_duplicated_titles", "method": "btree", "predicate": "(title IS NOT NULL)", "columns": [ "title" ], "definition": "EXCLUDE USING btree (title WITH =) WHERE ((title IS NOT NULL))" } }, ... } ``` Furthermore, I added a new attribute called `exclusion` to indexes to show if the index belongs to an exclusion constraint.
1 parent bbdb2f8 commit c0dc021

File tree

4 files changed

+205
-122
lines changed

4 files changed

+205
-122
lines changed

pkg/migrations/op_create_table.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,9 @@ func (o *OpCreateTable) updateSchema(s *schema.Schema) *schema.Schema {
244244
}
245245
case ConstraintTypeExclude:
246246
excludeConstraints[c.Name] = &schema.ExcludeConstraint{
247-
Name: c.Name,
248-
IndexMethod: c.Exclude.IndexMethod,
249-
Elements: c.Exclude.Elements,
250-
Predicate: c.Exclude.Predicate,
247+
Name: c.Name,
248+
Method: c.Exclude.IndexMethod,
249+
Predicate: c.Exclude.Predicate,
251250
}
252251
}
253252
}

pkg/schema/schema.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ type Index struct {
9999
// Unique indicates whether or not the index is unique
100100
Unique bool `json:"unique"`
101101

102+
// Exclusion indicates whether or not the index is an exclusion index
103+
Exclusion bool `json:"exclusion"`
104+
102105
// Columns is the set of key columns on which the index is defined
103106
Columns []string `json:"columns"`
104107

@@ -162,14 +165,17 @@ type ExcludeConstraint struct {
162165
// Name is the name of the exclude constraint in postgres
163166
Name string `json:"name"`
164167

165-
// IndexMethod is the index method of the exclude constraint
166-
IndexMethod string `json:"indexMethod"`
167-
168-
// Elements is the elements of the exclude constraint
169-
Elements string `json:"elements"`
168+
// Method is the index method of the exclude constraint
169+
Method string `json:"method"`
170170

171171
// Predicate is the predicate of the index
172172
Predicate string `json:"predicate"`
173+
174+
// The columns that the exclusion constraint is defined on
175+
Columns []string `json:"columns"`
176+
177+
// The definition of the exclusion
178+
Definition string `json:"definition"`
173179
}
174180

175181
// GetTable returns a table by name

pkg/state/init.sql

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,10 @@ BEGIN
204204
AND pg_attribute.attnum = ANY (pg_index.indkey)
205205
AND indisprimary), 'indexes', (
206206
SELECT
207-
COALESCE(json_object_agg(ix_details.name, json_build_object('name', ix_details.name, 'unique', ix_details.indisunique, 'columns', ix_details.columns, 'predicate', ix_details.predicate, 'method', ix_details.method, 'definition', ix_details.definition)), '{}'::json)
207+
COALESCE(json_object_agg(ix_details.name, json_build_object('name', ix_details.name, 'unique', ix_details.indisunique, 'exclusion', ix_details.indisexclusion, 'columns', ix_details.columns, 'predicate', ix_details.predicate, 'method', ix_details.method, 'definition', ix_details.definition)), '{}'::json)
208208
FROM (
209209
SELECT
210-
replace(reverse(split_part(reverse(pi.indexrelid::regclass::text), '.', 1)), '"', '') AS name, pi.indisunique, array_agg(a.attname) AS columns, pg_get_expr(pi.indpred, t.oid) AS predicate, am.amname AS method, pg_get_indexdef(pi.indexrelid) AS definition
210+
replace(reverse(split_part(reverse(pi.indexrelid::regclass::text), '.', 1)), '"', '') AS name, pi.indisunique, pi.indisexclusion, array_agg(a.attname) AS columns, pg_get_expr(pi.indpred, t.oid) AS predicate, am.amname AS method, pg_get_indexdef(pi.indexrelid) AS definition
211211
FROM pg_index pi
212212
JOIN pg_attribute a ON a.attrelid = pi.indrelid
213213
AND a.attnum = ANY (pi.indkey)
@@ -234,56 +234,69 @@ BEGIN
234234
AND uc_attr.attnum = ANY (uc_constraint.conkey)
235235
WHERE
236236
uc_constraint.conrelid = t.oid
237-
AND uc_constraint.contype = 'u' GROUP BY uc_constraint.oid, uc_constraint.conname) AS uc_details), 'foreignKeys', (
237+
AND uc_constraint.contype = 'u' GROUP BY uc_constraint.oid, uc_constraint.conname) AS uc_details), 'excludeConstraints', (
238238
SELECT
239-
COALESCE(json_object_agg(fk_details.conname, json_build_object('name', fk_details.conname, 'columns', fk_details.columns, 'referencedTable', fk_details.referencedTable, 'referencedColumns', fk_details.referencedColumns, 'matchType', fk_details.matchType, 'onDelete', fk_details.onDelete, 'onUpdate', fk_details.onUpdate)), '{}'::json)
239+
COALESCE(json_object_agg(xc_details.conname, json_build_object('name', xc_details.conname, 'columns', xc_details.columns, 'definition', xc_details.definition, 'predicate', xc_details.predicate, 'method', xc_details.method)), '{}'::json)
240240
FROM (
241241
SELECT
242-
fk_info.conname AS conname, fk_info.columns AS columns, fk_info.relname AS referencedTable, array_agg(ref_attr.attname ORDER BY ref_attr.attname) AS referencedColumns, CASE WHEN fk_info.confmatchtype = 'f' THEN
243-
'FULL'
244-
WHEN fk_info.confmatchtype = 'p' THEN
245-
'PARTIAL'
246-
WHEN fk_info.confmatchtype = 's' THEN
247-
'SIMPLE'
248-
END AS matchType, CASE WHEN fk_info.confdeltype = 'a' THEN
249-
'NO ACTION'
250-
WHEN fk_info.confdeltype = 'r' THEN
251-
'RESTRICT'
252-
WHEN fk_info.confdeltype = 'c' THEN
253-
'CASCADE'
254-
WHEN fk_info.confdeltype = 'd' THEN
255-
'SET DEFAULT'
256-
WHEN fk_info.confdeltype = 'n' THEN
257-
'SET NULL'
258-
END AS onDelete, CASE WHEN fk_info.confupdtype = 'a' THEN
259-
'NO ACTION'
260-
WHEN fk_info.confupdtype = 'r' THEN
261-
'RESTRICT'
262-
WHEN fk_info.confupdtype = 'c' THEN
263-
'CASCADE'
264-
WHEN fk_info.confupdtype = 'd' THEN
265-
'SET DEFAULT'
266-
WHEN fk_info.confupdtype = 'n' THEN
267-
'SET NULL'
268-
END AS onUpdate FROM (
242+
xc_constraint.conname, array_agg(xc_attr.attname ORDER BY xc_constraint.conkey::int[]) AS columns, pg_get_expr(pi.indpred, t.oid) AS predicate, am.amname AS method, pg_get_constraintdef(xc_constraint.oid) AS definition FROM pg_constraint AS xc_constraint
243+
INNER JOIN pg_attribute xc_attr ON xc_attr.attrelid = xc_constraint.conrelid
244+
AND xc_attr.attnum = ANY (xc_constraint.conkey)
245+
JOIN pg_index pi ON pi.indexrelid = xc_constraint.conindid
246+
JOIN pg_class cls ON cls.oid = pi.indexrelid
247+
JOIN pg_am am ON am.oid = cls.relam
248+
WHERE
249+
xc_constraint.conrelid = t.oid
250+
AND xc_constraint.contype = 'x' GROUP BY xc_constraint.oid, xc_constraint.conname, pi.indpred, pi.indexrelid, am.amname) AS xc_details), 'foreignKeys', (
251+
SELECT
252+
COALESCE(json_object_agg(fk_details.conname, json_build_object('name', fk_details.conname, 'columns', fk_details.columns, 'referencedTable', fk_details.referencedTable, 'referencedColumns', fk_details.referencedColumns, 'matchType', fk_details.matchType, 'onDelete', fk_details.onDelete, 'onUpdate', fk_details.onUpdate)), '{}'::json)
253+
FROM (
269254
SELECT
270-
fk_constraint.conname, fk_constraint.conrelid, fk_constraint.confrelid, fk_constraint.confkey, fk_cl.relname, fk_constraint.confmatchtype, fk_constraint.confdeltype, fk_constraint.confupdtype, array_agg(fk_attr.attname ORDER BY fk_attr.attname) AS columns FROM pg_constraint AS fk_constraint
271-
INNER JOIN pg_class fk_cl ON fk_constraint.confrelid = fk_cl.oid -- join the referenced table
272-
INNER JOIN pg_attribute fk_attr ON fk_attr.attrelid = fk_constraint.conrelid
273-
AND fk_attr.attnum = ANY (fk_constraint.conkey) -- join the columns of the referencing table
274-
WHERE
275-
fk_constraint.conrelid = t.oid
276-
AND fk_constraint.contype = 'f' GROUP BY fk_constraint.conrelid, fk_constraint.conname, fk_constraint.confrelid, fk_cl.relname, fk_constraint.confkey, fk_constraint.confmatchtype, fk_constraint.confdeltype, fk_constraint.confupdtype) AS fk_info
277-
INNER JOIN pg_attribute ref_attr ON ref_attr.attrelid = fk_info.confrelid
278-
AND ref_attr.attnum = ANY (fk_info.confkey) -- join the columns of the referenced table
279-
GROUP BY fk_info.conname, fk_info.conrelid, fk_info.columns, fk_info.confrelid, fk_info.confmatchtype, fk_info.confdeltype, fk_info.confupdtype, fk_info.relname) AS fk_details))), '{}'::json)
280-
FROM pg_class AS t
281-
INNER JOIN pg_namespace AS ns ON t.relnamespace = ns.oid
282-
LEFT JOIN pg_description AS descr ON t.oid = descr.objoid
283-
AND descr.objsubid = 0
284-
WHERE
285-
ns.nspname = schemaname
286-
AND t.relkind IN ('r', 'p') -- tables only (ignores views, materialized views & foreign tables)
255+
fk_info.conname AS conname, fk_info.columns AS columns, fk_info.relname AS referencedTable, array_agg(ref_attr.attname ORDER BY ref_attr.attname) AS referencedColumns, CASE WHEN fk_info.confmatchtype = 'f' THEN
256+
'FULL'
257+
WHEN fk_info.confmatchtype = 'p' THEN
258+
'PARTIAL'
259+
WHEN fk_info.confmatchtype = 's' THEN
260+
'SIMPLE'
261+
END AS matchType, CASE WHEN fk_info.confdeltype = 'a' THEN
262+
'NO ACTION'
263+
WHEN fk_info.confdeltype = 'r' THEN
264+
'RESTRICT'
265+
WHEN fk_info.confdeltype = 'c' THEN
266+
'CASCADE'
267+
WHEN fk_info.confdeltype = 'd' THEN
268+
'SET DEFAULT'
269+
WHEN fk_info.confdeltype = 'n' THEN
270+
'SET NULL'
271+
END AS onDelete, CASE WHEN fk_info.confupdtype = 'a' THEN
272+
'NO ACTION'
273+
WHEN fk_info.confupdtype = 'r' THEN
274+
'RESTRICT'
275+
WHEN fk_info.confupdtype = 'c' THEN
276+
'CASCADE'
277+
WHEN fk_info.confupdtype = 'd' THEN
278+
'SET DEFAULT'
279+
WHEN fk_info.confupdtype = 'n' THEN
280+
'SET NULL'
281+
END AS onUpdate FROM (
282+
SELECT
283+
fk_constraint.conname, fk_constraint.conrelid, fk_constraint.confrelid, fk_constraint.confkey, fk_cl.relname, fk_constraint.confmatchtype, fk_constraint.confdeltype, fk_constraint.confupdtype, array_agg(fk_attr.attname ORDER BY fk_attr.attname) AS columns FROM pg_constraint AS fk_constraint
284+
INNER JOIN pg_class fk_cl ON fk_constraint.confrelid = fk_cl.oid -- join the referenced table
285+
INNER JOIN pg_attribute fk_attr ON fk_attr.attrelid = fk_constraint.conrelid
286+
AND fk_attr.attnum = ANY (fk_constraint.conkey) -- join the columns of the referencing table
287+
WHERE
288+
fk_constraint.conrelid = t.oid
289+
AND fk_constraint.contype = 'f' GROUP BY fk_constraint.conrelid, fk_constraint.conname, fk_constraint.confrelid, fk_cl.relname, fk_constraint.confkey, fk_constraint.confmatchtype, fk_constraint.confdeltype, fk_constraint.confupdtype) AS fk_info
290+
INNER JOIN pg_attribute ref_attr ON ref_attr.attrelid = fk_info.confrelid
291+
AND ref_attr.attnum = ANY (fk_info.confkey) -- join the columns of the referenced table
292+
GROUP BY fk_info.conname, fk_info.conrelid, fk_info.columns, fk_info.confrelid, fk_info.confmatchtype, fk_info.confdeltype, fk_info.confupdtype, fk_info.relname) AS fk_details))), '{}'::json)
293+
FROM pg_class AS t
294+
INNER JOIN pg_namespace AS ns ON t.relnamespace = ns.oid
295+
LEFT JOIN pg_description AS descr ON t.oid = descr.objoid
296+
AND descr.objsubid = 0
297+
WHERE
298+
ns.nspname = schemaname
299+
AND t.relkind IN ('r', 'p') -- tables only (ignores views, materialized views & foreign tables)
287300
)) INTO tables;
288301
RETURN tables;
289302
END;

0 commit comments

Comments
 (0)