# The upsert tests need to exercise these dimensions:
#  - inbound vs outbound FKs
#  - single vs multiple FK columns
#  - all table columns specified vs subset of table (and FK) columns specified
#  - statement type:
#     - UPSERT
#     - INSERT ON CONFLICT DO UPDATE
#     - INSERT ON CONFLICT with a secondary index
#
# Note that ON CONFLICT DO NOTHING is not built as an Upsert so it is not
# tested here.

exec-ddl
CREATE TABLE xyzw (x INT, y INT, z INT, w INT)
----

exec-ddl
CREATE TABLE uv (u INT NOT NULL, v INT NOT NULL)
----

# ---------------------------------------
# Outbound FK tests with single FK column
# ---------------------------------------

exec-ddl
CREATE TABLE p (p INT PRIMARY KEY, other INT)
----

exec-ddl
CREATE TABLE c1 (c INT PRIMARY KEY, p INT NOT NULL DEFAULT 5 REFERENCES p(p), i INT)
----

build
UPSERT INTO c1 VALUES (100, 1), (200, 1)
----
upsert c1
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── column1:6 => c:1
 │    ├── column2:7 => c1.p:2
 │    └── i_default:8 => i:3
 ├── input binding: &1
 ├── project
 │    ├── columns: i_default:8 column1:6!null column2:7!null
 │    ├── values
 │    │    ├── columns: column1:6!null column2:7!null
 │    │    ├── (100, 1)
 │    │    └── (200, 1)
 │    └── projections
 │         └── NULL::INT8 [as=i_default:8]
 └── f-k-checks
      └── f-k-checks-item: c1(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:9!null
                ├── with-scan &1
                │    ├── columns: p:9!null
                │    └── mapping:
                │         └──  column2:7 => p:9
                ├── scan p
                │    ├── columns: p.p:10!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:9 = p.p:10

build
UPSERT INTO c1(c) VALUES (100), (200)
----
upsert c1
 ├── arbiter indexes: c1_pkey
 ├── columns: <none>
 ├── canary column: c:9
 ├── fetch columns: c:9 c1.p:10 i:11
 ├── insert-mapping:
 │    ├── column1:6 => c:1
 │    ├── p_default:7 => c1.p:2
 │    └── i_default:8 => i:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:14 upsert_p:15 upsert_i:16 column1:6!null p_default:7!null i_default:8 c:9 c1.p:10 i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13
 │    ├── left-join (hash)
 │    │    ├── columns: column1:6!null p_default:7!null i_default:8 c:9 c1.p:10 i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: column1:6!null p_default:7!null i_default:8
 │    │    │    ├── grouping columns: column1:6!null
 │    │    │    ├── project
 │    │    │    │    ├── columns: p_default:7!null i_default:8 column1:6!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:6!null
 │    │    │    │    │    ├── (100,)
 │    │    │    │    │    └── (200,)
 │    │    │    │    └── projections
 │    │    │    │         ├── 5 [as=p_default:7]
 │    │    │    │         └── NULL::INT8 [as=i_default:8]
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=p_default:7]
 │    │    │         │    └── p_default:7
 │    │    │         └── first-agg [as=i_default:8]
 │    │    │              └── i_default:8
 │    │    ├── scan c1
 │    │    │    ├── columns: c:9!null c1.p:10!null i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── column1:6 = c:9
 │    └── projections
 │         ├── CASE WHEN c:9 IS NULL THEN column1:6 ELSE c:9 END [as=upsert_c:14]
 │         ├── CASE WHEN c:9 IS NULL THEN p_default:7 ELSE c1.p:10 END [as=upsert_p:15]
 │         └── CASE WHEN c:9 IS NULL THEN i_default:8 ELSE i:11 END [as=upsert_i:16]
 └── f-k-checks
      └── f-k-checks-item: c1(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:17
                ├── with-scan &1
                │    ├── columns: p:17
                │    └── mapping:
                │         └──  upsert_p:15 => p:17
                ├── scan p
                │    ├── columns: p.p:18!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:17 = p.p:18

# Use a non-constant input.
build
UPSERT INTO c1 SELECT x, y FROM xyzw
----
upsert c1
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── x:6 => c:1
 │    ├── y:7 => c1.p:2
 │    └── i_default:13 => i:3
 ├── input binding: &1
 ├── project
 │    ├── columns: i_default:13 x:6 y:7
 │    ├── project
 │    │    ├── columns: x:6 y:7
 │    │    └── scan xyzw
 │    │         └── columns: x:6 y:7 z:8 w:9 rowid:10!null xyzw.crdb_internal_mvcc_timestamp:11 xyzw.tableoid:12
 │    └── projections
 │         └── NULL::INT8 [as=i_default:13]
 └── f-k-checks
      └── f-k-checks-item: c1(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:14
                ├── with-scan &1
                │    ├── columns: p:14
                │    └── mapping:
                │         └──  y:7 => p:14
                ├── scan p
                │    ├── columns: p.p:15!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:14 = p.p:15

build
INSERT INTO c1 VALUES (100, 1), (200, 1) ON CONFLICT (c) DO UPDATE SET p = excluded.p + 1
----
upsert c1
 ├── arbiter indexes: c1_pkey
 ├── columns: <none>
 ├── canary column: c:9
 ├── fetch columns: c:9 c1.p:10 i:11
 ├── insert-mapping:
 │    ├── column1:6 => c:1
 │    ├── column2:7 => c1.p:2
 │    └── i_default:8 => i:3
 ├── update-mapping:
 │    └── upsert_p:16 => c1.p:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:15 upsert_p:16!null upsert_i:17 column1:6!null column2:7!null i_default:8 c:9 c1.p:10 i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13 p_new:14!null
 │    ├── project
 │    │    ├── columns: p_new:14!null column1:6!null column2:7!null i_default:8 c:9 c1.p:10 i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:6!null column2:7!null i_default:8 c:9 c1.p:10 i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:6!null column2:7!null i_default:8
 │    │    │    │    ├── grouping columns: column1:6!null
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: i_default:8 column1:6!null column2:7!null
 │    │    │    │    │    ├── values
 │    │    │    │    │    │    ├── columns: column1:6!null column2:7!null
 │    │    │    │    │    │    ├── (100, 1)
 │    │    │    │    │    │    └── (200, 1)
 │    │    │    │    │    └── projections
 │    │    │    │    │         └── NULL::INT8 [as=i_default:8]
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=column2:7]
 │    │    │    │         │    └── column2:7
 │    │    │    │         └── first-agg [as=i_default:8]
 │    │    │    │              └── i_default:8
 │    │    │    ├── scan c1
 │    │    │    │    ├── columns: c:9!null c1.p:10!null i:11 c1.crdb_internal_mvcc_timestamp:12 c1.tableoid:13
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:6 = c:9
 │    │    └── projections
 │    │         └── column2:7 + 1 [as=p_new:14]
 │    └── projections
 │         ├── CASE WHEN c:9 IS NULL THEN column1:6 ELSE c:9 END [as=upsert_c:15]
 │         ├── CASE WHEN c:9 IS NULL THEN column2:7 ELSE p_new:14 END [as=upsert_p:16]
 │         └── CASE WHEN c:9 IS NULL THEN i_default:8 ELSE i:11 END [as=upsert_i:17]
 └── f-k-checks
      └── f-k-checks-item: c1(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:18!null
                ├── with-scan &1
                │    ├── columns: p:18!null
                │    └── mapping:
                │         └──  upsert_p:16 => p:18
                ├── scan p
                │    ├── columns: p.p:19!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:18 = p.p:19

build
INSERT INTO c1 SELECT u, v FROM uv ON CONFLICT (c) DO UPDATE SET i = c1.c + 1
----
upsert c1
 ├── arbiter indexes: c1_pkey
 ├── columns: <none>
 ├── canary column: c:12
 ├── fetch columns: c:12 c1.p:13 i:14
 ├── insert-mapping:
 │    ├── u:6 => c:1
 │    ├── v:7 => c1.p:2
 │    └── i_default:11 => i:3
 ├── update-mapping:
 │    └── upsert_i:20 => i:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:18 upsert_p:19 upsert_i:20 u:6!null v:7!null i_default:11 c:12 c1.p:13 i:14 c1.crdb_internal_mvcc_timestamp:15 c1.tableoid:16 i_new:17
 │    ├── project
 │    │    ├── columns: i_new:17 u:6!null v:7!null i_default:11 c:12 c1.p:13 i:14 c1.crdb_internal_mvcc_timestamp:15 c1.tableoid:16
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: u:6!null v:7!null i_default:11 c:12 c1.p:13 i:14 c1.crdb_internal_mvcc_timestamp:15 c1.tableoid:16
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: u:6!null v:7!null i_default:11
 │    │    │    │    ├── grouping columns: u:6!null
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: i_default:11 u:6!null v:7!null
 │    │    │    │    │    ├── project
 │    │    │    │    │    │    ├── columns: u:6!null v:7!null
 │    │    │    │    │    │    └── scan uv
 │    │    │    │    │    │         └── columns: u:6!null v:7!null rowid:8!null uv.crdb_internal_mvcc_timestamp:9 uv.tableoid:10
 │    │    │    │    │    └── projections
 │    │    │    │    │         └── NULL::INT8 [as=i_default:11]
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=v:7]
 │    │    │    │         │    └── v:7
 │    │    │    │         └── first-agg [as=i_default:11]
 │    │    │    │              └── i_default:11
 │    │    │    ├── scan c1
 │    │    │    │    ├── columns: c:12!null c1.p:13!null i:14 c1.crdb_internal_mvcc_timestamp:15 c1.tableoid:16
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── u:6 = c:12
 │    │    └── projections
 │    │         └── c:12 + 1 [as=i_new:17]
 │    └── projections
 │         ├── CASE WHEN c:12 IS NULL THEN u:6 ELSE c:12 END [as=upsert_c:18]
 │         ├── CASE WHEN c:12 IS NULL THEN v:7 ELSE c1.p:13 END [as=upsert_p:19]
 │         └── CASE WHEN c:12 IS NULL THEN i_default:11 ELSE i_new:17 END [as=upsert_i:20]
 └── f-k-checks
      └── f-k-checks-item: c1(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:21
                ├── with-scan &1
                │    ├── columns: p:21
                │    └── mapping:
                │         └──  upsert_p:19 => p:21
                ├── scan p
                │    ├── columns: p.p:22!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:21 = p.p:22

exec-ddl
CREATE TABLE c2 (c INT PRIMARY KEY, FOREIGN KEY (c) REFERENCES p(p))
----

build
INSERT INTO c2 VALUES (1), (2) ON CONFLICT (c) DO UPDATE SET c = 1
----
upsert c2
 ├── arbiter indexes: c2_pkey
 ├── columns: <none>
 ├── canary column: c2.c:5
 ├── fetch columns: c2.c:5
 ├── insert-mapping:
 │    └── column1:4 => c2.c:1
 ├── update-mapping:
 │    └── upsert_c:9 => c2.c:1
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:9!null column1:4!null c2.c:5 c2.crdb_internal_mvcc_timestamp:6 c2.tableoid:7 c_new:8!null
 │    ├── project
 │    │    ├── columns: c_new:8!null column1:4!null c2.c:5 c2.crdb_internal_mvcc_timestamp:6 c2.tableoid:7
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:4!null c2.c:5 c2.crdb_internal_mvcc_timestamp:6 c2.tableoid:7
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:4!null
 │    │    │    │    ├── grouping columns: column1:4!null
 │    │    │    │    └── values
 │    │    │    │         ├── columns: column1:4!null
 │    │    │    │         ├── (1,)
 │    │    │    │         └── (2,)
 │    │    │    ├── scan c2
 │    │    │    │    ├── columns: c2.c:5!null c2.crdb_internal_mvcc_timestamp:6 c2.tableoid:7
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:4 = c2.c:5
 │    │    └── projections
 │    │         └── 1 [as=c_new:8]
 │    └── projections
 │         └── CASE WHEN c2.c:5 IS NULL THEN column1:4 ELSE c_new:8 END [as=upsert_c:9]
 └── f-k-checks
      └── f-k-checks-item: c2(c) -> p(p)
           └── anti-join (hash)
                ├── columns: c:10!null
                ├── with-scan &1
                │    ├── columns: c:10!null
                │    └── mapping:
                │         └──  upsert_c:9 => c:10
                ├── scan p
                │    ├── columns: p:11!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── c:10 = p:11

exec-ddl
CREATE TABLE c3 (c INT PRIMARY KEY, p INT REFERENCES p(p));
----

# Because the input column can be NULL (in which case it requires no FK match),
# we have to add an extra filter.
build
UPSERT INTO c3 VALUES (100, 1), (200, NULL)
----
upsert c3
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── column1:5 => c:1
 │    └── column2:6 => c3.p:2
 ├── input binding: &1
 ├── values
 │    ├── columns: column1:5!null column2:6
 │    ├── (100, 1)
 │    └── (200, NULL::INT8)
 └── f-k-checks
      └── f-k-checks-item: c3(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:7!null
                ├── select
                │    ├── columns: p:7!null
                │    ├── with-scan &1
                │    │    ├── columns: p:7
                │    │    └── mapping:
                │    │         └──  column2:6 => p:7
                │    └── filters
                │         └── p:7 IS NOT NULL
                ├── scan p
                │    ├── columns: p.p:8!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:7 = p.p:8

build
UPSERT INTO c3(c) VALUES (100), (200)
----
upsert c3
 ├── arbiter indexes: c3_pkey
 ├── columns: <none>
 ├── canary column: c:7
 ├── fetch columns: c:7 c3.p:8
 ├── insert-mapping:
 │    ├── column1:5 => c:1
 │    └── p_default:6 => c3.p:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:11 upsert_p:12 column1:5!null p_default:6 c:7 c3.p:8 c3.crdb_internal_mvcc_timestamp:9 c3.tableoid:10
 │    ├── left-join (hash)
 │    │    ├── columns: column1:5!null p_default:6 c:7 c3.p:8 c3.crdb_internal_mvcc_timestamp:9 c3.tableoid:10
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: column1:5!null p_default:6
 │    │    │    ├── grouping columns: column1:5!null
 │    │    │    ├── project
 │    │    │    │    ├── columns: p_default:6 column1:5!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:5!null
 │    │    │    │    │    ├── (100,)
 │    │    │    │    │    └── (200,)
 │    │    │    │    └── projections
 │    │    │    │         └── NULL::INT8 [as=p_default:6]
 │    │    │    └── aggregations
 │    │    │         └── first-agg [as=p_default:6]
 │    │    │              └── p_default:6
 │    │    ├── scan c3
 │    │    │    ├── columns: c:7!null c3.p:8 c3.crdb_internal_mvcc_timestamp:9 c3.tableoid:10
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── column1:5 = c:7
 │    └── projections
 │         ├── CASE WHEN c:7 IS NULL THEN column1:5 ELSE c:7 END [as=upsert_c:11]
 │         └── CASE WHEN c:7 IS NULL THEN p_default:6 ELSE c3.p:8 END [as=upsert_p:12]
 └── f-k-checks
      └── f-k-checks-item: c3(p) -> p(p)
           └── anti-join (hash)
                ├── columns: p:13!null
                ├── select
                │    ├── columns: p:13!null
                │    ├── with-scan &1
                │    │    ├── columns: p:13
                │    │    └── mapping:
                │    │         └──  upsert_p:12 => p:13
                │    └── filters
                │         └── p:13 IS NOT NULL
                ├── scan p
                │    ├── columns: p.p:14!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:13 = p.p:14

exec-ddl
CREATE TABLE c4 (c INT PRIMARY KEY, a INT REFERENCES p(p), other INT, UNIQUE(a))
----

build
INSERT INTO c4 SELECT x, y, z FROM xyzw ON CONFLICT (a) DO UPDATE SET other = 1
----
upsert c4
 ├── arbiter indexes: c4_a_key
 ├── columns: <none>
 ├── canary column: c:13
 ├── fetch columns: c:13 c4.a:14 c4.other:15
 ├── insert-mapping:
 │    ├── x:6 => c:1
 │    ├── y:7 => c4.a:2
 │    └── z:8 => c4.other:3
 ├── update-mapping:
 │    └── upsert_other:21 => c4.other:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:19 upsert_a:20 upsert_other:21 x:6 y:7 z:8 c:13 c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17 other_new:18!null
 │    ├── project
 │    │    ├── columns: other_new:18!null x:6 y:7 z:8 c:13 c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: x:6 y:7 z:8 c:13 c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: x:6 y:7 z:8
 │    │    │    │    ├── grouping columns: y:7
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: x:6 y:7 z:8
 │    │    │    │    │    └── scan xyzw
 │    │    │    │    │         └── columns: x:6 y:7 z:8 w:9 rowid:10!null xyzw.crdb_internal_mvcc_timestamp:11 xyzw.tableoid:12
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=x:6]
 │    │    │    │         │    └── x:6
 │    │    │    │         └── first-agg [as=z:8]
 │    │    │    │              └── z:8
 │    │    │    ├── scan c4
 │    │    │    │    ├── columns: c:13!null c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── y:7 = c4.a:14
 │    │    └── projections
 │    │         └── 1 [as=other_new:18]
 │    └── projections
 │         ├── CASE WHEN c:13 IS NULL THEN x:6 ELSE c:13 END [as=upsert_c:19]
 │         ├── CASE WHEN c:13 IS NULL THEN y:7 ELSE c4.a:14 END [as=upsert_a:20]
 │         └── CASE WHEN c:13 IS NULL THEN z:8 ELSE other_new:18 END [as=upsert_other:21]
 └── f-k-checks
      └── f-k-checks-item: c4(a) -> p(p)
           └── anti-join (hash)
                ├── columns: a:22!null
                ├── select
                │    ├── columns: a:22!null
                │    ├── with-scan &1
                │    │    ├── columns: a:22
                │    │    └── mapping:
                │    │         └──  upsert_a:20 => a:22
                │    └── filters
                │         └── a:22 IS NOT NULL
                ├── scan p
                │    ├── columns: p:23!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── a:22 = p:23

build
INSERT INTO c4 SELECT x, y, z FROM xyzw ON CONFLICT (a) DO UPDATE SET a = 5
----
upsert c4
 ├── arbiter indexes: c4_a_key
 ├── columns: <none>
 ├── canary column: c:13
 ├── fetch columns: c:13 c4.a:14 c4.other:15
 ├── insert-mapping:
 │    ├── x:6 => c:1
 │    ├── y:7 => c4.a:2
 │    └── z:8 => c4.other:3
 ├── update-mapping:
 │    └── upsert_a:20 => c4.a:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:19 upsert_a:20 upsert_other:21 x:6 y:7 z:8 c:13 c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17 a_new:18!null
 │    ├── project
 │    │    ├── columns: a_new:18!null x:6 y:7 z:8 c:13 c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: x:6 y:7 z:8 c:13 c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: x:6 y:7 z:8
 │    │    │    │    ├── grouping columns: y:7
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: x:6 y:7 z:8
 │    │    │    │    │    └── scan xyzw
 │    │    │    │    │         └── columns: x:6 y:7 z:8 w:9 rowid:10!null xyzw.crdb_internal_mvcc_timestamp:11 xyzw.tableoid:12
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=x:6]
 │    │    │    │         │    └── x:6
 │    │    │    │         └── first-agg [as=z:8]
 │    │    │    │              └── z:8
 │    │    │    ├── scan c4
 │    │    │    │    ├── columns: c:13!null c4.a:14 c4.other:15 c4.crdb_internal_mvcc_timestamp:16 c4.tableoid:17
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── y:7 = c4.a:14
 │    │    └── projections
 │    │         └── 5 [as=a_new:18]
 │    └── projections
 │         ├── CASE WHEN c:13 IS NULL THEN x:6 ELSE c:13 END [as=upsert_c:19]
 │         ├── CASE WHEN c:13 IS NULL THEN y:7 ELSE a_new:18 END [as=upsert_a:20]
 │         └── CASE WHEN c:13 IS NULL THEN z:8 ELSE c4.other:15 END [as=upsert_other:21]
 └── f-k-checks
      └── f-k-checks-item: c4(a) -> p(p)
           └── anti-join (hash)
                ├── columns: a:22!null
                ├── select
                │    ├── columns: a:22!null
                │    ├── with-scan &1
                │    │    ├── columns: a:22
                │    │    └── mapping:
                │    │         └──  upsert_a:20 => a:22
                │    └── filters
                │         └── a:22 IS NOT NULL
                ├── scan p
                │    ├── columns: p:23!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── a:22 = p:23


# ------------------------------------------
# Outbound FK tests with multiple FK columns
# ------------------------------------------

exec-ddl
CREATE TABLE pq (
  k INT PRIMARY KEY,
  p INT,
  q INT,
  other INT,
  UNIQUE(p,q),
  FAMILY (k), FAMILY (p), FAMILY (q), FAMILY (other)
)
----

exec-ddl
CREATE TABLE cpq (
  c INT PRIMARY KEY,
  p INT DEFAULT 4,
  q INT DEFAULT 8,
  other INT,
  FAMILY (c), FAMILY (p), FAMILY (q), FAMILY (other),
  CONSTRAINT fk FOREIGN KEY (p,q) REFERENCES pq(p,q) MATCH SIMPLE
)
----

build
UPSERT INTO cpq VALUES (1, 1, 1, 1)
----
upsert cpq
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── column1:7 => c:1
 │    ├── column2:8 => cpq.p:2
 │    ├── column3:9 => cpq.q:3
 │    └── column4:10 => cpq.other:4
 ├── input binding: &1
 ├── values
 │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    └── (1, 1, 1, 1)
 └── f-k-checks
      └── f-k-checks-item: cpq(p,q) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: p:11!null q:12!null
                ├── with-scan &1
                │    ├── columns: p:11!null q:12!null
                │    └── mapping:
                │         ├──  column2:8 => p:11
                │         └──  column3:9 => q:12
                ├── scan pq
                │    ├── columns: pq.p:14 pq.q:15
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:11 = pq.p:14
                     └── q:12 = pq.q:15

# In this case, the input columns can be null.
build
UPSERT INTO cpq SELECT x,y,z,w FROM xyzw
----
upsert cpq
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── x:7 => c:1
 │    ├── y:8 => cpq.p:2
 │    ├── z:9 => cpq.q:3
 │    └── w:10 => cpq.other:4
 ├── input binding: &1
 ├── project
 │    ├── columns: x:7 y:8 z:9 w:10
 │    └── scan xyzw
 │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 └── f-k-checks
      └── f-k-checks-item: cpq(p,q) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: p:14!null q:15!null
                ├── select
                │    ├── columns: p:14!null q:15!null
                │    ├── with-scan &1
                │    │    ├── columns: p:14 q:15
                │    │    └── mapping:
                │    │         ├──  y:8 => p:14
                │    │         └──  z:9 => q:15
                │    └── filters
                │         ├── p:14 IS NOT NULL
                │         └── q:15 IS NOT NULL
                ├── scan pq
                │    ├── columns: pq.p:17 pq.q:18
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:14 = pq.p:17
                     └── q:15 = pq.q:18

build
UPSERT INTO cpq(c,p) SELECT x,y FROM xyzw
----
upsert cpq
 ├── arbiter indexes: cpq_pkey
 ├── columns: <none>
 ├── canary column: c:16
 ├── fetch columns: c:16 cpq.p:17 cpq.q:18 cpq.other:19
 ├── insert-mapping:
 │    ├── x:7 => c:1
 │    ├── y:8 => cpq.p:2
 │    ├── q_default:14 => cpq.q:3
 │    └── other_default:15 => cpq.other:4
 ├── update-mapping:
 │    └── y:8 => cpq.p:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:22 upsert_q:23 upsert_other:24 x:7 y:8 q_default:14!null other_default:15 c:16 cpq.p:17 cpq.q:18 cpq.other:19 cpq.crdb_internal_mvcc_timestamp:20 cpq.tableoid:21
 │    ├── left-join (hash)
 │    │    ├── columns: x:7 y:8 q_default:14!null other_default:15 c:16 cpq.p:17 cpq.q:18 cpq.other:19 cpq.crdb_internal_mvcc_timestamp:20 cpq.tableoid:21
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: x:7 y:8 q_default:14!null other_default:15
 │    │    │    ├── grouping columns: x:7
 │    │    │    ├── project
 │    │    │    │    ├── columns: q_default:14!null other_default:15 x:7 y:8
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: x:7 y:8
 │    │    │    │    │    └── scan xyzw
 │    │    │    │    │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 │    │    │    │    └── projections
 │    │    │    │         ├── 8 [as=q_default:14]
 │    │    │    │         └── NULL::INT8 [as=other_default:15]
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=y:8]
 │    │    │         │    └── y:8
 │    │    │         ├── first-agg [as=q_default:14]
 │    │    │         │    └── q_default:14
 │    │    │         └── first-agg [as=other_default:15]
 │    │    │              └── other_default:15
 │    │    ├── scan cpq
 │    │    │    ├── columns: c:16!null cpq.p:17 cpq.q:18 cpq.other:19 cpq.crdb_internal_mvcc_timestamp:20 cpq.tableoid:21
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── x:7 = c:16
 │    └── projections
 │         ├── CASE WHEN c:16 IS NULL THEN x:7 ELSE c:16 END [as=upsert_c:22]
 │         ├── CASE WHEN c:16 IS NULL THEN q_default:14 ELSE cpq.q:18 END [as=upsert_q:23]
 │         └── CASE WHEN c:16 IS NULL THEN other_default:15 ELSE cpq.other:19 END [as=upsert_other:24]
 └── f-k-checks
      └── f-k-checks-item: cpq(p,q) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: p:25!null q:26!null
                ├── select
                │    ├── columns: p:25!null q:26!null
                │    ├── with-scan &1
                │    │    ├── columns: p:25 q:26
                │    │    └── mapping:
                │    │         ├──  y:8 => p:25
                │    │         └──  upsert_q:23 => q:26
                │    └── filters
                │         ├── p:25 IS NOT NULL
                │         └── q:26 IS NOT NULL
                ├── scan pq
                │    ├── columns: pq.p:28 pq.q:29
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:25 = pq.p:28
                     └── q:26 = pq.q:29

