From a878d16962731173842c1b6b489c0c90dec931b6 Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Wed, 25 Nov 2020 17:25:26 -0800 Subject: [PATCH] opt: fix FK cascades to child tables with partial indexes Previously, the optimizer was not synthesizing partial index DEL columns for FK cascading updates and deletes. As a result, a cascading `UPDATE` could corrupt a child table's partial index, ultimately resulting in incorrect query results. A cascading `DELETE` would not corrupt partial indexes, but unnecessary `DEL` operations would be issued on the partial index. The optbuilder has been refactored so that these columns are correctly projected. There are now three functions for projecting PUT columns, DEL columns, and both PUT and DEL columns, each ensuring that the input scopes are non-nil. These three functions are called from principal functions in the optbuilder where CHECK constraint columns are also projected, like `mutationBuilder.buildUpdate`. In theory this should make it harder in the future to omit these necessary projections. Additionally, the execution engine was unable to handle extraneous columns that can be added as input to FK cascading updates. These extraneous columns would be incorrectly interpreted as synthesized partial index columns. This commit works around this issue by slicing the source values with an upper bound in `updateNode.processSourceRow`. The longer term fix is to not produce these columns (see issue #57097). Fixes #57085 Fixes #57084 Release justification: This is a critical bug fix to a new feature, partial indexes. Release note (bug fix): A bug has been fixed that caused errors or corrupted partial indexes of child tables in foreign key relationships with cascading `UPDATE`s and `DELETE`s. The corrupt partial indexes could result in incorrect query results. Any partial indexes on child tables of foreign key relationships with `ON DELETE CASCADE` or `ON UPDATE CASCADE` actions may be corrupt and should be dropped and re-created. This bug was introduce in version 20.2. --- .../testdata/logic_test/partial_index | 89 +++++ pkg/sql/opt/exec/execbuilder/mutation.go | 2 + .../exec/execbuilder/testdata/partial_index | 29 ++ pkg/sql/opt/norm/testdata/rules/prune_cols | 18 +- pkg/sql/opt/optbuilder/delete.go | 3 + pkg/sql/opt/optbuilder/fk_cascade.go | 12 +- pkg/sql/opt/optbuilder/insert.go | 56 ++- pkg/sql/opt/optbuilder/mutation_builder.go | 109 +++-- pkg/sql/opt/optbuilder/testdata/delete | 12 +- .../optbuilder/testdata/fk-on-delete-cascade | 58 +++ .../testdata/fk-on-delete-set-default | 78 ++++ .../optbuilder/testdata/fk-on-delete-set-null | 65 +++ .../optbuilder/testdata/fk-on-update-cascade | 292 ++++++++++++++ .../testdata/fk-on-update-set-default | 89 +++++ .../optbuilder/testdata/fk-on-update-set-null | 76 ++++ pkg/sql/opt/optbuilder/testdata/insert | 12 +- pkg/sql/opt/optbuilder/testdata/update | 115 ++++-- pkg/sql/opt/optbuilder/testdata/upsert | 375 +++++++++++------- pkg/sql/opt/optbuilder/update.go | 4 +- pkg/sql/update.go | 4 +- 20 files changed, 1214 insertions(+), 284 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/partial_index b/pkg/sql/logictest/testdata/logic_test/partial_index index c9a2c2cff8b4..ab9079a7dfb1 100644 --- a/pkg/sql/logictest/testdata/logic_test/partial_index +++ b/pkg/sql/logictest/testdata/logic_test/partial_index @@ -1298,6 +1298,7 @@ SELECT k FROM t55387 WHERE a > 1 AND b > 3 # Regression test for #55672. Do not build partial index predicates when the # scope does not include all table columns, like FK check scans. +subtest regression_55672 statement ok CREATE TABLE t55672_a ( @@ -1323,3 +1324,91 @@ INSERT INTO t55672_a (a, t) VALUES (2, now()) statement ok INSERT INTO t55672_b (b,a) VALUES (2,2) + +# Regression test for #57085. Cascading UPDATEs should correctly update partial +# indexes of the child table. +subtest regression_57085 + +# Update a partial index in a child table. +statement ok +CREATE TABLE t57085_p1 ( + p INT PRIMARY KEY +); +CREATE TABLE t57085_c1 ( + c INT PRIMARY KEY, + p INT REFERENCES t57085_p1 ON UPDATE CASCADE, + i INT, + INDEX idx (p) WHERE i > 0 +); + +statement ok +INSERT INTO t57085_p1 VALUES (1); +INSERT INTO t57085_c1 VALUES (10, 1, 100), (20, 1, -100); +UPDATE t57085_p1 SET p = 2 WHERE p = 1; + +query III rowsort +SELECT c, p, i FROM t57085_c1@idx WHERE p = 2 AND i > 0 +---- +10 2 100 + +# Update a partial index in a child table with a single variable boolean +# predicate. +statement ok +CREATE TABLE t57085_p2 ( + p INT PRIMARY KEY +); +CREATE TABLE t57085_c2 ( + c INT PRIMARY KEY, + p INT REFERENCES t57085_p2 ON UPDATE CASCADE, + b BOOL, + INDEX idx (p) WHERE b +); + +statement ok +INSERT INTO t57085_p2 VALUES (1); +INSERT INTO t57085_c2 VALUES (10, 1, true), (20, 1, false); +UPDATE t57085_p2 SET p = 2 WHERE p = 1; + +query IIB rowsort +SELECT c, p, b FROM t57085_c2@idx WHERE p = 2 AND b +---- +10 2 true + +# Update the parent with an INSERT ON CONFLICT DO UPDATE. +statement ok +INSERT INTO t57085_p2 VALUES (2) ON CONFLICT (p) DO UPDATE SET p = 3 + +query IIB rowsort +SELECT c, p, b FROM t57085_c2@idx WHERE p = 3 AND b +---- +10 3 true + +# Update a partial index that references the column being updated in the +# cascade. +statement ok +CREATE TABLE t57085_p3 ( + p INT PRIMARY KEY +); +CREATE TABLE t57085_c3 ( + c INT PRIMARY KEY, + p INT REFERENCES t57085_p3 ON UPDATE CASCADE, + i INT, + INDEX idx (i) WHERE p = 3 +); + +statement ok +INSERT INTO t57085_p3 VALUES (1), (2); +INSERT INTO t57085_c3 VALUES (10, 1, 100), (20, 2, 200); +UPDATE t57085_p3 SET p = 3 WHERE p = 1; + +query III rowsort +SELECT c, p, i FROM t57085_c3@idx WHERE p = 3 AND i = 100 +---- +10 3 100 + +statement ok +UPDATE t57085_p3 SET p = 4 WHERE p = 3; + +query III rowsort +SELECT c, p, i FROM t57085_c3@idx WHERE p = 3 AND i = 100 +---- diff --git a/pkg/sql/opt/exec/execbuilder/mutation.go b/pkg/sql/opt/exec/execbuilder/mutation.go index 55f51cb6979f..df5d8e8c3433 100644 --- a/pkg/sql/opt/exec/execbuilder/mutation.go +++ b/pkg/sql/opt/exec/execbuilder/mutation.go @@ -43,6 +43,8 @@ func (b *Builder) buildMutationInput( return execPlan{}, err } + // TODO(mgartner/radu): This can incorrectly append columns in a FK cascade + // update that are never used during execution. See issue #57097. if p.WithID != 0 { // The input might have extra columns that are used only by FK checks; make // sure we don't project them away. diff --git a/pkg/sql/opt/exec/execbuilder/testdata/partial_index b/pkg/sql/opt/exec/execbuilder/testdata/partial_index index 301bc771a7c8..9d46a446af13 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/partial_index +++ b/pkg/sql/opt/exec/execbuilder/testdata/partial_index @@ -685,3 +685,32 @@ scan · · · missing stats · · table t@b_partial (partial index) · spans FULL SCAN + +# Regression test for #57085. Cascading DELETEs should not issue DEL operations +# for partial indexes of a child table when the deleted row was not in the +# partial index. +statement ok +CREATE TABLE t57085_p ( + p INT PRIMARY KEY +); +CREATE TABLE t57085_c ( + c INT PRIMARY KEY, + p INT REFERENCES t57085_p ON DELETE CASCADE, + b BOOL, + INDEX idx (p) WHERE b, + FAMILY (c, p, b) +); + +statement ok +INSERT INTO t57085_p VALUES (1), (2); +INSERT INTO t57085_c VALUES (10, 1, true), (20, 1, false), (30, 2, true); + +query T kvtrace +DELETE FROM t57085_p WHERE p = 1; +---- +Scan /Table/55/1/1{-/#} +Del /Table/55/1/1/0 +Scan /Table/56/{1-2} +Del /Table/56/2/1/10/0 +Del /Table/56/1/10/0 +Del /Table/56/1/20/0 diff --git a/pkg/sql/opt/norm/testdata/rules/prune_cols b/pkg/sql/opt/norm/testdata/rules/prune_cols index f171b6da3dbd..b079eac470bd 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -1947,23 +1947,23 @@ update partial_indexes ├── columns: ├── fetch columns: a:5 b:6 c:7 ├── update-mapping: - │ └── b_new:10 => b:2 - ├── partial index put columns: column11:11 - ├── partial index del columns: column9:9 + │ └── b_new:9 => b:2 + ├── partial index put columns: partial_index_put1:10 + ├── partial index del columns: partial_index_del1:11 ├── cardinality: [0 - 0] ├── volatile, mutations └── project - ├── columns: column11:11 a:5!null b:6 c:7 column9:9 b_new:10 + ├── columns: partial_index_put1:10 partial_index_del1:11 a:5!null b:6 c:7 b_new:9 ├── cardinality: [0 - 1] ├── immutable ├── key: () ├── fd: ()-->(5-7,9-11) ├── project - │ ├── columns: b_new:10 column9:9 a:5!null b:6 c:7 + │ ├── columns: b_new:9 a:5!null b:6 c:7 │ ├── cardinality: [0 - 1] │ ├── immutable │ ├── key: () - │ ├── fd: ()-->(5-7,9,10) + │ ├── fd: ()-->(5-7,9) │ ├── select │ │ ├── columns: a:5!null b:6 c:7 │ │ ├── cardinality: [0 - 1] @@ -1979,10 +1979,10 @@ update partial_indexes │ │ └── filters │ │ └── a:5 = 1 [outer=(5), constraints=(/5: [/1 - /1]; tight), fd=()-->(5)] │ └── projections - │ ├── b:6 + 1 [as=b_new:10, outer=(6), immutable] - │ └── b:6 > 1 [as=column9:9, outer=(6)] + │ └── b:6 + 1 [as=b_new:9, outer=(6), immutable] └── projections - └── b_new:10 > 1 [as=column11:11, outer=(10)] + ├── b_new:9 > 1 [as=partial_index_put1:10, outer=(9)] + └── b:6 > 1 [as=partial_index_del1:11, outer=(6)] # Prune secondary family column not needed for the update. norm expect=(PruneMutationFetchCols,PruneMutationInputCols) diff --git a/pkg/sql/opt/optbuilder/delete.go b/pkg/sql/opt/optbuilder/delete.go index 8b86009d108f..3e1e34319249 100644 --- a/pkg/sql/opt/optbuilder/delete.go +++ b/pkg/sql/opt/optbuilder/delete.go @@ -76,6 +76,9 @@ func (b *Builder) buildDelete(del *tree.Delete, inScope *scope) (outScope *scope func (mb *mutationBuilder) buildDelete(returning tree.ReturningExprs) { mb.buildFKChecksAndCascadesForDelete() + // Project partial index DEL boolean columns. + mb.projectPartialIndexDelCols(mb.fetchScope) + private := mb.makeMutationPrivate(returning != nil) mb.outScope.expr = mb.b.factory.ConstructDelete(mb.outScope.expr, mb.checks, private) diff --git a/pkg/sql/opt/optbuilder/fk_cascade.go b/pkg/sql/opt/optbuilder/fk_cascade.go index 4eb849826841..23d7f40e6704 100644 --- a/pkg/sql/opt/optbuilder/fk_cascade.go +++ b/pkg/sql/opt/optbuilder/fk_cascade.go @@ -107,9 +107,10 @@ func (cb *onDeleteCascadeBuilder) Build( mb.init(b, "delete", cb.childTable, tree.MakeUnqualifiedTableName(cb.childTable.Name())) // Build a semi join of the table with the mutation input. - mb.outScope = b.buildDeleteCascadeMutationInput( + mb.fetchScope = b.buildDeleteCascadeMutationInput( cb.childTable, &mb.alias, fk, binding, bindingProps, oldValues, ) + mb.outScope = mb.fetchScope // Set list of columns that will be fetched by the input expression. mb.setFetchColIDs(mb.outScope.cols) @@ -215,9 +216,14 @@ func (cb *onDeleteSetBuilder) Build( mb.init(b, "update", cb.childTable, tree.MakeUnqualifiedTableName(cb.childTable.Name())) // Build a semi join of the table with the mutation input. - mb.outScope = b.buildDeleteCascadeMutationInput( + // + // The scope returned by buildDeleteCascadeMutationInput has one column + // for each public table column, making it appropriate to set it as + // mb.fetchScope. + mb.fetchScope = b.buildDeleteCascadeMutationInput( cb.childTable, &mb.alias, fk, binding, bindingProps, oldValues, ) + mb.outScope = mb.fetchScope // Set list of columns that will be fetched by the input expression. mb.setFetchColIDs(mb.outScope.cols) @@ -445,6 +451,8 @@ func (cb *onUpdateCascadeBuilder) Build( numFKCols := fk.ColumnCount() tableScopeCols := mb.outScope.cols[:len(mb.outScope.cols)-2*numFKCols] newValScopeCols := mb.outScope.cols[len(mb.outScope.cols)-numFKCols:] + mb.fetchScope = b.allocScope() + mb.fetchScope.appendColumns(tableScopeCols) // Set list of columns that will be fetched by the input expression. mb.setFetchColIDs(tableScopeCols) diff --git a/pkg/sql/opt/optbuilder/insert.go b/pkg/sql/opt/optbuilder/insert.go index 40e815dd7b03..110e28d8e3b3 100644 --- a/pkg/sql/opt/optbuilder/insert.go +++ b/pkg/sql/opt/optbuilder/insert.go @@ -649,7 +649,7 @@ func (mb *mutationBuilder) buildInsert(returning tree.ReturningExprs) { // Add any check constraint boolean columns to the input. mb.addCheckConstraintCols() - // Add any partial index put boolean columns to the input. + // Project partial index PUT boolean columns. mb.projectPartialIndexPutCols(preCheckScope) mb.buildFKChecksForInsert() @@ -900,7 +900,7 @@ func (mb *mutationBuilder) buildInputForUpsert( // NOTE: Include mutation columns, but be careful to never use them for any // reason other than as "fetch columns". See buildScan comment. // TODO(andyk): Why does execution engine need mutation columns for Insert? - fetchScope := mb.b.buildScan( + mb.fetchScope = mb.b.buildScan( mb.b.addTable(mb.tab, &mb.alias), tableOrdinals(mb.tab, columnKinds{ includeMutations: true, @@ -917,10 +917,10 @@ func (mb *mutationBuilder) buildInputForUpsert( // the scan on the right side of the left outer join with the partial index // predicate expression as the filter. if isPartial { - texpr := fetchScope.resolveAndRequireType(predExpr, types.Bool) - predScalar := mb.b.buildScalar(texpr, fetchScope, nil, nil, nil) - fetchScope.expr = mb.b.factory.ConstructSelect( - fetchScope.expr, + texpr := mb.fetchScope.resolveAndRequireType(predExpr, types.Bool) + predScalar := mb.b.buildScalar(texpr, mb.fetchScope, nil, nil, nil) + mb.fetchScope.expr = mb.b.factory.ConstructSelect( + mb.fetchScope.expr, memo.FiltersExpr{mb.b.factory.ConstructFiltersItem(predScalar)}, ) } @@ -928,16 +928,16 @@ func (mb *mutationBuilder) buildInputForUpsert( // Record a not-null "canary" column. After the left-join, this will be 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. - canaryScopeCol := &fetchScope.cols[findNotNullIndexCol(index)] + canaryScopeCol := &mb.fetchScope.cols[findNotNullIndexCol(index)] mb.canaryColID = canaryScopeCol.id // Set fetchColIDs to reference the columns created for the fetch values. - mb.setFetchColIDs(fetchScope.cols) + mb.setFetchColIDs(mb.fetchScope.cols) // Add the fetch columns to the current scope. It's OK to modify the current // scope because it contains only INSERT columns that were added by the // mutationBuilder, and which aren't needed for any other purpose. - mb.outScope.appendColumnsFromScope(fetchScope) + mb.outScope.appendColumnsFromScope(mb.fetchScope) // Build the join condition by creating a conjunction of equality conditions // that test each conflict column: @@ -945,12 +945,12 @@ func (mb *mutationBuilder) buildInputForUpsert( // ON ins.x = scan.a AND ins.y = scan.b // var on memo.FiltersExpr - for i := range fetchScope.cols { + for i := range mb.fetchScope.cols { // Include fetch columns with ordinal positions in conflictOrds. if conflictOrds.Contains(i) { condition := mb.b.factory.ConstructEq( mb.b.factory.ConstructVariable(mb.insertColIDs[i]), - mb.b.factory.ConstructVariable(fetchScope.cols[i].id), + mb.b.factory.ConstructVariable(mb.fetchScope.cols[i].id), ) on = append(on, mb.b.factory.ConstructFiltersItem(condition)) } @@ -969,7 +969,7 @@ func (mb *mutationBuilder) buildInputForUpsert( // Construct the left join. mb.outScope.expr = mb.b.factory.ConstructLeftJoin( mb.outScope.expr, - fetchScope.expr, + mb.fetchScope.expr, on, memo.EmptyJoinPrivate, ) @@ -992,9 +992,6 @@ func (mb *mutationBuilder) buildInputForUpsert( mb.targetColList = make(opt.ColList, 0, mb.tab.ColumnCount()) mb.targetColSet = opt.ColSet{} - - // Add any partial index del boolean columns to the input for UPSERTs. - mb.projectPartialIndexDelCols(fetchScope) } // setUpsertCols sets the list of columns to be updated in case of conflict. @@ -1064,25 +1061,18 @@ func (mb *mutationBuilder) buildUpsert(returning tree.ReturningExprs) { // Add any check constraint boolean columns to the input. mb.addCheckConstraintCols() - // Add any partial index put boolean columns. The variables in these partial - // index predicates must resolve to the new column values of the row which - // are either the existing values of the columns or new values provided in - // the upsert. Therefore, the variables must resolve to the upsert CASE - // expression columns, so the project must be added after the upsert columns - // are. - // - // For example, consider the table and upsert: - // - // CREATE TABLE t (a INT PRIMARY KEY, b INT, INDEX (b) WHERE a > 1) - // INSERT INTO t (a, b) VALUES (1, 2) ON CONFLICT (a) DO UPDATE a = t.a + 1 + // Project partial index PUT and DEL boolean columns. // - // An entry in the partial index should only be added when a > 1. The - // resulting value of a is dependent on whether or not there is a conflict. - // In the case of no conflict, the (1, 2) is inserted into the table, and no - // partial index entry should be added. But if there is a conflict, The - // existing row where a = 1 has a incremented to 2, and an entry should be - // added to the partial index. - mb.projectPartialIndexPutCols(preCheckScope) + // In some cases existing rows may not be fetched for an UPSERT (see + // mutationBuilder.needExistingRows for more details). In theses cases + // there is no need to project partial index DEL columns and + // mb.fetchScope will be nil. Therefore, we only project partial index + // PUT columns. + if mb.needExistingRows() { + mb.projectPartialIndexPutAndDelCols(preCheckScope, mb.fetchScope) + } else { + mb.projectPartialIndexPutCols(preCheckScope) + } mb.buildFKChecksForUpsert() diff --git a/pkg/sql/opt/optbuilder/mutation_builder.go b/pkg/sql/opt/optbuilder/mutation_builder.go index 0b985de14846..8fae68a17b0e 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder.go +++ b/pkg/sql/opt/optbuilder/mutation_builder.go @@ -56,6 +56,9 @@ type mutationBuilder struct { // expression is completed, it will be contained in outScope.expr. outScope *scope + // fetchScope contains the set of columns fetched from the target table. + fetchScope *scope + // targetColList is an ordered list of IDs of the table columns into which // values will be inserted, or which will be updated with new values. It is // incrementally built as the mutation operator is built. @@ -251,7 +254,7 @@ func (mb *mutationBuilder) buildInputForUpdate( // // NOTE: Include mutation columns, but be careful to never use them for any // reason other than as "fetch columns". See buildScan comment. - scanScope := mb.b.buildScan( + mb.fetchScope = mb.b.buildScan( mb.b.addTable(mb.tab, &mb.alias), tableOrdinals(mb.tab, columnKinds{ includeMutations: true, @@ -262,7 +265,7 @@ func (mb *mutationBuilder) buildInputForUpdate( noRowLocking, inScope, ) - mb.outScope = scanScope + mb.outScope = mb.fetchScope // Set list of columns that will be fetched by the input expression. mb.setFetchColIDs(mb.outScope.cols) @@ -327,9 +330,6 @@ func (mb *mutationBuilder) buildInputForUpdate( pkCols, mb.outScope, false /* nullsAreDistinct */, "" /* errorOnDup */) } } - - // Add partial index del boolean columns to the input. - mb.projectPartialIndexDelCols(scanScope) } // buildInputForDelete constructs a Select expression from the fields in @@ -361,7 +361,7 @@ func (mb *mutationBuilder) buildInputForDelete( // NOTE: Include mutation columns, but be careful to never use them for any // reason other than as "fetch columns". See buildScan comment. // TODO(andyk): Why does execution engine need mutation columns for Delete? - scanScope := mb.b.buildScan( + mb.fetchScope = mb.b.buildScan( mb.b.addTable(mb.tab, &mb.alias), tableOrdinals(mb.tab, columnKinds{ includeMutations: true, @@ -372,7 +372,7 @@ func (mb *mutationBuilder) buildInputForDelete( noRowLocking, inScope, ) - mb.outScope = scanScope + mb.outScope = mb.fetchScope // WHERE mb.b.buildWhere(where, mb.outScope) @@ -393,9 +393,6 @@ func (mb *mutationBuilder) buildInputForDelete( // Set list of columns that will be fetched by the input expression. mb.setFetchColIDs(mb.outScope.cols) - - // Add partial index boolean columns to the input. - mb.projectPartialIndexDelCols(scanScope) } // addTargetColsByName adds one target column for each of the names in the given @@ -763,31 +760,60 @@ func (mb *mutationBuilder) addCheckConstraintCols() { } } -// projectPartialIndexPutCols builds a Project that synthesizes boolean output -// columns for each partial index defined on the target table. The execution -// code uses these booleans to determine whether or not to add a row in the -// partial index. +// projectPartialIndexPutCols builds a Project that synthesizes boolean PUT +// columns for each partial index defined on the target table. See +// partialIndexPutColIDs for more info on these columns. // -// predScope is the scope of columns available to the partial index predicate -// expression. -func (mb *mutationBuilder) projectPartialIndexPutCols(predScope *scope) { - mb.projectPartialIndexCols(mb.partialIndexPutColIDs, predScope) +// putScope must contain the columns representing the values of each mutated row +// AFTER the mutation is applied. +func (mb *mutationBuilder) projectPartialIndexPutCols(putScope *scope) { + if putScope == nil { + panic(errors.AssertionFailedf("cannot project partial index PUT columns with nil scope")) + } + mb.projectPartialIndexColsImpl(putScope, nil /* delScope */) } -// projectPartialIndexPutCols builds a Project that synthesizes boolean output -// columns for each partial index defined on the target table. The execution -// code uses these booleans to determine whether or not to remove a row in the -// partial index. +// projectPartialIndexDelCols builds a Project that synthesizes boolean PUT +// columns for each partial index defined on the target table. See +// partialIndexDelColIDs for more info on these columns. +// +// delScope must contain the columns representing the values of each mutated row +// BEFORE the mutation is applied. +func (mb *mutationBuilder) projectPartialIndexDelCols(delScope *scope) { + if delScope == nil { + panic(errors.AssertionFailedf("cannot project partial index DEL columns with nil scope")) + } + mb.projectPartialIndexColsImpl(nil /* putScope */, delScope) +} + +// projectPartialIndexPutAndDelCols builds a Project that synthesizes boolean PUT and +// DEL columns for each partial index defined on the target table. See +// partialIndexPutColIDs and partialIndexDelColIDs for more info on these +// columns. +// +// putScope must contain the columns representing the values of each mutated row +// AFTER the mutation is applied. // -// predScope is the scope of columns available to the partial index predicate -// expression. -func (mb *mutationBuilder) projectPartialIndexDelCols(predScope *scope) { - mb.projectPartialIndexCols(mb.partialIndexDelColIDs, predScope) +// delScope must contain the columns representing the values of each mutated row +// BEFORE the mutation is applied. +func (mb *mutationBuilder) projectPartialIndexPutAndDelCols(putScope, delScope *scope) { + if putScope == nil { + panic(errors.AssertionFailedf("cannot project partial index PUT columns with nil scope")) + } + if delScope == nil { + panic(errors.AssertionFailedf("cannot project partial index DEL columns with nil scope")) + } + mb.projectPartialIndexColsImpl(putScope, delScope) } -// projectPartialIndexCols builds a Project that synthesizes boolean output -// columns for each partial index defined on the target table. -func (mb *mutationBuilder) projectPartialIndexCols(colIDs opt.ColList, predScope *scope) { +// projectPartialIndexColsImpl builds a Project that synthesizes boolean PUT and +// DEL columns for each partial index defined on the target table. PUT columns +// are only projected if putScope is non-nil and DEL columns are only projected +// if delScope is non-nil. +// +// NOTE: This function should only be called via projectPartialIndexPutCols, +// projectPartialIndexDelCols, or projectPartialIndexPutAndDelCols. +func (mb *mutationBuilder) projectPartialIndexColsImpl(putScope, delScope *scope) { if partialIndexCount(mb.tab) > 0 { projectionScope := mb.outScope.replace() projectionScope.appendColumnsFromScope(mb.outScope) @@ -795,16 +821,33 @@ func (mb *mutationBuilder) projectPartialIndexCols(colIDs opt.ColList, predScope ord := 0 for i, n := 0, mb.tab.DeletableIndexCount(); i < n; i++ { index := mb.tab.Index(i) + + // Skip non-partial indexes. if _, isPartial := index.Predicate(); !isPartial { continue } expr := mb.parsePartialIndexPredicateExpr(i) - texpr := predScope.resolveAndRequireType(expr, types.Bool) - scopeCol := mb.b.addColumn(projectionScope, "", texpr) - mb.b.buildScalar(texpr, predScope, projectionScope, scopeCol, nil) - colIDs[ord] = scopeCol.id + // Build synthesized PUT columns. + if putScope != nil { + texpr := putScope.resolveAndRequireType(expr, types.Bool) + alias := fmt.Sprintf("partial_index_put%d", ord+1) + scopeCol := mb.b.addColumn(projectionScope, alias, texpr) + + mb.b.buildScalar(texpr, putScope, projectionScope, scopeCol, nil) + mb.partialIndexPutColIDs[ord] = scopeCol.id + } + + // Build synthesized DEL columns. + if delScope != nil { + texpr := delScope.resolveAndRequireType(expr, types.Bool) + alias := fmt.Sprintf("partial_index_del%d", ord+1) + scopeCol := mb.b.addColumn(projectionScope, alias, texpr) + + mb.b.buildScalar(texpr, delScope, projectionScope, scopeCol, nil) + mb.partialIndexDelColIDs[ord] = scopeCol.id + } ord++ } diff --git a/pkg/sql/opt/optbuilder/testdata/delete b/pkg/sql/opt/optbuilder/testdata/delete index d49c6b186526..841e0be9a0e4 100644 --- a/pkg/sql/opt/optbuilder/testdata/delete +++ b/pkg/sql/opt/optbuilder/testdata/delete @@ -461,9 +461,9 @@ DELETE FROM partial_indexes delete partial_indexes ├── columns: ├── fetch columns: a:5 b:6 c:7 - ├── partial index del columns: column9:9 column10:10 column11:11 column12:12 + ├── partial index del columns: partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4:12 └── project - ├── columns: column9:9 column10:10 column11:11 column12:12 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 + ├── columns: partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4:12 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 ├── scan partial_indexes │ ├── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 │ └── partial index predicates @@ -472,7 +472,7 @@ delete partial_indexes │ └── secondary: filters │ └── (a:5 > b:6) AND (c:7 = 'bar') └── projections - ├── c:7 = 'foo' [as=column9:9] - ├── (a:5 > b:6) AND (c:7 = 'bar') [as=column10:10] - ├── c:7 = 'delete-only' [as=column11:11] - └── c:7 = 'write-only' [as=column12:12] + ├── c:7 = 'foo' [as=partial_index_del1:9] + ├── (a:5 > b:6) AND (c:7 = 'bar') [as=partial_index_del2:10] + ├── c:7 = 'delete-only' [as=partial_index_del3:11] + └── c:7 = 'write-only' [as=partial_index_del4:12] diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade index c09eb8aae3c0..6bc3d46dda0d 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade @@ -309,3 +309,61 @@ root │ └── cd.c:17 => c:27 └── filters └── b:25 = c:27 + +# Test cascades to a child with a partial index. +exec-ddl +CREATE TABLE parent_partial (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial ( + c INT PRIMARY KEY, + p INT REFERENCES parent_partial(p) ON DELETE CASCADE, + i INT, + INDEX (p) WHERE i > 0, + INDEX (i) WHERE p > 0 +) +---- + +# Test a cascade to a child with a partial index. +build-cascades +DELETE FROM parent_partial WHERE p > 1 AND random() < 0.5 +---- +root + ├── delete parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── select + │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── scan parent_partial + │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ └── filters + │ └── (p:3 > 1) AND (random() < 0.5) + └── cascade + └── delete child_partial + ├── columns: + ├── fetch columns: c:9 child_partial.p:10 i:11 + ├── partial index del columns: partial_index_del1:14 partial_index_del2:15 + └── project + ├── columns: partial_index_del1:14 partial_index_del2:15 c:9!null child_partial.p:10 i:11 + ├── semi-join (hash) + │ ├── columns: c:9!null child_partial.p:10 i:11 + │ ├── scan child_partial + │ │ ├── columns: c:9!null child_partial.p:10 i:11 + │ │ └── partial index predicates + │ │ ├── secondary: filters + │ │ │ └── i:11 > 0 + │ │ └── secondary: filters + │ │ └── child_partial.p:10 > 0 + │ ├── with-scan &1 + │ │ ├── columns: p:13!null + │ │ └── mapping: + │ │ └── parent_partial.p:3 => p:13 + │ └── filters + │ └── child_partial.p:10 = p:13 + └── projections + ├── i:11 > 0 [as=partial_index_del1:14] + └── child_partial.p:10 > 0 [as=partial_index_del2:15] diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-default b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-default index a8c3f844824e..ca79a3e07e8a 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-default +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-default @@ -200,3 +200,81 @@ root ├── c:28 = parent_multicol.p:31 ├── q_new:29 = parent_multicol.q:32 └── r_new:30 = parent_multicol.r:33 + +# Test a cascade to a child with a partial index. +exec-ddl +CREATE TABLE parent_partial (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial ( + c INT PRIMARY KEY, + p INT DEFAULT 0 REFERENCES parent_partial(p) ON DELETE SET DEFAULT, + i INT, + INDEX (p) WHERE i > 0, + INDEX (i) WHERE p > 0 +) +---- + +build-cascades +DELETE FROM parent_partial WHERE p > 1 +---- +root + ├── delete parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── select + │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── scan parent_partial + │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ └── filters + │ └── p:3 > 1 + └── cascade + └── update child_partial + ├── columns: + ├── fetch columns: c:9 child_partial.p:10 i:11 + ├── update-mapping: + │ └── p_new:14 => child_partial.p:6 + ├── partial index put columns: partial_index_put1:15 partial_index_put2:16 + ├── partial index del columns: partial_index_put1:15 partial_index_del2:17 + ├── input binding: &2 + ├── project + │ ├── columns: partial_index_put1:15 partial_index_put2:16!null partial_index_del2:17 c:9!null child_partial.p:10 i:11 p_new:14!null + │ ├── project + │ │ ├── columns: p_new:14!null c:9!null child_partial.p:10 i:11 + │ │ ├── semi-join (hash) + │ │ │ ├── columns: c:9!null child_partial.p:10 i:11 + │ │ │ ├── scan child_partial + │ │ │ │ ├── columns: c:9!null child_partial.p:10 i:11 + │ │ │ │ └── partial index predicates + │ │ │ │ ├── secondary: filters + │ │ │ │ │ └── i:11 > 0 + │ │ │ │ └── secondary: filters + │ │ │ │ └── child_partial.p:10 > 0 + │ │ │ ├── with-scan &1 + │ │ │ │ ├── columns: p:13!null + │ │ │ │ └── mapping: + │ │ │ │ └── parent_partial.p:3 => p:13 + │ │ │ └── filters + │ │ │ └── child_partial.p:10 = p:13 + │ │ └── projections + │ │ └── 0 [as=p_new:14] + │ └── projections + │ ├── i:11 > 0 [as=partial_index_put1:15] + │ ├── p_new:14 > 0 [as=partial_index_put2:16] + │ └── child_partial.p:10 > 0 [as=partial_index_del2:17] + └── f-k-checks + └── f-k-checks-item: child_partial(p) -> parent_partial(p) + └── anti-join (hash) + ├── columns: p_new:18!null + ├── with-scan &2 + │ ├── columns: p_new:18!null + │ └── mapping: + │ └── p_new:14 => p_new:18 + ├── scan parent_partial + │ └── columns: parent_partial.p:19!null + └── filters + └── p_new:18 = parent_partial.p:19 diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-null b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-null index e59b4e276f18..3c0338ec9cf7 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-null +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-set-null @@ -118,3 +118,68 @@ root │ └── (p_new:24 + p_new:24) + p_new:24 [as=column25:25] └── projections └── (c:15 > 100) OR (p_new:24 IS NOT NULL) [as=check1:26] + +# Test a cascade to a child with a partial index. +exec-ddl +CREATE TABLE parent_partial (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial ( + c INT PRIMARY KEY, + p INT REFERENCES parent_partial(p) ON DELETE SET NULL, + i INT, + INDEX (p) WHERE i > 0, + INDEX (i) WHERE p > 0 +) +---- + +build-cascades +DELETE FROM parent_partial WHERE p > 1 +---- +root + ├── delete parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── select + │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── scan parent_partial + │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ └── filters + │ └── p:3 > 1 + └── cascade + └── update child_partial + ├── columns: + ├── fetch columns: c:9 child_partial.p:10 i:11 + ├── update-mapping: + │ └── p_new:14 => child_partial.p:6 + ├── partial index put columns: partial_index_put1:15 partial_index_put2:16 + ├── partial index del columns: partial_index_put1:15 partial_index_del2:17 + └── project + ├── columns: partial_index_put1:15 partial_index_put2:16 partial_index_del2:17 c:9!null child_partial.p:10 i:11 p_new:14 + ├── project + │ ├── columns: p_new:14 c:9!null child_partial.p:10 i:11 + │ ├── semi-join (hash) + │ │ ├── columns: c:9!null child_partial.p:10 i:11 + │ │ ├── scan child_partial + │ │ │ ├── columns: c:9!null child_partial.p:10 i:11 + │ │ │ └── partial index predicates + │ │ │ ├── secondary: filters + │ │ │ │ └── i:11 > 0 + │ │ │ └── secondary: filters + │ │ │ └── child_partial.p:10 > 0 + │ │ ├── with-scan &1 + │ │ │ ├── columns: p:13!null + │ │ │ └── mapping: + │ │ │ └── parent_partial.p:3 => p:13 + │ │ └── filters + │ │ └── child_partial.p:10 = p:13 + │ └── projections + │ └── NULL::INT8 [as=p_new:14] + └── projections + ├── i:11 > 0 [as=partial_index_put1:15] + ├── p_new:14 > 0 [as=partial_index_put2:16] + └── child_partial.p:10 > 0 [as=partial_index_del2:17] diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-update-cascade b/pkg/sql/opt/optbuilder/testdata/fk-on-update-cascade index 9f32026a2d8a..33cebdf137fc 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-update-cascade +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-update-cascade @@ -731,6 +731,298 @@ root ├── c:43 = child_multi.c:45 └── column3:44 = child_multi.q:47 +# Test a cascade to a child with a partial index. +exec-ddl +CREATE TABLE parent_partial (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial ( + c INT PRIMARY KEY, + p INT REFERENCES parent_partial(p) ON UPDATE CASCADE, + i INT, + INDEX (p) WHERE i > 0, + INDEX (i) WHERE p > 0 +) +---- + +build-cascades +UPDATE parent_partial SET p = p * 10 WHERE p > 1 +---- +root + ├── update parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── update-mapping: + │ │ └── p_new:5 => p:1 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── project + │ ├── columns: p_new:5!null p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── select + │ │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ ├── scan parent_partial + │ │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ └── filters + │ │ └── p:3 > 1 + │ └── projections + │ └── p:3 * 10 [as=p_new:5] + └── cascade + └── update child_partial + ├── columns: + ├── fetch columns: c:10 child_partial.p:11 i:12 + ├── update-mapping: + │ └── p_new:15 => child_partial.p:7 + ├── partial index put columns: partial_index_put1:16 partial_index_put2:17 + ├── partial index del columns: partial_index_put1:16 partial_index_del2:18 + ├── input binding: &2 + ├── project + │ ├── columns: partial_index_put1:16 partial_index_put2:17!null partial_index_del2:18!null c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null + │ ├── inner-join (hash) + │ │ ├── columns: c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null + │ │ ├── scan child_partial + │ │ │ ├── columns: c:10!null child_partial.p:11 i:12 + │ │ │ └── partial index predicates + │ │ │ ├── secondary: filters + │ │ │ │ └── i:12 > 0 + │ │ │ └── secondary: filters + │ │ │ └── child_partial.p:11 > 0 + │ │ ├── select + │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ ├── with-scan &1 + │ │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ │ └── mapping: + │ │ │ │ ├── parent_partial.p:3 => p:14 + │ │ │ │ └── p_new:5 => p_new:15 + │ │ │ └── filters + │ │ │ └── p:14 IS DISTINCT FROM p_new:15 + │ │ └── filters + │ │ └── child_partial.p:11 = p:14 + │ └── projections + │ ├── i:12 > 0 [as=partial_index_put1:16] + │ ├── p_new:15 > 0 [as=partial_index_put2:17] + │ └── child_partial.p:11 > 0 [as=partial_index_del2:18] + └── f-k-checks + └── f-k-checks-item: child_partial(p) -> parent_partial(p) + └── anti-join (hash) + ├── columns: p_new:19!null + ├── with-scan &2 + │ ├── columns: p_new:19!null + │ └── mapping: + │ └── p_new:15 => p_new:19 + ├── scan parent_partial + │ └── columns: parent_partial.p:20!null + └── filters + └── p_new:19 = parent_partial.p:20 + +# Test a cascade to a child with a partial index with an ambiguous name. +exec-ddl +CREATE TABLE parent_partial_ambig (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial_ambig ( + c INT PRIMARY KEY, + p_new INT REFERENCES parent_partial_ambig(p) ON UPDATE CASCADE, + i INT, + INDEX (i) WHERE p_new > 0 +) +---- + +build-cascades +UPDATE parent_partial_ambig SET p = p * 10 WHERE p > 1 +---- +root + ├── update parent_partial_ambig + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── update-mapping: + │ │ └── p_new:5 => p:1 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_new_ref_parent_partial_ambig + │ └── project + │ ├── columns: p_new:5!null p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── select + │ │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ ├── scan parent_partial_ambig + │ │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ └── filters + │ │ └── p:3 > 1 + │ └── projections + │ └── p:3 * 10 [as=p_new:5] + └── cascade + └── update child_partial_ambig + ├── columns: + ├── fetch columns: c:10 child_partial_ambig.p_new:11 i:12 + ├── update-mapping: + │ └── p_new:15 => child_partial_ambig.p_new:7 + ├── partial index put columns: partial_index_put1:16 + ├── partial index del columns: partial_index_del1:17 + ├── input binding: &2 + ├── project + │ ├── columns: partial_index_put1:16!null partial_index_del1:17!null c:10!null child_partial_ambig.p_new:11!null i:12 p:14!null p_new:15!null + │ ├── inner-join (hash) + │ │ ├── columns: c:10!null child_partial_ambig.p_new:11!null i:12 p:14!null p_new:15!null + │ │ ├── scan child_partial_ambig + │ │ │ ├── columns: c:10!null child_partial_ambig.p_new:11 i:12 + │ │ │ └── partial index predicates + │ │ │ └── secondary: filters + │ │ │ └── child_partial_ambig.p_new:11 > 0 + │ │ ├── select + │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ ├── with-scan &1 + │ │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ │ └── mapping: + │ │ │ │ ├── parent_partial_ambig.p:3 => p:14 + │ │ │ │ └── p_new:5 => p_new:15 + │ │ │ └── filters + │ │ │ └── p:14 IS DISTINCT FROM p_new:15 + │ │ └── filters + │ │ └── child_partial_ambig.p_new:11 = p:14 + │ └── projections + │ ├── p_new:15 > 0 [as=partial_index_put1:16] + │ └── child_partial_ambig.p_new:11 > 0 [as=partial_index_del1:17] + └── f-k-checks + └── f-k-checks-item: child_partial_ambig(p_new) -> parent_partial_ambig(p) + └── anti-join (hash) + ├── columns: p_new:18!null + ├── with-scan &2 + │ ├── columns: p_new:18!null + │ └── mapping: + │ └── p_new:15 => p_new:18 + ├── scan parent_partial_ambig + │ └── columns: parent_partial_ambig.p:19!null + └── filters + └── p_new:18 = parent_partial_ambig.p:19 + +# Test an UPSERT that cascades to a child with a partial index. +exec-ddl +CREATE TABLE parent_multi_partial ( + pk INT PRIMARY KEY, + p INT, q INT, + UNIQUE (p, q), + FAMILY (pk), + FAMILY (p), + FAMILY (q) +) +---- + +exec-ddl +CREATE TABLE child_multi_partial ( + c INT PRIMARY KEY, + p INT, q INT, + i INT, + UNIQUE (c, q), + INDEX (p, q) WHERE i > 0, + INDEX (i) WHERE p > 0 AND q > 0, + CONSTRAINT fk FOREIGN KEY (p, q) REFERENCES parent_multi_partial(p, q) ON UPDATE CASCADE +) +---- + +build-cascades +UPSERT INTO parent_multi_partial VALUES (1), (2) +---- +root + ├── upsert parent_multi_partial + │ ├── columns: + │ ├── arbiter indexes: primary + │ ├── canary column: pk:7 + │ ├── fetch columns: pk:7 p:8 q:9 + │ ├── insert-mapping: + │ │ ├── column1:5 => pk:1 + │ │ ├── column6:6 => p:2 + │ │ └── column6:6 => q:3 + │ ├── update-mapping: + │ │ ├── column6:6 => p:2 + │ │ └── column6:6 => q:3 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk + │ └── project + │ ├── columns: upsert_pk:11 column1:5!null column6:6 pk:7 p:8 q:9 crdb_internal_mvcc_timestamp:10 + │ ├── left-join (hash) + │ │ ├── columns: column1:5!null column6:6 pk:7 p:8 q:9 crdb_internal_mvcc_timestamp:10 + │ │ ├── ensure-upsert-distinct-on + │ │ │ ├── columns: column1:5!null column6:6 + │ │ │ ├── grouping columns: column1:5!null + │ │ │ ├── project + │ │ │ │ ├── columns: column6:6 column1:5!null + │ │ │ │ ├── values + │ │ │ │ │ ├── columns: column1:5!null + │ │ │ │ │ ├── (1,) + │ │ │ │ │ └── (2,) + │ │ │ │ └── projections + │ │ │ │ └── NULL::INT8 [as=column6:6] + │ │ │ └── aggregations + │ │ │ └── first-agg [as=column6:6] + │ │ │ └── column6:6 + │ │ ├── scan parent_multi_partial + │ │ │ └── columns: pk:7!null p:8 q:9 crdb_internal_mvcc_timestamp:10 + │ │ └── filters + │ │ └── column1:5 = pk:7 + │ └── projections + │ └── CASE WHEN pk:7 IS NULL THEN column1:5 ELSE pk:7 END [as=upsert_pk:11] + └── cascade + └── update child_multi_partial + ├── columns: + ├── fetch columns: c:17 child_multi_partial.p:18 child_multi_partial.q:19 i:20 + ├── update-mapping: + │ ├── column6:24 => child_multi_partial.p:13 + │ └── column6:25 => child_multi_partial.q:14 + ├── partial index put columns: partial_index_put1:26 partial_index_put2:27 + ├── partial index del columns: partial_index_put1:26 partial_index_del2:28 + ├── input binding: &2 + ├── project + │ ├── columns: partial_index_put1:26 partial_index_put2:27 partial_index_del2:28!null c:17!null child_multi_partial.p:18!null child_multi_partial.q:19!null i:20 p:22!null q:23!null column6:24 column6:25 + │ ├── inner-join (hash) + │ │ ├── columns: c:17!null child_multi_partial.p:18!null child_multi_partial.q:19!null i:20 p:22!null q:23!null column6:24 column6:25 + │ │ ├── scan child_multi_partial + │ │ │ ├── columns: c:17!null child_multi_partial.p:18 child_multi_partial.q:19 i:20 + │ │ │ └── partial index predicates + │ │ │ ├── secondary: filters + │ │ │ │ └── i:20 > 0 + │ │ │ └── secondary: filters + │ │ │ └── (child_multi_partial.p:18 > 0) AND (child_multi_partial.q:19 > 0) + │ │ ├── select + │ │ │ ├── columns: p:22 q:23 column6:24 column6:25 + │ │ │ ├── with-scan &1 + │ │ │ │ ├── columns: p:22 q:23 column6:24 column6:25 + │ │ │ │ └── mapping: + │ │ │ │ ├── parent_multi_partial.p:8 => p:22 + │ │ │ │ ├── parent_multi_partial.q:9 => q:23 + │ │ │ │ ├── column6:6 => column6:24 + │ │ │ │ └── column6:6 => column6:25 + │ │ │ └── filters + │ │ │ └── (p:22 IS DISTINCT FROM column6:24) OR (q:23 IS DISTINCT FROM column6:25) + │ │ └── filters + │ │ ├── child_multi_partial.p:18 = p:22 + │ │ └── child_multi_partial.q:19 = q:23 + │ └── projections + │ ├── i:20 > 0 [as=partial_index_put1:26] + │ ├── (column6:24 > 0) AND (column6:25 > 0) [as=partial_index_put2:27] + │ └── (child_multi_partial.p:18 > 0) AND (child_multi_partial.q:19 > 0) [as=partial_index_del2:28] + └── f-k-checks + └── f-k-checks-item: child_multi_partial(p,q) -> parent_multi_partial(p,q) + └── anti-join (hash) + ├── columns: column6:29!null column6:30!null + ├── select + │ ├── columns: column6:29!null column6:30!null + │ ├── with-scan &2 + │ │ ├── columns: column6:29 column6:30 + │ │ └── mapping: + │ │ ├── column6:24 => column6:29 + │ │ └── column6:25 => column6:30 + │ └── filters + │ ├── column6:29 IS NOT NULL + │ └── column6:30 IS NOT NULL + ├── scan parent_multi_partial + │ └── columns: parent_multi_partial.p:32 parent_multi_partial.q:33 + └── filters + ├── column6:29 = parent_multi_partial.p:32 + └── column6:30 = parent_multi_partial.q:33 + # Regression test for #57148. A check constraint or computed column in a child # table that references a column with the same name as the parent's synthesized # update column should not result in an ambiguous column reference error. In diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-default b/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-default index d1c37311ef44..7d34595e9117 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-default +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-default @@ -762,3 +762,92 @@ root └── filters ├── c_new:50 = child_multi.c:52 └── q_new:51 = child_multi.q:54 + +# Test a cascade to a child with a partial index. +exec-ddl +CREATE TABLE parent_partial (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial ( + c INT PRIMARY KEY, + p INT DEFAULT 0 NOT NULL REFERENCES parent_partial(p) ON UPDATE SET DEFAULT, + i INT, + INDEX (p) WHERE i > 0, + INDEX (i) WHERE p > 0 +) +---- + +build-cascades +UPDATE parent_partial SET p = p * 10 WHERE p > 1 +---- +root + ├── update parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── update-mapping: + │ │ └── p_new:5 => p:1 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── project + │ ├── columns: p_new:5!null p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── select + │ │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ ├── scan parent_partial + │ │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ └── filters + │ │ └── p:3 > 1 + │ └── projections + │ └── p:3 * 10 [as=p_new:5] + └── cascade + └── update child_partial + ├── columns: + ├── fetch columns: c:10 child_partial.p:11 i:12 + ├── update-mapping: + │ └── p_new:16 => child_partial.p:7 + ├── partial index put columns: partial_index_put1:17 partial_index_put2:18 + ├── partial index del columns: partial_index_put1:17 partial_index_del2:19 + ├── input binding: &2 + ├── project + │ ├── columns: partial_index_put1:17 partial_index_put2:18!null partial_index_del2:19!null c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null p_new:16!null + │ ├── project + │ │ ├── columns: p_new:16!null c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null + │ │ ├── inner-join (hash) + │ │ │ ├── columns: c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null + │ │ │ ├── scan child_partial + │ │ │ │ ├── columns: c:10!null child_partial.p:11!null i:12 + │ │ │ │ └── partial index predicates + │ │ │ │ ├── secondary: filters + │ │ │ │ │ └── i:12 > 0 + │ │ │ │ └── secondary: filters + │ │ │ │ └── child_partial.p:11 > 0 + │ │ │ ├── select + │ │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ │ ├── with-scan &1 + │ │ │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ │ │ └── mapping: + │ │ │ │ │ ├── parent_partial.p:3 => p:14 + │ │ │ │ │ └── p_new:5 => p_new:15 + │ │ │ │ └── filters + │ │ │ │ └── p:14 IS DISTINCT FROM p_new:15 + │ │ │ └── filters + │ │ │ └── child_partial.p:11 = p:14 + │ │ └── projections + │ │ └── 0 [as=p_new:16] + │ └── projections + │ ├── i:12 > 0 [as=partial_index_put1:17] + │ ├── p_new:16 > 0 [as=partial_index_put2:18] + │ └── child_partial.p:11 > 0 [as=partial_index_del2:19] + └── f-k-checks + └── f-k-checks-item: child_partial(p) -> parent_partial(p) + └── anti-join (hash) + ├── columns: p_new:20!null + ├── with-scan &2 + │ ├── columns: p_new:20!null + │ └── mapping: + │ └── p_new:16 => p_new:20 + ├── scan parent_partial + │ └── columns: parent_partial.p:21!null + └── filters + └── p_new:20 = parent_partial.p:21 diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-null b/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-null index fdb9556d0e0d..6173c8e5990e 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-null +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-update-set-null @@ -591,3 +591,79 @@ root │ └── grandchild.q:35 = q:38 └── projections └── NULL::INT8 [as=c_new:41] + +# Test a cascade to a child with a partial index. +exec-ddl +CREATE TABLE parent_partial (p INT PRIMARY KEY) +---- + +exec-ddl +CREATE TABLE child_partial ( + c INT PRIMARY KEY, + p INT DEFAULT 0 NOT NULL REFERENCES parent_partial(p) ON UPDATE SET NULL, + i INT, + INDEX (p) WHERE i > 0, + INDEX (i) WHERE p > 0 +) +---- + +build-cascades +UPDATE parent_partial SET p = p * 10 WHERE p > 1 +---- +root + ├── update parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── update-mapping: + │ │ └── p_new:5 => p:1 + │ ├── input binding: &1 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── project + │ ├── columns: p_new:5!null p:3!null crdb_internal_mvcc_timestamp:4 + │ ├── select + │ │ ├── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ ├── scan parent_partial + │ │ │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + │ │ └── filters + │ │ └── p:3 > 1 + │ └── projections + │ └── p:3 * 10 [as=p_new:5] + └── cascade + └── update child_partial + ├── columns: + ├── fetch columns: c:10 child_partial.p:11 i:12 + ├── update-mapping: + │ └── p_new:16 => child_partial.p:7 + ├── partial index put columns: partial_index_put1:17 partial_index_put2:18 + ├── partial index del columns: partial_index_put1:17 partial_index_del2:19 + └── project + ├── columns: partial_index_put1:17 partial_index_put2:18 partial_index_del2:19!null c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null p_new:16 + ├── project + │ ├── columns: p_new:16 c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null + │ ├── inner-join (hash) + │ │ ├── columns: c:10!null child_partial.p:11!null i:12 p:14!null p_new:15!null + │ │ ├── scan child_partial + │ │ │ ├── columns: c:10!null child_partial.p:11!null i:12 + │ │ │ └── partial index predicates + │ │ │ ├── secondary: filters + │ │ │ │ └── i:12 > 0 + │ │ │ └── secondary: filters + │ │ │ └── child_partial.p:11 > 0 + │ │ ├── select + │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ ├── with-scan &1 + │ │ │ │ ├── columns: p:14!null p_new:15!null + │ │ │ │ └── mapping: + │ │ │ │ ├── parent_partial.p:3 => p:14 + │ │ │ │ └── p_new:5 => p_new:15 + │ │ │ └── filters + │ │ │ └── p:14 IS DISTINCT FROM p_new:15 + │ │ └── filters + │ │ └── child_partial.p:11 = p:14 + │ └── projections + │ └── NULL::INT8 [as=p_new:16] + └── projections + ├── i:12 > 0 [as=partial_index_put1:17] + ├── p_new:16 > 0 [as=partial_index_put2:18] + └── child_partial.p:11 > 0 [as=partial_index_del2:19] diff --git a/pkg/sql/opt/optbuilder/testdata/insert b/pkg/sql/opt/optbuilder/testdata/insert index fe726be3cbce..38ef42aa2dad 100644 --- a/pkg/sql/opt/optbuilder/testdata/insert +++ b/pkg/sql/opt/optbuilder/testdata/insert @@ -1260,17 +1260,17 @@ insert partial_indexes │ ├── column1:5 => a:1 │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column8:8 column9:9 column10:10 column11:11 + ├── partial index put columns: partial_index_put1:8 partial_index_put2:9 partial_index_put3:10 partial_index_put4:11 └── project - ├── columns: column8:8!null column9:9!null column10:10!null column11:11!null column1:5!null column2:6!null column3:7!null + ├── columns: partial_index_put1:8!null partial_index_put2:9!null partial_index_put3:10!null partial_index_put4:11!null column1:5!null column2:6!null column3:7!null ├── values │ ├── columns: column1:5!null column2:6!null column3:7!null │ └── (2, 1, 'bar') └── projections - ├── column3:7 = 'foo' [as=column8:8] - ├── (column1:5 > column2:6) AND (column3:7 = 'bar') [as=column9:9] - ├── column3:7 = 'delete-only' [as=column10:10] - └── column3:7 = 'write-only' [as=column11:11] + ├── column3:7 = 'foo' [as=partial_index_put1:8] + ├── (column1:5 > column2:6) AND (column3:7 = 'bar') [as=partial_index_put2:9] + ├── column3:7 = 'delete-only' [as=partial_index_put3:10] + └── column3:7 = 'write-only' [as=partial_index_put4:11] # Regression test for issue #52546. Building partial index predicate expressions # that are only a single column reference should not panic. diff --git a/pkg/sql/opt/optbuilder/testdata/update b/pkg/sql/opt/optbuilder/testdata/update index b5f2260a0d1b..5d5314012935 100644 --- a/pkg/sql/opt/optbuilder/testdata/update +++ b/pkg/sql/opt/optbuilder/testdata/update @@ -1685,31 +1685,28 @@ update partial_indexes ├── columns: ├── fetch columns: a:5 b:6 c:7 ├── update-mapping: - │ └── a_new:13 => a:1 - ├── partial index put columns: column9:9 column14:14 column11:11 column12:12 - ├── partial index del columns: column9:9 column10:10 column11:11 column12:12 + │ └── a_new:9 => a:1 + ├── partial index put columns: partial_index_put1:10 partial_index_put2:11 partial_index_put3:13 partial_index_put4:14 + ├── partial index del columns: partial_index_put1:10 partial_index_del2:12 partial_index_put3:13 partial_index_put4:14 └── project - ├── columns: column14:14 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 column9:9 column10:10 column11:11 column12:12 a_new:13!null + ├── columns: partial_index_put1:10 partial_index_put2:11 partial_index_del2:12 partial_index_put3:13 partial_index_put4:14 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 a_new:9!null ├── project - │ ├── columns: a_new:13!null a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 column9:9 column10:10 column11:11 column12:12 - │ ├── project - │ │ ├── columns: column9:9 column10:10 column11:11 column12:12 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 - │ │ ├── scan partial_indexes - │ │ │ ├── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 - │ │ │ └── partial index predicates - │ │ │ ├── secondary: filters - │ │ │ │ └── c:7 = 'foo' - │ │ │ └── secondary: filters - │ │ │ └── (a:5 > b:6) AND (c:7 = 'bar') - │ │ └── projections - │ │ ├── c:7 = 'foo' [as=column9:9] - │ │ ├── (a:5 > b:6) AND (c:7 = 'bar') [as=column10:10] - │ │ ├── c:7 = 'delete-only' [as=column11:11] - │ │ └── c:7 = 'write-only' [as=column12:12] + │ ├── columns: a_new:9!null a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 + │ ├── scan partial_indexes + │ │ ├── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 + │ │ └── partial index predicates + │ │ ├── secondary: filters + │ │ │ └── c:7 = 'foo' + │ │ └── secondary: filters + │ │ └── (a:5 > b:6) AND (c:7 = 'bar') │ └── projections - │ └── 1 [as=a_new:13] + │ └── 1 [as=a_new:9] └── projections - └── (a_new:13 > b:6) AND (c:7 = 'bar') [as=column14:14] + ├── c:7 = 'foo' [as=partial_index_put1:10] + ├── (a_new:9 > b:6) AND (c:7 = 'bar') [as=partial_index_put2:11] + ├── (a:5 > b:6) AND (c:7 = 'bar') [as=partial_index_del2:12] + ├── c:7 = 'delete-only' [as=partial_index_put3:13] + └── c:7 = 'write-only' [as=partial_index_put4:14] build UPDATE partial_indexes SET a = a + 5 RETURNING * @@ -1718,28 +1715,66 @@ update partial_indexes ├── columns: a:1!null b:2 c:3 ├── fetch columns: a:5 b:6 c:7 ├── update-mapping: - │ └── a_new:13 => a:1 - ├── partial index put columns: column9:9 column14:14 column11:11 column12:12 - ├── partial index del columns: column9:9 column10:10 column11:11 column12:12 + │ └── a_new:9 => a:1 + ├── partial index put columns: partial_index_put1:10 partial_index_put2:11 partial_index_put3:13 partial_index_put4:14 + ├── partial index del columns: partial_index_put1:10 partial_index_del2:12 partial_index_put3:13 partial_index_put4:14 └── project - ├── columns: column14:14 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 column9:9 column10:10 column11:11 column12:12 a_new:13!null + ├── columns: partial_index_put1:10 partial_index_put2:11 partial_index_del2:12 partial_index_put3:13 partial_index_put4:14 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 a_new:9!null ├── project - │ ├── columns: a_new:13!null a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 column9:9 column10:10 column11:11 column12:12 - │ ├── project - │ │ ├── columns: column9:9 column10:10 column11:11 column12:12 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 - │ │ ├── scan partial_indexes - │ │ │ ├── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 + │ ├── columns: a_new:9!null a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 + │ ├── scan partial_indexes + │ │ ├── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 + │ │ └── partial index predicates + │ │ ├── secondary: filters + │ │ │ └── c:7 = 'foo' + │ │ └── secondary: filters + │ │ └── (a:5 > b:6) AND (c:7 = 'bar') + │ └── projections + │ └── a:5 + 5 [as=a_new:9] + └── projections + ├── c:7 = 'foo' [as=partial_index_put1:10] + ├── (a_new:9 > b:6) AND (c:7 = 'bar') [as=partial_index_put2:11] + ├── (a:5 > b:6) AND (c:7 = 'bar') [as=partial_index_del2:12] + ├── c:7 = 'delete-only' [as=partial_index_put3:13] + └── c:7 = 'write-only' [as=partial_index_put4:14] + +# Do not error with "column reference is ambiguous" when table column names +# match synthesized column names. +exec-ddl +CREATE TABLE t ( + partial_index_put1 INT, + partial_index_del1 INT, + UNIQUE INDEX (partial_index_put1) WHERE partial_index_put1 > 0 AND partial_index_del1 > 0 +) +---- + +build +UPDATE t SET partial_index_put1 = 1, partial_index_del1 = 2 WHERE partial_index_put1 = 10 and partial_index_del1 = 20 +---- +update t + ├── columns: + ├── fetch columns: t.partial_index_put1:5 t.partial_index_del1:6 rowid:7 + ├── update-mapping: + │ ├── partial_index_put1_new:9 => t.partial_index_put1:1 + │ └── partial_index_del1_new:10 => t.partial_index_del1:2 + ├── partial index put columns: partial_index_put1:11 + ├── partial index del columns: partial_index_del1:12 + └── project + ├── columns: partial_index_put1:11!null partial_index_del1:12!null t.partial_index_put1:5!null t.partial_index_del1:6!null rowid:7!null crdb_internal_mvcc_timestamp:8 partial_index_put1_new:9!null partial_index_del1_new:10!null + ├── project + │ ├── columns: partial_index_put1_new:9!null partial_index_del1_new:10!null t.partial_index_put1:5!null t.partial_index_del1:6!null rowid:7!null crdb_internal_mvcc_timestamp:8 + │ ├── select + │ │ ├── columns: t.partial_index_put1:5!null t.partial_index_del1:6!null rowid:7!null crdb_internal_mvcc_timestamp:8 + │ │ ├── scan t + │ │ │ ├── columns: t.partial_index_put1:5 t.partial_index_del1:6 rowid:7!null crdb_internal_mvcc_timestamp:8 │ │ │ └── partial index predicates - │ │ │ ├── secondary: filters - │ │ │ │ └── c:7 = 'foo' │ │ │ └── secondary: filters - │ │ │ └── (a:5 > b:6) AND (c:7 = 'bar') - │ │ └── projections - │ │ ├── c:7 = 'foo' [as=column9:9] - │ │ ├── (a:5 > b:6) AND (c:7 = 'bar') [as=column10:10] - │ │ ├── c:7 = 'delete-only' [as=column11:11] - │ │ └── c:7 = 'write-only' [as=column12:12] + │ │ │ └── (t.partial_index_put1:5 > 0) AND (t.partial_index_del1:6 > 0) + │ │ └── filters + │ │ └── (t.partial_index_put1:5 = 10) AND (t.partial_index_del1:6 = 20) │ └── projections - │ └── a:5 + 5 [as=a_new:13] + │ ├── 1 [as=partial_index_put1_new:9] + │ └── 2 [as=partial_index_del1_new:10] └── projections - └── (a_new:13 > b:6) AND (c:7 = 'bar') [as=column14:14] + ├── (partial_index_put1_new:9 > 0) AND (partial_index_del1_new:10 > 0) [as=partial_index_put1:11] + └── (t.partial_index_put1:5 > 0) AND (t.partial_index_del1:6 > 0) [as=partial_index_del1:12] diff --git a/pkg/sql/opt/optbuilder/testdata/upsert b/pkg/sql/opt/optbuilder/testdata/upsert index e98ec16cd6b6..03594847e1ec 100644 --- a/pkg/sql/opt/optbuilder/testdata/upsert +++ b/pkg/sql/opt/optbuilder/testdata/upsert @@ -1924,9 +1924,9 @@ insert partial_indexes │ ├── column1:5 => a:1 │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column16:16 column17:17 column18:18 column19:19 + ├── partial index put columns: partial_index_put1:16 partial_index_put2:17 partial_index_put3:18 partial_index_put4:19 └── project - ├── columns: column16:16!null column17:17!null column18:18!null column19:19!null column1:5!null column2:6!null column3:7!null + ├── columns: partial_index_put1:16!null partial_index_put2:17!null partial_index_put3:18!null partial_index_put4:19!null column1:5!null column2:6!null column3:7!null ├── upsert-distinct-on │ ├── columns: column1:5!null column2:6!null column3:7!null │ ├── grouping columns: column2:6!null column3:7!null @@ -1980,10 +1980,10 @@ insert partial_indexes │ └── first-agg [as=column1:5] │ └── column1:5 └── projections - ├── column3:7 = 'foo' [as=column16:16] - ├── (column1:5 > column2:6) AND (column3:7 = 'bar') [as=column17:17] - ├── column3:7 = 'delete-only' [as=column18:18] - └── column3:7 = 'write-only' [as=column19:19] + ├── column3:7 = 'foo' [as=partial_index_put1:16] + ├── (column1:5 > column2:6) AND (column3:7 = 'bar') [as=partial_index_put2:17] + ├── column3:7 = 'delete-only' [as=partial_index_put3:18] + └── column3:7 = 'write-only' [as=partial_index_put4:19] build INSERT INTO partial_indexes VALUES (2, 1, 'bar') ON CONFLICT (b, c) DO UPDATE SET b = partial_indexes.b + 1, c = 'baz' @@ -1998,56 +1998,53 @@ upsert partial_indexes │ ├── column2:6 => b:2 │ └── column3:7 => c:3 ├── update-mapping: - │ ├── upsert_b:19 => b:2 - │ └── upsert_c:20 => c:3 - ├── partial index put columns: column21:21 column22:22 column23:23 column24:24 - ├── partial index del columns: column12:12 column13:13 column14:14 column15:15 + │ ├── upsert_b:15 => b:2 + │ └── upsert_c:16 => c:3 + ├── partial index put columns: partial_index_put1:17 partial_index_put2:19 partial_index_put3:21 partial_index_put4:23 + ├── partial index del columns: partial_index_del1:18 partial_index_del2:20 partial_index_del3:22 partial_index_del4:24 └── project - ├── columns: column21:21!null column22:22 column23:23!null column24:24!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 column12:12 column13:13 column14:14 column15:15 b_new:16 c_new:17!null upsert_a:18 upsert_b:19 upsert_c:20!null + ├── columns: partial_index_put1:17!null partial_index_del1:18 partial_index_put2:19 partial_index_del2:20 partial_index_put3:21!null partial_index_del3:22 partial_index_put4:23!null partial_index_del4:24 column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 b_new:12 c_new:13!null upsert_a:14 upsert_b:15 upsert_c:16!null ├── project - │ ├── columns: upsert_a:18 upsert_b:19 upsert_c:20!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 column12:12 column13:13 column14:14 column15:15 b_new:16 c_new:17!null + │ ├── columns: upsert_a:14 upsert_b:15 upsert_c:16!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 b_new:12 c_new:13!null │ ├── project - │ │ ├── columns: b_new:16 c_new:17!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 column12:12 column13:13 column14:14 column15:15 - │ │ ├── project - │ │ │ ├── columns: column12:12 column13:13 column14:14 column15:15 column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 - │ │ │ ├── left-join (hash) - │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 - │ │ │ │ ├── ensure-upsert-distinct-on + │ │ ├── columns: b_new:12 c_new:13!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 + │ │ ├── left-join (hash) + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 + │ │ │ ├── ensure-upsert-distinct-on + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ ├── grouping columns: column2:6!null column3:7!null + │ │ │ │ ├── values │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null - │ │ │ │ │ ├── grouping columns: column2:6!null column3:7!null - │ │ │ │ │ ├── values - │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null - │ │ │ │ │ │ └── (2, 1, 'bar') - │ │ │ │ │ └── aggregations - │ │ │ │ │ └── first-agg [as=column1:5] - │ │ │ │ │ └── column1:5 - │ │ │ │ ├── scan partial_indexes - │ │ │ │ │ ├── columns: a:8!null b:9 c:10 crdb_internal_mvcc_timestamp:11 - │ │ │ │ │ └── partial index predicates - │ │ │ │ │ ├── secondary: filters - │ │ │ │ │ │ └── c:10 = 'foo' - │ │ │ │ │ └── secondary: filters - │ │ │ │ │ └── (a:8 > b:9) AND (c:10 = 'bar') - │ │ │ │ └── filters - │ │ │ │ ├── column2:6 = b:9 - │ │ │ │ └── column3:7 = c:10 - │ │ │ └── projections - │ │ │ ├── c:10 = 'foo' [as=column12:12] - │ │ │ ├── (a:8 > b:9) AND (c:10 = 'bar') [as=column13:13] - │ │ │ ├── c:10 = 'delete-only' [as=column14:14] - │ │ │ └── c:10 = 'write-only' [as=column15:15] + │ │ │ │ │ └── (2, 1, 'bar') + │ │ │ │ └── aggregations + │ │ │ │ └── first-agg [as=column1:5] + │ │ │ │ └── column1:5 + │ │ │ ├── scan partial_indexes + │ │ │ │ ├── columns: a:8!null b:9 c:10 crdb_internal_mvcc_timestamp:11 + │ │ │ │ └── partial index predicates + │ │ │ │ ├── secondary: filters + │ │ │ │ │ └── c:10 = 'foo' + │ │ │ │ └── secondary: filters + │ │ │ │ └── (a:8 > b:9) AND (c:10 = 'bar') + │ │ │ └── filters + │ │ │ ├── column2:6 = b:9 + │ │ │ └── column3:7 = c:10 │ │ └── projections - │ │ ├── b:9 + 1 [as=b_new:16] - │ │ └── 'baz' [as=c_new:17] + │ │ ├── b:9 + 1 [as=b_new:12] + │ │ └── 'baz' [as=c_new:13] │ └── projections - │ ├── CASE WHEN a:8 IS NULL THEN column1:5 ELSE a:8 END [as=upsert_a:18] - │ ├── CASE WHEN a:8 IS NULL THEN column2:6 ELSE b_new:16 END [as=upsert_b:19] - │ └── CASE WHEN a:8 IS NULL THEN column3:7 ELSE c_new:17 END [as=upsert_c:20] + │ ├── CASE WHEN a:8 IS NULL THEN column1:5 ELSE a:8 END [as=upsert_a:14] + │ ├── CASE WHEN a:8 IS NULL THEN column2:6 ELSE b_new:12 END [as=upsert_b:15] + │ └── CASE WHEN a:8 IS NULL THEN column3:7 ELSE c_new:13 END [as=upsert_c:16] └── projections - ├── upsert_c:20 = 'foo' [as=column21:21] - ├── (upsert_a:18 > upsert_b:19) AND (upsert_c:20 = 'bar') [as=column22:22] - ├── upsert_c:20 = 'delete-only' [as=column23:23] - └── upsert_c:20 = 'write-only' [as=column24:24] + ├── upsert_c:16 = 'foo' [as=partial_index_put1:17] + ├── c:10 = 'foo' [as=partial_index_del1:18] + ├── (upsert_a:14 > upsert_b:15) AND (upsert_c:16 = 'bar') [as=partial_index_put2:19] + ├── (a:8 > b:9) AND (c:10 = 'bar') [as=partial_index_del2:20] + ├── upsert_c:16 = 'delete-only' [as=partial_index_put3:21] + ├── c:10 = 'delete-only' [as=partial_index_del3:22] + ├── upsert_c:16 = 'write-only' [as=partial_index_put4:23] + └── c:10 = 'write-only' [as=partial_index_del4:24] build UPSERT INTO partial_indexes VALUES (2, 1, 'bar') @@ -2064,48 +2061,45 @@ upsert partial_indexes ├── update-mapping: │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column17:17 column18:18 column19:19 column20:20 - ├── partial index del columns: column12:12 column13:13 column14:14 column15:15 + ├── partial index put columns: partial_index_put1:13 partial_index_put2:15 partial_index_put3:17 partial_index_put4:19 + ├── partial index del columns: partial_index_del1:14 partial_index_del2:16 partial_index_del3:18 partial_index_del4:20 └── project - ├── columns: column17:17!null column18:18 column19:19!null column20:20!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 column12:12 column13:13 column14:14 column15:15 upsert_a:16 + ├── columns: partial_index_put1:13!null partial_index_del1:14 partial_index_put2:15 partial_index_del2:16 partial_index_put3:17!null partial_index_del3:18 partial_index_put4:19!null partial_index_del4:20 column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 upsert_a:12 ├── project - │ ├── columns: upsert_a:16 column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 column12:12 column13:13 column14:14 column15:15 - │ ├── project - │ │ ├── columns: column12:12 column13:13 column14:14 column15:15 column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 - │ │ ├── left-join (hash) - │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 - │ │ │ ├── ensure-upsert-distinct-on + │ ├── columns: upsert_a:12 column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 + │ ├── left-join (hash) + │ │ ├── columns: column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 + │ │ ├── ensure-upsert-distinct-on + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ ├── grouping columns: column1:5!null + │ │ │ ├── values │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null - │ │ │ │ ├── grouping columns: column1:5!null - │ │ │ │ ├── values - │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null - │ │ │ │ │ └── (2, 1, 'bar') - │ │ │ │ └── aggregations - │ │ │ │ ├── first-agg [as=column2:6] - │ │ │ │ │ └── column2:6 - │ │ │ │ └── first-agg [as=column3:7] - │ │ │ │ └── column3:7 - │ │ │ ├── scan partial_indexes - │ │ │ │ ├── columns: a:8!null b:9 c:10 crdb_internal_mvcc_timestamp:11 - │ │ │ │ └── partial index predicates - │ │ │ │ ├── secondary: filters - │ │ │ │ │ └── c:10 = 'foo' - │ │ │ │ └── secondary: filters - │ │ │ │ └── (a:8 > b:9) AND (c:10 = 'bar') - │ │ │ └── filters - │ │ │ └── column1:5 = a:8 - │ │ └── projections - │ │ ├── c:10 = 'foo' [as=column12:12] - │ │ ├── (a:8 > b:9) AND (c:10 = 'bar') [as=column13:13] - │ │ ├── c:10 = 'delete-only' [as=column14:14] - │ │ └── c:10 = 'write-only' [as=column15:15] + │ │ │ │ └── (2, 1, 'bar') + │ │ │ └── aggregations + │ │ │ ├── first-agg [as=column2:6] + │ │ │ │ └── column2:6 + │ │ │ └── first-agg [as=column3:7] + │ │ │ └── column3:7 + │ │ ├── scan partial_indexes + │ │ │ ├── columns: a:8!null b:9 c:10 crdb_internal_mvcc_timestamp:11 + │ │ │ └── partial index predicates + │ │ │ ├── secondary: filters + │ │ │ │ └── c:10 = 'foo' + │ │ │ └── secondary: filters + │ │ │ └── (a:8 > b:9) AND (c:10 = 'bar') + │ │ └── filters + │ │ └── column1:5 = a:8 │ └── projections - │ └── CASE WHEN a:8 IS NULL THEN column1:5 ELSE a:8 END [as=upsert_a:16] + │ └── CASE WHEN a:8 IS NULL THEN column1:5 ELSE a:8 END [as=upsert_a:12] └── projections - ├── column3:7 = 'foo' [as=column17:17] - ├── (upsert_a:16 > column2:6) AND (column3:7 = 'bar') [as=column18:18] - ├── column3:7 = 'delete-only' [as=column19:19] - └── column3:7 = 'write-only' [as=column20:20] + ├── column3:7 = 'foo' [as=partial_index_put1:13] + ├── c:10 = 'foo' [as=partial_index_del1:14] + ├── (upsert_a:12 > column2:6) AND (column3:7 = 'bar') [as=partial_index_put2:15] + ├── (a:8 > b:9) AND (c:10 = 'bar') [as=partial_index_del2:16] + ├── column3:7 = 'delete-only' [as=partial_index_put3:17] + ├── c:10 = 'delete-only' [as=partial_index_del3:18] + ├── column3:7 = 'write-only' [as=partial_index_put4:19] + └── c:10 = 'write-only' [as=partial_index_del4:20] # Columns referenced in the SET expression are ambiguous without a table name. # Is it the value of the column being inserted or the value of the column @@ -2138,9 +2132,9 @@ insert unique_partial_indexes │ ├── column1:5 => a:1 │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column17:17 + ├── partial index put columns: partial_index_put1:17 └── project - ├── columns: column17:17!null column1:5!null column2:6!null column3:7!null + ├── columns: partial_index_put1:17!null column1:5!null column2:6!null column3:7!null ├── project │ ├── columns: column1:5!null column2:6!null column3:7!null │ └── upsert-distinct-on @@ -2202,7 +2196,7 @@ insert unique_partial_indexes │ └── first-agg [as=column3:7] │ └── column3:7 └── projections - └── column3:7 = 'foo' [as=column17:17] + └── column3:7 = 'foo' [as=partial_index_put1:17] exec-ddl CREATE UNIQUE INDEX u2 ON unique_partial_indexes (b) WHERE c = 'bar' @@ -2219,9 +2213,9 @@ insert unique_partial_indexes │ ├── column1:5 => a:1 │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column18:18 column19:19 + ├── partial index put columns: partial_index_put1:18 partial_index_put2:19 └── project - ├── columns: column18:18!null column19:19!null column1:5!null column2:6!null column3:7!null + ├── columns: partial_index_put1:18!null partial_index_put2:19!null column1:5!null column2:6!null column3:7!null ├── project │ ├── columns: column1:5!null column2:6!null column3:7!null │ └── upsert-distinct-on @@ -2298,8 +2292,8 @@ insert unique_partial_indexes │ └── first-agg [as=column3:7] │ └── column3:7 └── projections - ├── column3:7 = 'foo' [as=column18:18] - └── column3:7 = 'bar' [as=column19:19] + ├── column3:7 = 'foo' [as=partial_index_put1:18] + └── column3:7 = 'bar' [as=partial_index_put2:19] exec-ddl CREATE UNIQUE INDEX u3 ON unique_partial_indexes (b) @@ -2316,9 +2310,9 @@ insert unique_partial_indexes │ ├── column1:5 => a:1 │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column12:12 column13:13 + ├── partial index put columns: partial_index_put1:12 partial_index_put2:13 └── project - ├── columns: column12:12!null column13:13!null column1:5!null column2:6!null column3:7!null + ├── columns: partial_index_put1:12!null partial_index_put2:13!null column1:5!null column2:6!null column3:7!null ├── upsert-distinct-on │ ├── columns: column1:5!null column2:6!null column3:7!null │ ├── grouping columns: column2:6!null @@ -2348,8 +2342,8 @@ insert unique_partial_indexes │ └── first-agg [as=column3:7] │ └── column3:7 └── projections - ├── column3:7 = 'foo' [as=column12:12] - └── column3:7 = 'bar' [as=column13:13] + ├── column3:7 = 'foo' [as=partial_index_put1:12] + └── column3:7 = 'bar' [as=partial_index_put2:13] exec-ddl DROP INDEX u3 @@ -2378,9 +2372,9 @@ insert unique_partial_indexes │ ├── column1:5 => a:1 │ ├── column2:6 => b:2 │ └── column3:7 => c:3 - ├── partial index put columns: column13:13 column14:14 column15:15 + ├── partial index put columns: partial_index_put1:13 partial_index_put2:14 partial_index_put3:15 └── project - ├── columns: column13:13!null column14:14!null column15:15!null column1:5!null column2:6!null column3:7!null + ├── columns: partial_index_put1:13!null partial_index_put2:14!null partial_index_put3:15!null column1:5!null column2:6!null column3:7!null ├── select │ ├── columns: column1:5!null column2:6!null column3:7!null a:8 b:9 │ ├── right-join (cross) @@ -2398,9 +2392,9 @@ insert unique_partial_indexes │ └── filters │ └── a:8 IS NULL └── projections - ├── column3:7 = 'foo' [as=column13:13] - ├── column3:7 = 'bar' [as=column14:14] - └── true [as=column15:15] + ├── column3:7 = 'foo' [as=partial_index_put1:13] + ├── column3:7 = 'bar' [as=partial_index_put2:14] + └── true [as=partial_index_put3:15] exec-ddl DROP INDEX u4 @@ -2425,59 +2419,138 @@ upsert unique_partial_indexes │ ├── column2:6 => b:2 │ └── column3:7 => c:3 ├── update-mapping: - │ └── upsert_b:17 => b:2 - ├── partial index put columns: column19:19 column20:20 - ├── partial index del columns: column13:13 column14:14 + │ └── upsert_b:15 => b:2 + ├── partial index put columns: partial_index_put1:17 partial_index_put2:19 + ├── partial index del columns: partial_index_del1:18 partial_index_del2:20 └── project - ├── columns: column19:19 column20:20 column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 column13:13 column14:14 b_new:15!null upsert_a:16 upsert_b:17!null upsert_c:18 + ├── columns: partial_index_put1:17 partial_index_del1:18 partial_index_put2:19 partial_index_del2:20 column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 b_new:13!null upsert_a:14 upsert_b:15!null upsert_c:16 ├── project - │ ├── columns: upsert_a:16 upsert_b:17!null upsert_c:18 column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 column13:13 column14:14 b_new:15!null + │ ├── columns: upsert_a:14 upsert_b:15!null upsert_c:16 column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 b_new:13!null │ ├── project - │ │ ├── columns: b_new:15!null column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 column13:13 column14:14 - │ │ ├── project - │ │ │ ├── columns: column13:13 column14:14 column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 - │ │ │ ├── left-join (hash) - │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null a:9 b:10 c: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 upsert_partial_index_distinct1:8 - │ │ │ │ │ ├── grouping columns: column2:6!null upsert_partial_index_distinct1:8 - │ │ │ │ │ ├── project - │ │ │ │ │ │ ├── columns: upsert_partial_index_distinct1:8 column1:5!null column2:6!null column3:7!null - │ │ │ │ │ │ ├── values - │ │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null - │ │ │ │ │ │ │ └── (1, 1, 'bar') - │ │ │ │ │ │ └── projections - │ │ │ │ │ │ └── (column3:7 = 'foo') OR NULL::BOOL [as=upsert_partial_index_distinct1:8] - │ │ │ │ │ └── aggregations - │ │ │ │ │ ├── first-agg [as=column1:5] - │ │ │ │ │ │ └── column1:5 - │ │ │ │ │ └── first-agg [as=column3:7] - │ │ │ │ │ └── column3:7 - │ │ │ │ ├── select - │ │ │ │ │ ├── columns: a:9!null b:10 c:11!null crdb_internal_mvcc_timestamp:12 - │ │ │ │ │ ├── scan unique_partial_indexes - │ │ │ │ │ │ ├── columns: a:9!null b:10 c:11 crdb_internal_mvcc_timestamp:12 - │ │ │ │ │ │ └── partial index predicates - │ │ │ │ │ │ ├── secondary: filters - │ │ │ │ │ │ │ └── c:11 = 'foo' - │ │ │ │ │ │ └── u2: filters - │ │ │ │ │ │ └── c:11 = 'bar' - │ │ │ │ │ └── filters - │ │ │ │ │ └── c:11 = 'foo' + │ │ ├── columns: b_new:13!null column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 + │ │ ├── left-join (hash) + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null a:9 b:10 c: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 upsert_partial_index_distinct1:8 + │ │ │ │ ├── grouping columns: column2:6!null upsert_partial_index_distinct1:8 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: upsert_partial_index_distinct1:8 column1:5!null column2:6!null column3:7!null + │ │ │ │ │ ├── values + │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ └── (1, 1, 'bar') + │ │ │ │ │ └── projections + │ │ │ │ │ └── (column3:7 = 'foo') OR NULL::BOOL [as=upsert_partial_index_distinct1:8] + │ │ │ │ └── aggregations + │ │ │ │ ├── first-agg [as=column1:5] + │ │ │ │ │ └── column1:5 + │ │ │ │ └── first-agg [as=column3:7] + │ │ │ │ └── column3:7 + │ │ │ ├── select + │ │ │ │ ├── columns: a:9!null b:10 c:11!null crdb_internal_mvcc_timestamp:12 + │ │ │ │ ├── scan unique_partial_indexes + │ │ │ │ │ ├── columns: a:9!null b:10 c:11 crdb_internal_mvcc_timestamp:12 + │ │ │ │ │ └── partial index predicates + │ │ │ │ │ ├── secondary: filters + │ │ │ │ │ │ └── c:11 = 'foo' + │ │ │ │ │ └── u2: filters + │ │ │ │ │ └── c:11 = 'bar' │ │ │ │ └── filters - │ │ │ │ ├── column2:6 = b:10 - │ │ │ │ └── column3:7 = 'foo' - │ │ │ └── projections - │ │ │ ├── c:11 = 'foo' [as=column13:13] - │ │ │ └── c:11 = 'bar' [as=column14:14] + │ │ │ │ └── c:11 = 'foo' + │ │ │ └── filters + │ │ │ ├── column2:6 = b:10 + │ │ │ └── column3:7 = 'foo' + │ │ └── projections + │ │ └── 10 [as=b_new:13] + │ └── projections + │ ├── CASE WHEN a:9 IS NULL THEN column1:5 ELSE a:9 END [as=upsert_a:14] + │ ├── CASE WHEN a:9 IS NULL THEN column2:6 ELSE b_new:13 END [as=upsert_b:15] + │ └── CASE WHEN a:9 IS NULL THEN column3:7 ELSE c:11 END [as=upsert_c:16] + └── projections + ├── upsert_c:16 = 'foo' [as=partial_index_put1:17] + ├── c:11 = 'foo' [as=partial_index_del1:18] + ├── upsert_c:16 = 'bar' [as=partial_index_put2:19] + └── c:11 = 'bar' [as=partial_index_del2:20] + + +# Do not error with "column reference is ambiguous" when table column names +# match synthesized column names. +exec-ddl +CREATE TABLE t ( + partial_index_put1 INT, + partial_index_del1 INT, + UNIQUE INDEX (partial_index_put1) WHERE partial_index_put1 > 0 AND partial_index_del1 > 0 +) +---- + +build +INSERT INTO t VALUES (1, 2) +ON CONFLICT (partial_index_put1) WHERE partial_index_put1 > 0 AND partial_index_del1 > 0 +DO UPDATE SET partial_index_put1 = 10, partial_index_del1 = 20 +---- +upsert t + ├── columns: + ├── arbiter indexes: secondary + ├── canary column: rowid:11 + ├── fetch columns: t.partial_index_put1:9 t.partial_index_del1:10 rowid:11 + ├── insert-mapping: + │ ├── column1:5 => t.partial_index_put1:1 + │ ├── column2:6 => t.partial_index_del1:2 + │ └── column7:7 => rowid:3 + ├── update-mapping: + │ ├── upsert_partial_index_put1:15 => t.partial_index_put1:1 + │ └── upsert_partial_index_del1:16 => t.partial_index_del1:2 + ├── partial index put columns: partial_index_put1:18 + ├── partial index del columns: partial_index_del1:19 + └── project + ├── columns: partial_index_put1:18!null partial_index_del1:19 column1:5!null column2:6!null column7:7 t.partial_index_put1:9 t.partial_index_del1:10 rowid:11 crdb_internal_mvcc_timestamp:12 partial_index_put1_new:13!null partial_index_del1_new:14!null upsert_partial_index_put1:15!null upsert_partial_index_del1:16!null upsert_rowid:17 + ├── project + │ ├── columns: upsert_partial_index_put1:15!null upsert_partial_index_del1:16!null upsert_rowid:17 column1:5!null column2:6!null column7:7 t.partial_index_put1:9 t.partial_index_del1:10 rowid:11 crdb_internal_mvcc_timestamp:12 partial_index_put1_new:13!null partial_index_del1_new:14!null + │ ├── project + │ │ ├── columns: partial_index_put1_new:13!null partial_index_del1_new:14!null column1:5!null column2:6!null column7:7 t.partial_index_put1:9 t.partial_index_del1:10 rowid:11 crdb_internal_mvcc_timestamp:12 + │ │ ├── left-join (hash) + │ │ │ ├── columns: column1:5!null column2:6!null column7:7 t.partial_index_put1:9 t.partial_index_del1:10 rowid:11 crdb_internal_mvcc_timestamp:12 + │ │ │ ├── project + │ │ │ │ ├── columns: column1:5!null column2:6!null column7:7 + │ │ │ │ └── ensure-upsert-distinct-on + │ │ │ │ ├── columns: column1:5!null column2:6!null column7:7 upsert_partial_index_distinct1:8 + │ │ │ │ ├── grouping columns: column1:5!null upsert_partial_index_distinct1:8 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: upsert_partial_index_distinct1:8 column1:5!null column2:6!null column7:7 + │ │ │ │ │ ├── project + │ │ │ │ │ │ ├── columns: column7:7 column1:5!null column2:6!null + │ │ │ │ │ │ ├── values + │ │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null + │ │ │ │ │ │ │ └── (1, 2) + │ │ │ │ │ │ └── projections + │ │ │ │ │ │ └── unique_rowid() [as=column7:7] + │ │ │ │ │ └── projections + │ │ │ │ │ └── ((column1:5 > 0) AND (column2:6 > 0)) OR NULL::BOOL [as=upsert_partial_index_distinct1:8] + │ │ │ │ └── aggregations + │ │ │ │ ├── first-agg [as=column2:6] + │ │ │ │ │ └── column2:6 + │ │ │ │ └── first-agg [as=column7:7] + │ │ │ │ └── column7:7 + │ │ │ ├── select + │ │ │ │ ├── columns: t.partial_index_put1:9!null t.partial_index_del1:10!null rowid:11!null crdb_internal_mvcc_timestamp:12 + │ │ │ │ ├── scan t + │ │ │ │ │ ├── columns: t.partial_index_put1:9 t.partial_index_del1:10 rowid:11!null crdb_internal_mvcc_timestamp:12 + │ │ │ │ │ └── partial index predicates + │ │ │ │ │ └── secondary: filters + │ │ │ │ │ └── (t.partial_index_put1:9 > 0) AND (t.partial_index_del1:10 > 0) + │ │ │ │ └── filters + │ │ │ │ └── (t.partial_index_put1:9 > 0) AND (t.partial_index_del1:10 > 0) + │ │ │ └── filters + │ │ │ ├── column1:5 = t.partial_index_put1:9 + │ │ │ └── (column1:5 > 0) AND (column2:6 > 0) │ │ └── projections - │ │ └── 10 [as=b_new:15] + │ │ ├── 10 [as=partial_index_put1_new:13] + │ │ └── 20 [as=partial_index_del1_new:14] │ └── projections - │ ├── CASE WHEN a:9 IS NULL THEN column1:5 ELSE a:9 END [as=upsert_a:16] - │ ├── CASE WHEN a:9 IS NULL THEN column2:6 ELSE b_new:15 END [as=upsert_b:17] - │ └── CASE WHEN a:9 IS NULL THEN column3:7 ELSE c:11 END [as=upsert_c:18] + │ ├── CASE WHEN rowid:11 IS NULL THEN column1:5 ELSE partial_index_put1_new:13 END [as=upsert_partial_index_put1:15] + │ ├── CASE WHEN rowid:11 IS NULL THEN column2:6 ELSE partial_index_del1_new:14 END [as=upsert_partial_index_del1:16] + │ └── CASE WHEN rowid:11 IS NULL THEN column7:7 ELSE rowid:11 END [as=upsert_rowid:17] └── projections - ├── upsert_c:18 = 'foo' [as=column19:19] - └── upsert_c:18 = 'bar' [as=column20:20] + ├── (upsert_partial_index_put1:15 > 0) AND (upsert_partial_index_del1:16 > 0) [as=partial_index_put1:18] + └── (t.partial_index_put1:9 > 0) AND (t.partial_index_del1:10 > 0) [as=partial_index_del1:19] diff --git a/pkg/sql/opt/optbuilder/update.go b/pkg/sql/opt/optbuilder/update.go index ea2dc1f19ec9..e0742f70fd84 100644 --- a/pkg/sql/opt/optbuilder/update.go +++ b/pkg/sql/opt/optbuilder/update.go @@ -345,8 +345,8 @@ func (mb *mutationBuilder) buildUpdate(returning tree.ReturningExprs) { mb.addCheckConstraintCols() - // Add partial index put boolean columns to the input. - mb.projectPartialIndexPutCols(preCheckScope) + // Project partial index PUT and DEL boolean columns. + mb.projectPartialIndexPutAndDelCols(preCheckScope, mb.fetchScope) mb.buildFKChecksForUpdate() diff --git a/pkg/sql/update.go b/pkg/sql/update.go index a64f9b36ea11..8a63975598ee 100644 --- a/pkg/sql/update.go +++ b/pkg/sql/update.go @@ -299,8 +299,8 @@ func (u *updateNode) processSourceRow(params runParams, sourceVals tree.Datums) if !partialIndexOrds.Empty() { partialIndexValOffset := len(u.run.tu.ru.FetchCols) + len(u.run.tu.ru.UpdateCols) + u.run.checkOrds.Len() + u.run.numPassthrough partialIndexVals := sourceVals[partialIndexValOffset:] - partialIndexPutVals := partialIndexVals[:len(partialIndexVals)/2] - partialIndexDelVals := partialIndexVals[len(partialIndexVals)/2:] + partialIndexPutVals := partialIndexVals[:partialIndexOrds.Len()] + partialIndexDelVals := partialIndexVals[partialIndexOrds.Len() : partialIndexOrds.Len()*2] err := pm.Init(partialIndexPutVals, partialIndexDelVals, u.run.tu.tableDesc()) if err != nil {