exec-ddl
CREATE TABLE parent (p INT PRIMARY KEY)
----

exec-ddl
CREATE TABLE child (c INT PRIMARY KEY, p INT NOT NULL REFERENCES parent(p) ON UPDATE SET NULL)
----

build-post-queries
UPDATE parent SET p = p * 10 WHERE p > 1
----
root
 ├── update parent
 │    ├── columns: <none>
 │    ├── fetch columns: p:4
 │    ├── update-mapping:
 │    │    └── p_new:7 => p:1
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── child_p_fkey
 │    └── project
 │         ├── columns: p_new:7!null p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         ├── select
 │         │    ├── columns: p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         │    ├── scan parent
 │         │    │    ├── columns: p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         │    │    └── flags: avoid-full-scan
 │         │    └── filters
 │         │         └── p:4 > 1
 │         └── projections
 │              └── p:4 * 10 [as=p_new:7]
 └── cascade
      └── update child
           ├── columns: <none>
           ├── fetch columns: c:12 child.p:13
           ├── update-mapping:
           │    └── p_new:18 => child.p:9
           └── project
                ├── columns: p_new:18 c:12!null child.p:13!null p_old:16!null p_new:17!null
                ├── inner-join (hash)
                │    ├── columns: c:12!null child.p:13!null p_old:16!null p_new:17!null
                │    ├── scan child
                │    │    ├── columns: c:12!null child.p:13!null
                │    │    └── flags: avoid-full-scan disabled not visible index feature
                │    ├── select
                │    │    ├── columns: p_old:16!null p_new:17!null
                │    │    ├── with-scan &1
                │    │    │    ├── columns: p_old:16!null p_new:17!null
                │    │    │    └── mapping:
                │    │    │         ├──  parent.p:4 => p_old:16
                │    │    │         └──  p_new:7 => p_new:17
                │    │    └── filters
                │    │         └── p_old:16 IS DISTINCT FROM p_new:17
                │    └── filters
                │         └── child.p:13 = p_old:16
                └── projections
                     └── NULL::INT8 [as=p_new:18]

exec-ddl
CREATE TABLE parent_multi (
  pk INT PRIMARY KEY,
  p INT, q INT,
  UNIQUE (p, q),
  FAMILY (pk),
  FAMILY (p),
  FAMILY (q)
)
----

exec-ddl
CREATE TABLE child_multi (
  c INT PRIMARY KEY,
  p INT, q INT,
  UNIQUE (c, q),
  CONSTRAINT fk FOREIGN KEY (p, q) REFERENCES parent_multi(p, q) ON UPDATE SET NULL 
)
----

build-post-queries
UPDATE parent_multi SET p = p * 10, q = q + 1 WHERE pk > 1
----
root
 ├── update parent_multi
 │    ├── columns: <none>
 │    ├── fetch columns: pk:6 p:7 q:8
 │    ├── update-mapping:
 │    │    ├── p_new:11 => p:2
 │    │    └── q_new:12 => q:3
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── fk
 │    └── project
 │         ├── columns: p_new:11 q_new:12 pk:6!null p:7 q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         ├── select
 │         │    ├── columns: pk:6!null p:7 q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         │    ├── scan parent_multi
 │         │    │    ├── columns: pk:6!null p:7 q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         │    │    └── flags: avoid-full-scan
 │         │    └── filters
 │         │         └── pk:6 > 1
 │         └── projections
 │              ├── p:7 * 10 [as=p_new:11]
 │              └── q:8 + 1 [as=q_new:12]
 └── cascade
      └── update child_multi
           ├── columns: <none>
           ├── fetch columns: c:18 child_multi.p:19 child_multi.q:20
           ├── update-mapping:
           │    ├── p_new:27 => child_multi.p:14
           │    └── p_new:27 => child_multi.q:15
           └── project
                ├── columns: p_new:27 c:18!null child_multi.p:19!null child_multi.q:20!null p_old:23!null p_new:24 q_old:25!null q_new:26
                ├── inner-join (hash)
                │    ├── columns: c:18!null child_multi.p:19!null child_multi.q:20!null p_old:23!null p_new:24 q_old:25!null q_new:26
                │    ├── scan child_multi
                │    │    ├── columns: c:18!null child_multi.p:19 child_multi.q:20
                │    │    └── flags: avoid-full-scan disabled not visible index feature
                │    ├── select
                │    │    ├── columns: p_old:23 p_new:24 q_old:25 q_new:26
                │    │    ├── with-scan &1
                │    │    │    ├── columns: p_old:23 p_new:24 q_old:25 q_new:26
                │    │    │    └── mapping:
                │    │    │         ├──  parent_multi.p:7 => p_old:23
                │    │    │         ├──  parent_multi.q:8 => q_old:25
                │    │    │         ├──  p_new:11 => p_new:24
                │    │    │         └──  q_new:12 => q_new:26
                │    │    └── filters
                │    │         └── (p_old:23 IS DISTINCT FROM p_new:24) OR (q_old:25 IS DISTINCT FROM q_new:26)
                │    └── filters
                │         ├── child_multi.p:19 = p_old:23
                │         └── child_multi.q:20 = q_old:25
                └── projections
                     └── NULL::INT8 [as=p_new:27]