build
UPSERT INTO cpq(c) SELECT x FROM xyzw
----
upsert cpq
 ├── arbiter indexes: cpq_pkey
 ├── columns: <none>
 ├── canary column: c:17
 ├── fetch columns: c:17 cpq.p:18 cpq.q:19 cpq.other:20
 ├── insert-mapping:
 │    ├── x:7 => c:1
 │    ├── p_default:14 => cpq.p:2
 │    ├── q_default:15 => cpq.q:3
 │    └── other_default:16 => cpq.other:4
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:23 upsert_p:24 upsert_q:25 upsert_other:26 x:7 p_default:14!null q_default:15!null other_default:16 c:17 cpq.p:18 cpq.q:19 cpq.other:20 cpq.crdb_internal_mvcc_timestamp:21 cpq.tableoid:22
 │    ├── left-join (hash)
 │    │    ├── columns: x:7 p_default:14!null q_default:15!null other_default:16 c:17 cpq.p:18 cpq.q:19 cpq.other:20 cpq.crdb_internal_mvcc_timestamp:21 cpq.tableoid:22
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: x:7 p_default:14!null q_default:15!null other_default:16
 │    │    │    ├── grouping columns: x:7
 │    │    │    ├── project
 │    │    │    │    ├── columns: p_default:14!null q_default:15!null other_default:16 x:7
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: x:7
 │    │    │    │    │    └── scan xyzw
 │    │    │    │    │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 │    │    │    │    └── projections
 │    │    │    │         ├── 4 [as=p_default:14]
 │    │    │    │         ├── 8 [as=q_default:15]
 │    │    │    │         └── NULL::INT8 [as=other_default:16]
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=p_default:14]
 │    │    │         │    └── p_default:14
 │    │    │         ├── first-agg [as=q_default:15]
 │    │    │         │    └── q_default:15
 │    │    │         └── first-agg [as=other_default:16]
 │    │    │              └── other_default:16
 │    │    ├── scan cpq
 │    │    │    ├── columns: c:17!null cpq.p:18 cpq.q:19 cpq.other:20 cpq.crdb_internal_mvcc_timestamp:21 cpq.tableoid:22
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── x:7 = c:17
 │    └── projections
 │         ├── CASE WHEN c:17 IS NULL THEN x:7 ELSE c:17 END [as=upsert_c:23]
 │         ├── CASE WHEN c:17 IS NULL THEN p_default:14 ELSE cpq.p:18 END [as=upsert_p:24]
 │         ├── CASE WHEN c:17 IS NULL THEN q_default:15 ELSE cpq.q:19 END [as=upsert_q:25]
 │         └── CASE WHEN c:17 IS NULL THEN other_default:16 ELSE cpq.other:20 END [as=upsert_other:26]
 └── f-k-checks
      └── f-k-checks-item: cpq(p,q) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: p:27!null q:28!null
                ├── select
                │    ├── columns: p:27!null q:28!null
                │    ├── with-scan &1
                │    │    ├── columns: p:27 q:28
                │    │    └── mapping:
                │    │         ├──  upsert_p:24 => p:27
                │    │         └──  upsert_q:25 => q:28
                │    └── filters
                │         ├── p:27 IS NOT NULL
                │         └── q:28 IS NOT NULL
                ├── scan pq
                │    ├── columns: pq.p:30 pq.q:31
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:27 = pq.p:30
                     └── q:28 = pq.q:31

