From dd249fcd495251d2bdfaa3101ea96e03d1bb42c4 Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Wed, 24 Feb 2021 14:22:57 -0800 Subject: [PATCH] opt: support UPSERT with partial UNIQUE WITHOUT INDEX constraints This commit adds support for both `UPSERT` and `INSERT...ON CONFLICT...DO UPDATE` statements on tables with partial `UNIQUE WITHOUT INDEX` constraints. There is no release note because these constraints are gated behind the experimental_enable_unique_without_index_constraints session variable. Release note: None Release justification: This is required for supporting implicitly partitioned unique partial indexes. --- pkg/sql/logictest/testdata/logic_test/unique | 79 ++- pkg/sql/opt/exec/execbuilder/testdata/unique | 605 ++++++++++++++++++ pkg/sql/opt/optbuilder/arbiter_set.go | 12 +- pkg/sql/opt/optbuilder/insert.go | 85 +-- .../optbuilder/testdata/unique-checks-upsert | 345 +++++++++- 5 files changed, 1057 insertions(+), 69 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/unique b/pkg/sql/logictest/testdata/logic_test/unique index 2735642d0766..f9e5e899e823 100644 --- a/pkg/sql/logictest/testdata/logic_test/unique +++ b/pkg/sql/logictest/testdata/logic_test/unique @@ -69,6 +69,14 @@ CREATE TABLE uniq_partial ( UNIQUE WITHOUT INDEX (a) WHERE b > 0 ) +statement ok +CREATE TABLE uniq_partial_pk ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE WITHOUT INDEX (a) WHERE b > 0 +) + statement ok CREATE TYPE region AS ENUM ('us-east', 'us-west', 'eu-west') @@ -713,7 +721,7 @@ a b c d e 2 2 2 2 2 -# Upsert into a table in which the unique constraints are the suffix of an +# Upsert into a table in which the unique constraints are the suffix of an # index, and the prefix of the index is an enum. This case uses the default # value for columns r and j. statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_i"\nDETAIL: Key \(i\)=\(2\) already exists\. @@ -726,6 +734,75 @@ r s i j us-west foo 1 1 eu-west bar 2 2 +# Upsert into a table with a partial unique constraint. + +# Upsert non-conflicting rows. +statement ok +UPSERT INTO uniq_partial_pk VALUES (1, 1, 1), (2, 2, 2), (3, 1, -1) + +# Duplicate of a on insert path. +statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +UPSERT INTO uniq_partial_pk VALUES (4, 1, 1) + +# Duplicate of a on update path. +statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +UPSERT INTO uniq_partial_pk VALUES (3, 1, 1) + +# No duplicate on insert path. +statement ok +UPSERT INTO uniq_partial_pk VALUES (4, 1, -1) + +# No duplicate on update path. +statement ok +UPSERT INTO uniq_partial_pk VALUES (2, 1, -1) + +query III colnames,rowsort +SELECT * FROM uniq_partial_pk +---- +k a b +1 1 1 +2 1 -1 +3 1 -1 +4 1 -1 + +# On conflict do update with a partial unique constraint. + +# Clear the table and insert new rows. +statement ok +DELETE FROM uniq_partial; +INSERT INTO uniq_partial VALUES (1, 1), (2, 2), (1, -1) + +# Insert non-conflicting rows. +statement ok +INSERT INTO uniq_partial VALUES (3, 3), (1, -2) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET b = -10 + +# Insert one conflicting row. +statement ok +INSERT INTO uniq_partial VALUES (4, 4), (3, 30) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET b = 33 + +# Insert two rows that conflict with each other. +statement error pgcode 21000 UPSERT or INSERT...ON CONFLICT command cannot affect row a second time +INSERT INTO uniq_partial VALUES (5, 5), (5, 50) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET b = 33 + +# Insert a conflicting row that conflicts after update. +statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +INSERT INTO uniq_partial VALUES (4, 40) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET a = 1 + +# Insert a conflicting row that does not conflict after update. +statement ok +INSERT INTO uniq_partial VALUES (4, 40) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET a = 1, b = -40 + +query II colnames,rowsort +SELECT * FROM uniq_partial +---- +a b +1 1 +1 -1 +1 -2 +1 -40 +2 2 +3 33 + # Ensure that we do not choose a partial index as the arbiter when there is a # UNIQUE WITHOUT INDEX constraint. statement ok diff --git a/pkg/sql/opt/exec/execbuilder/testdata/unique b/pkg/sql/opt/exec/execbuilder/testdata/unique index b9ef58766286..f5f86a128044 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/unique +++ b/pkg/sql/opt/exec/execbuilder/testdata/unique @@ -3574,6 +3574,611 @@ vectorized: true columns: (column1, column2, column3, column4, r, s, i, j, upsert_i, r, check1, upsert_r, upsert_s, upsert_j) label: buffer 1 +# None of the upserted values have nulls. +query T +EXPLAIN UPSERT INTO uniq_partial VALUES (1, 1, 1), (2, 2, 2) +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: uniq_partial(k, a, b) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 3 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (right semi) +│ │ equality: (a) = (column2) +│ │ pred: column1 != k +│ │ +│ ├── • filter +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ missing stats +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • filter +│ │ estimated row count: 1 +│ │ filter: column3 > 0 +│ │ +│ └── • scan buffer +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (right semi) + │ equality: (b) = (column3) + │ pred: column1 != k + │ + ├── • filter + │ │ filter: b > 0 + │ │ + │ └── • scan + │ missing stats + │ table: uniq_partial@primary + │ spans: FULL SCAN + │ + └── • filter + │ estimated row count: 1 + │ filter: column3 > 0 + │ + └── • scan buffer + label: buffer 1 + +# TODO(rytaft/mgartner): The default value for b is NULL, and we're not updating +# it. Therefore, we could avoid planning checks for (b) (see #58300). +query T +EXPLAIN UPSERT INTO uniq_partial (k, a) VALUES (1, 1), (2, 2) +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: uniq_partial(k, a, b) +│ │ arbiter indexes: primary +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • lookup join (left outer) +│ │ table: uniq_partial@primary +│ │ equality: (column1) = (k) +│ │ equality cols are key +│ │ locking strength: for update +│ │ +│ └── • render +│ │ estimated row count: 2 +│ │ +│ └── • values +│ size: 2 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (right semi) +│ │ equality: (a) = (column2) +│ │ pred: upsert_k != k +│ │ +│ ├── • filter +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ missing stats +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • filter +│ │ filter: upsert_b > 0 +│ │ +│ └── • scan buffer +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (right semi) + │ equality: (b) = (upsert_b) + │ pred: upsert_k != k + │ + ├── • filter + │ │ filter: b > 0 + │ │ + │ └── • scan + │ missing stats + │ table: uniq_partial@primary + │ spans: FULL SCAN + │ + └── • filter + │ filter: upsert_b > 0 + │ + └── • scan buffer + label: buffer 1 + +# No need to plan checks for a since it's always NULL. +query T +EXPLAIN UPSERT INTO uniq_partial (k, a, b) VALUES (1, NULL, 1), (2, NULL, NULL) +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: uniq_partial(k, a, b) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 3 columns, 2 rows +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (right semi) + │ equality: (b) = (column3) + │ pred: column1 != k + │ + ├── • filter + │ │ filter: b > 0 + │ │ + │ └── • scan + │ missing stats + │ table: uniq_partial@primary + │ spans: FULL SCAN + │ + └── • filter + │ estimated row count: 1 + │ filter: column3 > 0 + │ + └── • scan buffer + label: buffer 1 + +# On conflict do update with constant input. +# TODO(rytaft/mgartner): The default value for b is NULL, and we're not updating +# it. Therefore, we could avoid planning checks for (b) (see #58300). +query T +EXPLAIN INSERT INTO uniq_partial (k, a) VALUES (100, 1), (200, 1) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET a = excluded.a + 1 +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: uniq_partial(k, a, b) +│ │ arbiter constraints: unique_a +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • hash join (right outer) +│ │ equality: (a) = (column2) +│ │ pred: column8 > 0 +│ │ +│ ├── • filter +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ missing stats +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • distinct +│ │ estimated row count: 2 +│ │ distinct on: arbiter_unique_a_distinct, column2 +│ │ nulls are distinct +│ │ error on duplicate +│ │ +│ └── • render +│ │ estimated row count: 2 +│ │ +│ └── • values +│ size: 2 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (right semi) +│ │ equality: (a) = (upsert_a) +│ │ pred: upsert_k != k +│ │ +│ ├── • filter +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ missing stats +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • filter +│ │ filter: upsert_b > 0 +│ │ +│ └── • scan buffer +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (right semi) + │ equality: (b) = (upsert_b) + │ pred: upsert_k != k + │ + ├── • filter + │ │ filter: b > 0 + │ │ + │ └── • scan + │ missing stats + │ table: uniq_partial@primary + │ spans: FULL SCAN + │ + └── • filter + │ filter: upsert_b > 0 + │ + └── • scan buffer + label: buffer 1 + +# On conflict do update with non-constant input. +# TODO(rytaft/mgartner): The default value for b is NULL, and we're not updating +# it. Therefore, we could avoid planning checks for (b) (see #58300). +query T +EXPLAIN INSERT INTO uniq_partial SELECT k, v FROM other ON CONFLICT (a) WHERE b > 0 DO UPDATE SET a = uniq_partial.k + 1 +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: uniq_partial(k, a, b) +│ │ arbiter constraints: unique_a +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • hash join (left outer) +│ │ equality: (v) = (a) +│ │ pred: column15 > 0 +│ │ +│ ├── • distinct +│ │ │ distinct on: arbiter_unique_a_distinct, v +│ │ │ nulls are distinct +│ │ │ error on duplicate +│ │ │ +│ │ └── • render +│ │ │ +│ │ └── • scan +│ │ missing stats +│ │ table: other@primary +│ │ spans: FULL SCAN +│ │ +│ └── • filter +│ │ filter: b > 0 +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (semi) +│ │ equality: (upsert_a) = (a) +│ │ pred: upsert_k != k +│ │ +│ ├── • filter +│ │ │ filter: upsert_b > 0 +│ │ │ +│ │ └── • scan buffer +│ │ label: buffer 1 +│ │ +│ └── • filter +│ │ filter: b > 0 +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (semi) + │ equality: (upsert_b) = (b) + │ pred: upsert_k != k + │ + ├── • filter + │ │ filter: upsert_b > 0 + │ │ + │ └── • scan buffer + │ label: buffer 1 + │ + └── • filter + │ filter: b > 0 + │ + └── • scan + missing stats + table: uniq_partial@primary + spans: FULL SCAN + +# No need to build uniqueness checks when the primary key columns are a subset +# of the partial unique constraint columns. +query T +EXPLAIN UPSERT INTO uniq_partial_overlaps_pk VALUES (1, 1, 1), (2, 2, 2) +---- +distribution: local +vectorized: true +· +• upsert +│ into: uniq_partial_overlaps_pk(k, a, b) +│ auto commit +│ +└── • values + size: 3 columns, 2 rows + +# Upsert with non-constant input. +# Add inequality filters for the hidden primary key column. +query T +EXPLAIN UPSERT INTO uniq_partial_hidden_pk SELECT k, v, x FROM other +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: uniq_partial_hidden_pk(a, b, c, rowid) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • scan +│ missing stats +│ table: other@primary +│ spans: FULL SCAN +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (semi) + │ equality: (v) = (b) + │ pred: column16 != rowid + │ + ├── • filter + │ │ filter: x > 0 + │ │ + │ └── • scan buffer + │ label: buffer 1 + │ + └── • filter + │ filter: c > 0 + │ + └── • scan + missing stats + table: uniq_partial_hidden_pk@primary + spans: FULL SCAN + +# Test that we use the index when available for the upsert checks. +query T +EXPLAIN (VERBOSE) UPSERT INTO uniq_partial_enum VALUES ('us-west', 1, 1, 'foo'), ('us-east', 2, 2, 'bar') +---- +distribution: local +vectorized: true +· +• root +│ columns: () +│ +├── • upsert +│ │ columns: () +│ │ estimated row count: 0 (missing stats) +│ │ into: uniq_partial_enum(r, a, b, c) +│ │ arbiter indexes: primary +│ │ +│ └── • buffer +│ │ columns: (column1, column2, column3, column4, r, a, b, c, column3, column4, r, check1, partial_index_put1, partial_index_del1, upsert_r, upsert_a) +│ │ label: buffer 1 +│ │ +│ └── • project +│ │ columns: (column1, column2, column3, column4, r, a, b, c, column3, column4, r, check1, partial_index_put1, partial_index_del1, upsert_r, upsert_a) +│ │ +│ └── • render +│ │ columns: (partial_index_put1, partial_index_del1, check1, column1, column2, column3, column4, r, a, b, c, upsert_r, upsert_a) +│ │ estimated row count: 2 +│ │ render partial_index_put1: column4 IN ('bar', 'baz', 'foo') +│ │ render partial_index_del1: c IN ('bar', 'baz', 'foo') +│ │ render check1: upsert_r IN ('us-east', 'us-west', 'eu-west') +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ render column4: column4 +│ │ render r: r +│ │ render a: a +│ │ render b: b +│ │ render c: c +│ │ render upsert_r: upsert_r +│ │ render upsert_a: upsert_a +│ │ +│ └── • render +│ │ columns: (upsert_r, upsert_a, column1, column2, column3, column4, r, a, b, c) +│ │ estimated row count: 2 +│ │ render upsert_r: CASE WHEN r IS NULL THEN column1 ELSE r END +│ │ render upsert_a: CASE WHEN r IS NULL THEN column2 ELSE a END +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ render column4: column4 +│ │ render r: r +│ │ render a: a +│ │ render b: b +│ │ render c: c +│ │ +│ └── • lookup join (left outer) +│ │ columns: (column1, column2, column3, column4, r, a, b, c) +│ │ estimated row count: 2 +│ │ table: uniq_partial_enum@primary +│ │ equality: (column1, column2) = (r,a) +│ │ equality cols are key +│ │ +│ └── • values +│ columns: (column1, column2, column3, column4) +│ size: 4 columns, 2 rows +│ row 0, expr 0: 'us-west' +│ row 0, expr 1: 1 +│ row 0, expr 2: 1 +│ row 0, expr 3: 'foo' +│ row 1, expr 0: 'us-east' +│ row 1, expr 1: 2 +│ row 1, expr 2: 2 +│ row 1, expr 3: 'bar' +│ +└── • constraint-check + │ + └── • error if rows + │ columns: () + │ + └── • project + │ columns: (upsert_r, upsert_a, column3, column4) + │ estimated row count: 1 + │ + └── • lookup join (semi) + │ columns: ("lookup_join_const_col_@22", upsert_r, upsert_a, column3, column4) + │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) + │ equality: (lookup_join_const_col_@22, column3) = (r,b) + │ pred: (upsert_r != r) OR (upsert_a != a) + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@22", upsert_r, upsert_a, column3, column4) + │ estimated row count: 6 + │ + ├── • 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: (upsert_r, upsert_a, column3, column4) + │ estimated row count: 2 + │ filter: column4 IN ('bar', 'baz', 'foo') + │ + └── • project + │ columns: (upsert_r, upsert_a, column3, column4) + │ estimated row count: 2 + │ + └── • scan buffer + columns: (column1, column2, column3, column4, r, a, b, c, column3, column4, r, check1, partial_index_put1, partial_index_del1, upsert_r, upsert_a) + label: buffer 1 + +# Test that we use the partial index when available for de-duplicating INSERT ON +# CONFLICT DO UPDATE rows before inserting. +query T +EXPLAIN (VERBOSE) INSERT INTO uniq_partial_enum VALUES ('us-west', 1, 1, 'foo'), ('us-east', 2, 2, 'bar') +ON CONFLICT (b) WHERE c IN ('foo', 'bar', 'baz') DO UPDATE SET a = 10 +---- +distribution: local +vectorized: true +· +• upsert +│ columns: () +│ estimated row count: 0 (missing stats) +│ into: uniq_partial_enum(r, a, b, c) +│ auto commit +│ arbiter constraints: unique_b +│ +└── • project + │ columns: (column1, column2, column3, column4, r, a, b, c, upsert_a, r, check1, partial_index_put1, partial_index_del1) + │ + └── • render + │ columns: (partial_index_put1, partial_index_del1, check1, upsert_a, column1, column2, column3, column4, r, a, b, c) + │ estimated row count: 2 + │ render partial_index_put1: CASE WHEN r IS NULL THEN column4 ELSE c END IN ('bar', 'baz', 'foo') + │ render partial_index_del1: c IN ('bar', 'baz', 'foo') + │ render check1: CASE WHEN r IS NULL THEN column1 ELSE r END IN ('us-east', 'us-west', 'eu-west') + │ render upsert_a: CASE WHEN r IS NULL THEN column2 ELSE 10 END + │ render column1: column1 + │ render column2: column2 + │ render column3: column3 + │ render column4: column4 + │ render r: r + │ render a: a + │ render b: b + │ render c: c + │ + └── • lookup join (left outer) + │ columns: (arbiter_unique_b_distinct, column1, column2, column3, column4, r, a, b, c) + │ estimated row count: 2 + │ table: uniq_partial_enum@primary + │ equality: (r, a) = (r,a) + │ equality cols are key + │ + └── • lookup join (left outer) + │ columns: (arbiter_unique_b_distinct, column1, column2, column3, column4, r, a, b) + │ estimated row count: 2 + │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) + │ lookup condition: (column3 = b) AND (r IN ('us-east', 'us-west', 'eu-west')) + │ pred: column4 IN ('bar', 'baz', 'foo') + │ + └── • distinct + │ columns: (arbiter_unique_b_distinct, column1, column2, column3, column4) + │ estimated row count: 2 + │ distinct on: arbiter_unique_b_distinct, column3 + │ nulls are distinct + │ error on duplicate + │ + └── • render + │ columns: (arbiter_unique_b_distinct, column1, column2, column3, column4) + │ estimated row count: 2 + │ render arbiter_unique_b_distinct: (column4 IN ('bar', 'baz', 'foo')) OR CAST(NULL AS BOOL) + │ render column1: column1 + │ render column2: column2 + │ render column3: column3 + │ render column4: column4 + │ + └── • values + columns: (column1, column2, column3, column4) + size: 4 columns, 2 rows + row 0, expr 0: 'us-west' + row 0, expr 1: 1 + row 0, expr 2: 1 + row 0, expr 3: 'foo' + row 1, expr 0: 'us-east' + row 1, expr 1: 2 + row 1, expr 2: 2 + row 1, expr 3: 'bar' + # By default, we do not require checks on UUID columns set to gen_random_uuid(), # but we do for UUID columns set to other values. query T diff --git a/pkg/sql/opt/optbuilder/arbiter_set.go b/pkg/sql/opt/optbuilder/arbiter_set.go index d34610397901..61ebf8309d0e 100644 --- a/pkg/sql/opt/optbuilder/arbiter_set.go +++ b/pkg/sql/opt/optbuilder/arbiter_set.go @@ -102,20 +102,25 @@ func (a *arbiterSet) ContainsUniqueConstraint(uniq cat.UniqueOrdinal) bool { // - pred is the partial predicate expression of the arbiter, if the arbiter // is a partial index or partial constraint. If the arbiter is not partial, // pred is nil. +// - canaryOrd is the table column ordinal of a not-null column in the +// constraint's table. // -func (a *arbiterSet) ForEach(f func(name string, conflictOrds util.FastIntSet, pred tree.Expr)) { +func (a *arbiterSet) ForEach( + f func(name string, conflictOrds util.FastIntSet, pred tree.Expr, canaryOrd int), +) { // Call the callback for each index arbiter. a.indexes.ForEach(func(i int) { index := a.mb.tab.Index(i) conflictOrds := getIndexLaxKeyOrdinals(index) + canaryOrd := findNotNullIndexCol(index) var pred tree.Expr if _, isPartial := index.Predicate(); isPartial { pred = a.mb.parsePartialIndexPredicateExpr(i) } - f(string(index.Name()), conflictOrds, pred) + f(string(index.Name()), conflictOrds, pred, canaryOrd) }) // Call the callback for each unique constraint arbiter. @@ -123,12 +128,13 @@ func (a *arbiterSet) ForEach(f func(name string, conflictOrds util.FastIntSet, p uniqueConstraint := a.mb.tab.Unique(i) conflictOrds := getUniqueConstraintOrdinals(a.mb.tab, uniqueConstraint) + canaryOrd := findNotNullIndexCol(a.mb.tab.Index(cat.PrimaryIndex)) var pred tree.Expr if _, isPartial := uniqueConstraint.Predicate(); isPartial { pred = a.mb.parseUniqueConstraintPredicateExpr(i) } - f(uniqueConstraint.Name(), conflictOrds, pred) + f(uniqueConstraint.Name(), conflictOrds, pred, canaryOrd) }) } diff --git a/pkg/sql/opt/optbuilder/insert.go b/pkg/sql/opt/optbuilder/insert.go index ffdcff2be12b..c9f75fd98b01 100644 --- a/pkg/sql/opt/optbuilder/insert.go +++ b/pkg/sql/opt/optbuilder/insert.go @@ -685,14 +685,14 @@ func (mb *mutationBuilder) buildInputForDoNothing( mb.outScope.ordering = nil // Create an anti-join for each arbiter. - mb.arbiters.ForEach(func(name string, conflictOrds util.FastIntSet, pred tree.Expr) { + mb.arbiters.ForEach(func(name string, conflictOrds util.FastIntSet, pred tree.Expr, canaryOrd int) { mb.buildAntiJoinForDoNothingArbiter(inScope, conflictOrds, pred) }) // Create an UpsertDistinctOn for each arbiter. This must happen after all // conflicting rows are removed with the anti-joins created above, to avoid // removing valid rows (see #59125). - mb.arbiters.ForEach(func(name string, conflictOrds util.FastIntSet, pred tree.Expr) { + mb.arbiters.ForEach(func(name string, conflictOrds util.FastIntSet, pred tree.Expr, canaryOrd int) { // If the arbiter has a partial predicate, project a new column that // allows the UpsertDistinctOn to only de-duplicate insert rows that // satisfy the predicate. See projectPartialArbiterDistinctColumn for @@ -723,11 +723,6 @@ func (mb *mutationBuilder) buildInputForUpsert( // Determine the set of arbiter indexes and constraints to use to check for // conflicts. mb.arbiters = mb.findArbiters(conflictOrds, arbiterPredicate) - // TODO(mgartner): Use arbiters.ForEach to iterate over arbiters rather than - // directly accessing the index and constraint sets. This will require - // support for partial unique constraints in this function. - arbiterIndexes := mb.arbiters.indexes - arbiterConstraints := mb.arbiters.uniqueConstraints // TODO(mgartner): Add support for multiple arbiter indexes or constraints, // similar to buildInputForDoNothing. @@ -742,40 +737,32 @@ func (mb *mutationBuilder) buildInputForUpsert( // Ignore any ordering requested by the input. mb.outScope.ordering = nil + // Create an UpsertDistinctOn and a left-join for the single arbiter. var canaryCol *scopeColumn - if arbiterIndexes.Len() > 0 { - idx, _ := arbiterIndexes.Next(0) - index := mb.tab.Index(idx) - - _, isPartial := index.Predicate() - var pred tree.Expr - if isPartial { - 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 projectPartialArbiterDistinctColumn for + mb.arbiters.ForEach(func(name string, conflictOrds util.FastIntSet, pred tree.Expr, canaryOrd int) { + // If the arbiter has a partial predicate, project a new column that + // allows the UpsertDistinctOn to only de-duplicate insert rows that + // satisfy the predicate. See projectPartialArbiterDistinctColumn for // more details. - var partialIndexDistinctCol *scopeColumn - if isPartial { - partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn( - insertColScope, pred, string(index.Name()), + var partialDistinctCol *scopeColumn + if pred != nil { + partialDistinctCol = mb.projectPartialArbiterDistinctColumn( + insertColScope, pred, name, ) } // Ensure that input is distinct on the conflict columns. Otherwise, the - // Upsert could affect the same row more than once, which can lead to index - // corruption. See issue #44466 for more context. + // Upsert could affect the same row more than once, which can lead to + // index corruption. See issue #44466 for more context. // // Ignore any ordering requested by the input. Since the - // EnsureUpsertDistinctOn operator does not allow multiple rows in distinct - // groupings, the internal ordering is meaningless (and can trigger a - // misleading error in buildDistinctOn if present). - mb.buildDistinctOnForArbiter(insertColScope, conflictOrds, partialIndexDistinctCol, duplicateUpsertErrText) + // EnsureUpsertDistinctOn operator does not allow multiple rows in + // distinct groupings, the internal ordering is meaningless (and can + // trigger a misleading error in buildDistinctOn if present). + mb.buildDistinctOnForArbiter(insertColScope, conflictOrds, partialDistinctCol, duplicateUpsertErrText) - // Re-alias all INSERT columns so that they are accessible as if they were - // part of a special data source named "crdb_internal.excluded". + // Re-alias all INSERT columns so that they are accessible as if they + // were part of a special data source named "crdb_internal.excluded". for i := range mb.outScope.cols { mb.outScope.cols[i].table = excludedTableName } @@ -789,39 +776,9 @@ func (mb *mutationBuilder) buildInputForUpsert( // null if no conflict has been detected, or not null otherwise. At // least one not-null column must exist, since primary key columns are // not-null. - canaryCol = &mb.fetchScope.cols[findNotNullIndexCol(index)] + canaryCol = &mb.fetchScope.cols[canaryOrd] mb.canaryColID = canaryCol.id - } else if arbiterConstraints.Len() > 0 { - // Ensure that input is distinct on the conflict columns. Otherwise, the - // Upsert could affect the same row more than once, which can lead to index - // corruption. See issue #44466 for more context. - // - // Ignore any ordering requested by the input. Since the - // EnsureUpsertDistinctOn operator does not allow multiple rows in distinct - // groupings, the internal ordering is meaningless (and can trigger a - // misleading error in buildDistinctOn if present). - mb.buildDistinctOnForArbiter(insertColScope, conflictOrds, nil /* partialIndexDistinctCol */, duplicateUpsertErrText) - - // Re-alias all INSERT columns so that they are accessible as if they were - // part of a special data source named "crdb_internal.excluded". - for i := range mb.outScope.cols { - mb.outScope.cols[i].table = excludedTableName - } - - // Create a left-join for the arbiter. - mb.buildLeftJoinForUpsertArbiter( - inScope, conflictOrds, nil, /* pred */ - ) - - // Record a not-null "canary" column. Use the primary index, since we - // don't know at this point whether another index will be used for this - // plan. This should select one of the primary keys. - canaryCol = &mb.fetchScope.cols[findNotNullIndexCol(mb.tab.Index(cat.PrimaryIndex))] - mb.canaryColID = canaryCol.id - } else { - // This should never happen. - panic(errors.AssertionFailedf("no arbiter index or constraint available")) - } + }) // Add a filter from the WHERE clause if one exists. if whereClause != nil { diff --git a/pkg/sql/opt/optbuilder/testdata/unique-checks-upsert b/pkg/sql/opt/optbuilder/testdata/unique-checks-upsert index 1cfa884e1486..0ce52179b695 100644 --- a/pkg/sql/opt/optbuilder/testdata/unique-checks-upsert +++ b/pkg/sql/opt/optbuilder/testdata/unique-checks-upsert @@ -183,7 +183,7 @@ upsert uniq ├── y:42 = uniq.y:36 └── k:38 != uniq.k:32 -# TODO(rytaft): No need to plan checks for w since it's aways NULL. +# TODO(rytaft): No need to plan checks for w since it's always NULL. # We currently can't determine that w is always NULL since the function # OutputColumnIsAlwaysNull doesn't recurse into joins or group bys (see #58300). build @@ -1533,6 +1533,349 @@ upsert t ├── i:17 = t.i:14 └── rowid:18 != t.rowid:15 +exec-ddl +CREATE TABLE uniq_partial ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE WITHOUT INDEX (a) WHERE b > 0 +) +---- + +# None of the upserted values have nulls. +build +UPSERT INTO uniq_partial VALUES (1, 1, 1), (2, 2, 2) +---- +upsert uniq_partial + ├── columns: + ├── upsert-mapping: + │ ├── column1:5 => uniq_partial.k:1 + │ ├── column2:6 => uniq_partial.a:2 + │ └── column3:7 => uniq_partial.b:3 + ├── input binding: &1 + ├── values + │ ├── columns: column1:5!null column2:6!null column3:7!null + │ ├── (1, 1, 1) + │ └── (2, 2, 2) + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:12!null a:13!null b:14!null + ├── with-scan &1 + │ ├── columns: k:12!null a:13!null b:14!null + │ └── mapping: + │ ├── column1:5 => k:12 + │ ├── column2:6 => a:13 + │ └── column3:7 => b:14 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10 + └── filters + ├── a:13 = uniq_partial.a:9 + ├── b:14 > 0 + ├── uniq_partial.b:10 > 0 + └── k:12 != uniq_partial.k:8 + +# No need to plan checks for a since it's always null. +build +UPSERT INTO uniq_partial VALUES (1, NULL, 1), (2, NULL, NULL) +---- +upsert uniq_partial + ├── columns: + ├── upsert-mapping: + │ ├── column1:5 => k:1 + │ ├── column2:6 => a:2 + │ └── column3:7 => b:3 + └── values + ├── columns: column1:5!null column2:6 column3:7 + ├── (1, NULL::INT8, 1) + └── (2, NULL::INT8, NULL::INT8) + +# Upsert with non-constant input. +build +UPSERT INTO uniq_partial SELECT k, v, w FROM other +---- +upsert uniq_partial + ├── columns: + ├── upsert-mapping: + │ ├── other.k:5 => uniq_partial.k:1 + │ ├── v:6 => uniq_partial.a:2 + │ └── w:7 => uniq_partial.b:3 + ├── input binding: &1 + ├── project + │ ├── columns: other.k:5 v:6 w:7!null + │ └── scan other + │ └── columns: other.k:5 v:6 w:7!null x:8 y:9 rowid:10!null other.crdb_internal_mvcc_timestamp:11 + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:16 a:17 b:18!null + ├── with-scan &1 + │ ├── columns: k:16 a:17 b:18!null + │ └── mapping: + │ ├── other.k:5 => k:16 + │ ├── v:6 => a:17 + │ └── w:7 => b:18 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14 + └── filters + ├── a:17 = uniq_partial.a:13 + ├── b:18 > 0 + ├── uniq_partial.b:14 > 0 + └── k:16 != uniq_partial.k:12 + +# On conflict do update with constant input, conflict on UNIQUE WITHOUT INDEX +# column and predicate. +build +INSERT INTO uniq_partial VALUES (100, 10, 1), (200, 20, 2) ON CONFLICT (a) WHERE b > 0 DO UPDATE SET a = 10 +---- +upsert uniq_partial + ├── columns: + ├── arbiter constraints: unique_a + ├── canary column: uniq_partial.k:9 + ├── fetch columns: uniq_partial.k:9 uniq_partial.a:10 uniq_partial.b:11 + ├── insert-mapping: + │ ├── column1:5 => uniq_partial.k:1 + │ ├── column2:6 => uniq_partial.a:2 + │ └── column3:7 => uniq_partial.b:3 + ├── update-mapping: + │ └── upsert_a:15 => uniq_partial.a:2 + ├── input binding: &1 + ├── project + │ ├── columns: upsert_k:14 upsert_a:15!null upsert_b:16 column1:5!null column2:6!null column3:7!null uniq_partial.k:9 uniq_partial.a:10 uniq_partial.b:11 crdb_internal_mvcc_timestamp:12 a_new:13!null + │ ├── project + │ │ ├── columns: a_new:13!null column1:5!null column2:6!null column3:7!null uniq_partial.k:9 uniq_partial.a:10 uniq_partial.b:11 crdb_internal_mvcc_timestamp:12 + │ │ ├── left-join (hash) + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null uniq_partial.k:9 uniq_partial.a:10 uniq_partial.b:11 crdb_internal_mvcc_timestamp:12 + │ │ │ ├── project + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ └── ensure-upsert-distinct-on + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null arbiter_unique_a_distinct:8 + │ │ │ │ ├── grouping columns: column2:6!null arbiter_unique_a_distinct:8 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: arbiter_unique_a_distinct:8 column1:5!null column2:6!null column3:7!null + │ │ │ │ │ ├── values + │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ ├── (100, 10, 1) + │ │ │ │ │ │ └── (200, 20, 2) + │ │ │ │ │ └── projections + │ │ │ │ │ └── (column3:7 > 0) OR NULL::BOOL [as=arbiter_unique_a_distinct:8] + │ │ │ │ └── aggregations + │ │ │ │ ├── first-agg [as=column1:5] + │ │ │ │ │ └── column1:5 + │ │ │ │ └── first-agg [as=column3:7] + │ │ │ │ └── column3:7 + │ │ │ ├── select + │ │ │ │ ├── columns: uniq_partial.k:9!null uniq_partial.a:10 uniq_partial.b:11!null crdb_internal_mvcc_timestamp:12 + │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ └── columns: uniq_partial.k:9!null uniq_partial.a:10 uniq_partial.b:11 crdb_internal_mvcc_timestamp:12 + │ │ │ │ └── filters + │ │ │ │ └── uniq_partial.b:11 > 0 + │ │ │ └── filters + │ │ │ ├── column2:6 = uniq_partial.a:10 + │ │ │ └── column3:7 > 0 + │ │ └── projections + │ │ └── 10 [as=a_new:13] + │ └── projections + │ ├── CASE WHEN uniq_partial.k:9 IS NULL THEN column1:5 ELSE uniq_partial.k:9 END [as=upsert_k:14] + │ ├── CASE WHEN uniq_partial.k:9 IS NULL THEN column2:6 ELSE a_new:13 END [as=upsert_a:15] + │ └── CASE WHEN uniq_partial.k:9 IS NULL THEN column3:7 ELSE uniq_partial.b:11 END [as=upsert_b:16] + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:21 a:22!null b:23 + ├── with-scan &1 + │ ├── columns: k:21 a:22!null b:23 + │ └── mapping: + │ ├── upsert_k:14 => k:21 + │ ├── upsert_a:15 => a:22 + │ └── upsert_b:16 => 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, 1, 1) ON CONFLICT (a) DO UPDATE SET a = 2 +---- +error (42P10): there is no unique or exclusion constraint matching the ON CONFLICT specification + +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 UPDATE SET a = 10 +---- +upsert uniq_partial_constraint_and_index + ├── columns: + ├── arbiter indexes: secondary + ├── canary column: uniq_partial_constraint_and_index.k:9 + ├── fetch columns: uniq_partial_constraint_and_index.k:9 uniq_partial_constraint_and_index.a:10 uniq_partial_constraint_and_index.b:11 + ├── 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 + ├── update-mapping: + │ └── upsert_a:15 => uniq_partial_constraint_and_index.a:2 + ├── partial index put columns: partial_index_put1:17 + ├── partial index del columns: partial_index_put1:17 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:17!null upsert_k:14 upsert_a:15!null upsert_b:16 column1:5!null column2:6!null column3:7!null uniq_partial_constraint_and_index.k:9 uniq_partial_constraint_and_index.a:10 uniq_partial_constraint_and_index.b:11 + │ ├── left-join (cross) + │ │ ├── columns: column1:5!null column2:6!null column3:7!null uniq_partial_constraint_and_index.k:9 uniq_partial_constraint_and_index.a:10 uniq_partial_constraint_and_index.b:11 + │ │ ├── values + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ └── (1, 1, 1) + │ │ ├── select + │ │ │ ├── columns: uniq_partial_constraint_and_index.k:9!null uniq_partial_constraint_and_index.a:10!null uniq_partial_constraint_and_index.b:11 + │ │ │ ├── scan uniq_partial_constraint_and_index + │ │ │ │ ├── columns: uniq_partial_constraint_and_index.k:9!null uniq_partial_constraint_and_index.a:10 uniq_partial_constraint_and_index.b:11 + │ │ │ │ └── partial index predicates + │ │ │ │ └── secondary: filters (true) + │ │ │ └── filters + │ │ │ └── uniq_partial_constraint_and_index.a:10 = 1 + │ │ └── filters (true) + │ └── projections + │ ├── true [as=partial_index_put1:17] + │ ├── CASE WHEN uniq_partial_constraint_and_index.k:9 IS NULL THEN column1:5 ELSE uniq_partial_constraint_and_index.k:9 END [as=upsert_k:14] + │ ├── CASE WHEN uniq_partial_constraint_and_index.k:9 IS NULL THEN column2:6 ELSE 10 END [as=upsert_a:15] + │ └── CASE WHEN uniq_partial_constraint_and_index.k:9 IS NULL THEN column3:7 ELSE uniq_partial_constraint_and_index.b:11 END [as=upsert_b:16] + └── unique-checks + └── unique-checks-item: uniq_partial_constraint_and_index(a) + └── semi-join (hash) + ├── columns: k:22 a:23!null b:24!null + ├── select + │ ├── columns: k:22 a:23!null b:24!null + │ ├── with-scan &1 + │ │ ├── columns: k:22 a:23!null b:24 + │ │ └── mapping: + │ │ ├── upsert_k:14 => k:22 + │ │ ├── upsert_a:15 => a:23 + │ │ └── upsert_b:16 => b:24 + │ └── filters + │ └── b:24 > 10 + ├── select + │ ├── columns: uniq_partial_constraint_and_index.k:18!null uniq_partial_constraint_and_index.a:19 uniq_partial_constraint_and_index.b:20!null + │ ├── scan uniq_partial_constraint_and_index + │ │ ├── columns: uniq_partial_constraint_and_index.k:18!null uniq_partial_constraint_and_index.a:19 uniq_partial_constraint_and_index.b:20 + │ │ └── partial index predicates + │ │ └── secondary: filters (true) + │ └── filters + │ └── uniq_partial_constraint_and_index.b:20 > 10 + └── filters + ├── a:23 = uniq_partial_constraint_and_index.a:19 + └── k:22 != uniq_partial_constraint_and_index.k:18 + +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 UPDATE SET a = 10 +---- +upsert uniq_constraint_and_partial_index + ├── columns: + ├── arbiter constraints: unique_a + ├── canary column: uniq_constraint_and_partial_index.k:9 + ├── fetch columns: uniq_constraint_and_partial_index.k:9 uniq_constraint_and_partial_index.a:10 uniq_constraint_and_partial_index.b:11 + ├── 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 + ├── update-mapping: + │ └── upsert_a:15 => uniq_constraint_and_partial_index.a:2 + ├── partial index put columns: partial_index_put1:17 + ├── partial index del columns: partial_index_del1:18 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:17 partial_index_del1:18 column1:5!null column2:6!null column3:7!null uniq_constraint_and_partial_index.k:9 uniq_constraint_and_partial_index.a:10 uniq_constraint_and_partial_index.b:11 upsert_k:14 upsert_a:15!null upsert_b:16 + │ ├── project + │ │ ├── columns: upsert_k:14 upsert_a:15!null upsert_b:16 column1:5!null column2:6!null column3:7!null uniq_constraint_and_partial_index.k:9 uniq_constraint_and_partial_index.a:10 uniq_constraint_and_partial_index.b:11 + │ │ ├── left-join (cross) + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null uniq_constraint_and_partial_index.k:9 uniq_constraint_and_partial_index.a:10 uniq_constraint_and_partial_index.b:11 + │ │ │ ├── values + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ └── (1, 1, 1) + │ │ │ ├── select + │ │ │ │ ├── columns: uniq_constraint_and_partial_index.k:9!null uniq_constraint_and_partial_index.a:10!null uniq_constraint_and_partial_index.b:11 + │ │ │ │ ├── scan uniq_constraint_and_partial_index + │ │ │ │ │ ├── columns: uniq_constraint_and_partial_index.k:9!null uniq_constraint_and_partial_index.a:10 uniq_constraint_and_partial_index.b:11 + │ │ │ │ │ └── partial index predicates + │ │ │ │ │ └── secondary: filters + │ │ │ │ │ └── uniq_constraint_and_partial_index.b:11 > 0 + │ │ │ │ └── filters + │ │ │ │ └── uniq_constraint_and_partial_index.a:10 = 1 + │ │ │ └── filters (true) + │ │ └── projections + │ │ ├── CASE WHEN uniq_constraint_and_partial_index.k:9 IS NULL THEN column1:5 ELSE uniq_constraint_and_partial_index.k:9 END [as=upsert_k:14] + │ │ ├── CASE WHEN uniq_constraint_and_partial_index.k:9 IS NULL THEN column2:6 ELSE 10 END [as=upsert_a:15] + │ │ └── CASE WHEN uniq_constraint_and_partial_index.k:9 IS NULL THEN column3:7 ELSE uniq_constraint_and_partial_index.b:11 END [as=upsert_b:16] + │ └── projections + │ ├── upsert_b:16 > 0 [as=partial_index_put1:17] + │ └── uniq_constraint_and_partial_index.b:11 > 0 [as=partial_index_del1:18] + └── unique-checks + └── unique-checks-item: uniq_constraint_and_partial_index(a) + └── semi-join (hash) + ├── columns: k:23 a:24!null b:25 + ├── with-scan &1 + │ ├── columns: k:23 a:24!null b:25 + │ └── mapping: + │ ├── upsert_k:14 => k:23 + │ ├── upsert_a:15 => a:24 + │ └── upsert_b:16 => b:25 + ├── scan uniq_constraint_and_partial_index + │ ├── columns: uniq_constraint_and_partial_index.k:19!null uniq_constraint_and_partial_index.a:20 + │ └── partial index predicates + │ └── secondary: filters + │ └── uniq_constraint_and_partial_index.b:21 > 0 + └── filters + ├── a:24 = uniq_constraint_and_partial_index.a:20 + └── k:23 != uniq_constraint_and_partial_index.k:19 + +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 +) +---- + +# Error when both a partial index and partial constraint match the conflict +# columns and arbiter predicate. +build +INSERT INTO uniq_partial_constraint_and_partial_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 10 DO UPDATE SET a = 10 +---- +error (0A000): unimplemented: there are multiple unique or exclusion constraints matching the ON CONFLICT specification + exec-ddl CREATE TABLE uniq_computed_pk ( i INT,