Skip to content

Commit 5663cd7

Browse files
authored
sql/postgres: initial support for exclude constraints (#2730)
1 parent 9e09f73 commit 5663cd7

File tree

6 files changed

+163
-37
lines changed

6 files changed

+163
-37
lines changed

sql/postgres/diff.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func (*diff) IndexAttrChanged(from, to []schema.Attr) bool {
189189
if indexNullsDistinct(to) != indexNullsDistinct(from) {
190190
return true
191191
}
192-
if uniqueConstChanged(from, to) {
192+
if uniqueConstChanged(from, to) || excludeConstChanged(from, to) {
193193
return true
194194
}
195195
var p1, p2 IndexPredicate
@@ -214,6 +214,14 @@ func (*diff) IndexPartAttrChanged(fromI, toI *schema.Index, i int) bool {
214214
if p1.NullsFirst != p2.NullsFirst || p1.NullsLast != p2.NullsLast {
215215
return true
216216
}
217+
_, ok1 := excludeConst(from.Attrs)
218+
_, ok2 := excludeConst(to.Attrs)
219+
if ok1 && ok2 {
220+
// In case the index(es) are EXCLUDE constraint, we compare its operator
221+
// (and not its class) because the class is derived from the operator.
222+
return sqlx.AttrOr(from.Attrs, &Operator{}).Name != sqlx.AttrOr(to.Attrs, &Operator{}).Name
223+
}
224+
// Op class for non-exclude.
217225
var fromOp, toOp IndexOpClass
218226
switch fromHas, toHas := sqlx.Has(from.Attrs, &fromOp), sqlx.Has(to.Attrs, &toOp); {
219227
case fromHas && toHas:
@@ -509,6 +517,16 @@ func uniqueConst(attrs []schema.Attr) (*Constraint, bool) {
509517
return nil, false
510518
}
511519

520+
// excludeConst returns the first exclude constraint from the given attributes.
521+
func excludeConst(attrs []schema.Attr) (*Constraint, bool) {
522+
for _, a := range attrs {
523+
if c, ok := a.(*Constraint); ok && c.IsExclude() {
524+
return c, true
525+
}
526+
}
527+
return nil, false
528+
}
529+
512530
func trimCast(s string) string {
513531
i := strings.LastIndex(s, "::")
514532
if i == -1 {

sql/postgres/driver_oss.go

+14
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,20 @@ func uniqueConstChanged(_, _ []schema.Attr) bool {
346346
return false
347347
}
348348

349+
func excludeConstChanged(_, _ []schema.Attr) bool {
350+
// Unsupported change in package mode (ariga.io/sql/postgres)
351+
// to keep BC with old versions.
352+
return false
353+
}
354+
355+
func convertExclude(schemahcl.Resource, *schema.Table) error {
356+
return nil // unimplemented.
357+
}
358+
349359
func detachCycles(changes []schema.Change) ([]schema.Change, error) {
350360
return sqlx.DetachCycles(changes)
351361
}
362+
363+
func excludeSpec(*sqlspec.Table, *sqlspec.Index, *schema.Index, *Constraint) error {
364+
return nil // unimplemented.
365+
}

sql/postgres/inspect.go

+46-6
Original file line numberDiff line numberDiff line change
@@ -500,13 +500,13 @@ func (i *inspect) addIndexes(s *schema.Schema, rows *sql.Rows, scope queryScope)
500500
names := make(map[string]*schema.Index)
501501
for rows.Next() {
502502
var (
503-
table, name, typ string
504-
uniq, primary, included, nullsnotdistinct bool
505-
desc, nullsfirst, nullslast, opcdefault sql.NullBool
506-
column, constraints, pred, expr, comment, options, opcname, opcparams sql.NullString
503+
table, name, typ string
504+
uniq, primary, included, nullsnotdistinct bool
505+
desc, nullsfirst, nullslast, opcdefault sql.NullBool
506+
column, constraints, pred, expr, comment, options, opcname, opcparams, exoper sql.NullString
507507
)
508508
if err := rows.Scan(
509-
&table, &name, &typ, &column, &included, &primary, &uniq, &constraints, &pred, &expr, &desc,
509+
&table, &name, &typ, &column, &included, &primary, &uniq, &exoper, &constraints, &pred, &expr, &desc,
510510
&nullsfirst, &nullslast, &comment, &options, &opcname, &opcdefault, &opcparams, &nullsnotdistinct,
511511
); err != nil {
512512
return fmt.Errorf("postgres: scanning indexes for schema %q: %w", s.Name, err)
@@ -566,6 +566,9 @@ func (i *inspect) addIndexes(s *schema.Schema, rows *sql.Rows, scope queryScope)
566566
NullsLast: nullslast.Bool,
567567
})
568568
}
569+
if sqlx.ValidString(exoper) {
570+
part.AddAttrs(NewOperator(i.schema, exoper.String))
571+
}
569572
switch {
570573
case included:
571574
c, ok := scope.column(table, column.String)
@@ -991,6 +994,21 @@ type (
991994
T string // c, f, p, u, t, x.
992995
}
993996

997+
// Operator describes an operator.
998+
// https://www.postgresql.org/docs/current/sql-createoperator.html
999+
Operator struct {
1000+
schema.Attr
1001+
schema.Object
1002+
// Schema where the operator is defined. If nil, the operator
1003+
// is not managed by the current scope.
1004+
Schema *schema.Schema
1005+
// Operator name. Might include the schema name if the schema
1006+
// is not managed by the current scope or extension based.
1007+
// e.g., "public.&&".
1008+
Name string
1009+
Attrs []schema.Attr
1010+
}
1011+
9941012
// Sequence defines (the supported) sequence options.
9951013
// https://postgresql.org/docs/current/sql-createsequence.html
9961014
Sequence struct {
@@ -1206,14 +1224,35 @@ func (o *ReferenceOption) Scan(v any) error {
12061224
return nil
12071225
}
12081226

1209-
// IsUnique reports if the type is unique constraint.
1227+
// IsUnique reports if the type is a unique constraint.
12101228
func (c Constraint) IsUnique() bool { return strings.ToLower(c.T) == "u" }
12111229

1230+
// IsExclude reports if the type is an exclude constraint.
1231+
func (c Constraint) IsExclude() bool { return strings.ToLower(c.T) == "x" }
1232+
12121233
// UniqueConstraint returns constraint with type "u".
12131234
func UniqueConstraint(name string) *Constraint {
12141235
return &Constraint{T: "u", N: name}
12151236
}
12161237

1238+
// ExcludeConstraint returns constraint with type "x".
1239+
func ExcludeConstraint(name string) *Constraint {
1240+
return &Constraint{T: "x", N: name}
1241+
}
1242+
1243+
// NewOperator returns the string representation of the operator.
1244+
func NewOperator(scope string, name string) *Operator {
1245+
// When scanned from the database, the operator is returned as: "<schema>.<operator>".
1246+
// The common case is that operators are the default and defined in pg_catalog, or are
1247+
// installed by extensions.
1248+
if parts := strings.FieldsFunc(name, func(r rune) bool {
1249+
return r == '.'
1250+
}); len(parts) == 2 && scope == "" || parts[0] == "pg_catalog" || parts[0] == scope {
1251+
return &Operator{Name: parts[1]}
1252+
}
1253+
return &Operator{Name: name}
1254+
}
1255+
12171256
// IntegerType returns the underlying integer type this serial type represents.
12181257
func (s *SerialType) IntegerType() *schema.IntegerType {
12191258
t := &schema.IntegerType{T: TypeInteger}
@@ -1615,6 +1654,7 @@ SELECT
16151654
%s AS included,
16161655
idx.indisprimary AS primary,
16171656
idx.indisunique AS unique,
1657+
(CASE WHEN idx.indisexclusion THEN (SELECT conexclop[idx.ord]::regoper FROM pg_constraint WHERE conindid = idx.indexrelid) END) AS excoper,
16181658
con.nametypes AS constraints,
16191659
pg_get_expr(idx.indpred, idx.indrelid) AS predicate,
16201660
pg_get_indexdef(idx.indexrelid, idx.ord, false) AS expression,

sql/postgres/inspect_test.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -178,23 +178,23 @@ users | ts | tsvector | tsvector | NO |
178178
m.ExpectQuery(queryIndexes).
179179
WithArgs("public", "users").
180180
WillReturnRows(sqltest.Rows(`
181-
table_name | index_name | index_type | column_name | included | primary | unique | constraints | predicate | expression | desc | nulls_first | nulls_last | comment | options | opclass_name | opclass_default | opclass_params | indnullsnotdistinct
182-
----------------+-----------------+-------------+-------------+----------+---------+--------+-----------------+-----------------------+---------------------------+------+-------------+------------+-----------+---------------------------------------+-------------------+-----------------+----------------+---------------------
183-
users | idx | hash | | f | f | f | | | "left"((c11)::text, 100) | t | t | f | boring | | int4_ops | t | | f
184-
users | idx1 | btree | | f | f | f | | (id <> NULL::integer) | "left"((c11)::text, 100) | t | t | f | | | int4_ops | t | | f
185-
users | t1_c1_key | btree | c1 | f | f | t | {"name": "u"} | | c1 | t | t | f | | | int4_ops | t | | f
186-
users | t1_pkey | btree | id | f | t | t | {"t_pkey": "p"} | | id | t | f | f | | | int4_ops | t | | f
187-
users | idx4 | btree | c1 | f | f | t | | | c1 | f | f | f | | | int4_ops | t | | f
188-
users | idx4 | btree | id | f | f | t | | | id | f | f | t | | | int4_ops | t | | f
189-
users | idx5 | btree | c1 | f | f | t | | | c1 | f | f | f | | | int4_ops | t | | f
190-
users | idx5 | btree | | f | f | t | | | coalesce(parent_id, 0) | f | f | f | | | int4_ops | t | | f
191-
users | idx6 | brin | c1 | f | f | t | | | | f | f | f | | {autosummarize=true,pages_per_range=2}| int4_ops | t | | f
192-
users | idx2 | btree | | f | f | f | | | ((c * 2)) | f | f | t | | | int4_ops | t | | f
193-
users | idx2 | btree | c1 | f | f | f | | | c | f | f | t | | | int4_ops | t | | f
194-
users | idx2 | btree | id | f | f | f | | | d | f | f | t | | | int4_ops | t | | f
195-
users | idx2 | btree | c1 | t | f | f | | | c | | | | | | int4_ops | t | | f
196-
users | idx2 | btree | parent_id | t | f | f | | | d | | | | | | int4_ops | t | | f
197-
users | tsx | gist | ts | f | f | f | | | ts | | | | | | tsvector_ops | f | {siglen=1} | f
181+
table_name | index_name | index_type | column_name | included | primary | unique | opexpr | constraints | predicate | expression | desc | nulls_first | nulls_last | comment | options | opclass_name | opclass_default | opclass_params | indnullsnotdistinct
182+
----------------+-----------------+-------------+-------------+----------+---------+--------+--------+-----------------+-----------------------+---------------------------+------+-------------+------------+-----------+---------------------------------------+-------------------+-----------------+----------------+---------------------
183+
users | idx | hash | | f | f | f | | | | "left"((c11)::text, 100) | t | t | f | boring | | int4_ops | t | | f
184+
users | idx1 | btree | | f | f | f | | | (id <> NULL::integer) | "left"((c11)::text, 100) | t | t | f | | | int4_ops | t | | f
185+
users | t1_c1_key | btree | c1 | f | f | t | | {"name": "u"} | | c1 | t | t | f | | | int4_ops | t | | f
186+
users | t1_pkey | btree | id | f | t | t | | {"t_pkey": "p"} | | id | t | f | f | | | int4_ops | t | | f
187+
users | idx4 | btree | c1 | f | f | t | | | | c1 | f | f | f | | | int4_ops | t | | f
188+
users | idx4 | btree | id | f | f | t | | | | id | f | f | t | | | int4_ops | t | | f
189+
users | idx5 | btree | c1 | f | f | t | | | | c1 | f | f | f | | | int4_ops | t | | f
190+
users | idx5 | btree | | f | f | t | | | | coalesce(parent_id, 0) | f | f | f | | | int4_ops | t | | f
191+
users | idx6 | brin | c1 | f | f | t | | | | | f | f | f | | {autosummarize=true,pages_per_range=2}| int4_ops | t | | f
192+
users | idx2 | btree | | f | f | f | | | | ((c * 2)) | f | f | t | | | int4_ops | t | | f
193+
users | idx2 | btree | c1 | f | f | f | | | | c | f | f | t | | | int4_ops | t | | f
194+
users | idx2 | btree | id | f | f | f | | | | d | f | f | t | | | int4_ops | t | | f
195+
users | idx2 | btree | c1 | t | f | f | | | | c | | | | | | int4_ops | t | | f
196+
users | idx2 | btree | parent_id | t | f | f | | | | d | | | | | | int4_ops | t | | f
197+
users | tsx | gist | ts | f | f | f | | | | ts | | | | | | tsvector_ops | f | {siglen=1} | f
198198
`))
199199
m.noFKs()
200200
m.noChecks()

0 commit comments

Comments
 (0)