# This has different semantics from the UPSERT INTO cpq(c) version - here we
# upsert default values for all unspecified columns.
build
UPSERT INTO cpq SELECT x FROM xyzw
----
upsert cpq
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── x:7 => c:1
 │    ├── p_default:14 => cpq.p:2
 │    ├── q_default:15 => cpq.q:3
 │    └── other_default:16 => cpq.other:4
 ├── input binding: &1
 ├── project
 │    ├── columns: p_default:14!null q_default:15!null other_default:16 x:7
 │    ├── project
 │    │    ├── columns: x:7
 │    │    └── scan xyzw
 │    │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 │    └── projections
 │         ├── 4 [as=p_default:14]
 │         ├── 8 [as=q_default:15]
 │         └── NULL::INT8 [as=other_default:16]
 └── f-k-checks
      └── f-k-checks-item: cpq(p,q) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: p:17!null q:18!null
                ├── with-scan &1
                │    ├── columns: p:17!null q:18!null
                │    └── mapping:
                │         ├──  p_default:14 => p:17
                │         └──  q_default:15 => q:18
                ├── scan pq
                │    ├── columns: pq.p:20 pq.q:21
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:17 = pq.p:20
                     └── q:18 = pq.q:21

build
INSERT INTO cpq VALUES (1), (2) ON CONFLICT (c) DO UPDATE SET p = 10
----
upsert cpq
 ├── arbiter indexes: cpq_pkey
 ├── columns: <none>
 ├── canary column: c:11
 ├── fetch columns: c:11 cpq.p:12 cpq.q:13 cpq.other:14
 ├── insert-mapping:
 │    ├── column1:7 => c:1
 │    ├── p_default:8 => cpq.p:2
 │    ├── q_default:9 => cpq.q:3
 │    └── other_default:10 => cpq.other:4
 ├── update-mapping:
 │    └── upsert_p:19 => cpq.p:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:18 upsert_p:19!null upsert_q:20 upsert_other:21 column1:7!null p_default:8!null q_default:9!null other_default:10 c:11 cpq.p:12 cpq.q:13 cpq.other:14 cpq.crdb_internal_mvcc_timestamp:15 cpq.tableoid:16 p_new:17!null
 │    ├── project
 │    │    ├── columns: p_new:17!null column1:7!null p_default:8!null q_default:9!null other_default:10 c:11 cpq.p:12 cpq.q:13 cpq.other:14 cpq.crdb_internal_mvcc_timestamp:15 cpq.tableoid:16
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:7!null p_default:8!null q_default:9!null other_default:10 c:11 cpq.p:12 cpq.q:13 cpq.other:14 cpq.crdb_internal_mvcc_timestamp:15 cpq.tableoid:16
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:7!null p_default:8!null q_default:9!null other_default:10
 │    │    │    │    ├── grouping columns: column1:7!null
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: p_default:8!null q_default:9!null other_default:10 column1:7!null
 │    │    │    │    │    ├── values
 │    │    │    │    │    │    ├── columns: column1:7!null
 │    │    │    │    │    │    ├── (1,)
 │    │    │    │    │    │    └── (2,)
 │    │    │    │    │    └── projections
 │    │    │    │    │         ├── 4 [as=p_default:8]
 │    │    │    │    │         ├── 8 [as=q_default:9]
 │    │    │    │    │         └── NULL::INT8 [as=other_default:10]
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=p_default:8]
 │    │    │    │         │    └── p_default:8
 │    │    │    │         ├── first-agg [as=q_default:9]
 │    │    │    │         │    └── q_default:9
 │    │    │    │         └── first-agg [as=other_default:10]
 │    │    │    │              └── other_default:10
 │    │    │    ├── scan cpq
 │    │    │    │    ├── columns: c:11!null cpq.p:12 cpq.q:13 cpq.other:14 cpq.crdb_internal_mvcc_timestamp:15 cpq.tableoid:16
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:7 = c:11
 │    │    └── projections
 │    │         └── 10 [as=p_new:17]
 │    └── projections
 │         ├── CASE WHEN c:11 IS NULL THEN column1:7 ELSE c:11 END [as=upsert_c:18]
 │         ├── CASE WHEN c:11 IS NULL THEN p_default:8 ELSE p_new:17 END [as=upsert_p:19]
 │         ├── CASE WHEN c:11 IS NULL THEN q_default:9 ELSE cpq.q:13 END [as=upsert_q:20]
 │         └── CASE WHEN c:11 IS NULL THEN other_default:10 ELSE cpq.other:14 END [as=upsert_other:21]
 └── f-k-checks
      └── f-k-checks-item: cpq(p,q) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: p:22!null q:23!null
                ├── select
                │    ├── columns: p:22!null q:23!null
                │    ├── with-scan &1
                │    │    ├── columns: p:22!null q:23
                │    │    └── mapping:
                │    │         ├──  upsert_p:19 => p:22
                │    │         └──  upsert_q:20 => q:23
                │    └── filters
                │         └── q:23 IS NOT NULL
                ├── scan pq
                │    ├── columns: pq.p:25 pq.q:26
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:22 = pq.p:25
                     └── q:23 = pq.q:26