# Update only one of the two FK columns.
build-post-queries
UPDATE parent_multi SET p = p * 10 WHERE p > 1
----
root
 ├── update parent_multi
 │    ├── columns: <none>
 │    ├── fetch columns: pk:6 p:7 q:8
 │    ├── update-mapping:
 │    │    └── p_new:11 => p:2
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── fk
 │    └── project
 │         ├── columns: p_new:11!null pk:6!null p:7!null q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         ├── select
 │         │    ├── columns: pk:6!null p:7!null q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         │    ├── scan parent_multi
 │         │    │    ├── columns: pk:6!null p:7 q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         │    │    └── flags: avoid-full-scan
 │         │    └── filters
 │         │         └── p:7 > 1
 │         └── projections
 │              └── p:7 * 10 [as=p_new:11]
 └── cascade
      └── update child_multi
           ├── columns: <none>
           ├── fetch columns: c:17 child_multi.p:18 child_multi.q:19
           ├── update-mapping:
           │    ├── p_new:26 => child_multi.p:13
           │    └── p_new:26 => child_multi.q:14
           └── project
                ├── columns: p_new:26 c:17!null child_multi.p:18!null child_multi.q:19!null p_old:22!null p_new:23!null q_old:24!null q_new:25
                ├── inner-join (hash)
                │    ├── columns: c:17!null child_multi.p:18!null child_multi.q:19!null p_old:22!null p_new:23!null q_old:24!null q_new:25
                │    ├── scan child_multi
                │    │    ├── columns: c:17!null child_multi.p:18 child_multi.q:19
                │    │    └── flags: avoid-full-scan disabled not visible index feature
                │    ├── select
                │    │    ├── columns: p_old:22!null p_new:23!null q_old:24 q_new:25
                │    │    ├── with-scan &1
                │    │    │    ├── columns: p_old:22!null p_new:23!null q_old:24 q_new:25
                │    │    │    └── mapping:
                │    │    │         ├──  parent_multi.p:7 => p_old:22
                │    │    │         ├──  parent_multi.q:8 => q_old:24
                │    │    │         ├──  p_new:11 => p_new:23
                │    │    │         └──  parent_multi.q:8 => q_new:25
                │    │    └── filters
                │    │         └── (p_old:22 IS DISTINCT FROM p_new:23) OR (q_old:24 IS DISTINCT FROM q_new:25)
                │    └── filters
                │         ├── child_multi.p:18 = p_old:22
                │         └── child_multi.q:19 = q_old:24
                └── projections
                     └── NULL::INT8 [as=p_new:26]

# Test a two-level cascade.
exec-ddl
CREATE TABLE grandchild (
  g INT PRIMARY KEY,
  c INT, q INT,
  CONSTRAINT fk2 FOREIGN KEY (c, q) REFERENCES child_multi(c, q) ON UPDATE SET NULL
)
----

