From 51f3ecdfda8daf2866087b086238cf29ef58f580 Mon Sep 17 00:00:00 2001 From: DrewKimball Date: Wed, 27 May 2026 18:55:53 -0500 Subject: [PATCH] sql/opt: handle non-unique parent for RBR region inference When a REGIONAL BY ROW child sets infer_rbr_region_col_using_constraint, the optimizer plans a lookup join into the parent on the FK's non-region columns to infer the region. If those columns aren't a lax key on the parent, the join could return multiple rows per child input row, which tripped an assertion failure in mutation_builder. Allow this configuration: wrap the LeftJoin in an unordered DistinctOn grouped by the child input's key (added via EnsureKey when needed), selecting an arbitrary matching parent row via FirstAgg on the parent's region column. Also moves EnsureKey from decorrelate_funcs.go to general_funcs.go so it can be shared with optbuilder. Fixes #170768 Release note (bug fix): Fixed an internal error that could occur during INSERT/UPDATE/UPSERT on a REGIONAL BY ROW table configured with the infer_rbr_region_col_using_constraint storage parameter when the referenced parent table did not have a unique constraint covering the foreign key's non-region columns. The optimizer now picks an arbitrary matching parent row to infer the region from. --- .../logic_test/regional_by_row_foreign_key | 51 ++++ pkg/sql/opt/norm/decorrelate_funcs.go | 75 ----- pkg/sql/opt/norm/general_funcs.go | 75 +++++ pkg/sql/opt/optbuilder/mutation_builder.go | 34 ++- pkg/sql/opt/optbuilder/testdata/region_lookup | 277 ++++++++++++++++++ 5 files changed, 430 insertions(+), 82 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key b/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key index 1bfa3e14e1e..a39a1e8cf1e 100644 --- a/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key +++ b/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key @@ -2440,3 +2440,54 @@ statement ok DROP TABLE parent_160013 subtest end + +subtest non_unique_parent + +# Regression test for #170768: a REGIONAL BY ROW child can set +# infer_rbr_region_col_using_constraint even when the parent's non-region +# foreign-key columns aren't covered by a unique constraint. The optimizer +# wraps the lookup join in a DistinctOn so each child input row maps to at +# most one parent row; the chosen parent row's region is arbitrary if more +# than one parent row matches. +statement ok +CREATE TABLE p_nonunique ( + region crdb_internal_region NOT NULL, + k INT NOT NULL, + PRIMARY KEY (region, k) +) + +statement ok +INSERT INTO p_nonunique VALUES ('ca-central-1', 1), ('ap-southeast-2', 2), ('us-east-1', 3) + +statement ok +CREATE TABLE c_nonunique ( + pk INT PRIMARY KEY, + data INT, + CONSTRAINT fk_region FOREIGN KEY (crdb_region, pk) REFERENCES p_nonunique (region, k) +) WITH (infer_rbr_region_col_using_constraint = 'fk_region') LOCALITY REGIONAL BY ROW + +# Before #170768's fix the INSERT panicked with an assertion failure +# ("lookup columns using constraint X must be a lax key"). It now succeeds: +# the single matching parent row's region is selected. +statement ok +INSERT INTO c_nonunique (pk, data) VALUES (1, 100) + +query IT +SELECT pk, crdb_region FROM c_nonunique +---- +1 ca-central-1 + +# With two parent rows sharing the join key, the inference still completes +# (picking one arbitrarily) rather than failing. +statement ok +INSERT INTO p_nonunique VALUES ('ca-central-1', 10), ('us-east-1', 10) + +statement ok +INSERT INTO c_nonunique (pk, data) VALUES (10, 100) + +query I +SELECT count(*) FROM c_nonunique WHERE pk = 10 +---- +1 + +subtest end diff --git a/pkg/sql/opt/norm/decorrelate_funcs.go b/pkg/sql/opt/norm/decorrelate_funcs.go index 2387ef7ec90..b532c8ed9af 100644 --- a/pkg/sql/opt/norm/decorrelate_funcs.go +++ b/pkg/sql/opt/norm/decorrelate_funcs.go @@ -518,81 +518,6 @@ func (c *CustomFuncs) ConstructApplyJoin( panic(errors.AssertionFailedf("unexpected join operator: %v", redact.Safe(joinOp))) } -// EnsureKey finds the shortest strong key for the input expression. If no -// strong key exists and the input expression is a Scan or a Scan wrapped in a -// Select, EnsureKey returns a new Scan (possibly wrapped in a Select) with the -// preexisting primary key for the table. If the input is not a Scan or -// Select(Scan), EnsureKey wraps the input in an Ordinality operator, which -// provides a key column by uniquely numbering the rows. EnsureKey returns the -// input expression (perhaps augmented with a key column(s) or wrapped by -// Ordinality). -func (c *CustomFuncs) EnsureKey(in memo.RelExpr) memo.RelExpr { - // Try to add the preexisting primary key if the input is a Scan or Scan - // wrapped in a Select. - if res, ok := c.tryFindExistingKey(in); ok { - return res - } - - // Otherwise, wrap the input in an Ordinality operator. - colID := c.f.Metadata().AddColumn("rownum", types.Int) - private := memo.OrdinalityPrivate{ColID: colID, ForDuplicateRemoval: true} - return c.f.ConstructOrdinality(in, &private) -} - -// tryFindExistingKey attempts to find an existing key for the input expression. -// It may modify the expression in order to project the key column. -func (c *CustomFuncs) tryFindExistingKey(in memo.RelExpr) (_ memo.RelExpr, ok bool) { - _, hasKey := c.CandidateKey(in) - if hasKey { - return in, true - } - switch t := in.(type) { - case *memo.ProjectExpr: - input, foundKey := c.tryFindExistingKey(t.Input) - if foundKey { - return c.f.ConstructProject(input, t.Projections, input.Relational().OutputCols), true - } - - case *memo.ScanExpr: - private := t.ScanPrivate - tableID := private.Table - table := c.f.Metadata().Table(tableID) - if !table.IsVirtualTable() { - keyCols := c.PrimaryKeyCols(tableID) - private.Cols = private.Cols.Union(keyCols) - return c.f.ConstructScan(&private), true - } - - case *memo.SelectExpr: - input, foundKey := c.tryFindExistingKey(t.Input) - if foundKey { - return c.f.ConstructSelect(input, t.Filters), true - } - } - - return nil, false -} - -// KeyCols returns a column set consisting of the columns that make up the -// candidate key for the input expression (a key must be present). -func (c *CustomFuncs) KeyCols(in memo.RelExpr) opt.ColSet { - keyCols, ok := c.CandidateKey(in) - if !ok { - panic(errors.AssertionFailedf("expected expression to have key")) - } - return keyCols -} - -// NonKeyCols returns a column set consisting of the output columns of the given -// input, minus the columns that make up its candidate key (which it must have). -func (c *CustomFuncs) NonKeyCols(in memo.RelExpr) opt.ColSet { - keyCols, ok := c.CandidateKey(in) - if !ok { - panic(errors.AssertionFailedf("expected expression to have key")) - } - return c.OutputCols(in).Difference(keyCols) -} - // MakeAggCols2 is similar to MakeAggCols, except that it allows two different // sets of aggregate functions to be added to the resulting Aggregations // operator, with one set appended to the other, like this: diff --git a/pkg/sql/opt/norm/general_funcs.go b/pkg/sql/opt/norm/general_funcs.go index a2f988801d1..8838fb14a83 100644 --- a/pkg/sql/opt/norm/general_funcs.go +++ b/pkg/sql/opt/norm/general_funcs.go @@ -611,6 +611,81 @@ func (c *CustomFuncs) PrimaryKeyCols(table opt.TableID) opt.ColSet { return tabMeta.IndexKeyColumns(cat.PrimaryIndex) } +// KeyCols returns a column set consisting of the columns that make up the +// candidate key for the input expression (a key must be present). +func (c *CustomFuncs) KeyCols(in memo.RelExpr) opt.ColSet { + keyCols, ok := c.CandidateKey(in) + if !ok { + panic(errors.AssertionFailedf("expected expression to have key")) + } + return keyCols +} + +// NonKeyCols returns a column set consisting of the output columns of the given +// input, minus the columns that make up its candidate key (which it must have). +func (c *CustomFuncs) NonKeyCols(in memo.RelExpr) opt.ColSet { + keyCols, ok := c.CandidateKey(in) + if !ok { + panic(errors.AssertionFailedf("expected expression to have key")) + } + return c.OutputCols(in).Difference(keyCols) +} + +// EnsureKey finds the shortest strong key for the input expression. If no +// strong key exists and the input expression is a Scan or a Scan wrapped in a +// Select, EnsureKey returns a new Scan (possibly wrapped in a Select) with the +// preexisting primary key for the table. If the input is not a Scan or +// Select(Scan), EnsureKey wraps the input in an Ordinality operator, which +// provides a key column by uniquely numbering the rows. EnsureKey returns the +// input expression (perhaps augmented with a key column(s) or wrapped by +// Ordinality). +func (c *CustomFuncs) EnsureKey(in memo.RelExpr) memo.RelExpr { + // Try to add the preexisting primary key if the input is a Scan or Scan + // wrapped in a Select. + if res, ok := c.tryFindExistingKey(in); ok { + return res + } + + // Otherwise, wrap the input in an Ordinality operator. + colID := c.f.Metadata().AddColumn("rownum", types.Int) + private := memo.OrdinalityPrivate{ColID: colID, ForDuplicateRemoval: true} + return c.f.ConstructOrdinality(in, &private) +} + +// tryFindExistingKey attempts to find an existing key for the input expression. +// It may modify the expression in order to project the key column. +func (c *CustomFuncs) tryFindExistingKey(in memo.RelExpr) (_ memo.RelExpr, ok bool) { + _, hasKey := c.CandidateKey(in) + if hasKey { + return in, true + } + switch t := in.(type) { + case *memo.ProjectExpr: + input, foundKey := c.tryFindExistingKey(t.Input) + if foundKey { + return c.f.ConstructProject(input, t.Projections, input.Relational().OutputCols), true + } + + case *memo.ScanExpr: + private := t.ScanPrivate + tableID := private.Table + table := c.f.Metadata().Table(tableID) + if !table.IsVirtualTable() { + keyCols := c.PrimaryKeyCols(tableID) + private.Cols = private.Cols.Union(keyCols) + return c.f.ConstructScan(&private), true + } + + case *memo.SelectExpr: + input, foundKey := c.tryFindExistingKey(t.Input) + if foundKey { + return c.f.ConstructSelect(input, t.Filters), true + } + } + + return nil, false +} + // ---------------------------------------------------------------------- // // Property functions diff --git a/pkg/sql/opt/optbuilder/mutation_builder.go b/pkg/sql/opt/optbuilder/mutation_builder.go index 008d515ee4c..ee4953af430 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder.go +++ b/pkg/sql/opt/optbuilder/mutation_builder.go @@ -1048,20 +1048,40 @@ func (mb *mutationBuilder) maybeAddRegionColLookup(op opt.Operator) { // The scan is exempt from RLS to maintain data integrity. cat.PolicyScopeExempt, ) - if !refScope.expr.Relational().FuncDeps.ColsAreLaxKey(refLookupCols) { - // The lookup columns must be a lax key, otherwise the join may return - // multiple rows for a single row in the target table. This should already - // be enforced by the foreign-key constraint. - panic(errors.AssertionFailedf( - "lookup columns using constraint %q must be a lax key", lookupFK.Name())) - } var joinFlags memo.JoinFlags if mb.b.evalCtx.SessionData().PreferLookupJoinsForFKs { joinFlags = memo.PreferLookupJoinIntoRight } + parentNonKey := !refScope.expr.Relational().FuncDeps.ColsAreLaxKey(refLookupCols) + if parentNonKey { + // The non-region foreign-key columns aren't a lax key on the parent, so + // the join could return multiple rows per child input row. We will use an + // unordered DistinctOn to select an arbitrary matching parent row for each + // child row. This requires a key on the child rows, so ensure it here. + mb.outScope.expr = mb.b.factory.CustomFuncs().EnsureKey(mb.outScope.expr) + } + inputCols := mb.outScope.expr.Relational().OutputCols mb.outScope.expr = mb.b.factory.ConstructLeftJoin( mb.outScope.expr, refScope.expr, joinCond, &memo.JoinPrivate{Flags: joinFlags}, ) + if parentNonKey { + // Use an unordered DistinctOn to arbitrarily select matching parent rows. + // Group on all input columns from before the join (they form a superkey + // after EnsureKey); optimizer rules can simplify the grouping columns + // using the input key. The parent's region column is the only parent + // column needed downstream, and FirstAgg picks one arbitrary value from + // the matching parent rows. + aggs := memo.AggregationsExpr{ + f.ConstructAggregationsItem( + f.ConstructFirstAgg(f.ConstructVariable(lookupRegionColID)), + lookupRegionColID, + ), + } + groupingPrivate := memo.GroupingPrivate{GroupingCols: inputCols} + mb.outScope.expr = f.ConstructDistinctOn( + mb.outScope.expr, aggs, &groupingPrivate, + ) + } // Build a CASE expression to determine the final value of the region column. // Use the looked-up value if non-NULL, and otherwise use the default value // which was already projected in the input. diff --git a/pkg/sql/opt/optbuilder/testdata/region_lookup b/pkg/sql/opt/optbuilder/testdata/region_lookup index a34faa6ba63..cce6df959ca 100644 --- a/pkg/sql/opt/optbuilder/testdata/region_lookup +++ b/pkg/sql/opt/optbuilder/testdata/region_lookup @@ -1675,3 +1675,280 @@ upsert child │ ├── column1:8 => x:35 │ └── column2:9 => y:36 └── filters (true) + +# -------------------------------------------------- +# Parent without a unique constraint on the non-region inference columns. +# The optimizer wraps the right side of the lookup join in a DistinctOn so +# at most one parent row is paired with each child input row. The parent +# here is a non-RBR table whose PK is (region, a, b), so (a, b) alone is +# not a key. +# -------------------------------------------------- + +exec-ddl +CREATE TABLE parent_nonunique ( + region STRING NOT NULL, + a INT NOT NULL, + b INT NOT NULL, + PRIMARY KEY (region, a, b), + INDEX a_b_idx (a, b) +); +---- + +exec-ddl +CREATE TABLE child_nonunique ( + x INT, + y INT, + z INT, + UNIQUE (z), + CONSTRAINT foo FOREIGN KEY (crdb_region, x, y) REFERENCES parent_nonunique (region, a, b) +) WITH (infer_rbr_region_col_using_constraint = 'foo') LOCALITY REGIONAL BY ROW; +---- + +opt locality=(region=east) format=(hide-all,show-columns) +INSERT INTO child_nonunique VALUES (1, 2, 3); +---- +insert child_nonunique + ├── columns: + ├── insert-mapping: + │ ├── column1:8 => child_nonunique.x:1 + │ ├── column2:9 => child_nonunique.y:2 + │ ├── column3:10 => child_nonunique.z:3 + │ ├── fk_lookup_crdb_region:18 => child_nonunique.crdb_region:4 + │ └── rowid_default:12 => child_nonunique.rowid:5 + ├── check columns: check1:19 + ├── input binding: &1 + ├── project + │ ├── columns: check1:19 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 fk_lookup_crdb_region:18 + │ ├── project + │ │ ├── columns: fk_lookup_crdb_region:18 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 + │ │ ├── limit + │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 a:14 b:15 + │ │ │ ├── left-join (cross) + │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 a:14 b:15 + │ │ │ │ ├── values + │ │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 + │ │ │ │ │ └── (1, 2, 3, default_to_database_primary_region('east'), unique_rowid()) + │ │ │ │ ├── scan parent_nonunique@a_b_idx + │ │ │ │ │ ├── columns: region:13 a:14 b:15 + │ │ │ │ │ ├── constraint: /14/15/13: [/1/2 - /1/2] + │ │ │ │ │ └── flags: avoid-full-scan + │ │ │ │ └── filters (true) + │ │ │ └── 1 + │ │ └── projections + │ │ └── CASE WHEN region:13 IS NOT DISTINCT FROM CAST(NULL AS STRING) THEN crdb_region_default:11 ELSE region:13 END [as=fk_lookup_crdb_region:18] + │ └── projections + │ └── fk_lookup_crdb_region:18 IN ('central', 'east', 'west') [as=check1:19] + ├── unique-checks + │ ├── unique-checks-item: child_nonunique(rowid) + │ │ └── project + │ │ ├── columns: rowid:31 + │ │ └── semi-join (lookup child_nonunique) + │ │ ├── columns: crdb_region:30 rowid:31 + │ │ ├── lookup expression + │ │ │ └── filters + │ │ │ ├── child_nonunique.crdb_region:23 IN ('central', 'east', 'west') + │ │ │ └── rowid:31 = child_nonunique.rowid:24 + │ │ ├── with-scan &1 + │ │ │ ├── columns: crdb_region:30 rowid:31 + │ │ │ └── mapping: + │ │ │ ├── fk_lookup_crdb_region:18 => crdb_region:30 + │ │ │ └── rowid_default:12 => rowid:31 + │ │ └── filters + │ │ └── crdb_region:30 != child_nonunique.crdb_region:23 + │ └── unique-checks-item: child_nonunique(z) + │ └── project + │ ├── columns: z:41 + │ └── semi-join (lookup child_nonunique@child_nonunique_crdb_region_z_key) + │ ├── columns: z:41 crdb_region:42 rowid:43 + │ ├── lookup expression + │ │ └── filters + │ │ ├── child_nonunique.crdb_region:35 IN ('central', 'east', 'west') + │ │ └── z:41 = child_nonunique.z:34 + │ ├── with-scan &1 + │ │ ├── columns: z:41 crdb_region:42 rowid:43 + │ │ └── mapping: + │ │ ├── column3:10 => z:41 + │ │ ├── fk_lookup_crdb_region:18 => crdb_region:42 + │ │ └── rowid_default:12 => rowid:43 + │ └── filters + │ └── (crdb_region:42 != child_nonunique.crdb_region:35) OR (rowid:43 != child_nonunique.rowid:36) + └── f-k-checks + └── f-k-checks-item: child_nonunique(crdb_region,x,y) -> parent_nonunique(region,a,b) + └── anti-join (lookup parent_nonunique) + ├── columns: crdb_region:44 x:45 y:46 + ├── key columns: [44 45 46] = [47 48 49] + ├── lookup columns are key + ├── with-scan &1 + │ ├── columns: crdb_region:44 x:45 y:46 + │ └── mapping: + │ ├── fk_lookup_crdb_region:18 => crdb_region:44 + │ ├── column1:8 => x:45 + │ └── column2:9 => y:46 + └── filters (true) + +# A multi-row INSERT exercises the non-constant path, where the DistinctOn +# explicitly appears in the plan. +opt locality=(region=east) format=(hide-all,show-columns) +INSERT INTO child_nonunique VALUES (1, 2, 3), (4, 5, 6); +---- +insert child_nonunique + ├── columns: + ├── insert-mapping: + │ ├── column1:8 => child_nonunique.x:1 + │ ├── column2:9 => child_nonunique.y:2 + │ ├── column3:10 => child_nonunique.z:3 + │ ├── fk_lookup_crdb_region:19 => child_nonunique.crdb_region:4 + │ └── rowid_default:12 => child_nonunique.rowid:5 + ├── check columns: check1:20 + ├── input binding: &1 + ├── project + │ ├── columns: check1:20 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 fk_lookup_crdb_region:19 + │ ├── project + │ │ ├── columns: fk_lookup_crdb_region:19 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 + │ │ ├── distinct-on + │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 rownum:18 + │ │ │ ├── grouping columns: rownum:18 + │ │ │ ├── left-join (lookup parent_nonunique@a_b_idx) + │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 a:14 b:15 rownum:18 + │ │ │ │ ├── key columns: [8 9] = [14 15] + │ │ │ │ ├── ordinality + │ │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 rownum:18 + │ │ │ │ │ └── project + │ │ │ │ │ ├── columns: crdb_region_default:11 rowid_default:12 column1:8 column2:9 column3:10 + │ │ │ │ │ ├── values + │ │ │ │ │ │ ├── columns: column1:8 column2:9 column3:10 + │ │ │ │ │ │ ├── (1, 2, 3) + │ │ │ │ │ │ └── (4, 5, 6) + │ │ │ │ │ └── projections + │ │ │ │ │ ├── default_to_database_primary_region('east') [as=crdb_region_default:11] + │ │ │ │ │ └── unique_rowid() [as=rowid_default:12] + │ │ │ │ └── filters (true) + │ │ │ └── aggregations + │ │ │ ├── first-agg [as=region:13] + │ │ │ │ └── region:13 + │ │ │ ├── const-agg [as=column1:8] + │ │ │ │ └── column1:8 + │ │ │ ├── const-agg [as=column2:9] + │ │ │ │ └── column2:9 + │ │ │ ├── const-agg [as=column3:10] + │ │ │ │ └── column3:10 + │ │ │ ├── const-agg [as=crdb_region_default:11] + │ │ │ │ └── crdb_region_default:11 + │ │ │ └── const-agg [as=rowid_default:12] + │ │ │ └── rowid_default:12 + │ │ └── projections + │ │ └── CASE WHEN region:13 IS NOT DISTINCT FROM CAST(NULL AS STRING) THEN crdb_region_default:11 ELSE region:13 END [as=fk_lookup_crdb_region:19] + │ └── projections + │ └── fk_lookup_crdb_region:19 IN ('central', 'east', 'west') [as=check1:20] + ├── unique-checks + │ ├── unique-checks-item: child_nonunique(rowid) + │ │ └── project + │ │ ├── columns: rowid:32 + │ │ └── semi-join (lookup child_nonunique) + │ │ ├── columns: crdb_region:31 rowid:32 + │ │ ├── lookup expression + │ │ │ └── filters + │ │ │ ├── child_nonunique.crdb_region:24 IN ('central', 'east', 'west') + │ │ │ └── rowid:32 = child_nonunique.rowid:25 + │ │ ├── with-scan &1 + │ │ │ ├── columns: crdb_region:31 rowid:32 + │ │ │ └── mapping: + │ │ │ ├── fk_lookup_crdb_region:19 => crdb_region:31 + │ │ │ └── rowid_default:12 => rowid:32 + │ │ └── filters + │ │ └── crdb_region:31 != child_nonunique.crdb_region:24 + │ └── unique-checks-item: child_nonunique(z) + │ └── project + │ ├── columns: z:42 + │ └── semi-join (lookup child_nonunique@child_nonunique_crdb_region_z_key) + │ ├── columns: z:42 crdb_region:43 rowid:44 + │ ├── lookup expression + │ │ └── filters + │ │ ├── child_nonunique.crdb_region:36 IN ('central', 'east', 'west') + │ │ └── z:42 = child_nonunique.z:35 + │ ├── with-scan &1 + │ │ ├── columns: z:42 crdb_region:43 rowid:44 + │ │ └── mapping: + │ │ ├── column3:10 => z:42 + │ │ ├── fk_lookup_crdb_region:19 => crdb_region:43 + │ │ └── rowid_default:12 => rowid:44 + │ └── filters + │ └── (crdb_region:43 != child_nonunique.crdb_region:36) OR (rowid:44 != child_nonunique.rowid:37) + └── f-k-checks + └── f-k-checks-item: child_nonunique(crdb_region,x,y) -> parent_nonunique(region,a,b) + └── anti-join (lookup parent_nonunique) + ├── columns: crdb_region:45 x:46 y:47 + ├── key columns: [45 46 47] = [48 49 50] + ├── lookup columns are key + ├── with-scan &1 + │ ├── columns: crdb_region:45 x:46 y:47 + │ └── mapping: + │ ├── fk_lookup_crdb_region:19 => crdb_region:45 + │ ├── column1:8 => x:46 + │ └── column2:9 => y:47 + └── filters (true) + +opt locality=(region=east) format=(hide-all,show-columns) +UPDATE child_nonunique SET y = 100 WHERE z = 7; +---- +update child_nonunique + ├── columns: + ├── fetch columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + ├── update-mapping: + │ ├── y_new:15 => child_nonunique.y:2 + │ └── fk_lookup_crdb_region:21 => child_nonunique.crdb_region:4 + ├── check columns: check1:22 + ├── input binding: &1 + ├── project + │ ├── columns: check1:22 child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 y_new:15 fk_lookup_crdb_region:21 + │ ├── project + │ │ ├── columns: fk_lookup_crdb_region:21 y_new:15 child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ ├── limit + │ │ │ ├── columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 region:16 a:17 b:18 + │ │ │ ├── left-join (lookup parent_nonunique@a_b_idx) + │ │ │ │ ├── columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 region:16 a:17 b:18 + │ │ │ │ ├── key columns: [8 45] = [17 18] + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: "lookup_join_const_col_@18":45 child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ │ │ │ ├── index-join child_nonunique + │ │ │ │ │ │ ├── columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ │ │ │ │ └── locality-optimized-search + │ │ │ │ │ │ ├── columns: z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ │ │ │ │ ├── left columns: z:33 child_nonunique.crdb_region:34 rowid:35 + │ │ │ │ │ │ ├── right columns: z:40 child_nonunique.crdb_region:41 rowid:42 + │ │ │ │ │ │ ├── scan child_nonunique@child_nonunique_crdb_region_z_key + │ │ │ │ │ │ │ ├── columns: z:33 child_nonunique.crdb_region:34 rowid:35 + │ │ │ │ │ │ │ ├── constraint: /34/33: [/'east'/7 - /'east'/7] + │ │ │ │ │ │ │ └── flags: avoid-full-scan + │ │ │ │ │ │ └── scan child_nonunique@child_nonunique_crdb_region_z_key + │ │ │ │ │ │ ├── columns: z:40 child_nonunique.crdb_region:41 rowid:42 + │ │ │ │ │ │ ├── constraint: /41/40 + │ │ │ │ │ │ │ ├── [/'central'/7 - /'central'/7] + │ │ │ │ │ │ │ └── [/'west'/7 - /'west'/7] + │ │ │ │ │ │ └── flags: avoid-full-scan + │ │ │ │ │ └── projections + │ │ │ │ │ └── 100 [as="lookup_join_const_col_@18":45] + │ │ │ │ └── filters (true) + │ │ │ └── 1 + │ │ └── projections + │ │ ├── CASE WHEN region:16 IS NOT DISTINCT FROM CAST(NULL AS STRING) THEN child_nonunique.crdb_region:11 ELSE region:16 END [as=fk_lookup_crdb_region:21] + │ │ └── 100 [as=y_new:15] + │ └── projections + │ └── fk_lookup_crdb_region:21 IN ('central', 'east', 'west') [as=check1:22] + └── f-k-checks + └── f-k-checks-item: child_nonunique(crdb_region,x,y) -> parent_nonunique(region,a,b) + └── anti-join (lookup parent_nonunique) + ├── columns: crdb_region:23 x:24 y:25 + ├── key columns: [23 24 25] = [26 27 28] + ├── lookup columns are key + ├── select + │ ├── columns: crdb_region:23 x:24 y:25 + │ ├── with-scan &1 + │ │ ├── columns: crdb_region:23 x:24 y:25 + │ │ └── mapping: + │ │ ├── fk_lookup_crdb_region:21 => crdb_region:23 + │ │ ├── child_nonunique.x:8 => x:24 + │ │ └── y_new:15 => y:25 + │ └── filters + │ └── x:24 IS NOT NULL + └── filters (true)