# ------------------------------------------
# Multiple outbound FKs
# ------------------------------------------

exec-ddl
CREATE TABLE cmulti (
  a INT,
  b INT,
  c INT DEFAULT 4,
  d INT DEFAULT 8,
  PRIMARY KEY (a,b),
  FOREIGN KEY (a) REFERENCES p(p),
  FOREIGN KEY (b,c) REFERENCES pq(p,q) MATCH FULL
)
----

build
UPSERT INTO cmulti SELECT x,y,z,w FROM xyzw
----
upsert cmulti
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── x:7 => cmulti.a:1
 │    ├── y:8 => cmulti.b:2
 │    ├── z:9 => cmulti.c:3
 │    └── w:10 => d:4
 ├── input binding: &1
 ├── project
 │    ├── columns: x:7 y:8 z:9 w:10
 │    └── scan xyzw
 │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 └── f-k-checks
      ├── f-k-checks-item: cmulti(a) -> p(p)
      │    └── anti-join (hash)
      │         ├── columns: a:14
      │         ├── with-scan &1
      │         │    ├── columns: a:14
      │         │    └── mapping:
      │         │         └──  x:7 => a:14
      │         ├── scan p
      │         │    ├── columns: p.p:15!null
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              └── a:14 = p.p:15
      └── f-k-checks-item: cmulti(b,c) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: b:19 c:20
                ├── with-scan &1
                │    ├── columns: b:19 c:20
                │    └── mapping:
                │         ├──  y:8 => b:19
                │         └──  z:9 => c:20
                ├── scan pq
                │    ├── columns: pq.p:22 q:23
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── b:19 = pq.p:22
                     └── c:20 = q:23

build
UPSERT INTO cmulti(a,b,c) SELECT x,y,z FROM xyzw
----
upsert cmulti
 ├── arbiter indexes: cmulti_pkey
 ├── columns: <none>
 ├── canary column: cmulti.a:15
 ├── fetch columns: cmulti.a:15 cmulti.b:16 cmulti.c:17 d:18
 ├── insert-mapping:
 │    ├── x:7 => cmulti.a:1
 │    ├── y:8 => cmulti.b:2
 │    ├── z:9 => cmulti.c:3
 │    └── d_default:14 => d:4
 ├── update-mapping:
 │    └── z:9 => cmulti.c:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_a:21 upsert_b:22 upsert_d:23 x:7 y:8 z:9 d_default:14!null cmulti.a:15 cmulti.b:16 cmulti.c:17 d:18 cmulti.crdb_internal_mvcc_timestamp:19 cmulti.tableoid:20
 │    ├── left-join (hash)
 │    │    ├── columns: x:7 y:8 z:9 d_default:14!null cmulti.a:15 cmulti.b:16 cmulti.c:17 d:18 cmulti.crdb_internal_mvcc_timestamp:19 cmulti.tableoid:20
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: x:7 y:8 z:9 d_default:14!null
 │    │    │    ├── grouping columns: x:7 y:8
 │    │    │    ├── project
 │    │    │    │    ├── columns: d_default:14!null x:7 y:8 z:9
 │    │    │    │    ├── project
 │    │    │    │    │    ├── columns: x:7 y:8 z:9
 │    │    │    │    │    └── scan xyzw
 │    │    │    │    │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 │    │    │    │    └── projections
 │    │    │    │         └── 8 [as=d_default:14]
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=z:9]
 │    │    │         │    └── z:9
 │    │    │         └── first-agg [as=d_default:14]
 │    │    │              └── d_default:14
 │    │    ├── scan cmulti
 │    │    │    ├── columns: cmulti.a:15!null cmulti.b:16!null cmulti.c:17 d:18 cmulti.crdb_internal_mvcc_timestamp:19 cmulti.tableoid:20
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         ├── x:7 = cmulti.a:15
 │    │         └── y:8 = cmulti.b:16
 │    └── projections
 │         ├── CASE WHEN cmulti.a:15 IS NULL THEN x:7 ELSE cmulti.a:15 END [as=upsert_a:21]
 │         ├── CASE WHEN cmulti.a:15 IS NULL THEN y:8 ELSE cmulti.b:16 END [as=upsert_b:22]
 │         └── CASE WHEN cmulti.a:15 IS NULL THEN d_default:14 ELSE d:18 END [as=upsert_d:23]
 └── f-k-checks
      ├── f-k-checks-item: cmulti(a) -> p(p)
      │    └── anti-join (hash)
      │         ├── columns: a:24
      │         ├── with-scan &1
      │         │    ├── columns: a:24
      │         │    └── mapping:
      │         │         └──  upsert_a:21 => a:24
      │         ├── scan p
      │         │    ├── columns: p.p:25!null
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              └── a:24 = p.p:25
      └── f-k-checks-item: cmulti(b,c) -> pq(p,q)
           └── anti-join (hash)
                ├── columns: b:29 c:30
                ├── with-scan &1
                │    ├── columns: b:29 c:30
                │    └── mapping:
                │         ├──  upsert_b:22 => b:29
                │         └──  z:9 => c:30
                ├── scan pq
                │    ├── columns: pq.p:32 q:33
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── b:29 = pq.p:32
                     └── c:30 = q:33

# ---------------------------------------
# Inbound FK tests with single FK column
# ---------------------------------------

# No need to check inbound FKs since PK values never get removed by an upsert.
build
UPSERT INTO p VALUES (1, 1), (2, 2)
----
upsert p
 ├── columns: <none>
 ├── upsert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => other:2
 └── values
      ├── columns: column1:5!null column2:6!null
      ├── (1, 1)
      └── (2, 2)

exec-ddl
CREATE TABLE p1 (p INT PRIMARY KEY, other INT, INDEX(other))
----

exec-ddl
CREATE TABLE p1c (c INT PRIMARY KEY, p INT NOT NULL DEFAULT 5 REFERENCES p1(p))
----

# No need to check inbound FKs since PK values never get removed by an upsert.
build
UPSERT INTO p1 VALUES (1, 1), (2, 2)
----
upsert p1
 ├── arbiter indexes: p1_pkey
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 other:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => other:2
 ├── update-mapping:
 │    └── column2:6 => other:2
 └── project
      ├── columns: upsert_p:11 column1:5!null column2:6!null p:7 other:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      ├── left-join (hash)
      │    ├── columns: column1:5!null column2:6!null p:7 other:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: column1:5!null column2:6!null
      │    │    ├── grouping columns: column1:5!null
      │    │    ├── values
      │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    ├── (1, 1)
      │    │    │    └── (2, 2)
      │    │    └── aggregations
      │    │         └── first-agg [as=column2:6]
      │    │              └── column2:6
      │    ├── scan p1
      │    │    ├── columns: p:7!null other:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    └── filters
      │         └── column1:5 = p:7
      └── projections
           └── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p:7 END [as=upsert_p:11]

# This statement can modify existing values of p so we need to perform the FK
# check.
build
INSERT INTO p1 VALUES (100, 1), (200, 1) ON CONFLICT (p) DO UPDATE SET p = excluded.p + 1
----
upsert p1
 ├── arbiter indexes: p1_pkey
 ├── columns: <none>
 ├── canary column: p1.p:7
 ├── fetch columns: p1.p:7 other:8
 ├── insert-mapping:
 │    ├── column1:5 => p1.p:1
 │    └── column2:6 => other:2
 ├── update-mapping:
 │    └── upsert_p:12 => p1.p:1
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_p:12!null upsert_other:13 column1:5!null column2:6!null p1.p:7 other:8 p1.crdb_internal_mvcc_timestamp:9 p1.tableoid:10 p_new:11!null
 │    ├── project
 │    │    ├── columns: p_new:11!null column1:5!null column2:6!null p1.p:7 other:8 p1.crdb_internal_mvcc_timestamp:9 p1.tableoid:10
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:5!null column2:6!null p1.p:7 other:8 p1.crdb_internal_mvcc_timestamp:9 p1.tableoid:10
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    ├── grouping columns: column1:5!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    │    ├── (100, 1)
 │    │    │    │    │    └── (200, 1)
 │    │    │    │    └── aggregations
 │    │    │    │         └── first-agg [as=column2:6]
 │    │    │    │              └── column2:6
 │    │    │    ├── scan p1
 │    │    │    │    ├── columns: p1.p:7!null other:8 p1.crdb_internal_mvcc_timestamp:9 p1.tableoid:10
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:5 = p1.p:7
 │    │    └── projections
 │    │         └── column1:5 + 1 [as=p_new:11]
 │    └── projections
 │         ├── CASE WHEN p1.p:7 IS NULL THEN column1:5 ELSE p_new:11 END [as=upsert_p:12]
 │         └── CASE WHEN p1.p:7 IS NULL THEN column2:6 ELSE other:8 END [as=upsert_other:13]
 └── f-k-checks
      └── f-k-checks-item: p1c(p) -> p1(p)
           └── semi-join (hash)
                ├── columns: p:14
                ├── except
                │    ├── columns: p:14
                │    ├── left columns: p:14
                │    ├── right columns: p:15
                │    ├── with-scan &1
                │    │    ├── columns: p:14
                │    │    └── mapping:
                │    │         └──  p1.p:7 => p:14
                │    └── with-scan &1
                │         ├── columns: p:15!null
                │         └── mapping:
                │              └──  upsert_p:12 => p:15
                ├── scan p1c
                │    ├── columns: p1c.p:17!null
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── p:14 = p1c.p:17

# No need to check the inbound FK: we never modify existing values of p.
build
INSERT INTO p1 VALUES (100, 1), (200, 1) ON CONFLICT (p) DO UPDATE SET other = p1.other + 1
----
upsert p1
 ├── arbiter indexes: p1_pkey
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 other:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => other:2
 ├── update-mapping:
 │    └── upsert_other:13 => other:2
 └── project
      ├── columns: upsert_p:12 upsert_other:13 column1:5!null column2:6!null p:7 other:8 crdb_internal_mvcc_timestamp:9 tableoid:10 other_new:11
      ├── project
      │    ├── columns: other_new:11 column1:5!null column2:6!null p:7 other:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    ├── left-join (hash)
      │    │    ├── columns: column1:5!null column2:6!null p:7 other:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    ├── ensure-upsert-distinct-on
      │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    ├── grouping columns: column1:5!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    │    ├── (100, 1)
      │    │    │    │    └── (200, 1)
      │    │    │    └── aggregations
      │    │    │         └── first-agg [as=column2:6]
      │    │    │              └── column2:6
      │    │    ├── scan p1
      │    │    │    ├── columns: p:7!null other:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    │    └── filters
      │    │         └── column1:5 = p:7
      │    └── projections
      │         └── other:8 + 1 [as=other_new:11]
      └── projections
           ├── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p:7 END [as=upsert_p:12]
           └── CASE WHEN p:7 IS NULL THEN column2:6 ELSE other_new:11 END [as=upsert_other:13]

# Similar tests when the FK column is not the PK.
exec-ddl
CREATE TABLE p2 (p INT PRIMARY KEY, fk INT UNIQUE)
----

exec-ddl
CREATE TABLE p2c (c INT PRIMARY KEY, fk INT REFERENCES p2(fk))
----