build-post-queries
UPDATE parent_multi SET q = q * 10 WHERE p > 1
----
root
 ├── update parent_multi
 │    ├── columns: <none>
 │    ├── fetch columns: pk:6 p:7 q:8
 │    ├── update-mapping:
 │    │    └── q_new:11 => q:3
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── fk
 │    └── project
 │         ├── columns: q_new:11 pk:6!null p:7!null q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         ├── select
 │         │    ├── columns: pk:6!null p:7!null q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         │    ├── scan parent_multi
 │         │    │    ├── columns: pk:6!null p:7 q:8 crdb_internal_mvcc_timestamp:9 tableoid:10
 │         │    │    └── flags: avoid-full-scan
 │         │    └── filters
 │         │         └── p:7 > 1
 │         └── projections
 │              └── q:8 * 10 [as=q_new:11]
 └── cascade
      ├── update child_multi
      │    ├── columns: <none>
      │    ├── fetch columns: c:17 child_multi.p:18 child_multi.q:19
      │    ├── update-mapping:
      │    │    ├── p_new:26 => child_multi.p:13
      │    │    └── p_new:26 => child_multi.q:14
      │    ├── input binding: &2
      │    ├── cascades
      │    │    └── fk2
      │    └── project
      │         ├── columns: p_new:26 c:17!null child_multi.p:18!null child_multi.q:19!null p_old:22!null p_new:23!null q_old:24!null q_new:25
      │         ├── inner-join (hash)
      │         │    ├── columns: c:17!null child_multi.p:18!null child_multi.q:19!null p_old:22!null p_new:23!null q_old:24!null q_new:25
      │         │    ├── scan child_multi
      │         │    │    ├── columns: c:17!null child_multi.p:18 child_multi.q:19
      │         │    │    └── flags: avoid-full-scan disabled not visible index feature
      │         │    ├── select
      │         │    │    ├── columns: p_old:22!null p_new:23!null q_old:24 q_new:25
      │         │    │    ├── with-scan &1
      │         │    │    │    ├── columns: p_old:22!null p_new:23!null q_old:24 q_new:25
      │         │    │    │    └── mapping:
      │         │    │    │         ├──  parent_multi.p:7 => p_old:22
      │         │    │    │         ├──  parent_multi.q:8 => q_old:24
      │         │    │    │         ├──  parent_multi.p:7 => p_new:23
      │         │    │    │         └──  q_new:11 => q_new:25
      │         │    │    └── filters
      │         │    │         └── (p_old:22 IS DISTINCT FROM p_new:23) OR (q_old:24 IS DISTINCT FROM q_new:25)
      │         │    └── filters
      │         │         ├── child_multi.p:18 = p_old:22
      │         │         └── child_multi.q:19 = q_old:24
      │         └── projections
      │              └── NULL::INT8 [as=p_new:26]
      └── cascade
           └── update grandchild
                ├── columns: <none>
                ├── fetch columns: g:32 grandchild.c:33 grandchild.q:34
                ├── update-mapping:
                │    ├── c_new:41 => grandchild.c:28
                │    └── c_new:41 => grandchild.q:29
                └── project
                     ├── columns: c_new:41 g:32!null grandchild.c:33!null grandchild.q:34!null c_old:37!null c_new:38!null q_old:39!null q_new:40
                     ├── inner-join (hash)
                     │    ├── columns: g:32!null grandchild.c:33!null grandchild.q:34!null c_old:37!null c_new:38!null q_old:39!null q_new:40
                     │    ├── scan grandchild
                     │    │    ├── columns: g:32!null grandchild.c:33 grandchild.q:34
                     │    │    └── flags: avoid-full-scan disabled not visible index feature
                     │    ├── select
                     │    │    ├── columns: c_old:37!null c_new:38!null q_old:39!null q_new:40
                     │    │    ├── with-scan &2
                     │    │    │    ├── columns: c_old:37!null c_new:38!null q_old:39!null q_new:40
                     │    │    │    └── mapping:
                     │    │    │         ├──  child_multi.c:17 => c_old:37
                     │    │    │         ├──  child_multi.q:19 => q_old:39
                     │    │    │         ├──  child_multi.c:17 => c_new:38
                     │    │    │         └──  p_new:26 => q_new:40
                     │    │    └── filters
                     │    │         └── (c_old:37 IS DISTINCT FROM c_new:38) OR (q_old:39 IS DISTINCT FROM q_new:40)
                     │    └── filters
                     │         ├── grandchild.c:33 = c_old:37
                     │         └── grandchild.q:34 = q_old:39
                     └── projections
                          └── NULL::INT8 [as=c_new:41]

