diff --git a/pkg/sql/logictest/testdata/logic_test/unique b/pkg/sql/logictest/testdata/logic_test/unique index acf32539420c..6abbf7863fa7 100644 --- a/pkg/sql/logictest/testdata/logic_test/unique +++ b/pkg/sql/logictest/testdata/logic_test/unique @@ -144,6 +144,12 @@ INSERT INTO uniq SELECT k, v, w, x, y FROM other statement ok INSERT INTO uniq VALUES (100, 10, 1), (200, 20, 2), (400, 40, 4) ON CONFLICT (w) DO NOTHING +# On conflict do nothing with constant input, conflict on UNIQUE WITHOUT INDEX +# column, conflicting insert rows. +# Only row (500, 50, 50) is inserted. +statement ok +INSERT INTO uniq VALUES (500, 50, 50), (600, 50, 50) ON CONFLICT (w) DO NOTHING + # On conflict do nothing with constant input, no conflict columns. # The only row that is successfully inserted here is (20, 20, 20, 20, 20). statement ok @@ -163,6 +169,7 @@ k v w x y 7 7 NULL 2 NULL 20 20 20 20 20 400 40 4 NULL 5 +500 50 50 NULL 5 # Insert into a table in which the primary key overlaps some of the unique @@ -281,6 +288,30 @@ INSERT INTO uniq_partial VALUES (NULL, 5), (5, 5), (NULL, 5) statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. INSERT INTO uniq_partial SELECT w, x FROM other +statement error there is no unique or exclusion constraint matching the ON CONFLICT specification +INSERT INTO uniq_partial VALUES (1, 6), (6, 6) ON CONFLICT (a) DO NOTHING + +# On conflict do nothing with constant input, conflict on UNIQUE WITHOUT INDEX +# column. Only the non-conflicting row (6, 6) is inserted. +statement ok +INSERT INTO uniq_partial VALUES (1, 6), (6, 6) ON CONFLICT (a) WHERE b > 0 DO NOTHING + +# On conflict do nothing with constant input, conflict on UNIQUE WITHOUT INDEX +# column, conflicting insert rows. +# Only rows (7, 7) and (7, -7) are inserted. +statement ok +INSERT INTO uniq_partial VALUES (7, 7), (7, 8), (7, -7) ON CONFLICT (a) WHERE b > 0 DO NOTHING + +# On conflict do nothing with constant input, no conflict columns. +# Only rows (9, 9) and (9, -9) are inserted. +statement ok +INSERT INTO uniq_partial VALUES (1, 9), (9, 9), (9, 10), (9, -9) ON CONFLICT DO NOTHING + +# On conflict do nothing with non-constant input. +# The (1, 10) row is not inserted because of a conflict with (1, 1). +statement ok +INSERT INTO uniq_partial SELECT w, k FROM other ON CONFLICT DO NOTHING + query II colnames,rowsort SELECT * FROM uniq_partial ---- @@ -290,6 +321,11 @@ a b 1 -3 2 2 5 5 +6 6 +7 7 +7 -7 +9 9 +9 -9 NULL 5 NULL 5 @@ -335,6 +371,7 @@ k v w x y 11 11 10 NULL 2 20 20 20 20 20 400 40 4 NULL 5 +500 50 50 NULL 5 # Update a table with multiple primary key columns. @@ -489,6 +526,7 @@ k v w x y 20 20 20 20 20 100 100 1 NULL 5 400 40 4 NULL 5 +500 50 50 NULL 5 # Upsert into a table in which the primary key overlaps some of the unique diff --git a/pkg/sql/opt/exec/execbuilder/testdata/unique b/pkg/sql/opt/exec/execbuilder/testdata/unique index 4fb2e7c4db0c..f9f2fbf1b712 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/unique +++ b/pkg/sql/opt/exec/execbuilder/testdata/unique @@ -847,7 +847,8 @@ vectorized: true columns: (column9, column1, column2, column10, check1) label: buffer 1 -# Test that we use the index when available for the ON CONFLICT checks. +# Test that we use the index when available for de-duplicating INSERT ON +# CONFLICT DO NOTHING rows before inserting. query T EXPLAIN (VERBOSE) INSERT INTO uniq_enum VALUES ('us-west', 'foo', 1, 1), ('us-east', 'bar', 2, 2) ON CONFLICT DO NOTHING @@ -1090,6 +1091,180 @@ vectorized: true └── • scan buffer label: buffer 1 +# Use all the unique indexes and constraints as arbiters for DO NOTHING with no +# conflict columns. +# TODO(mgartner): we should be able to remove the unique checks in this case +# (see #59119). +query T +EXPLAIN (VERBOSE) INSERT INTO uniq_partial VALUES (1, 2, 3) ON CONFLICT DO NOTHING +---- +distribution: local +vectorized: true +· +• root +│ columns: () +│ +├── • insert +│ │ columns: () +│ │ estimated row count: 0 (missing stats) +│ │ into: uniq_partial(k, a, b) +│ │ arbiter indexes: primary +│ │ arbiter constraints: unique_a, unique_b +│ │ +│ └── • buffer +│ │ columns: (column1, column2, column3) +│ │ label: buffer 1 +│ │ +│ └── • project +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ +│ └── • distinct +│ │ columns: (upsert_partial_constraint_distinct1, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ distinct on: upsert_partial_constraint_distinct1 +│ │ nulls are distinct +│ │ +│ └── • render +│ │ columns: (upsert_partial_constraint_distinct1, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ render upsert_partial_constraint_distinct1: (column3 > 0) OR CAST(NULL AS BOOL) +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • distinct +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ distinct on: upsert_partial_constraint_distinct0 +│ │ nulls are distinct +│ │ +│ └── • render +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ render upsert_partial_constraint_distinct0: (column3 > 0) OR CAST(NULL AS BOOL) +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • hash join (right anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ equality: (b) = (column3) +│ │ right cols are key +│ │ +│ ├── • filter +│ │ │ columns: (b) +│ │ │ estimated row count: 333 (missing stats) +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ columns: (b) +│ │ estimated row count: 1,000 (missing stats) +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • hash join (right anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ equality: (a) = (column2) +│ │ right cols are key +│ │ pred: column3 > 0 +│ │ +│ ├── • filter +│ │ │ columns: (a, b) +│ │ │ estimated row count: 333 (missing stats) +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ columns: (a, b) +│ │ estimated row count: 1,000 (missing stats) +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • cross join (anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ +│ ├── • values +│ │ columns: (column1, column2, column3) +│ │ size: 3 columns, 1 row +│ │ row 0, expr 0: 1 +│ │ row 0, expr 1: 2 +│ │ row 0, expr 2: 3 +│ │ +│ └── • scan +│ columns: (k) +│ estimated row count: 1 (missing stats) +│ table: uniq_partial@primary +│ spans: /1/0-/1/1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ columns: () +│ │ +│ └── • hash join (right semi) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ equality: (a) = (column2) +│ │ right cols are key +│ │ pred: column1 != k +│ │ +│ ├── • filter +│ │ │ columns: (k, a, b) +│ │ │ estimated row count: 333 (missing stats) +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ columns: (k, a, b) +│ │ estimated row count: 1,000 (missing stats) +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • filter +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ filter: column3 > 0 +│ │ +│ └── • scan buffer +│ columns: (column1, column2, column3) +│ estimated row count: 0 (missing stats) +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ columns: () + │ + └── • hash join (right semi) + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ equality: (b) = (column3) + │ right cols are key + │ pred: column1 != k + │ + ├── • filter + │ │ columns: (k, b) + │ │ estimated row count: 333 (missing stats) + │ │ filter: b > 0 + │ │ + │ └── • scan + │ columns: (k, b) + │ estimated row count: 1,000 (missing stats) + │ table: uniq_partial@primary + │ spans: FULL SCAN + │ + └── • filter + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ filter: column3 > 0 + │ + └── • scan buffer + columns: (column1, column2, column3) + estimated row count: 0 (missing stats) + label: buffer 1 + # Insert with non-constant input. query T EXPLAIN INSERT INTO uniq_partial SELECT k, v, w FROM other @@ -1290,6 +1465,116 @@ vectorized: true columns: (column1, column2, column3, check1, partial_index_put1) label: buffer 1 +# Test that we use the partial index when available for de-duplicating INSERT ON +# CONFLICT DO NOTHING rows before inserting. +query T +EXPLAIN (VERBOSE) INSERT INTO uniq_partial_enum VALUES ('us-west', 1, 'foo'), ('us-east', 2, 'bar') +ON CONFLICT DO NOTHING +---- +distribution: local +vectorized: true +· +• root +│ columns: () +│ +├── • insert +│ │ columns: () +│ │ estimated row count: 0 (missing stats) +│ │ into: uniq_partial_enum(r, i, s) +│ │ arbiter indexes: primary +│ │ arbiter constraints: unique_i +│ │ +│ └── • buffer +│ │ columns: (column1, column2, column3, check1, partial_index_put1) +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ columns: (column1, column2, column3, check1, partial_index_put1) +│ │ estimated row count: 0 (missing stats) +│ │ render partial_index_put1: column3 IN ('bar', 'baz', 'foo') +│ │ render check1: column1 IN ('us-east', 'us-west', 'eu-west') +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • distinct +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ distinct on: upsert_partial_constraint_distinct0, column2 +│ │ nulls are distinct +│ │ +│ └── • render +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ render upsert_partial_constraint_distinct0: (column3 IN ('bar', 'baz', 'foo')) OR CAST(NULL AS BOOL) +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • lookup join (anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ table: uniq_partial_enum@uniq_partial_enum_r_i_idx (partial index) +│ │ lookup condition: (column2 = i) AND (r IN ('us-east', 'us-west', 'eu-west')) +│ │ pred: column3 IN ('bar', 'baz', 'foo') +│ │ +│ └── • lookup join (anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ table: uniq_partial_enum@primary +│ │ equality: (column1, column2) = (r,i) +│ │ equality cols are key +│ │ +│ └── • values +│ columns: (column1, column2, column3) +│ size: 3 columns, 2 rows +│ row 0, expr 0: 'us-west' +│ row 0, expr 1: 1 +│ row 0, expr 2: 'foo' +│ row 1, expr 0: 'us-east' +│ row 1, expr 1: 2 +│ row 1, expr 2: 'bar' +│ +└── • constraint-check + │ + └── • error if rows + │ columns: () + │ + └── • project + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ + └── • lookup join (semi) + │ columns: ("lookup_join_const_col_@22", column1, column2, column3) + │ table: uniq_partial_enum@uniq_partial_enum_r_i_idx (partial index) + │ equality: (lookup_join_const_col_@22, column2) = (r,i) + │ equality cols are key + │ pred: column1 != r + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@22", column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ + ├── • values + │ columns: ("lookup_join_const_col_@22") + │ size: 1 column, 3 rows + │ row 0, expr 0: 'us-east' + │ row 1, expr 0: 'us-west' + │ row 2, expr 0: 'eu-west' + │ + └── • filter + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ filter: column3 IN ('bar', 'baz', 'foo') + │ + └── • project + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ + └── • scan buffer + columns: (column1, column2, column3, check1, partial_index_put1) + label: buffer 1 + # -- Tests with UPDATE -- subtest Update diff --git a/pkg/sql/opt/optbuilder/insert.go b/pkg/sql/opt/optbuilder/insert.go index 0ed852c8d8a5..34d2156a87f8 100644 --- a/pkg/sql/opt/optbuilder/insert.go +++ b/pkg/sql/opt/optbuilder/insert.go @@ -690,24 +690,27 @@ func (mb *mutationBuilder) buildInputForDoNothing( // each one. arbiterIndexes.ForEach(func(idx int) { index := mb.tab.Index(idx) - _, isPartial := index.Predicate() - var predExpr tree.Expr - if isPartial { - predExpr = mb.parsePartialIndexPredicateExpr(idx) + var pred tree.Expr + if _, isPartial := index.Predicate(); isPartial { + pred = mb.parsePartialIndexPredicateExpr(idx) } mb.buildAntiJoinForDoNothingArbiter( inScope, getIndexLaxKeyOrdinals(index), - predExpr, + pred, ) }) arbiterConstraints.ForEach(func(uc int) { uniqueConstraint := mb.tab.Unique(uc) + var pred tree.Expr + if _, isPartial := uniqueConstraint.Predicate(); isPartial { + pred = mb.parseUniqueConstraintPredicateExpr(uc) + } mb.buildAntiJoinForDoNothingArbiter( inScope, getUniqueConstraintOrdinals(mb.tab, uniqueConstraint), - nil, /* predExpr */ + pred, ) }) @@ -716,15 +719,16 @@ func (mb *mutationBuilder) buildInputForDoNothing( // the anti-joins created above, to avoid removing valid rows (see #59125). arbiterIndexes.ForEach(func(idx int) { index := mb.tab.Index(idx) - _, isPartial := index.Predicate() // If the index is a partial index, project a new column that allows the // UpsertDistinctOn to only de-duplicate insert rows that satisfy the - // partial index predicate. See projectPartialIndexDistinctColumn for more + // partial index predicate. See projectPartialArbiterDistinctColumn for more // details. var partialIndexDistinctCol *scopeColumn - if isPartial { - partialIndexDistinctCol = mb.projectPartialIndexDistinctColumn(insertColScope, idx) + if _, isPartial := index.Predicate(); isPartial { + alias := fmt.Sprintf("upsert_partial_index_distinct%d", idx) + pred := mb.parsePartialIndexPredicateExpr(idx) + partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn(insertColScope, pred, alias) } mb.buildDistinctOnForDoNothingArbiter( @@ -735,10 +739,21 @@ func (mb *mutationBuilder) buildInputForDoNothing( }) arbiterConstraints.ForEach(func(uc int) { uniqueConstraint := mb.tab.Unique(uc) + + // If the constraint is partial, project a new column that allows the + // UpsertDistinctOn to only de-duplicate insert rows that satisfy the + // partial predicate. See projectPartialArbiterDistinctColumn for more + // details. + var partialIndexDistinctCol *scopeColumn + if _, isPartial := uniqueConstraint.Predicate(); isPartial { + alias := fmt.Sprintf("upsert_partial_constraint_distinct%d", uc) + pred := mb.parseUniqueConstraintPredicateExpr(uc) + partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn(insertColScope, pred, alias) + } mb.buildDistinctOnForDoNothingArbiter( insertColScope, getUniqueConstraintOrdinals(mb.tab, uniqueConstraint), - nil, /* partialIndexDistinctCol */ + partialIndexDistinctCol, ) }) @@ -925,23 +940,24 @@ func (mb *mutationBuilder) buildInputForUpsert( index := mb.tab.Index(idx) _, isPartial := index.Predicate() - var predExpr tree.Expr + var pred tree.Expr if isPartial { - predExpr = mb.parsePartialIndexPredicateExpr(idx) + pred = mb.parsePartialIndexPredicateExpr(idx) } // If the index is a partial index, project a new column that allows the // UpsertDistinctOn to only de-duplicate insert rows that satisfy the - // partial index predicate. See projectPartialIndexDistinctColumn for more - // details. + // partial index predicate. See projectPartialArbiterDistinctColumn for + // more details. var partialIndexDistinctCol *scopeColumn if isPartial { - partialIndexDistinctCol = mb.projectPartialIndexDistinctColumn(insertColScope, idx) + alias := fmt.Sprintf("upsert_partial_index_distinct%d", idx) + partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn(insertColScope, pred, alias) } buildInputForArbiter(func() *scopeColumn { return &mb.fetchScope.cols[findNotNullIndexCol(index)] - }, predExpr, partialIndexDistinctCol) + }, pred, partialIndexDistinctCol) } else if arbiterConstraints.Len() > 0 { buildInputForArbiter(func() *scopeColumn { // Use the primary index, since we don't know at this point whether diff --git a/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go b/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go index cad303940d15..f6e4054fcf4b 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go +++ b/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go @@ -11,8 +11,6 @@ package optbuilder import ( - "fmt" - "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" @@ -42,26 +40,38 @@ import ( // 2. If it is a partial index, its predicate must be implied by the // arbiterPredicate supplied by the user. // -// An arbiter constraint must have columns that match the columns in -// conflictOrds. Unique constraints without an index cannot be "partial", -// so they have no predicate to check for implication with arbiterPredicate. -// TODO(rytaft): revisit this for the case of implicitly partitioned partial -// unique indexes (see #59195). +// An arbiter constraint: +// +// 1. Must have columns that match the columns in conflictOrds. +// 2. If it is a partial constraint, its predicate must be implied by the +// arbiterPredicate supplied by the user. // // If conflictOrds is empty then all unique indexes and unique without index // constraints are returned as arbiters. This is required to support a // DO NOTHING with no ON CONFLICT columns. In this case, all unique indexes // and constraints are used to check for conflicts. // -// If a non-partial or pseudo-partial arbiter index is found, the return set -// contains only that index. No other arbiter is necessary because a non-partial -// or pseudo-partial index guarantees uniqueness of its columns across all -// rows. +// If conflictOrds is non-empty, there is an intentional preference for certain +// types of arbiters. Indexes are preferred over constraints because they will +// likely lead to a more efficient query plan. Non-partial or pseudo-partial +// indexes and constraints are preferred over partial indexes and constraints +// because a non-partial or pseudo-partial index or constraint guarantees +// uniqueness of its columns across all rows; there is no need for an additional +// arbiter for a subset of rows. If there are no non-partial or pseudo-partial +// indexes or constraints found, all valid partial indexes and partial +// constraints are returned so that uniqueness is guaranteed on the respective +// subsets of rows. In summary, if conflictOrds is non-empty, this function: +// +// 1. Returns a single non-partial or pseudo-partial arbiter index, if found. +// 2. Return a single non-partial or pseudo-partial arbiter constraint, if +// found. +// 3. Otherwise, returns all partial arbiter indexes and constraints. +// func (mb *mutationBuilder) arbiterIndexesAndConstraints( conflictOrds util.FastIntSet, arbiterPredicate tree.Expr, ) (indexes util.FastIntSet, uniqueConstraints util.FastIntSet) { - // If conflictOrds is empty, then all unique indexes and unique without index - // constraints are arbiters. + // If conflictOrds is empty, then all unique indexes and unique without + // index constraints are arbiters. if conflictOrds.Empty() { for idx, idxCount := 0, mb.tab.IndexCount(); idx < idxCount; idx++ { if mb.tab.Index(idx).IsUnique() { @@ -96,12 +106,10 @@ func (mb *mutationBuilder) arbiterIndexesAndConstraints( continue } - _, isPartial := index.Predicate() - // If the index is not a partial index, it can always be an arbiter. // Furthermore, it is the only arbiter needed because it guarantees // uniqueness of its columns across all rows. - if !isPartial { + if _, isPartial := index.Predicate(); !isPartial { return util.MakeFastIntSet(idx), util.FastIntSet{} } @@ -133,22 +141,47 @@ func (mb *mutationBuilder) arbiterIndexesAndConstraints( continue } - // Determine whether the conflict columns match the columns in the unique - // constraint. If not, the constraint cannot be an arbiter. + // Determine whether the conflict columns match the columns in the + // unique constraint. If not, the constraint cannot be an arbiter. We + // check the number of columns first to avoid unnecessarily collecting + // the unique constraint ordinals and determining set equality. + if uniqueConstraint.ColumnCount() != conflictOrds.Len() { + continue + } ucOrds := getUniqueConstraintOrdinals(mb.tab, uniqueConstraint) - if ucOrds.Equals(conflictOrds) { + if !ucOrds.Equals(conflictOrds) { + continue + } + + // If the unique constraint is not partial, it should be returned + // without any partial index arbiters. + if _, isPartial := uniqueConstraint.Predicate(); !isPartial { return util.FastIntSet{}, util.MakeFastIntSet(uc) } + + // If the constraint is a pseudo-partial unique constraint, it can + // always be an arbiter. It should be returned without any partial index + // arbiters. + pred := h.partialUniqueConstraintPredicate(uc) + if pred.IsTrue() { + return util.FastIntSet{}, util.MakeFastIntSet(uc) + } + + // If the unique constraint is partial, then it can only be an arbiter + // if the arbiterPredicate implies it. + if h.predicateIsImpliedByArbiterPredicate(pred) { + uniqueConstraints.Add(uc) + } } - // There are no full indexes or constraints, so return any partial indexes - // that were found. - if !indexes.Empty() { - return indexes, util.FastIntSet{} + // Err if we did not previously return and did not find partial indexes or + // partial unique constraints. + if indexes.Empty() && uniqueConstraints.Empty() { + panic(pgerror.Newf(pgcode.InvalidColumnReference, + "there is no unique or exclusion constraint matching the ON CONFLICT specification")) } - panic(pgerror.Newf(pgcode.InvalidColumnReference, - "there is no unique or exclusion constraint matching the ON CONFLICT specification")) + return indexes, uniqueConstraints } // buildAntiJoinForDoNothingArbiter builds an anti-join for a single arbiter index @@ -158,11 +191,11 @@ func (mb *mutationBuilder) arbiterIndexesAndConstraints( // // - columnOrds is the set of table column ordinals that the arbiter // guarantees uniqueness of. -// - predExpr is the partial index predicate. If the arbiter is not a partial -// index, predExpr is nil. +// - pred is the partial index or constraint predicate. If the arbiter is +// not a partial index or constraint, pred is nil. // func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( - inScope *scope, columnOrds util.FastIntSet, predExpr tree.Expr, + inScope *scope, columnOrds util.FastIntSet, pred tree.Expr, ) { // Build the right side of the anti-join. Use a new metadata instance // of the mutation table so that a different set of column IDs are used for @@ -184,8 +217,8 @@ func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( // partial index cannot conflict with insert rows. Therefore, a Select // wraps the scan on the right side of the anti-join with the partial // index predicate expression as the filter. - if predExpr != nil { - texpr := fetchScope.resolveAndRequireType(predExpr, types.Bool) + if pred != nil { + texpr := fetchScope.resolveAndRequireType(pred, types.Bool) predScalar := mb.b.buildScalar(texpr, fetchScope, nil, nil, nil) fetchScope.expr = mb.b.factory.ConstructSelect( fetchScope.expr, @@ -216,8 +249,8 @@ func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( // satisfy the partial index predicate cannot conflict with existing // rows in the unique partial index. Therefore, the partial index // predicate expression is added to the ON filters. - if predExpr != nil { - texpr := mb.outScope.resolveAndRequireType(predExpr, types.Bool) + if pred != nil { + texpr := mb.outScope.resolveAndRequireType(pred, types.Bool) predScalar := mb.b.buildScalar(texpr, mb.outScope, nil, nil, nil) on = append(on, mb.b.factory.ConstructFiltersItem(predScalar)) } @@ -236,12 +269,13 @@ func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( // // - columnOrds is the set of table column ordinals that the arbiter // guarantees uniqueness of. -// - partialIndexDistinctCol is a column that allows the UpsertDistinctOn to -// only de-duplicate insert rows that satisfy the partial index predicate. -// If the arbiter is not a partial index, partialIndexDistinctCol is nil. +// - partialArbiterDistinctCol is a column that allows the UpsertDistinctOn to +// only de-duplicate insert rows that satisfy the partial index or +// constraint predicate. If the arbiter is not a partial index or +// constraint, partialArbiterDistinctCol is nil. // func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( - insertColScope *scope, colOrds util.FastIntSet, partialIndexDistinctCol *scopeColumn, + insertColScope *scope, colOrds util.FastIntSet, partialArbiterDistinctCol *scopeColumn, ) { // Add an UpsertDistinctOn operator to ensure there are no duplicate input // rows for this arbiter. Duplicate rows can trigger conflict errors at @@ -250,8 +284,8 @@ func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( for i, ok := colOrds.Next(0); ok; i, ok = colOrds.Next(i + 1) { conflictCols.Add(mb.insertColIDs[i]) } - if partialIndexDistinctCol != nil { - conflictCols.Add(partialIndexDistinctCol.id) + if partialArbiterDistinctCol != nil { + conflictCols.Add(partialArbiterDistinctCol.id) } // Treat NULL values as distinct from one another. And if duplicates are @@ -259,8 +293,8 @@ func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( mb.outScope = mb.b.buildDistinctOn( conflictCols, mb.outScope, true /* nullsAreDistinct */, "" /* errorOnDup */) - // Remove the partialIndexDistinctCol from the output. - if partialIndexDistinctCol != nil { + // Remove the partialArbiterDistinctCol from the output. + if partialArbiterDistinctCol != nil { projectionScope := mb.outScope.replace() projectionScope.appendColumnsFromScope(insertColScope) mb.b.constructProjectForScope(mb.outScope, projectionScope) @@ -268,10 +302,11 @@ func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( } } -// projectPartialIndexDistinctColumn projects a column to facilitate +// projectPartialArbiterDistinctColumn projects a column to facilitate // de-duplicating insert rows for UPSERT/INSERT ON CONFLICT when the arbiter -// index is a partial index. Only those insert rows that satisfy the partial -// index predicate should be de-duplicated. For example: +// index or constraint is partial. Only those insert rows that satisfy the +// partial index or unique constraint predicate should be de-duplicated. For +// example: // // CREATE TABLE t (a INT, b INT, UNIQUE INDEX (a) WHERE b > 0) // INSERT INTO t VALUES (1, 1), (1, 2), (1, -1), (1, -10) ON CONFLICT DO NOTHING @@ -298,20 +333,18 @@ func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( // NULL). // // The newly project scopeColumn is returned. -func (mb *mutationBuilder) projectPartialIndexDistinctColumn( - insertScope *scope, idx cat.IndexOrdinal, +func (mb *mutationBuilder) projectPartialArbiterDistinctColumn( + insertScope *scope, pred tree.Expr, alias string, ) *scopeColumn { projectionScope := mb.outScope.replace() projectionScope.appendColumnsFromScope(insertScope) - predExpr := mb.parsePartialIndexPredicateExpr(idx) expr := &tree.OrExpr{ - Left: predExpr, + Left: pred, Right: tree.DNull, } texpr := insertScope.resolveAndRequireType(expr, types.Bool) - alias := fmt.Sprintf("upsert_partial_index_distinct%d", idx) scopeCol := projectionScope.addColumn(alias, texpr) mb.b.buildScalar(texpr, mb.outScope, projectionScope, scopeCol, nil) @@ -321,8 +354,8 @@ func (mb *mutationBuilder) projectPartialIndexDistinctColumn( return scopeCol } -// arbiterPredicateHelper is used to determine if a partial index can be used as -// an arbiter. +// arbiterPredicateHelper is used to determine if a partial index or constraint +// can be used as an arbiter. type arbiterPredicateHelper struct { mb *mutationBuilder tabMeta *opt.TableMeta @@ -390,6 +423,21 @@ func (h *arbiterPredicateHelper) partialIndexPredicate(idx cat.IndexOrdinal) mem return *pred.(*memo.FiltersExpr) } +// partialUniqueConstraintPredicate returns the predicate of the given unique +// constraint. +func (h *arbiterPredicateHelper) partialUniqueConstraintPredicate( + idx cat.UniqueOrdinal, +) memo.FiltersExpr { + // Build and normalize the unique constraint predicate expression. + pred, err := h.mb.b.buildPartialIndexPredicate( + h.tabMeta, h.tableScope(), h.mb.parseUniqueConstraintPredicateExpr(idx), "unique constraint predicate", + ) + if err != nil { + panic(err) + } + return pred +} + // arbiterFilters returns a scalar expression representing the arbiter // predicate. If the arbiter predicate contains non-immutable operators, // ok=true is returned. diff --git a/pkg/sql/opt/optbuilder/testdata/unique-checks-insert b/pkg/sql/opt/optbuilder/testdata/unique-checks-insert index 84327424d230..c2df816c63d0 100644 --- a/pkg/sql/opt/optbuilder/testdata/unique-checks-insert +++ b/pkg/sql/opt/optbuilder/testdata/unique-checks-insert @@ -893,6 +893,150 @@ insert uniq_partial ├── (1, NULL::INT8, 1) └── (2, NULL::INT8, 2) +# Use all the unique constraint as an arbiter for DO NOTHING with no conflict +# columns. +# TODO(rytaft/mgartner): we should be able to remove the unique checks in this +# case (see #59119). +build +INSERT INTO uniq_partial VALUES (1, 2, 3), (2, 2, 3) ON CONFLICT DO NOTHING +---- +insert uniq_partial + ├── columns: + ├── arbiter indexes: primary + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_partial.k:1 + │ ├── column2:6 => uniq_partial.a:2 + │ └── column3:7 => uniq_partial.b:3 + ├── input binding: &1 + ├── project + │ ├── columns: column1:5!null column2:6!null column3:7!null + │ └── upsert-distinct-on + │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_constraint_distinct0:16 + │ ├── grouping columns: column2:6!null upsert_partial_constraint_distinct0:16 + │ ├── project + │ │ ├── columns: upsert_partial_constraint_distinct0:16 column1:5!null column2:6!null column3:7!null + │ │ ├── upsert-distinct-on + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ ├── grouping columns: column1:5!null + │ │ │ ├── anti-join (hash) + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ ├── anti-join (hash) + │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ ├── values + │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ ├── (1, 2, 3) + │ │ │ │ │ │ └── (2, 2, 3) + │ │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ │ └── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10 + │ │ │ │ │ └── filters + │ │ │ │ │ └── column1:5 = uniq_partial.k:8 + │ │ │ │ ├── select + │ │ │ │ │ ├── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14!null + │ │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ │ └── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14 + │ │ │ │ │ └── filters + │ │ │ │ │ └── uniq_partial.b:14 > 0 + │ │ │ │ └── filters + │ │ │ │ ├── column2:6 = uniq_partial.a:13 + │ │ │ │ └── column3:7 > 0 + │ │ │ └── aggregations + │ │ │ ├── first-agg [as=column2:6] + │ │ │ │ └── column2:6 + │ │ │ └── first-agg [as=column3:7] + │ │ │ └── column3:7 + │ │ └── projections + │ │ └── (column3:7 > 0) OR NULL::BOOL [as=upsert_partial_constraint_distinct0:16] + │ └── aggregations + │ ├── first-agg [as=column1:5] + │ │ └── column1:5 + │ └── first-agg [as=column3:7] + │ └── column3:7 + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:21!null a:22!null b:23!null + ├── with-scan &1 + │ ├── columns: k:21!null a:22!null b:23!null + │ └── mapping: + │ ├── column1:5 => k:21 + │ ├── column2:6 => a:22 + │ └── column3:7 => b:23 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:17!null uniq_partial.a:18 uniq_partial.b:19 + └── filters + ├── a:22 = uniq_partial.a:18 + ├── b:23 > 0 + ├── uniq_partial.b:19 > 0 + └── k:21 != uniq_partial.k:17 + +# Error when there is no arbiter predicate to match the partial unique +# constraint predicate. +build +INSERT INTO uniq_partial VALUES (1, 2, 3) ON CONFLICT (a) DO NOTHING +---- +error (42P10): there is no unique or exclusion constraint matching the ON CONFLICT specification + +# On conflict clause references unique without index constraint. +# TODO(rytaft/mgartner): we should be able to remove the unique check for a in +# this case (see #59119). +build +INSERT INTO uniq_partial VALUES (1, 2, 3) ON CONFLICT (a) WHERE b > 0 DO NOTHING +---- +insert uniq_partial + ├── columns: + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_partial.k:1 + │ ├── column2:6 => uniq_partial.a:2 + │ └── column3:7 => uniq_partial.b:3 + ├── input binding: &1 + ├── project + │ ├── columns: column1:5!null column2:6!null column3:7!null + │ └── upsert-distinct-on + │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_constraint_distinct0:12 + │ ├── grouping columns: column2:6!null upsert_partial_constraint_distinct0:12 + │ ├── project + │ │ ├── columns: upsert_partial_constraint_distinct0:12 column1:5!null column2:6!null column3:7!null + │ │ ├── anti-join (hash) + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ ├── values + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ └── (1, 2, 3) + │ │ │ ├── select + │ │ │ │ ├── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10!null + │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ └── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10 + │ │ │ │ └── filters + │ │ │ │ └── uniq_partial.b:10 > 0 + │ │ │ └── filters + │ │ │ ├── column2:6 = uniq_partial.a:9 + │ │ │ └── column3:7 > 0 + │ │ └── projections + │ │ └── (column3:7 > 0) OR NULL::BOOL [as=upsert_partial_constraint_distinct0:12] + │ └── aggregations + │ ├── first-agg [as=column1:5] + │ │ └── column1:5 + │ └── first-agg [as=column3:7] + │ └── column3:7 + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:17!null a:18!null b:19!null + ├── with-scan &1 + │ ├── columns: k:17!null a:18!null b:19!null + │ └── mapping: + │ ├── column1:5 => k:17 + │ ├── column2:6 => a:18 + │ └── column3:7 => b:19 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:13!null uniq_partial.a:14 uniq_partial.b:15 + └── filters + ├── a:18 = uniq_partial.a:14 + ├── b:19 > 0 + ├── uniq_partial.b:15 > 0 + └── k:17 != uniq_partial.k:13 + # Insert with non-constant input. build INSERT INTO uniq_partial SELECT k, v, w FROM other @@ -1174,3 +1318,247 @@ insert uniq_partial_hidden_pk ├── c:22 > 0 ├── uniq_partial_hidden_pk.c:17 > 0 └── rowid:23 != uniq_partial_hidden_pk.rowid:18 + +exec-ddl +CREATE TABLE uniq_partial_constraint_and_index ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE INDEX (a) WHERE true, + UNIQUE WITHOUT INDEX (a) WHERE b > 10 +) +---- + +# Use a pseudo-partial index as the only arbiter. Note that we use the "norm" +# directive instead of "build" to ensure that partial index predicates are fully +# normalized when choosing arbiter indexes. +norm +INSERT INTO uniq_partial_constraint_and_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 10 DO NOTHING +---- +insert uniq_partial_constraint_and_index + ├── columns: + ├── arbiter indexes: secondary + ├── insert-mapping: + │ ├── column1:5 => uniq_partial_constraint_and_index.k:1 + │ ├── column2:6 => uniq_partial_constraint_and_index.a:2 + │ └── column3:7 => uniq_partial_constraint_and_index.b:3 + ├── partial index put columns: partial_index_put1:13 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:13!null column1:5!null column2:6!null column3:7!null + │ ├── anti-join (cross) + │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ ├── values + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ └── (1, 1, 1) + │ │ ├── select + │ │ │ ├── columns: uniq_partial_constraint_and_index.a:9!null + │ │ │ ├── scan uniq_partial_constraint_and_index + │ │ │ │ ├── columns: uniq_partial_constraint_and_index.a:9 + │ │ │ │ └── partial index predicates + │ │ │ │ └── secondary: filters (true) + │ │ │ └── filters + │ │ │ └── uniq_partial_constraint_and_index.a:9 = 1 + │ │ └── filters (true) + │ └── projections + │ └── true [as=partial_index_put1:13] + └── unique-checks + └── unique-checks-item: uniq_partial_constraint_and_index(a) + └── semi-join (hash) + ├── columns: k:18!null a:19!null b:20!null + ├── select + │ ├── columns: k:18!null a:19!null b:20!null + │ ├── with-scan &1 + │ │ ├── columns: k:18!null a:19!null b:20!null + │ │ └── mapping: + │ │ ├── column1:5 => k:18 + │ │ ├── column2:6 => a:19 + │ │ └── column3:7 => b:20 + │ └── filters + │ └── b:20 > 10 + ├── select + │ ├── columns: uniq_partial_constraint_and_index.k:14!null uniq_partial_constraint_and_index.a:15 uniq_partial_constraint_and_index.b:16!null + │ ├── scan uniq_partial_constraint_and_index + │ │ ├── columns: uniq_partial_constraint_and_index.k:14!null uniq_partial_constraint_and_index.a:15 uniq_partial_constraint_and_index.b:16 + │ │ └── partial index predicates + │ │ └── secondary: filters (true) + │ └── filters + │ └── uniq_partial_constraint_and_index.b:16 > 10 + └── filters + ├── a:19 = uniq_partial_constraint_and_index.a:15 + └── k:18 != uniq_partial_constraint_and_index.k:14 + +exec-ddl +CREATE TABLE uniq_constraint_and_partial_index ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE INDEX (a) WHERE b > 0, + UNIQUE WITHOUT INDEX (a) WHERE true +) +---- + +# Use a pseudo-partial constraint as the only arbiter. Note that we use the +# "norm" directive instead of "build" to ensure that partial index predicates +# are fully normalized when choosing arbiter indexes. +norm +INSERT INTO uniq_constraint_and_partial_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 0 DO NOTHING +---- +insert uniq_constraint_and_partial_index + ├── columns: + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_constraint_and_partial_index.k:1 + │ ├── column2:6 => uniq_constraint_and_partial_index.a:2 + │ └── column3:7 => uniq_constraint_and_partial_index.b:3 + ├── partial index put columns: partial_index_put1:13 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:13!null column1:5!null column2:6!null column3:7!null + │ ├── anti-join (cross) + │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ ├── values + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ └── (1, 1, 1) + │ │ ├── select + │ │ │ ├── columns: uniq_constraint_and_partial_index.a:9!null + │ │ │ ├── scan uniq_constraint_and_partial_index + │ │ │ │ ├── columns: uniq_constraint_and_partial_index.a:9 + │ │ │ │ └── partial index predicates + │ │ │ │ └── secondary: filters + │ │ │ │ └── uniq_constraint_and_partial_index.b:10 > 0 + │ │ │ └── filters + │ │ │ └── uniq_constraint_and_partial_index.a:9 = 1 + │ │ └── filters (true) + │ └── projections + │ └── column3:7 > 0 [as=partial_index_put1:13] + └── unique-checks + └── unique-checks-item: uniq_constraint_and_partial_index(a) + └── semi-join (hash) + ├── columns: k:18!null a:19!null b:20!null + ├── with-scan &1 + │ ├── columns: k:18!null a:19!null b:20!null + │ └── mapping: + │ ├── column1:5 => k:18 + │ ├── column2:6 => a:19 + │ └── column3:7 => b:20 + ├── scan uniq_constraint_and_partial_index + │ ├── columns: uniq_constraint_and_partial_index.k:14!null uniq_constraint_and_partial_index.a:15 + │ └── partial index predicates + │ └── secondary: filters + │ └── uniq_constraint_and_partial_index.b:16 > 0 + └── filters + ├── a:19 = uniq_constraint_and_partial_index.a:15 + └── k:18 != uniq_constraint_and_partial_index.k:14 + +exec-ddl +CREATE TABLE uniq_partial_constraint_and_partial_index ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE INDEX (a) WHERE b > 0, + UNIQUE WITHOUT INDEX (a) WHERE b > 10 +) +---- + +# Use both a partial index and partial constraint as arbiters when both +# predicates are implied by the arbiter predicate. +build +INSERT INTO uniq_partial_constraint_and_partial_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 10 DO NOTHING +---- +insert uniq_partial_constraint_and_partial_index + ├── columns: + ├── arbiter indexes: secondary + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_partial_constraint_and_partial_index.k:1 + │ ├── column2:6 => uniq_partial_constraint_and_partial_index.a:2 + │ └── column3:7 => uniq_partial_constraint_and_partial_index.b:3 + ├── partial index put columns: partial_index_put1:18 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:18!null column1:5!null column2:6!null column3:7!null + │ ├── project + │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ └── upsert-distinct-on + │ │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_constraint_distinct0:17 + │ │ ├── grouping columns: column2:6!null upsert_partial_constraint_distinct0:17 + │ │ ├── project + │ │ │ ├── columns: upsert_partial_constraint_distinct0:17 column1:5!null column2:6!null column3:7!null + │ │ │ ├── project + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ └── upsert-distinct-on + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_index_distinct1:16 + │ │ │ │ ├── grouping columns: column2:6!null upsert_partial_index_distinct1:16 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: upsert_partial_index_distinct1:16 column1:5!null column2:6!null column3:7!null + │ │ │ │ │ ├── anti-join (hash) + │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ ├── anti-join (hash) + │ │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ │ ├── values + │ │ │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ │ │ └── (1, 1, 1) + │ │ │ │ │ │ │ ├── select + │ │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:8!null uniq_partial_constraint_and_partial_index.a:9 uniq_partial_constraint_and_partial_index.b:10!null + │ │ │ │ │ │ │ │ ├── scan uniq_partial_constraint_and_partial_index + │ │ │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:8!null uniq_partial_constraint_and_partial_index.a:9 uniq_partial_constraint_and_partial_index.b:10 + │ │ │ │ │ │ │ │ │ └── partial index predicates + │ │ │ │ │ │ │ │ │ └── secondary: filters + │ │ │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:10 > 0 + │ │ │ │ │ │ │ │ └── filters + │ │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:10 > 0 + │ │ │ │ │ │ │ └── filters + │ │ │ │ │ │ │ ├── column2:6 = uniq_partial_constraint_and_partial_index.a:9 + │ │ │ │ │ │ │ └── column3:7 > 0 + │ │ │ │ │ │ ├── select + │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:12!null uniq_partial_constraint_and_partial_index.a:13 uniq_partial_constraint_and_partial_index.b:14!null + │ │ │ │ │ │ │ ├── scan uniq_partial_constraint_and_partial_index + │ │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:12!null uniq_partial_constraint_and_partial_index.a:13 uniq_partial_constraint_and_partial_index.b:14 + │ │ │ │ │ │ │ │ └── partial index predicates + │ │ │ │ │ │ │ │ └── secondary: filters + │ │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:14 > 0 + │ │ │ │ │ │ │ └── filters + │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:14 > 10 + │ │ │ │ │ │ └── filters + │ │ │ │ │ │ ├── column2:6 = uniq_partial_constraint_and_partial_index.a:13 + │ │ │ │ │ │ └── column3:7 > 10 + │ │ │ │ │ └── projections + │ │ │ │ │ └── (column3:7 > 0) OR NULL::BOOL [as=upsert_partial_index_distinct1:16] + │ │ │ │ └── aggregations + │ │ │ │ ├── first-agg [as=column1:5] + │ │ │ │ │ └── column1:5 + │ │ │ │ └── first-agg [as=column3:7] + │ │ │ │ └── column3:7 + │ │ │ └── projections + │ │ │ └── (column3:7 > 10) OR NULL::BOOL [as=upsert_partial_constraint_distinct0:17] + │ │ └── aggregations + │ │ ├── first-agg [as=column1:5] + │ │ │ └── column1:5 + │ │ └── first-agg [as=column3:7] + │ │ └── column3:7 + │ └── projections + │ └── column3:7 > 0 [as=partial_index_put1:18] + └── unique-checks + └── unique-checks-item: uniq_partial_constraint_and_partial_index(a) + └── semi-join (hash) + ├── columns: k:23!null a:24!null b:25!null + ├── with-scan &1 + │ ├── columns: k:23!null a:24!null b:25!null + │ └── mapping: + │ ├── column1:5 => k:23 + │ ├── column2:6 => a:24 + │ └── column3:7 => b:25 + ├── scan uniq_partial_constraint_and_partial_index + │ ├── columns: uniq_partial_constraint_and_partial_index.k:19!null uniq_partial_constraint_and_partial_index.a:20 uniq_partial_constraint_and_partial_index.b:21 + │ └── partial index predicates + │ └── secondary: filters + │ └── uniq_partial_constraint_and_partial_index.b:21 > 0 + └── filters + ├── a:24 = uniq_partial_constraint_and_partial_index.a:20 + ├── b:25 > 10 + ├── uniq_partial_constraint_and_partial_index.b:21 > 10 + └── k:23 != uniq_partial_constraint_and_partial_index.k:19