build
UPSERT INTO p2 VALUES (1, 1), (2, 2)
----
upsert p2
 ├── arbiter indexes: p2_pkey
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 p2.fk:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => p2.fk:2
 ├── update-mapping:
 │    └── column2:6 => p2.fk:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_p:11 column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    ├── left-join (hash)
 │    │    ├── columns: column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    ├── grouping columns: column1:5!null
 │    │    │    ├── values
 │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    ├── (1, 1)
 │    │    │    │    └── (2, 2)
 │    │    │    └── aggregations
 │    │    │         └── first-agg [as=column2:6]
 │    │    │              └── column2:6
 │    │    ├── scan p2
 │    │    │    ├── columns: p:7!null p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── column1:5 = p:7
 │    └── projections
 │         └── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p:7 END [as=upsert_p:11]
 └── f-k-checks
      └── f-k-checks-item: p2c(fk) -> p2(fk)
           └── semi-join (hash)
                ├── columns: fk:12
                ├── except
                │    ├── columns: fk:12
                │    ├── left columns: fk:12
                │    ├── right columns: fk:13
                │    ├── with-scan &1
                │    │    ├── columns: fk:12
                │    │    └── mapping:
                │    │         └──  p2.fk:8 => fk:12
                │    └── with-scan &1
                │         ├── columns: fk:13!null
                │         └── mapping:
                │              └──  column2:6 => fk:13
                ├── scan p2c
                │    ├── columns: p2c.fk:15
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── fk:12 = p2c.fk:15

# This statement never removes existing values of the fk column; FK check is
# not needed.
build
INSERT INTO p2 VALUES (1, 1), (2, 2) ON CONFLICT (p) DO UPDATE SET p = excluded.p + 1
----
upsert p2
 ├── arbiter indexes: p2_pkey
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 fk:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => fk:2
 ├── update-mapping:
 │    └── upsert_p:12 => p:1
 └── project
      ├── columns: upsert_p:12!null upsert_fk:13 column1:5!null column2:6!null p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10 p_new:11!null
      ├── project
      │    ├── columns: p_new:11!null column1:5!null column2:6!null p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    ├── left-join (hash)
      │    │    ├── columns: column1:5!null column2:6!null p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    ├── ensure-upsert-distinct-on
      │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    ├── grouping columns: column1:5!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    │    ├── (1, 1)
      │    │    │    │    └── (2, 2)
      │    │    │    └── aggregations
      │    │    │         └── first-agg [as=column2:6]
      │    │    │              └── column2:6
      │    │    ├── scan p2
      │    │    │    ├── columns: p:7!null fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    │    └── filters
      │    │         └── column1:5 = p:7
      │    └── projections
      │         └── column1:5 + 1 [as=p_new:11]
      └── projections
           ├── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p_new:11 END [as=upsert_p:12]
           └── CASE WHEN p:7 IS NULL THEN column2:6 ELSE fk:8 END [as=upsert_fk:13]

# This statement can change existing values of the fk column, so the FK check
# is needed.
build
INSERT INTO p2 VALUES (1, 1), (2, 2) ON CONFLICT (p) DO UPDATE SET fk = excluded.fk + 1
----
upsert p2
 ├── arbiter indexes: p2_pkey
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 p2.fk:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => p2.fk:2
 ├── update-mapping:
 │    └── upsert_fk:13 => p2.fk:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_p:12 upsert_fk:13!null column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10 fk_new:11!null
 │    ├── project
 │    │    ├── columns: fk_new:11!null column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    ├── grouping columns: column1:5!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    │    ├── (1, 1)
 │    │    │    │    │    └── (2, 2)
 │    │    │    │    └── aggregations
 │    │    │    │         └── first-agg [as=column2:6]
 │    │    │    │              └── column2:6
 │    │    │    ├── scan p2
 │    │    │    │    ├── columns: p:7!null p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:5 = p:7
 │    │    └── projections
 │    │         └── column2:6 + 1 [as=fk_new:11]
 │    └── projections
 │         ├── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p:7 END [as=upsert_p:12]
 │         └── CASE WHEN p:7 IS NULL THEN column2:6 ELSE fk_new:11 END [as=upsert_fk:13]
 └── f-k-checks
      └── f-k-checks-item: p2c(fk) -> p2(fk)
           └── semi-join (hash)
                ├── columns: fk:14
                ├── except
                │    ├── columns: fk:14
                │    ├── left columns: fk:14
                │    ├── right columns: fk:15
                │    ├── with-scan &1
                │    │    ├── columns: fk:14
                │    │    └── mapping:
                │    │         └──  p2.fk:8 => fk:14
                │    └── with-scan &1
                │         ├── columns: fk:15!null
                │         └── mapping:
                │              └──  upsert_fk:13 => fk:15
                ├── scan p2c
                │    ├── columns: p2c.fk:17
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── fk:14 = p2c.fk:17

# This statement never removes existing values of the fk column; the FK check is
# not needed.
build
INSERT INTO p2 VALUES (1, 1), (2, 2) ON CONFLICT (fk) DO UPDATE SET p = excluded.p + 1
----
upsert p2
 ├── arbiter indexes: p2_fk_key
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 fk:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => fk:2
 ├── update-mapping:
 │    └── upsert_p:12 => p:1
 └── project
      ├── columns: upsert_p:12!null upsert_fk:13 column1:5!null column2:6!null p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10 p_new:11!null
      ├── project
      │    ├── columns: p_new:11!null column1:5!null column2:6!null p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    ├── left-join (hash)
      │    │    ├── columns: column1:5!null column2:6!null p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    ├── ensure-upsert-distinct-on
      │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    ├── grouping columns: column2:6!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:5!null column2:6!null
      │    │    │    │    ├── (1, 1)
      │    │    │    │    └── (2, 2)
      │    │    │    └── aggregations
      │    │    │         └── first-agg [as=column1:5]
      │    │    │              └── column1:5
      │    │    ├── scan p2
      │    │    │    ├── columns: p:7!null fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    │    └── filters
      │    │         └── column2:6 = fk:8
      │    └── projections
      │         └── column1:5 + 1 [as=p_new:11]
      └── projections
           ├── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p_new:11 END [as=upsert_p:12]
           └── CASE WHEN p:7 IS NULL THEN column2:6 ELSE fk:8 END [as=upsert_fk:13]

build
INSERT INTO p2 VALUES (1, 1), (2, 2) ON CONFLICT (fk) DO UPDATE SET fk = excluded.fk + 1
----
upsert p2
 ├── arbiter indexes: p2_fk_key
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 p2.fk:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── column2:6 => p2.fk:2
 ├── update-mapping:
 │    └── upsert_fk:13 => p2.fk:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_p:12 upsert_fk:13!null column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10 fk_new:11!null
 │    ├── project
 │    │    ├── columns: fk_new:11!null column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:5!null column2:6!null p:7 p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    ├── grouping columns: column2:6!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    │    │    ├── (1, 1)
 │    │    │    │    │    └── (2, 2)
 │    │    │    │    └── aggregations
 │    │    │    │         └── first-agg [as=column1:5]
 │    │    │    │              └── column1:5
 │    │    │    ├── scan p2
 │    │    │    │    ├── columns: p:7!null p2.fk:8 p2.crdb_internal_mvcc_timestamp:9 p2.tableoid:10
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column2:6 = p2.fk:8
 │    │    └── projections
 │    │         └── column2:6 + 1 [as=fk_new:11]
 │    └── projections
 │         ├── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p:7 END [as=upsert_p:12]
 │         └── CASE WHEN p:7 IS NULL THEN column2:6 ELSE fk_new:11 END [as=upsert_fk:13]
 └── f-k-checks
      └── f-k-checks-item: p2c(fk) -> p2(fk)
           └── semi-join (hash)
                ├── columns: fk:14
                ├── except
                │    ├── columns: fk:14
                │    ├── left columns: fk:14
                │    ├── right columns: fk:15
                │    ├── with-scan &1
                │    │    ├── columns: fk:14
                │    │    └── mapping:
                │    │         └──  p2.fk:8 => fk:14
                │    └── with-scan &1
                │         ├── columns: fk:15!null
                │         └── mapping:
                │              └──  upsert_fk:13 => fk:15
                ├── scan p2c
                │    ├── columns: p2c.fk:17
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── fk:14 = p2c.fk:17

# This partial upsert never removes existing values of the fk column; the FK
# check is not needed.
build
UPSERT INTO p2(p) VALUES (1), (2)
----
upsert p2
 ├── arbiter indexes: p2_pkey
 ├── columns: <none>
 ├── canary column: p:7
 ├── fetch columns: p:7 fk:8
 ├── insert-mapping:
 │    ├── column1:5 => p:1
 │    └── fk_default:6 => fk:2
 └── project
      ├── columns: upsert_p:11 upsert_fk:12 column1:5!null fk_default:6 p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      ├── left-join (hash)
      │    ├── columns: column1:5!null fk_default:6 p:7 fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: column1:5!null fk_default:6
      │    │    ├── grouping columns: column1:5!null
      │    │    ├── project
      │    │    │    ├── columns: fk_default:6 column1:5!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:5!null
      │    │    │    │    ├── (1,)
      │    │    │    │    └── (2,)
      │    │    │    └── projections
      │    │    │         └── NULL::INT8 [as=fk_default:6]
      │    │    └── aggregations
      │    │         └── first-agg [as=fk_default:6]
      │    │              └── fk_default:6
      │    ├── scan p2
      │    │    ├── columns: p:7!null fk:8 crdb_internal_mvcc_timestamp:9 tableoid:10
      │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    └── filters
      │         └── column1:5 = p:7
      └── projections
           ├── CASE WHEN p:7 IS NULL THEN column1:5 ELSE p:7 END [as=upsert_p:11]
           └── CASE WHEN p:7 IS NULL THEN fk_default:6 ELSE fk:8 END [as=upsert_fk:12]

# ------------------------------------------
# Inbound FK tests with multiple FK columns
# ------------------------------------------