build-post-queries
UPSERT INTO parent_multi VALUES (1, 10, 10), (2, 20, 20)
----
root
 ├── upsert parent_multi
 │    ├── arbiter indexes: parent_multi_pkey
 │    ├── columns: <none>
 │    ├── canary column: pk:9
 │    ├── fetch columns: pk:9 p:10 q:11
 │    ├── insert-mapping:
 │    │    ├── column1:6 => pk:1
 │    │    ├── column2:7 => p:2
 │    │    └── column3:8 => q:3
 │    ├── update-mapping:
 │    │    ├── column2:7 => p:2
 │    │    └── column3:8 => q:3
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── fk
 │    └── project
 │         ├── columns: upsert_pk:14 column1:6!null column2:7!null column3:8!null pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         ├── left-join (hash)
 │         │    ├── columns: column1:6!null column2:7!null column3:8!null pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    ├── ensure-upsert-distinct-on
 │         │    │    ├── columns: column1:6!null column2:7!null column3:8!null
 │         │    │    ├── grouping columns: column1:6!null
 │         │    │    ├── values
 │         │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null
 │         │    │    │    ├── (1, 10, 10)
 │         │    │    │    └── (2, 20, 20)
 │         │    │    └── aggregations
 │         │    │         ├── first-agg [as=column2:7]
 │         │    │         │    └── column2:7
 │         │    │         └── first-agg [as=column3:8]
 │         │    │              └── column3:8
 │         │    ├── scan parent_multi
 │         │    │    ├── columns: pk:9!null p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    │    └── flags: avoid-full-scan disabled not visible index feature
 │         │    └── filters
 │         │         └── column1:6 = pk:9
 │         └── projections
 │              └── CASE WHEN pk:9 IS NULL THEN column1:6 ELSE pk:9 END [as=upsert_pk:14]
 └── cascade
      ├── update child_multi
      │    ├── columns: <none>
      │    ├── fetch columns: c:20 child_multi.p:21 child_multi.q:22
      │    ├── update-mapping:
      │    │    ├── p_new:29 => child_multi.p:16
      │    │    └── p_new:29 => child_multi.q:17
      │    ├── input binding: &2
      │    ├── cascades
      │    │    └── fk2
      │    └── project
      │         ├── columns: p_new:29 c:20!null child_multi.p:21!null child_multi.q:22!null p_old:25!null p_new:26!null q_old:27!null q_new:28!null
      │         ├── inner-join (hash)
      │         │    ├── columns: c:20!null child_multi.p:21!null child_multi.q:22!null p_old:25!null p_new:26!null q_old:27!null q_new:28!null
      │         │    ├── scan child_multi
      │         │    │    ├── columns: c:20!null child_multi.p:21 child_multi.q:22
      │         │    │    └── flags: avoid-full-scan disabled not visible index feature
      │         │    ├── select
      │         │    │    ├── columns: p_old:25 p_new:26!null q_old:27 q_new:28!null
      │         │    │    ├── with-scan &1
      │         │    │    │    ├── columns: p_old:25 p_new:26!null q_old:27 q_new:28!null
      │         │    │    │    └── mapping:
      │         │    │    │         ├──  parent_multi.p:10 => p_old:25
      │         │    │    │         ├──  parent_multi.q:11 => q_old:27
      │         │    │    │         ├──  column2:7 => p_new:26
      │         │    │    │         └──  column3:8 => q_new:28
      │         │    │    └── filters
      │         │    │         └── (p_old:25 IS DISTINCT FROM p_new:26) OR (q_old:27 IS DISTINCT FROM q_new:28)
      │         │    └── filters
      │         │         ├── child_multi.p:21 = p_old:25
      │         │         └── child_multi.q:22 = q_old:27
      │         └── projections
      │              └── NULL::INT8 [as=p_new:29]
      └── cascade
           └── update grandchild
                ├── columns: <none>
                ├── fetch columns: g:35 grandchild.c:36 grandchild.q:37
                ├── update-mapping:
                │    ├── c_new:44 => grandchild.c:31
                │    └── c_new:44 => grandchild.q:32
                └── project
                     ├── columns: c_new:44 g:35!null grandchild.c:36!null grandchild.q:37!null c_old:40!null c_new:41!null q_old:42!null q_new:43
                     ├── inner-join (hash)
                     │    ├── columns: g:35!null grandchild.c:36!null grandchild.q:37!null c_old:40!null c_new:41!null q_old:42!null q_new:43
                     │    ├── scan grandchild
                     │    │    ├── columns: g:35!null grandchild.c:36 grandchild.q:37
                     │    │    └── flags: avoid-full-scan disabled not visible index feature
                     │    ├── select
                     │    │    ├── columns: c_old:40!null c_new:41!null q_old:42!null q_new:43
                     │    │    ├── with-scan &2
                     │    │    │    ├── columns: c_old:40!null c_new:41!null q_old:42!null q_new:43
                     │    │    │    └── mapping:
                     │    │    │         ├──  child_multi.c:20 => c_old:40
                     │    │    │         ├──  child_multi.q:22 => q_old:42
                     │    │    │         ├──  child_multi.c:20 => c_new:41
                     │    │    │         └──  p_new:29 => q_new:43
                     │    │    └── filters
                     │    │         └── (c_old:40 IS DISTINCT FROM c_new:41) OR (q_old:42 IS DISTINCT FROM q_new:43)
                     │    └── filters
                     │         ├── grandchild.c:36 = c_old:40
                     │         └── grandchild.q:37 = q_old:42
                     └── projections
                          └── NULL::INT8 [as=c_new:44]

