diff --git a/pkg/sql/logictest/testdata/logic_test/partial_index b/pkg/sql/logictest/testdata/logic_test/partial_index index f2016f852e9e..24280a2ffd56 100644 --- a/pkg/sql/logictest/testdata/logic_test/partial_index +++ b/pkg/sql/logictest/testdata/logic_test/partial_index @@ -1426,6 +1426,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 ( @@ -1451,3 +1452,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 4caa9c4c8a4a..4aecfb00b1f7 100644 --- a/pkg/sql/opt/exec/execbuilder/mutation.go +++ b/pkg/sql/opt/exec/execbuilder/mutation.go @@ -44,6 +44,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 or unique // 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 d7c8f56cf479..c6549545022a 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/partial_index +++ b/pkg/sql/opt/exec/execbuilder/testdata/partial_index @@ -939,3 +939,31 @@ vectorized: true missing stats table: inv@i (partial index) spans: 1 span + +# 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; +---- +DelRange /Table/56/1/1 - /Table/56/1/1/# +Scan /Table/57/{1-2} +Del /Table/57/2/1/10/0 +Del /Table/57/1/10/0 +Del /Table/57/1/20/0 diff --git a/pkg/sql/opt/norm/testdata/rules/prune_cols b/pkg/sql/opt/norm/testdata/rules/prune_cols index 379b6e3bcc33..d2598229f25b 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -1946,23 +1946,23 @@ update partial_indexes ├── columns: ├── fetch columns: a:5 b:6 c:7 ├── update-mapping: - │ └── b_new:10 => b:2 - ├── partial index put columns: partial_index_put1:11 - ├── partial index del columns: partial_index_del1: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: partial_index_put1:11 a:5!null b:6 c:7 partial_index_del1: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 partial_index_del1: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] @@ -1978,10 +1978,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=partial_index_del1:9, outer=(6)] + │ └── b:6 + 1 [as=b_new:9, outer=(6), immutable] └── projections - └── b_new:10 > 1 [as=partial_index_put1: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 6b28ed589374..1db1736ab8d4 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.uniqueChecks, mb.fkChecks, private, diff --git a/pkg/sql/opt/optbuilder/fk_cascade.go b/pkg/sql/opt/optbuilder/fk_cascade.go index 68781fcf02af..196374fbfa35 100644 --- a/pkg/sql/opt/optbuilder/fk_cascade.go +++ b/pkg/sql/opt/optbuilder/fk_cascade.go @@ -95,9 +95,14 @@ 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( + // + // 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) @@ -272,7 +277,7 @@ func (cb *onDeleteFastCascadeBuilder) Build( // Build the input to the delete mutation, which is simply a Scan with a // Select on top. - mb.outScope = b.buildScan( + mb.fetchScope = b.buildScan( b.addTable(cb.childTable, &mb.alias), tableOrdinals(cb.childTable, columnKinds{ includeMutations: false, @@ -284,6 +289,7 @@ func (cb *onDeleteFastCascadeBuilder) Build( noRowLocking, b.allocScope(), ) + mb.outScope = mb.fetchScope var filters memo.FiltersExpr @@ -420,9 +426,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) @@ -639,6 +650,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 d13a0e675d2e..2619e27601b2 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.buildUniqueChecksForInsert() @@ -905,7 +905,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, @@ -923,10 +923,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)}, ) } @@ -934,16 +934,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: @@ -951,12 +951,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)) } @@ -975,7 +975,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, ) @@ -998,9 +998,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. @@ -1070,25 +1067,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 22eb54a6c8d5..25e1093684bf 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. @@ -257,7 +260,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, @@ -269,7 +272,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) @@ -334,9 +337,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 @@ -368,7 +368,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, @@ -380,7 +380,7 @@ func (mb *mutationBuilder) buildInputForDelete( noRowLocking, inScope, ) - mb.outScope = scanScope + mb.outScope = mb.fetchScope // WHERE mb.b.buildWhere(where, mb.outScope) @@ -401,9 +401,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 @@ -796,33 +793,60 @@ func (mb *mutationBuilder) mutationColumnIDs() opt.ColSet { return cols } -// 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, "partial_index_put") +// 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. // -// predScope is the scope of columns available to the partial index predicate -// expression. -func (mb *mutationBuilder) projectPartialIndexDelCols(predScope *scope) { - mb.projectPartialIndexCols(mb.partialIndexDelColIDs, predScope, "partial_index_del") +// 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) } -// 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, aliasPrefix string, -) { +// 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. +// +// 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) +} + +// 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) @@ -830,17 +854,33 @@ func (mb *mutationBuilder) projectPartialIndexCols( 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) - alias := fmt.Sprintf("%s%d", aliasPrefix, ord+1) - scopeCol := mb.b.addColumn(projectionScope, alias, 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/fk-on-delete-cascade b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade index 2c5ab5ecc9ef..d67654c4aa87 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade @@ -828,3 +828,134 @@ root ├── m2.a:16 IS NULL ├── m2.a:16 IS DISTINCT FROM CAST(NULL AS INT8) └── m2.c:18 IS DISTINCT FROM CAST(NULL AS INT8) + +# 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; fast path. +build-cascades +DELETE FROM parent_partial WHERE p > 1 +---- +root + ├── delete parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── 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 + └── delete child_partial + ├── columns: + ├── fetch columns: c:9 child_partial.p:10 i:11 + ├── partial index del columns: partial_index_del1:13 partial_index_del2:14 + └── project + ├── columns: partial_index_del1:13 partial_index_del2:14!null c:9!null child_partial.p:10!null i:11 + ├── select + │ ├── columns: c:9!null child_partial.p:10!null 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 + │ └── filters + │ ├── child_partial.p:10 > 1 + │ └── child_partial.p:10 IS DISTINCT FROM CAST(NULL AS INT8) + └── projections + ├── i:11 > 0 [as=partial_index_del1:13] + └── child_partial.p:10 > 0 [as=partial_index_del2:14] + +# Test a cascade to a child with a partial index; no fast path. +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] + +# Test a cascade to a child with a partial index; delete everything. +build-cascades +DELETE FROM parent_partial +---- +root + ├── delete parent_partial + │ ├── columns: + │ ├── fetch columns: p:3 + │ ├── cascades + │ │ └── fk_p_ref_parent_partial + │ └── scan parent_partial + │ └── columns: p:3!null crdb_internal_mvcc_timestamp:4 + └── cascade + └── delete child_partial + ├── columns: + ├── fetch columns: c:9 child_partial.p:10 i:11 + ├── partial index del columns: partial_index_del1:13 partial_index_del2:14 + └── project + ├── columns: partial_index_del1:13 partial_index_del2:14!null c:9!null child_partial.p:10!null i:11 + ├── select + │ ├── columns: c:9!null child_partial.p:10!null 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 + │ └── filters + │ └── child_partial.p:10 IS DISTINCT FROM CAST(NULL AS INT8) + └── projections + ├── i:11 > 0 [as=partial_index_del1:13] + └── child_partial.p:10 > 0 [as=partial_index_del2:14] 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/update b/pkg/sql/opt/optbuilder/testdata/update index 7a378aa69b73..c993a4a247ea 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: partial_index_del1:9 partial_index_put2:14 partial_index_del3:11 partial_index_del4:12 - ├── partial index del columns: partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4: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: partial_index_put2:14 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4: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 partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4:12 - │ ├── project - │ │ ├── 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 - │ │ │ ├── secondary: filters - │ │ │ │ └── c:7 = 'foo' - │ │ │ └── secondary: filters - │ │ │ └── (a:5 > b:6) AND (c:7 = 'bar') - │ │ └── projections - │ │ ├── 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] + │ ├── 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=partial_index_put2: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,31 +1715,28 @@ 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: partial_index_del1:9 partial_index_put2:14 partial_index_del3:11 partial_index_del4:12 - ├── partial index del columns: partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4: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: partial_index_put2:14 a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4: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 partial_index_del1:9 partial_index_del2:10 partial_index_del3:11 partial_index_del4:12 - │ ├── project - │ │ ├── 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 - │ │ │ ├── secondary: filters - │ │ │ │ └── c:7 = 'foo' - │ │ │ └── secondary: filters - │ │ │ └── (a:5 > b:6) AND (c:7 = 'bar') - │ │ └── projections - │ │ ├── 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] + │ ├── 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:13] + │ └── a:5 + 5 [as=a_new:9] └── projections - └── (a_new:13 > b:6) AND (c:7 = 'bar') [as=partial_index_put2: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] # Do not error with "column reference is ambiguous" when table column names # match synthesized column names. @@ -1761,29 +1755,26 @@ update t ├── columns: ├── fetch columns: t.partial_index_put1:5 t.partial_index_del1:6 rowid:7 ├── update-mapping: - │ ├── partial_index_put1_new:10 => t.partial_index_put1:1 - │ └── partial_index_del1_new:11 => t.partial_index_del1:2 - ├── partial index put columns: partial_index_put1:12 - ├── partial index del columns: partial_index_del1:9 + │ ├── 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:12!null t.partial_index_put1:5!null t.partial_index_del1:6!null rowid:7!null crdb_internal_mvcc_timestamp:8 partial_index_del1:9!null partial_index_put1_new:10!null partial_index_del1_new:11!null + ├── 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:10!null partial_index_del1_new:11!null t.partial_index_put1:5!null t.partial_index_del1:6!null rowid:7!null crdb_internal_mvcc_timestamp:8 partial_index_del1:9!null - │ ├── project - │ │ ├── columns: partial_index_del1:9!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 - │ │ │ │ └── (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 - │ │ └── (t.partial_index_put1:5 > 0) AND (t.partial_index_del1:6 > 0) [as=partial_index_del1:9] + │ ├── 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 + │ │ │ └── (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 - │ ├── 1 [as=partial_index_put1_new:10] - │ └── 2 [as=partial_index_del1_new:11] + │ ├── 1 [as=partial_index_put1_new:9] + │ └── 2 [as=partial_index_del1_new:10] └── projections - └── (partial_index_put1_new:10 > 0) AND (partial_index_del1_new:11 > 0) [as=partial_index_put1:12] + ├── (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 79c75274fc5c..1438b77a0fae 100644 --- a/pkg/sql/opt/optbuilder/testdata/upsert +++ b/pkg/sql/opt/optbuilder/testdata/upsert @@ -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: partial_index_put1:21 partial_index_put2:22 partial_index_put3:23 partial_index_put4:24 - ├── partial index del columns: partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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: partial_index_put1:21!null partial_index_put2:22 partial_index_put3:23!null partial_index_put4:24!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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 partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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 partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4:15 - │ │ ├── project - │ │ │ ├── columns: partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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=partial_index_del1:12] - │ │ │ ├── (a:8 > b:9) AND (c:10 = 'bar') [as=partial_index_del2:13] - │ │ │ ├── c:10 = 'delete-only' [as=partial_index_del3:14] - │ │ │ └── c:10 = 'write-only' [as=partial_index_del4: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=partial_index_put1:21] - ├── (upsert_a:18 > upsert_b:19) AND (upsert_c:20 = 'bar') [as=partial_index_put2:22] - ├── upsert_c:20 = 'delete-only' [as=partial_index_put3:23] - └── upsert_c:20 = 'write-only' [as=partial_index_put4: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: partial_index_put1:17 partial_index_put2:18 partial_index_put3:19 partial_index_put4:20 - ├── partial index del columns: partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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: partial_index_put1:17!null partial_index_put2:18 partial_index_put3:19!null partial_index_put4:20!null column1:5!null column2:6!null column3:7!null a:8 b:9 c:10 crdb_internal_mvcc_timestamp:11 partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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 partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4:15 - │ ├── project - │ │ ├── columns: partial_index_del1:12 partial_index_del2:13 partial_index_del3:14 partial_index_del4: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=partial_index_del1:12] - │ │ ├── (a:8 > b:9) AND (c:10 = 'bar') [as=partial_index_del2:13] - │ │ ├── c:10 = 'delete-only' [as=partial_index_del3:14] - │ │ └── c:10 = 'write-only' [as=partial_index_del4: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=partial_index_put1:17] - ├── (upsert_a:16 > column2:6) AND (column3:7 = 'bar') [as=partial_index_put2:18] - ├── column3:7 = 'delete-only' [as=partial_index_put3:19] - └── column3:7 = 'write-only' [as=partial_index_put4: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 @@ -2425,62 +2419,59 @@ upsert unique_partial_indexes │ ├── column2:6 => b:2 │ └── column3:7 => c:3 ├── update-mapping: - │ └── upsert_b:17 => b:2 - ├── partial index put columns: partial_index_put1:19 partial_index_put2:20 - ├── partial index del columns: partial_index_del1:13 partial_index_del2: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: partial_index_put1:19 partial_index_put2:20 column1:5!null column2:6!null column3:7!null a:9 b:10 c:11 crdb_internal_mvcc_timestamp:12 partial_index_del1:13 partial_index_del2: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 partial_index_del1:13 partial_index_del2: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 partial_index_del1:13 partial_index_del2:14 - │ │ ├── project - │ │ │ ├── columns: partial_index_del1:13 partial_index_del2: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=partial_index_del1:13] - │ │ │ └── c:11 = 'bar' [as=partial_index_del2:14] + │ │ │ │ └── c:11 = 'foo' + │ │ │ └── filters + │ │ │ ├── column2:6 = b:10 + │ │ │ └── column3:7 = 'foo' │ │ └── projections - │ │ └── 10 [as=b_new:15] + │ │ └── 10 [as=b_new:13] │ └── 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 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:18 = 'foo' [as=partial_index_put1:19] - └── upsert_c:18 = 'bar' [as=partial_index_put2:20] + ├── 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 @@ -2508,61 +2499,58 @@ upsert t │ ├── column2:6 => t.partial_index_del1:2 │ └── column7:7 => rowid:3 ├── update-mapping: - │ ├── upsert_partial_index_put1:16 => t.partial_index_put1:1 - │ └── upsert_partial_index_del1:17 => t.partial_index_del1:2 - ├── partial index put columns: partial_index_put1:19 - ├── partial index del columns: partial_index_del1:13 + │ ├── 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:19!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 partial_index_del1:13 partial_index_put1_new:14!null partial_index_del1_new:15!null upsert_partial_index_put1:16!null upsert_partial_index_del1:17!null upsert_rowid:18 + ├── 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:16!null upsert_partial_index_del1:17!null upsert_rowid:18 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_del1:13 partial_index_put1_new:14!null partial_index_del1_new:15!null + │ ├── 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:14!null partial_index_del1_new:15!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 partial_index_del1:13 - │ │ ├── project - │ │ │ ├── columns: partial_index_del1:13 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) + │ │ ├── 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 - │ │ │ │ ├── column1:5 = t.partial_index_put1:9 - │ │ │ │ └── (column1:5 > 0) AND (column2:6 > 0) - │ │ │ └── projections - │ │ │ └── (t.partial_index_put1:9 > 0) AND (t.partial_index_del1:10 > 0) [as=partial_index_del1:13] + │ │ │ │ └── (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=partial_index_put1_new:14] - │ │ └── 20 [as=partial_index_del1_new:15] + │ │ ├── 10 [as=partial_index_put1_new:13] + │ │ └── 20 [as=partial_index_del1_new:14] │ └── projections - │ ├── CASE WHEN rowid:11 IS NULL THEN column1:5 ELSE partial_index_put1_new:14 END [as=upsert_partial_index_put1:16] - │ ├── CASE WHEN rowid:11 IS NULL THEN column2:6 ELSE partial_index_del1_new:15 END [as=upsert_partial_index_del1:17] - │ └── CASE WHEN rowid:11 IS NULL THEN column7:7 ELSE rowid:11 END [as=upsert_rowid: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_partial_index_put1:16 > 0) AND (upsert_partial_index_del1:17 > 0) [as=partial_index_put1:19] + ├── (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 cd646ec1f61c..ca787721e774 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 b0f6298c2723..bfce38bda461 100644 --- a/pkg/sql/update.go +++ b/pkg/sql/update.go @@ -303,8 +303,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 {