build
UPSERT INTO pq VALUES (1, 1, 1, 1), (2, 2, 2, 2)
----
upsert pq
 ├── arbiter indexes: pq_pkey
 ├── columns: <none>
 ├── canary column: k:11
 ├── fetch columns: k:11 pq.p:12 pq.q:13 pq.other:14
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── column2:8 => pq.p:2
 │    ├── column3:9 => pq.q:3
 │    └── column4:10 => pq.other:4
 ├── update-mapping:
 │    ├── column2:8 => pq.p:2
 │    ├── column3:9 => pq.q:3
 │    └── column4:10 => pq.other:4
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_k:17 column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    ├── left-join (hash)
 │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    │    │    ├── grouping columns: column1:7!null
 │    │    │    ├── values
 │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    │    │    │    ├── (1, 1, 1, 1)
 │    │    │    │    └── (2, 2, 2, 2)
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=column2:8]
 │    │    │         │    └── column2:8
 │    │    │         ├── first-agg [as=column3:9]
 │    │    │         │    └── column3:9
 │    │    │         └── first-agg [as=column4:10]
 │    │    │              └── column4:10
 │    │    ├── scan pq
 │    │    │    ├── columns: k:11!null pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── column1:7 = k:11
 │    └── projections
 │         └── CASE WHEN k:11 IS NULL THEN column1:7 ELSE k:11 END [as=upsert_k:17]
 └── f-k-checks
      ├── f-k-checks-item: cpq(p,q) -> pq(p,q)
      │    └── semi-join (hash)
      │         ├── columns: p:18 q:19
      │         ├── except
      │         │    ├── columns: p:18 q:19
      │         │    ├── left columns: p:18 q:19
      │         │    ├── right columns: p:20 q:21
      │         │    ├── with-scan &1
      │         │    │    ├── columns: p:18 q:19
      │         │    │    └── mapping:
      │         │    │         ├──  pq.p:12 => p:18
      │         │    │         └──  pq.q:13 => q:19
      │         │    └── with-scan &1
      │         │         ├── columns: p:20!null q:21!null
      │         │         └── mapping:
      │         │              ├──  column2:8 => p:20
      │         │              └──  column3:9 => q:21
      │         ├── scan cpq
      │         │    ├── columns: cpq.p:23 cpq.q:24
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              ├── p:18 = cpq.p:23
      │              └── q:19 = cpq.q:24
      └── f-k-checks-item: cmulti(b,c) -> pq(p,q)
           └── semi-join (hash)
                ├── columns: p:28 q:29
                ├── except
                │    ├── columns: p:28 q:29
                │    ├── left columns: p:28 q:29
                │    ├── right columns: p:30 q:31
                │    ├── with-scan &1
                │    │    ├── columns: p:28 q:29
                │    │    └── mapping:
                │    │         ├──  pq.p:12 => p:28
                │    │         └──  pq.q:13 => q:29
                │    └── with-scan &1
                │         ├── columns: p:30!null q:31!null
                │         └── mapping:
                │              ├──  column2:8 => p:30
                │              └──  column3:9 => q:31
                ├── scan cmulti
                │    ├── columns: b:33!null cmulti.c:34
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:28 = b:33
                     └── q:29 = cmulti.c:34

# Partial UPSERT doesn't remove (p,q) values; FK check not needed.
build
UPSERT INTO pq (k) VALUES (1), (2)
----
upsert pq
 ├── arbiter indexes: pq_pkey
 ├── columns: <none>
 ├── canary column: k:9
 ├── fetch columns: k:9 p:10 q:11 other:12
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── p_default:8 => p:2
 │    ├── p_default:8 => q:3
 │    └── p_default:8 => other:4
 └── project
      ├── columns: upsert_k:15 upsert_p:16 upsert_q:17 upsert_other:18 column1:7!null p_default:8 k:9 p:10 q:11 other:12 crdb_internal_mvcc_timestamp:13 tableoid:14
      ├── left-join (hash)
      │    ├── columns: column1:7!null p_default:8 k:9 p:10 q:11 other:12 crdb_internal_mvcc_timestamp:13 tableoid:14
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: column1:7!null p_default:8
      │    │    ├── grouping columns: column1:7!null
      │    │    ├── project
      │    │    │    ├── columns: p_default:8 column1:7!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:7!null
      │    │    │    │    ├── (1,)
      │    │    │    │    └── (2,)
      │    │    │    └── projections
      │    │    │         └── NULL::INT8 [as=p_default:8]
      │    │    └── aggregations
      │    │         └── first-agg [as=p_default:8]
      │    │              └── p_default:8
      │    ├── scan pq
      │    │    ├── columns: k:9!null p:10 q:11 other:12 crdb_internal_mvcc_timestamp:13 tableoid:14
      │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    └── filters
      │         └── column1:7 = k:9
      └── projections
           ├── CASE WHEN k:9 IS NULL THEN column1:7 ELSE k:9 END [as=upsert_k:15]
           ├── CASE WHEN k:9 IS NULL THEN p_default:8 ELSE p:10 END [as=upsert_p:16]
           ├── CASE WHEN k:9 IS NULL THEN p_default:8 ELSE q:11 END [as=upsert_q:17]
           └── CASE WHEN k:9 IS NULL THEN p_default:8 ELSE other:12 END [as=upsert_other:18]

build
UPSERT INTO pq (k,q) VALUES (1, 1), (2, 2)
----
upsert pq
 ├── arbiter indexes: pq_pkey
 ├── columns: <none>
 ├── canary column: k:10
 ├── fetch columns: k:10 pq.p:11 pq.q:12 pq.other:13
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── p_default:9 => pq.p:2
 │    ├── column2:8 => pq.q:3
 │    └── p_default:9 => pq.other:4
 ├── update-mapping:
 │    └── column2:8 => pq.q:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_k:16 upsert_p:17 upsert_other:18 column1:7!null column2:8!null p_default:9 k:10 pq.p:11 pq.q:12 pq.other:13 pq.crdb_internal_mvcc_timestamp:14 pq.tableoid:15
 │    ├── left-join (hash)
 │    │    ├── columns: column1:7!null column2:8!null p_default:9 k:10 pq.p:11 pq.q:12 pq.other:13 pq.crdb_internal_mvcc_timestamp:14 pq.tableoid:15
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: column1:7!null column2:8!null p_default:9
 │    │    │    ├── grouping columns: column1:7!null
 │    │    │    ├── project
 │    │    │    │    ├── columns: p_default:9 column1:7!null column2:8!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:7!null column2:8!null
 │    │    │    │    │    ├── (1, 1)
 │    │    │    │    │    └── (2, 2)
 │    │    │    │    └── projections
 │    │    │    │         └── NULL::INT8 [as=p_default:9]
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=column2:8]
 │    │    │         │    └── column2:8
 │    │    │         └── first-agg [as=p_default:9]
 │    │    │              └── p_default:9
 │    │    ├── scan pq
 │    │    │    ├── columns: k:10!null pq.p:11 pq.q:12 pq.other:13 pq.crdb_internal_mvcc_timestamp:14 pq.tableoid:15
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── column1:7 = k:10
 │    └── projections
 │         ├── CASE WHEN k:10 IS NULL THEN column1:7 ELSE k:10 END [as=upsert_k:16]
 │         ├── CASE WHEN k:10 IS NULL THEN p_default:9 ELSE pq.p:11 END [as=upsert_p:17]
 │         └── CASE WHEN k:10 IS NULL THEN p_default:9 ELSE pq.other:13 END [as=upsert_other:18]
 └── f-k-checks
      ├── f-k-checks-item: cpq(p,q) -> pq(p,q)
      │    └── semi-join (hash)
      │         ├── columns: p:19 q:20
      │         ├── except
      │         │    ├── columns: p:19 q:20
      │         │    ├── left columns: p:19 q:20
      │         │    ├── right columns: p:21 q:22
      │         │    ├── with-scan &1
      │         │    │    ├── columns: p:19 q:20
      │         │    │    └── mapping:
      │         │    │         ├──  pq.p:11 => p:19
      │         │    │         └──  pq.q:12 => q:20
      │         │    └── with-scan &1
      │         │         ├── columns: p:21 q:22!null
      │         │         └── mapping:
      │         │              ├──  upsert_p:17 => p:21
      │         │              └──  column2:8 => q:22
      │         ├── scan cpq
      │         │    ├── columns: cpq.p:24 cpq.q:25
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              ├── p:19 = cpq.p:24
      │              └── q:20 = cpq.q:25
      └── f-k-checks-item: cmulti(b,c) -> pq(p,q)
           └── semi-join (hash)
                ├── columns: p:29 q:30
                ├── except
                │    ├── columns: p:29 q:30
                │    ├── left columns: p:29 q:30
                │    ├── right columns: p:31 q:32
                │    ├── with-scan &1
                │    │    ├── columns: p:29 q:30
                │    │    └── mapping:
                │    │         ├──  pq.p:11 => p:29
                │    │         └──  pq.q:12 => q:30
                │    └── with-scan &1
                │         ├── columns: p:31 q:32!null
                │         └── mapping:
                │              ├──  upsert_p:17 => p:31
                │              └──  column2:8 => q:32
                ├── scan cmulti
                │    ├── columns: b:34!null cmulti.c:35
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:29 = b:34
                     └── q:30 = cmulti.c:35

# Statement doesn't remove (p,q) values; FK check not needed.
build
INSERT INTO pq VALUES (1, 1, 1, 1), (2, 2, 2, 2) ON CONFLICT (p,q) DO UPDATE SET k = pq.k + 1
----
upsert pq
 ├── arbiter indexes: pq_p_q_key
 ├── columns: <none>
 ├── canary column: k:11
 ├── fetch columns: k:11 p:12 q:13 other:14
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── column2:8 => p:2
 │    ├── column3:9 => q:3
 │    └── column4:10 => other:4
 ├── update-mapping:
 │    └── upsert_k:18 => k:1
 └── project
      ├── columns: upsert_k:18 upsert_p:19 upsert_q:20 upsert_other:21 column1:7!null column2:8!null column3:9!null column4:10!null k:11 p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16 k_new:17
      ├── project
      │    ├── columns: k_new:17 column1:7!null column2:8!null column3:9!null column4:10!null k:11 p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16
      │    ├── left-join (hash)
      │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null k:11 p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16
      │    │    ├── ensure-upsert-distinct-on
      │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
      │    │    │    ├── grouping columns: column2:8!null column3:9!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
      │    │    │    │    ├── (1, 1, 1, 1)
      │    │    │    │    └── (2, 2, 2, 2)
      │    │    │    └── aggregations
      │    │    │         ├── first-agg [as=column1:7]
      │    │    │         │    └── column1:7
      │    │    │         └── first-agg [as=column4:10]
      │    │    │              └── column4:10
      │    │    ├── scan pq
      │    │    │    ├── columns: k:11!null p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16
      │    │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    │    └── filters
      │    │         ├── column2:8 = p:12
      │    │         └── column3:9 = q:13
      │    └── projections
      │         └── k:11 + 1 [as=k_new:17]
      └── projections
           ├── CASE WHEN k:11 IS NULL THEN column1:7 ELSE k_new:17 END [as=upsert_k:18]
           ├── CASE WHEN k:11 IS NULL THEN column2:8 ELSE p:12 END [as=upsert_p:19]
           ├── CASE WHEN k:11 IS NULL THEN column3:9 ELSE q:13 END [as=upsert_q:20]
           └── CASE WHEN k:11 IS NULL THEN column4:10 ELSE other:14 END [as=upsert_other:21]