# Upsert that only touches one of the FK columns.
build-post-queries
UPSERT INTO parent_multi(pk, p) VALUES (1, 10), (2, 20)
----
root
 ├── upsert parent_multi
 │    ├── arbiter indexes: parent_multi_pkey
 │    ├── columns: <none>
 │    ├── canary column: pk:9
 │    ├── fetch columns: pk:9 p:10 q:11
 │    ├── insert-mapping:
 │    │    ├── column1:6 => pk:1
 │    │    ├── column2:7 => p:2
 │    │    └── q_default:8 => q:3
 │    ├── update-mapping:
 │    │    └── column2:7 => p:2
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── fk
 │    └── project
 │         ├── columns: upsert_pk:14 upsert_q:15 column1:6!null column2:7!null q_default:8 pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         ├── left-join (hash)
 │         │    ├── columns: column1:6!null column2:7!null q_default:8 pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    ├── ensure-upsert-distinct-on
 │         │    │    ├── columns: column1:6!null column2:7!null q_default:8
 │         │    │    ├── grouping columns: column1:6!null
 │         │    │    ├── project
 │         │    │    │    ├── columns: q_default:8 column1:6!null column2:7!null
 │         │    │    │    ├── values
 │         │    │    │    │    ├── columns: column1:6!null column2:7!null
 │         │    │    │    │    ├── (1, 10)
 │         │    │    │    │    └── (2, 20)
 │         │    │    │    └── projections
 │         │    │    │         └── NULL::INT8 [as=q_default:8]
 │         │    │    └── aggregations
 │         │    │         ├── first-agg [as=column2:7]
 │         │    │         │    └── column2:7
 │         │    │         └── first-agg [as=q_default:8]
 │         │    │              └── q_default:8
 │         │    ├── scan parent_multi
 │         │    │    ├── columns: pk:9!null p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    │    └── flags: avoid-full-scan disabled not visible index feature
 │         │    └── filters
 │         │         └── column1:6 = pk:9
 │         └── projections
 │              ├── CASE WHEN pk:9 IS NULL THEN column1:6 ELSE pk:9 END [as=upsert_pk:14]
 │              └── CASE WHEN pk:9 IS NULL THEN q_default:8 ELSE q:11 END [as=upsert_q:15]
 └── cascade
      ├── update child_multi
      │    ├── columns: <none>
      │    ├── fetch columns: c:21 child_multi.p:22 child_multi.q:23
      │    ├── update-mapping:
      │    │    ├── p_new:30 => child_multi.p:17
      │    │    └── p_new:30 => child_multi.q:18
      │    ├── input binding: &2
      │    ├── cascades
      │    │    └── fk2
      │    └── project
      │         ├── columns: p_new:30 c:21!null child_multi.p:22!null child_multi.q:23!null p_old:26!null p_new:27!null q_old:28!null q_new:29
      │         ├── inner-join (hash)
      │         │    ├── columns: c:21!null child_multi.p:22!null child_multi.q:23!null p_old:26!null p_new:27!null q_old:28!null q_new:29
      │         │    ├── scan child_multi
      │         │    │    ├── columns: c:21!null child_multi.p:22 child_multi.q:23
      │         │    │    └── flags: avoid-full-scan disabled not visible index feature
      │         │    ├── select
      │         │    │    ├── columns: p_old:26 p_new:27!null q_old:28 q_new:29
      │         │    │    ├── with-scan &1
      │         │    │    │    ├── columns: p_old:26 p_new:27!null q_old:28 q_new:29
      │         │    │    │    └── mapping:
      │         │    │    │         ├──  parent_multi.p:10 => p_old:26
      │         │    │    │         ├──  parent_multi.q:11 => q_old:28
      │         │    │    │         ├──  column2:7 => p_new:27
      │         │    │    │         └──  parent_multi.q:11 => q_new:29
      │         │    │    └── filters
      │         │    │         └── (p_old:26 IS DISTINCT FROM p_new:27) OR (q_old:28 IS DISTINCT FROM q_new:29)
      │         │    └── filters
      │         │         ├── child_multi.p:22 = p_old:26
      │         │         └── child_multi.q:23 = q_old:28
      │         └── projections
      │              └── NULL::INT8 [as=p_new:30]
      └── cascade
           └── update grandchild
                ├── columns: <none>
                ├── fetch columns: g:36 grandchild.c:37 grandchild.q:38
                ├── update-mapping:
                │    ├── c_new:45 => grandchild.c:32
                │    └── c_new:45 => grandchild.q:33
                └── project
                     ├── columns: c_new:45 g:36!null grandchild.c:37!null grandchild.q:38!null c_old:41!null c_new:42!null q_old:43!null q_new:44
                     ├── inner-join (hash)
                     │    ├── columns: g:36!null grandchild.c:37!null grandchild.q:38!null c_old:41!null c_new:42!null q_old:43!null q_new:44
                     │    ├── scan grandchild
                     │    │    ├── columns: g:36!null grandchild.c:37 grandchild.q:38
                     │    │    └── flags: avoid-full-scan disabled not visible index feature
                     │    ├── select
                     │    │    ├── columns: c_old:41!null c_new:42!null q_old:43!null q_new:44
                     │    │    ├── with-scan &2
                     │    │    │    ├── columns: c_old:41!null c_new:42!null q_old:43!null q_new:44
                     │    │    │    └── mapping:
                     │    │    │         ├──  child_multi.c:21 => c_old:41
                     │    │    │         ├──  child_multi.q:23 => q_old:43
                     │    │    │         ├──  child_multi.c:21 => c_new:42
                     │    │    │         └──  p_new:30 => q_new:44
                     │    │    └── filters
                     │    │         └── (c_old:41 IS DISTINCT FROM c_new:42) OR (q_old:43 IS DISTINCT FROM q_new:44)
                     │    └── filters
                     │         ├── grandchild.c:37 = c_old:41
                     │         └── grandchild.q:38 = q_old:43
                     └── projections
                          └── NULL::INT8 [as=c_new:45]

build-post-queries
INSERT INTO parent_multi VALUES (1, 10, 10), (2, 20, 20) ON CONFLICT (p,q) DO UPDATE SET p = 100
----
root
 ├── upsert parent_multi
 │    ├── arbiter indexes: parent_multi_p_q_key
 │    ├── columns: <none>
 │    ├── canary column: pk:9
 │    ├── fetch columns: pk:9 p:10 q:11
 │    ├── insert-mapping:
 │    │    ├── column1:6 => pk:1
 │    │    ├── column2:7 => p:2
 │    │    └── column3:8 => q:3
 │    ├── update-mapping:
 │    │    └── upsert_p:16 => p:2
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── fk
 │    └── project
 │         ├── columns: upsert_pk:15 upsert_p:16!null upsert_q:17 column1:6!null column2:7!null column3:8!null pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13 p_new:14!null
 │         ├── project
 │         │    ├── columns: p_new:14!null column1:6!null column2:7!null column3:8!null pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    ├── left-join (hash)
 │         │    │    ├── columns: column1:6!null column2:7!null column3:8!null pk:9 p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    │    ├── ensure-upsert-distinct-on
 │         │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null
 │         │    │    │    ├── grouping columns: column2:7!null column3:8!null
 │         │    │    │    ├── values
 │         │    │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null
 │         │    │    │    │    ├── (1, 10, 10)
 │         │    │    │    │    └── (2, 20, 20)
 │         │    │    │    └── aggregations
 │         │    │    │         └── first-agg [as=column1:6]
 │         │    │    │              └── column1:6
 │         │    │    ├── scan parent_multi
 │         │    │    │    ├── columns: pk:9!null p:10 q:11 crdb_internal_mvcc_timestamp:12 tableoid:13
 │         │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │         │    │    └── filters
 │         │    │         ├── column2:7 = p:10
 │         │    │         └── column3:8 = q:11
 │         │    └── projections
 │         │         └── 100 [as=p_new:14]
 │         └── projections
 │              ├── CASE WHEN pk:9 IS NULL THEN column1:6 ELSE pk:9 END [as=upsert_pk:15]
 │              ├── CASE WHEN pk:9 IS NULL THEN column2:7 ELSE p_new:14 END [as=upsert_p:16]
 │              └── CASE WHEN pk:9 IS NULL THEN column3:8 ELSE q:11 END [as=upsert_q:17]
 └── cascade
      ├── update child_multi
      │    ├── columns: <none>
      │    ├── fetch columns: c:23 child_multi.p:24 child_multi.q:25
      │    ├── update-mapping:
      │    │    ├── p_new:32 => child_multi.p:19
      │    │    └── p_new:32 => child_multi.q:20
      │    ├── input binding: &2
      │    ├── cascades
      │    │    └── fk2
      │    └── project
      │         ├── columns: p_new:32 c:23!null child_multi.p:24!null child_multi.q:25!null p_old:28!null p_new:29!null q_old:30!null q_new:31
      │         ├── inner-join (hash)
      │         │    ├── columns: c:23!null child_multi.p:24!null child_multi.q:25!null p_old:28!null p_new:29!null q_old:30!null q_new:31
      │         │    ├── scan child_multi
      │         │    │    ├── columns: c:23!null child_multi.p:24 child_multi.q:25
      │         │    │    └── flags: avoid-full-scan disabled not visible index feature
      │         │    ├── select
      │         │    │    ├── columns: p_old:28 p_new:29!null q_old:30 q_new:31
      │         │    │    ├── with-scan &1
      │         │    │    │    ├── columns: p_old:28 p_new:29!null q_old:30 q_new:31
      │         │    │    │    └── mapping:
      │         │    │    │         ├──  parent_multi.p:10 => p_old:28
      │         │    │    │         ├──  parent_multi.q:11 => q_old:30
      │         │    │    │         ├──  upsert_p:16 => p_new:29
      │         │    │    │         └──  parent_multi.q:11 => q_new:31
      │         │    │    └── filters
      │         │    │         └── (p_old:28 IS DISTINCT FROM p_new:29) OR (q_old:30 IS DISTINCT FROM q_new:31)
      │         │    └── filters
      │         │         ├── child_multi.p:24 = p_old:28
      │         │         └── child_multi.q:25 = q_old:30
      │         └── projections
      │              └── NULL::INT8 [as=p_new:32]
      └── cascade
           └── update grandchild
                ├── columns: <none>
                ├── fetch columns: g:38 grandchild.c:39 grandchild.q:40
                ├── update-mapping:
                │    ├── c_new:47 => grandchild.c:34
                │    └── c_new:47 => grandchild.q:35
                └── project
                     ├── columns: c_new:47 g:38!null grandchild.c:39!null grandchild.q:40!null c_old:43!null c_new:44!null q_old:45!null q_new:46
                     ├── inner-join (hash)
                     │    ├── columns: g:38!null grandchild.c:39!null grandchild.q:40!null c_old:43!null c_new:44!null q_old:45!null q_new:46
                     │    ├── scan grandchild
                     │    │    ├── columns: g:38!null grandchild.c:39 grandchild.q:40
                     │    │    └── flags: avoid-full-scan disabled not visible index feature
                     │    ├── select
                     │    │    ├── columns: c_old:43!null c_new:44!null q_old:45!null q_new:46
                     │    │    ├── with-scan &2
                     │    │    │    ├── columns: c_old:43!null c_new:44!null q_old:45!null q_new:46
                     │    │    │    └── mapping:
                     │    │    │         ├──  child_multi.c:23 => c_old:43
                     │    │    │         ├──  child_multi.q:25 => q_old:45
                     │    │    │         ├──  child_multi.c:23 => c_new:44
                     │    │    │         └──  p_new:32 => q_new:46
                     │    │    └── filters
                     │    │         └── (c_old:43 IS DISTINCT FROM c_new:44) OR (q_old:45 IS DISTINCT FROM q_new:46)
                     │    └── filters
                     │         ├── grandchild.c:39 = c_old:43
                     │         └── grandchild.q:40 = q_old:45
                     └── projections
                          └── NULL::INT8 [as=c_new: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 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-post-queries
UPDATE parent_partial SET p = p * 10 WHERE p > 1
----
root
 ├── update parent_partial
 │    ├── columns: <none>
 │    ├── fetch columns: p:4
 │    ├── update-mapping:
 │    │    └── p_new:7 => p:1
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── child_partial_p_fkey
 │    └── project
 │         ├── columns: p_new:7!null p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         ├── select
 │         │    ├── columns: p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         │    ├── scan parent_partial
 │         │    │    ├── columns: p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         │    │    └── flags: avoid-full-scan
 │         │    └── filters
 │         │         └── p:4 > 1
 │         └── projections
 │              └── p:4 * 10 [as=p_new:7]
 └── cascade
      └── update child_partial
           ├── columns: <none>
           ├── fetch columns: c:13 child_partial.p:14 i:15
           ├── update-mapping:
           │    └── p_new:20 => child_partial.p:9
           ├── partial index put columns: partial_index_put1:21 partial_index_put2:22
           ├── partial index del columns: partial_index_put1:21 partial_index_del2:23
           └── project
                ├── columns: partial_index_put1:21 partial_index_put2:22 partial_index_del2:23!null c:13!null child_partial.p:14!null i:15 p_old:18!null p_new:19!null p_new:20
                ├── project
                │    ├── columns: p_new:20 c:13!null child_partial.p:14!null i:15 p_old:18!null p_new:19!null
                │    ├── inner-join (hash)
                │    │    ├── columns: c:13!null child_partial.p:14!null i:15 p_old:18!null p_new:19!null
                │    │    ├── scan child_partial
                │    │    │    ├── columns: c:13!null child_partial.p:14!null i:15
                │    │    │    ├── partial index predicates
                │    │    │    │    ├── child_partial_p_idx: filters
                │    │    │    │    │    └── i:15 > 0
                │    │    │    │    └── child_partial_i_idx: filters
                │    │    │    │         └── child_partial.p:14 > 0
                │    │    │    └── flags: avoid-full-scan disabled not visible index feature
                │    │    ├── select
                │    │    │    ├── columns: p_old:18!null p_new:19!null
                │    │    │    ├── with-scan &1
                │    │    │    │    ├── columns: p_old:18!null p_new:19!null
                │    │    │    │    └── mapping:
                │    │    │    │         ├──  parent_partial.p:4 => p_old:18
                │    │    │    │         └──  p_new:7 => p_new:19
                │    │    │    └── filters
                │    │    │         └── p_old:18 IS DISTINCT FROM p_new:19
                │    │    └── filters
                │    │         └── child_partial.p:14 = p_old:18
                │    └── projections
                │         └── NULL::INT8 [as=p_new:20]
                └── projections
                     ├── i:15 > 0 [as=partial_index_put1:21]
                     ├── p_new:20 > 0 [as=partial_index_put2:22]
                     └── child_partial.p:14 > 0 [as=partial_index_del2:23]

# Test cascade to a child with a virtual column that references the FK.
exec-ddl
CREATE TABLE parent_virt (p INT PRIMARY KEY)
----

exec-ddl
CREATE TABLE child_virt (
  c INT PRIMARY KEY,
  p INT NOT NULL REFERENCES parent_virt(p) ON UPDATE SET NULL,
  v INT AS (p) VIRTUAL
)
----

build-post-queries
UPDATE parent_virt SET p = p * 10 WHERE p > 1
----
root
 ├── update parent_virt
 │    ├── columns: <none>
 │    ├── fetch columns: p:4
 │    ├── update-mapping:
 │    │    └── p_new:7 => p:1
 │    ├── input binding: &1
 │    ├── cascades
 │    │    └── child_virt_p_fkey
 │    └── project
 │         ├── columns: p_new:7!null p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         ├── select
 │         │    ├── columns: p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         │    ├── scan parent_virt
 │         │    │    ├── columns: p:4!null crdb_internal_mvcc_timestamp:5 tableoid:6
 │         │    │    └── flags: avoid-full-scan
 │         │    └── filters
 │         │         └── p:4 > 1
 │         └── projections
 │              └── p:4 * 10 [as=p_new:7]
 └── cascade
      └── update child_virt
           ├── columns: <none>
           ├── fetch columns: c:13 child_virt.p:14 v:15
           ├── update-mapping:
           │    ├── p_new:20 => child_virt.p:9
           │    └── p_new:20 => v:10
           └── project
                ├── columns: p_new:20 c:13!null child_virt.p:14!null v:15!null p_old:18!null p_new:19!null
                ├── inner-join (hash)
                │    ├── columns: c:13!null child_virt.p:14!null v:15!null p_old:18!null p_new:19!null
                │    ├── project
                │    │    ├── columns: v:15!null c:13!null child_virt.p:14!null
                │    │    ├── scan child_virt
                │    │    │    ├── columns: c:13!null child_virt.p:14!null
                │    │    │    ├── computed column expressions
                │    │    │    │    └── v:15
                │    │    │    │         └── child_virt.p:14
                │    │    │    └── flags: avoid-full-scan disabled not visible index feature
                │    │    └── projections
                │    │         └── child_virt.p:14 [as=v:15]
                │    ├── select
                │    │    ├── columns: p_old:18!null p_new:19!null
                │    │    ├── with-scan &1
                │    │    │    ├── columns: p_old:18!null p_new:19!null
                │    │    │    └── mapping:
                │    │    │         ├──  parent_virt.p:4 => p_old:18
                │    │    │         └──  p_new:7 => p_new:19
                │    │    └── filters
                │    │         └── p_old:18 IS DISTINCT FROM p_new:19
                │    └── filters
                │         └── child_virt.p:14 = p_old:18
                └── projections
                     └── NULL::INT8 [as=p_new:20]