build
INSERT INTO pq VALUES (1, 1, 1, 1), (2, 2, 2, 2) ON CONFLICT (p,q) DO UPDATE SET p = pq.p + 1
----
upsert pq
 ├── arbiter indexes: pq_p_q_key
 ├── columns: <none>
 ├── canary column: k:11
 ├── fetch columns: k:11 pq.p:12 pq.q:13 pq.other:14
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── column2:8 => pq.p:2
 │    ├── column3:9 => pq.q:3
 │    └── column4:10 => pq.other:4
 ├── update-mapping:
 │    └── upsert_p:19 => pq.p:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_k:18 upsert_p:19 upsert_q:20 upsert_other:21 column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16 p_new:17
 │    ├── project
 │    │    ├── columns: p_new:17 column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    │    │    │    ├── grouping columns: column2:8!null column3:9!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    │    │    │    │    ├── (1, 1, 1, 1)
 │    │    │    │    │    └── (2, 2, 2, 2)
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=column1:7]
 │    │    │    │         │    └── column1:7
 │    │    │    │         └── first-agg [as=column4:10]
 │    │    │    │              └── column4:10
 │    │    │    ├── scan pq
 │    │    │    │    ├── columns: k:11!null pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         ├── column2:8 = pq.p:12
 │    │    │         └── column3:9 = pq.q:13
 │    │    └── projections
 │    │         └── pq.p:12 + 1 [as=p_new:17]
 │    └── projections
 │         ├── CASE WHEN k:11 IS NULL THEN column1:7 ELSE k:11 END [as=upsert_k:18]
 │         ├── CASE WHEN k:11 IS NULL THEN column2:8 ELSE p_new:17 END [as=upsert_p:19]
 │         ├── CASE WHEN k:11 IS NULL THEN column3:9 ELSE pq.q:13 END [as=upsert_q:20]
 │         └── CASE WHEN k:11 IS NULL THEN column4:10 ELSE pq.other:14 END [as=upsert_other:21]
 └── f-k-checks
      ├── f-k-checks-item: cpq(p,q) -> pq(p,q)
      │    └── semi-join (hash)
      │         ├── columns: p:22 q:23
      │         ├── except
      │         │    ├── columns: p:22 q:23
      │         │    ├── left columns: p:22 q:23
      │         │    ├── right columns: p:24 q:25
      │         │    ├── with-scan &1
      │         │    │    ├── columns: p:22 q:23
      │         │    │    └── mapping:
      │         │    │         ├──  pq.p:12 => p:22
      │         │    │         └──  pq.q:13 => q:23
      │         │    └── with-scan &1
      │         │         ├── columns: p:24 q:25
      │         │         └── mapping:
      │         │              ├──  upsert_p:19 => p:24
      │         │              └──  upsert_q:20 => q:25
      │         ├── scan cpq
      │         │    ├── columns: cpq.p:27 cpq.q:28
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              ├── p:22 = cpq.p:27
      │              └── q:23 = cpq.q:28
      └── f-k-checks-item: cmulti(b,c) -> pq(p,q)
           └── semi-join (hash)
                ├── columns: p:32 q:33
                ├── except
                │    ├── columns: p:32 q:33
                │    ├── left columns: p:32 q:33
                │    ├── right columns: p:34 q:35
                │    ├── with-scan &1
                │    │    ├── columns: p:32 q:33
                │    │    └── mapping:
                │    │         ├──  pq.p:12 => p:32
                │    │         └──  pq.q:13 => q:33
                │    └── with-scan &1
                │         ├── columns: p:34 q:35
                │         └── mapping:
                │              ├──  upsert_p:19 => p:34
                │              └──  upsert_q:20 => q:35
                ├── scan cmulti
                │    ├── columns: b:37!null cmulti.c:38
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:32 = b:37
                     └── q:33 = cmulti.c:38

# Statement never removes (p,q) values; FK check not needed.
build
INSERT INTO pq VALUES (1, 1, 1, 1), (2, 2, 2, 2) ON CONFLICT (k) DO UPDATE SET other = 5
----
upsert pq
 ├── arbiter indexes: pq_pkey
 ├── columns: <none>
 ├── canary column: k:11
 ├── fetch columns: k:11 p:12 q:13 other:14
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── column2:8 => p:2
 │    ├── column3:9 => q:3
 │    └── column4:10 => other:4
 ├── update-mapping:
 │    └── upsert_other:21 => other:4
 └── project
      ├── columns: upsert_k:18 upsert_p:19 upsert_q:20 upsert_other:21!null column1:7!null column2:8!null column3:9!null column4:10!null k:11 p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16 other_new:17!null
      ├── project
      │    ├── columns: other_new:17!null column1:7!null column2:8!null column3:9!null column4:10!null k:11 p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16
      │    ├── left-join (hash)
      │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null k:11 p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16
      │    │    ├── ensure-upsert-distinct-on
      │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
      │    │    │    ├── grouping columns: column1:7!null
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
      │    │    │    │    ├── (1, 1, 1, 1)
      │    │    │    │    └── (2, 2, 2, 2)
      │    │    │    └── aggregations
      │    │    │         ├── first-agg [as=column2:8]
      │    │    │         │    └── column2:8
      │    │    │         ├── first-agg [as=column3:9]
      │    │    │         │    └── column3:9
      │    │    │         └── first-agg [as=column4:10]
      │    │    │              └── column4:10
      │    │    ├── scan pq
      │    │    │    ├── columns: k:11!null p:12 q:13 other:14 crdb_internal_mvcc_timestamp:15 tableoid:16
      │    │    │    └── flags: avoid-full-scan disabled not visible index feature
      │    │    └── filters
      │    │         └── column1:7 = k:11
      │    └── projections
      │         └── 5 [as=other_new:17]
      └── projections
           ├── CASE WHEN k:11 IS NULL THEN column1:7 ELSE k:11 END [as=upsert_k:18]
           ├── CASE WHEN k:11 IS NULL THEN column2:8 ELSE p:12 END [as=upsert_p:19]
           ├── CASE WHEN k:11 IS NULL THEN column3:9 ELSE q:13 END [as=upsert_q:20]
           └── CASE WHEN k:11 IS NULL THEN column4:10 ELSE other_new:17 END [as=upsert_other:21]

build
INSERT INTO pq VALUES (1, 1, 1, 1), (2, 2, 2, 2) ON CONFLICT (k) DO UPDATE SET q = 5
----
upsert pq
 ├── arbiter indexes: pq_pkey
 ├── columns: <none>
 ├── canary column: k:11
 ├── fetch columns: k:11 pq.p:12 pq.q:13 pq.other:14
 ├── insert-mapping:
 │    ├── column1:7 => k:1
 │    ├── column2:8 => pq.p:2
 │    ├── column3:9 => pq.q:3
 │    └── column4:10 => pq.other:4
 ├── update-mapping:
 │    └── upsert_q:20 => pq.q:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_k:18 upsert_p:19 upsert_q:20!null upsert_other:21 column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16 q_new:17!null
 │    ├── project
 │    │    ├── columns: q_new:17!null column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null k:11 pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    │    │    │    ├── grouping columns: column1:7!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null
 │    │    │    │    │    ├── (1, 1, 1, 1)
 │    │    │    │    │    └── (2, 2, 2, 2)
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=column2:8]
 │    │    │    │         │    └── column2:8
 │    │    │    │         ├── first-agg [as=column3:9]
 │    │    │    │         │    └── column3:9
 │    │    │    │         └── first-agg [as=column4:10]
 │    │    │    │              └── column4:10
 │    │    │    ├── scan pq
 │    │    │    │    ├── columns: k:11!null pq.p:12 pq.q:13 pq.other:14 pq.crdb_internal_mvcc_timestamp:15 pq.tableoid:16
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:7 = k:11
 │    │    └── projections
 │    │         └── 5 [as=q_new:17]
 │    └── projections
 │         ├── CASE WHEN k:11 IS NULL THEN column1:7 ELSE k:11 END [as=upsert_k:18]
 │         ├── CASE WHEN k:11 IS NULL THEN column2:8 ELSE pq.p:12 END [as=upsert_p:19]
 │         ├── CASE WHEN k:11 IS NULL THEN column3:9 ELSE q_new:17 END [as=upsert_q:20]
 │         └── CASE WHEN k:11 IS NULL THEN column4:10 ELSE pq.other:14 END [as=upsert_other:21]
 └── f-k-checks
      ├── f-k-checks-item: cpq(p,q) -> pq(p,q)
      │    └── semi-join (hash)
      │         ├── columns: p:22 q:23
      │         ├── except
      │         │    ├── columns: p:22 q:23
      │         │    ├── left columns: p:22 q:23
      │         │    ├── right columns: p:24 q:25
      │         │    ├── with-scan &1
      │         │    │    ├── columns: p:22 q:23
      │         │    │    └── mapping:
      │         │    │         ├──  pq.p:12 => p:22
      │         │    │         └──  pq.q:13 => q:23
      │         │    └── with-scan &1
      │         │         ├── columns: p:24 q:25!null
      │         │         └── mapping:
      │         │              ├──  upsert_p:19 => p:24
      │         │              └──  upsert_q:20 => q:25
      │         ├── scan cpq
      │         │    ├── columns: cpq.p:27 cpq.q:28
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              ├── p:22 = cpq.p:27
      │              └── q:23 = cpq.q:28
      └── f-k-checks-item: cmulti(b,c) -> pq(p,q)
           └── semi-join (hash)
                ├── columns: p:32 q:33
                ├── except
                │    ├── columns: p:32 q:33
                │    ├── left columns: p:32 q:33
                │    ├── right columns: p:34 q:35
                │    ├── with-scan &1
                │    │    ├── columns: p:32 q:33
                │    │    └── mapping:
                │    │         ├──  pq.p:12 => p:32
                │    │         └──  pq.q:13 => q:33
                │    └── with-scan &1
                │         ├── columns: p:34 q:35!null
                │         └── mapping:
                │              ├──  upsert_p:19 => p:34
                │              └──  upsert_q:20 => q:35
                ├── scan cmulti
                │    ├── columns: b:37!null cmulti.c:38
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     ├── p:32 = b:37
                     └── q:33 = cmulti.c:38

# -------------------------------------
# Inbound + outbound combination tests
# -------------------------------------

exec-ddl
CREATE TABLE tab1 (
  a INT PRIMARY KEY,
  b INT UNIQUE
)
----

exec-ddl
CREATE TABLE tab2 (
  c INT PRIMARY KEY,
  d INT REFERENCES tab1(b),
  e INT UNIQUE
)
----

exec-ddl
CREATE TABLE tab3 (
  f INT PRIMARY KEY,
  g INT REFERENCES tab2(e)
)
----

build
UPSERT INTO tab2 VALUES (1,NULL,NULL), (2,2,2)
----
upsert tab2
 ├── arbiter indexes: tab2_pkey
 ├── columns: <none>
 ├── canary column: c:9
 ├── fetch columns: c:9 tab2.d:10 tab2.e:11
 ├── insert-mapping:
 │    ├── column1:6 => c:1
 │    ├── column2:7 => tab2.d:2
 │    └── column3:8 => tab2.e:3
 ├── update-mapping:
 │    ├── column2:7 => tab2.d:2
 │    └── column3:8 => tab2.e:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:14 column1:6!null column2:7 column3:8 c:9 tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    ├── left-join (hash)
 │    │    ├── columns: column1:6!null column2:7 column3:8 c:9 tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: column1:6!null column2:7 column3:8
 │    │    │    ├── grouping columns: column1:6!null
 │    │    │    ├── values
 │    │    │    │    ├── columns: column1:6!null column2:7 column3:8
 │    │    │    │    ├── (1, NULL::INT8, NULL::INT8)
 │    │    │    │    └── (2, 2, 2)
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=column2:7]
 │    │    │         │    └── column2:7
 │    │    │         └── first-agg [as=column3:8]
 │    │    │              └── column3:8
 │    │    ├── scan tab2
 │    │    │    ├── columns: c:9!null tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         └── column1:6 = c:9
 │    └── projections
 │         └── CASE WHEN c:9 IS NULL THEN column1:6 ELSE c:9 END [as=upsert_c:14]
 └── f-k-checks
      ├── f-k-checks-item: tab2(d) -> tab1(b)
      │    └── anti-join (hash)
      │         ├── columns: d:15!null
      │         ├── select
      │         │    ├── columns: d:15!null
      │         │    ├── with-scan &1
      │         │    │    ├── columns: d:15
      │         │    │    └── mapping:
      │         │    │         └──  column2:7 => d:15
      │         │    └── filters
      │         │         └── d:15 IS NOT NULL
      │         ├── scan tab1
      │         │    ├── columns: b:17
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              └── d:15 = b:17
      └── f-k-checks-item: tab3(g) -> tab2(e)
           └── semi-join (hash)
                ├── columns: e:20
                ├── except
                │    ├── columns: e:20
                │    ├── left columns: e:20
                │    ├── right columns: e:21
                │    ├── with-scan &1
                │    │    ├── columns: e:20
                │    │    └── mapping:
                │    │         └──  tab2.e:11 => e:20
                │    └── with-scan &1
                │         ├── columns: e:21
                │         └── mapping:
                │              └──  column3:8 => e:21
                ├── scan tab3
                │    ├── columns: g:23
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── e:20 = g:23

build
INSERT INTO tab2 VALUES (1,1,1) ON CONFLICT (c) DO UPDATE SET e = tab2.e + 1
----
upsert tab2
 ├── arbiter indexes: tab2_pkey
 ├── columns: <none>
 ├── canary column: c:9
 ├── fetch columns: c:9 tab2.d:10 tab2.e:11
 ├── insert-mapping:
 │    ├── column1:6 => c:1
 │    ├── column2:7 => tab2.d:2
 │    └── column3:8 => tab2.e:3
 ├── update-mapping:
 │    └── upsert_e:17 => tab2.e:3
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:15 upsert_d:16 upsert_e:17 column1:6!null column2:7!null column3:8!null c:9 tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13 e_new:14
 │    ├── project
 │    │    ├── columns: e_new:14 column1:6!null column2:7!null column3:8!null c:9 tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null c:9 tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.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, 1, 1)
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=column2:7]
 │    │    │    │         │    └── column2:7
 │    │    │    │         └── first-agg [as=column3:8]
 │    │    │    │              └── column3:8
 │    │    │    ├── scan tab2
 │    │    │    │    ├── columns: c:9!null tab2.d:10 tab2.e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column1:6 = c:9
 │    │    └── projections
 │    │         └── tab2.e:11 + 1 [as=e_new:14]
 │    └── projections
 │         ├── CASE WHEN c:9 IS NULL THEN column1:6 ELSE c:9 END [as=upsert_c:15]
 │         ├── CASE WHEN c:9 IS NULL THEN column2:7 ELSE tab2.d:10 END [as=upsert_d:16]
 │         └── CASE WHEN c:9 IS NULL THEN column3:8 ELSE e_new:14 END [as=upsert_e:17]
 └── f-k-checks
      ├── f-k-checks-item: tab2(d) -> tab1(b)
      │    └── anti-join (hash)
      │         ├── columns: d:18!null
      │         ├── select
      │         │    ├── columns: d:18!null
      │         │    ├── with-scan &1
      │         │    │    ├── columns: d:18
      │         │    │    └── mapping:
      │         │    │         └──  upsert_d:16 => d:18
      │         │    └── filters
      │         │         └── d:18 IS NOT NULL
      │         ├── scan tab1
      │         │    ├── columns: b:20
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              └── d:18 = b:20
      └── f-k-checks-item: tab3(g) -> tab2(e)
           └── semi-join (hash)
                ├── columns: e:23
                ├── except
                │    ├── columns: e:23
                │    ├── left columns: e:23
                │    ├── right columns: e:24
                │    ├── with-scan &1
                │    │    ├── columns: e:23
                │    │    └── mapping:
                │    │         └──  tab2.e:11 => e:23
                │    └── with-scan &1
                │         ├── columns: e:24
                │         └── mapping:
                │              └──  upsert_e:17 => e:24
                ├── scan tab3
                │    ├── columns: g:26
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── e:23 = g:26

# Statement never removes values from e column; the inbound check is not necessary.
build
INSERT INTO tab2 VALUES (1,1,1) ON CONFLICT (e) DO UPDATE SET d = tab2.d + 1
----
upsert tab2
 ├── arbiter indexes: tab2_e_key
 ├── columns: <none>
 ├── canary column: c:9
 ├── fetch columns: c:9 tab2.d:10 e:11
 ├── insert-mapping:
 │    ├── column1:6 => c:1
 │    ├── column2:7 => tab2.d:2
 │    └── column3:8 => e:3
 ├── update-mapping:
 │    └── upsert_d:16 => tab2.d:2
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_c:15 upsert_d:16 upsert_e:17 column1:6!null column2:7!null column3:8!null c:9 tab2.d:10 e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13 d_new:14
 │    ├── project
 │    │    ├── columns: d_new:14 column1:6!null column2:7!null column3:8!null c:9 tab2.d:10 e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    ├── left-join (hash)
 │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null c:9 tab2.d:10 e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    │    ├── ensure-upsert-distinct-on
 │    │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null
 │    │    │    │    ├── grouping columns: column3:8!null
 │    │    │    │    ├── values
 │    │    │    │    │    ├── columns: column1:6!null column2:7!null column3:8!null
 │    │    │    │    │    └── (1, 1, 1)
 │    │    │    │    └── aggregations
 │    │    │    │         ├── first-agg [as=column1:6]
 │    │    │    │         │    └── column1:6
 │    │    │    │         └── first-agg [as=column2:7]
 │    │    │    │              └── column2:7
 │    │    │    ├── scan tab2
 │    │    │    │    ├── columns: c:9!null tab2.d:10 e:11 tab2.crdb_internal_mvcc_timestamp:12 tab2.tableoid:13
 │    │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    │    └── filters
 │    │    │         └── column3:8 = e:11
 │    │    └── projections
 │    │         └── tab2.d:10 + 1 [as=d_new:14]
 │    └── projections
 │         ├── CASE WHEN c:9 IS NULL THEN column1:6 ELSE c:9 END [as=upsert_c:15]
 │         ├── CASE WHEN c:9 IS NULL THEN column2:7 ELSE d_new:14 END [as=upsert_d:16]
 │         └── CASE WHEN c:9 IS NULL THEN column3:8 ELSE e:11 END [as=upsert_e:17]
 └── f-k-checks
      └── f-k-checks-item: tab2(d) -> tab1(b)
           └── anti-join (hash)
                ├── columns: d:18!null
                ├── select
                │    ├── columns: d:18!null
                │    ├── with-scan &1
                │    │    ├── columns: d:18
                │    │    └── mapping:
                │    │         └──  upsert_d:16 => d:18
                │    └── filters
                │         └── d:18 IS NOT NULL
                ├── scan tab1
                │    ├── columns: b:20
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── d:18 = b:20

exec-ddl
CREATE TABLE self (
 a INT,
 b INT,
 c INT,
 d INT,
 PRIMARY KEY (a,b),
 UNIQUE (b,d),
 UNIQUE (c),
 FOREIGN KEY (a,b) REFERENCES self(b,d),
 FOREIGN KEY (d) REFERENCES self(c)
)
----

build
UPSERT INTO self SELECT x, y, z, w FROM xyzw
----
upsert self
 ├── arbiter indexes: self_pkey
 ├── columns: <none>
 ├── canary column: self.a:14
 ├── fetch columns: self.a:14 self.b:15 self.c:16 self.d:17
 ├── insert-mapping:
 │    ├── x:7 => self.a:1
 │    ├── y:8 => self.b:2
 │    ├── z:9 => self.c:3
 │    └── w:10 => self.d:4
 ├── update-mapping:
 │    ├── z:9 => self.c:3
 │    └── w:10 => self.d:4
 ├── input binding: &1
 ├── project
 │    ├── columns: upsert_a:20 upsert_b:21 x:7 y:8 z:9 w:10 self.a:14 self.b:15 self.c:16 self.d:17 self.crdb_internal_mvcc_timestamp:18 self.tableoid:19
 │    ├── left-join (hash)
 │    │    ├── columns: x:7 y:8 z:9 w:10 self.a:14 self.b:15 self.c:16 self.d:17 self.crdb_internal_mvcc_timestamp:18 self.tableoid:19
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: x:7 y:8 z:9 w:10
 │    │    │    ├── grouping columns: x:7 y:8
 │    │    │    ├── project
 │    │    │    │    ├── columns: x:7 y:8 z:9 w:10
 │    │    │    │    └── scan xyzw
 │    │    │    │         └── columns: x:7 y:8 z:9 w:10 rowid:11!null xyzw.crdb_internal_mvcc_timestamp:12 xyzw.tableoid:13
 │    │    │    └── aggregations
 │    │    │         ├── first-agg [as=z:9]
 │    │    │         │    └── z:9
 │    │    │         └── first-agg [as=w:10]
 │    │    │              └── w:10
 │    │    ├── scan self
 │    │    │    ├── columns: self.a:14!null self.b:15!null self.c:16 self.d:17 self.crdb_internal_mvcc_timestamp:18 self.tableoid:19
 │    │    │    └── flags: avoid-full-scan disabled not visible index feature
 │    │    └── filters
 │    │         ├── x:7 = self.a:14
 │    │         └── y:8 = self.b:15
 │    └── projections
 │         ├── CASE WHEN self.a:14 IS NULL THEN x:7 ELSE self.a:14 END [as=upsert_a:20]
 │         └── CASE WHEN self.a:14 IS NULL THEN y:8 ELSE self.b:15 END [as=upsert_b:21]
 └── f-k-checks
      ├── f-k-checks-item: self(a,b) -> self(b,d)
      │    └── anti-join (hash)
      │         ├── columns: a:22 b:23
      │         ├── with-scan &1
      │         │    ├── columns: a:22 b:23
      │         │    └── mapping:
      │         │         ├──  upsert_a:20 => a:22
      │         │         └──  upsert_b:21 => b:23
      │         ├── scan self
      │         │    ├── columns: self.b:25!null self.d:27
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              ├── a:22 = self.b:25
      │              └── b:23 = self.d:27
      ├── f-k-checks-item: self(d) -> self(c)
      │    └── anti-join (hash)
      │         ├── columns: d:30!null
      │         ├── select
      │         │    ├── columns: d:30!null
      │         │    ├── with-scan &1
      │         │    │    ├── columns: d:30
      │         │    │    └── mapping:
      │         │    │         └──  w:10 => d:30
      │         │    └── filters
      │         │         └── d:30 IS NOT NULL
      │         ├── scan self
      │         │    ├── columns: self.c:33
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              └── d:30 = self.c:33
      ├── f-k-checks-item: self(a,b) -> self(b,d)
      │    └── semi-join (hash)
      │         ├── columns: b:37 d:38
      │         ├── except
      │         │    ├── columns: b:37 d:38
      │         │    ├── left columns: b:37 d:38
      │         │    ├── right columns: b:39 d:40
      │         │    ├── with-scan &1
      │         │    │    ├── columns: b:37 d:38
      │         │    │    └── mapping:
      │         │    │         ├──  self.b:15 => b:37
      │         │    │         └──  self.d:17 => d:38
      │         │    └── with-scan &1
      │         │         ├── columns: b:39 d:40
      │         │         └── mapping:
      │         │              ├──  upsert_b:21 => b:39
      │         │              └──  w:10 => d:40
      │         ├── scan self
      │         │    ├── columns: self.a:41!null self.b:42!null
      │         │    └── flags: avoid-full-scan disabled not visible index feature
      │         └── filters
      │              ├── b:37 = self.a:41
      │              └── d:38 = self.b:42
      └── f-k-checks-item: self(d) -> self(c)
           └── semi-join (hash)
                ├── columns: c:47
                ├── except
                │    ├── columns: c:47
                │    ├── left columns: c:47
                │    ├── right columns: c:48
                │    ├── with-scan &1
                │    │    ├── columns: c:47
                │    │    └── mapping:
                │    │         └──  self.c:16 => c:47
                │    └── with-scan &1
                │         ├── columns: c:48
                │         └── mapping:
                │              └──  z:9 => c:48
                ├── scan self
                │    ├── columns: self.d:52
                │    └── flags: avoid-full-scan disabled not visible index feature
                └── filters
                     └── c:47 = self.d:52
