exec-ddl
CREATE TABLE abc
(
    a INT,
    b INT,
    c INT,
    INDEX ab (a,b) STORING (c),
    INDEX bc (b,c) STORING (a)
)
----

exec-ddl
CREATE TABLE stu
(
    s INT,
    t INT,
    u INT,
    PRIMARY KEY (s,t,u),
    INDEX uts (u,t,s)
)
----

exec-ddl
CREATE TABLE xyz
(
    x INT,
    y INT,
    z INT,
    INDEX xy (x,y) STORING (z),
    INDEX yz (y,z) STORING (x)
)
----

exec-ddl
CREATE TABLE pqr
(
    p INT PRIMARY KEY,
    q INT,
    r INT,
    s STRING,
    t STRING,
    INDEX q (q),
    INDEX r (r),
    INDEX s (s) STORING (r),
    INDEX rs (r,s),
    INDEX ts (t,s)
)
----

exec-ddl
CREATE TABLE zz (
    a INT8 PRIMARY KEY,
    b INT8 NULL,
    c INT8 NULL,
    INDEX idx_b (b ASC),
    CONSTRAINT idx_c UNIQUE (c)
)
----

exec-ddl
CREATE TABLE virt (
  k INT PRIMARY KEY,
  i INT,
  j INT NOT NULL,
  v1 INT AS (i + 10) VIRTUAL,
  v2 INT AS (i + 100) VIRTUAL,
  v3 INT AS (i + j) VIRTUAL,
  v4 INT NOT NULL AS (i + 1) VIRTUAL,
  l INT,
  CHECK (j IN (10, 20, 30)),
  CHECK (v4 IN (1, 2, 3))
)
----

exec-ddl
CREATE TABLE shard (
  k INT PRIMARY KEY,
  a INT,
  b INT,
  shard_a INT NOT NULL AS (mod(fnv32(crdb_internal.datums_to_bytes(a)), 3)) STORED,
  shard_a_b INT NOT NULL AS (mod(fnv32(crdb_internal.datums_to_bytes(a, b)), 3)) VIRTUAL,
  shard_b_null INT AS (mod(fnv32(crdb_internal.datums_to_bytes(b)), 3)) STORED,
  CHECK (shard_a IN (0, 1, 2)),
  CHECK (shard_a_b IN (0, 1, 2)),
  CHECK (shard_b_null IN (0, 1, 2))
)
----

exec-ddl
CREATE TABLE tchar (
    a INT,
    c VARCHAR(2),
    PRIMARY KEY (a, c)
)
----

exec-ddl
CREATE TABLE large (m INT, n INT)
----

exec-ddl
CREATE TABLE medium (m INT, n INT)
----

exec-ddl
CREATE TABLE small (m INT, n INT)
----

exec-ddl
ALTER TABLE large INJECT STATISTICS '[
  {
    "columns": ["m"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 100000000000,
    "distinct_count": 100000000
  }
]'
----

exec-ddl
ALTER TABLE medium INJECT STATISTICS '[
  {
    "columns": ["m"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 100000,
    "distinct_count": 1000
  }
]'
----

exec-ddl
ALTER TABLE small INJECT STATISTICS '[
  {
    "columns": ["m"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 10,
    "distinct_count": 6
  }
]'
----

exec-ddl
ALTER TABLE virt INJECT STATISTICS '[
  {
    "columns": ["k"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 100000000,
    "distinct_count": 100000000
  },
  {
    "columns": ["l"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 100000000,
    "distinct_count": 100000
  }
]'
----

exec-ddl
ALTER TABLE shard INJECT STATISTICS '[
  {
    "columns": ["k"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 100000000,
    "distinct_count": 100000000
  }
]'
----

# --------------------------------------------------
# ReorderJoins
# --------------------------------------------------

exec-ddl
ALTER TABLE abc INJECT STATISTICS '[
  {
    "columns": ["a"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 1000,
    "distinct_count": 1000
  }
]'
----

exec-ddl
ALTER TABLE stu INJECT STATISTICS '[
  {
    "columns": ["s"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 10000,
    "distinct_count": 1000
  }
]'
----

exec-ddl
ALTER TABLE xyz INJECT STATISTICS '[
  {
    "columns": ["x"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 1000,
    "distinct_count": 1000
  }
]'
----

# Check that the equality condition abc.a = xyz.x is synthesized.
opt expect=ReorderJoins
SELECT * FROM abc, stu, xyz WHERE abc.a=stu.s AND stu.s=xyz.x
----
inner-join (merge)
 ├── columns: a:1!null b:2 c:3 s:7!null t:8!null u:9!null x:12!null y:13 z:14
 ├── left ordering: +7
 ├── right ordering: +12
 ├── fd: (1)==(7,12), (7)==(1,12), (12)==(1,7)
 ├── scan stu
 │    ├── columns: s:7!null t:8!null u:9!null
 │    ├── key: (7-9)
 │    └── ordering: +7
 ├── inner-join (merge)
 │    ├── columns: a:1!null b:2 c:3 x:12!null y:13 z:14
 │    ├── left ordering: +1
 │    ├── right ordering: +12
 │    ├── fd: (1)==(12), (12)==(1)
 │    ├── ordering: +(1|12) [actual: +1]
 │    ├── scan abc@ab
 │    │    ├── columns: a:1 b:2 c:3
 │    │    └── ordering: +1
 │    ├── scan xyz@xy
 │    │    ├── columns: x:12 y:13 z:14
 │    │    └── ordering: +12
 │    └── filters (true)
 └── filters (true)

memo expect=ReorderJoins
SELECT * FROM abc, stu, xyz WHERE abc.a=stu.s AND stu.s=xyz.x
----
memo (optimized, ~47KB, required=[presentation: a:1,b:2,c:3,s:7,t:8,u:9,x:12,y:13,z:14])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4) (inner-join G5 G6 G7) (inner-join G6 G5 G7) (inner-join G8 G9 G7) (inner-join G9 G8 G7) (merge-join G2 G3 G10 inner-join,+1,+7) (merge-join G3 G2 G10 inner-join,+7,+1) (lookup-join G3 G10 abc@ab,keyCols=[7],outCols=(1-3,7-9,12-14)) (merge-join G5 G6 G10 inner-join,+7,+12) (merge-join G6 G5 G10 inner-join,+12,+7) (lookup-join G6 G10 stu,keyCols=[12],outCols=(1-3,7-9,12-14)) (merge-join G8 G9 G10 inner-join,+7,+12) (lookup-join G8 G10 xyz@xy,keyCols=[7],outCols=(1-3,7-9,12-14)) (merge-join G9 G8 G10 inner-join,+12,+7)
 │    └── [presentation: a:1,b:2,c:3,s:7,t:8,u:9,x:12,y:13,z:14]
 │         ├── best: (merge-join G5="[ordering: +7]" G6="[ordering: +(1|12)]" G10 inner-join,+7,+12)
 │         └── cost: 13057.10
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    ├── [ordering: +1]
 │    │    ├── best: (scan abc@ab,cols=(1-3))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G3: (inner-join G5 G9 G7) (inner-join G9 G5 G7) (merge-join G5 G9 G10 inner-join,+7,+12) (lookup-join G5 G10 xyz@xy,keyCols=[7],outCols=(7-9,12-14)) (merge-join G9 G5 G10 inner-join,+12,+7) (lookup-join G9 G10 stu,keyCols=[12],outCols=(7-9,12-14))
 │    ├── [ordering: +(7|12)]
 │    │    ├── best: (merge-join G5="[ordering: +7]" G9="[ordering: +12]" G10 inner-join,+7,+12)
 │    │    └── cost: 11928.36
 │    └── []
 │         ├── best: (merge-join G5="[ordering: +7]" G9="[ordering: +12]" G10 inner-join,+7,+12)
 │         └── cost: 11928.36
 ├── G4: (filters G11)
 ├── G5: (scan stu,cols=(7-9)) (scan stu@uts,cols=(7-9))
 │    ├── [ordering: +7]
 │    │    ├── best: (scan stu,cols=(7-9))
 │    │    └── cost: 10628.62
 │    └── []
 │         ├── best: (scan stu,cols=(7-9))
 │         └── cost: 10628.62
 ├── G6: (inner-join G2 G9 G12) (inner-join G9 G2 G12) (merge-join G2 G9 G10 inner-join,+1,+12) (lookup-join G2 G10 xyz@xy,keyCols=[1],outCols=(1-3,12-14)) (merge-join G9 G2 G10 inner-join,+12,+1) (lookup-join G9 G10 abc@ab,keyCols=[12],outCols=(1-3,12-14))
 │    ├── [ordering: +(1|12)]
 │    │    ├── best: (merge-join G2="[ordering: +1]" G9="[ordering: +12]" G10 inner-join,+1,+12)
 │    │    └── cost: 2227.46
 │    └── []
 │         ├── best: (merge-join G2="[ordering: +1]" G9="[ordering: +12]" G10 inner-join,+1,+12)
 │         └── cost: 2227.46
 ├── G7: (filters G13)
 ├── G8: (inner-join G2 G5 G4) (inner-join G5 G2 G4) (merge-join G2 G5 G10 inner-join,+1,+7) (lookup-join G2 G10 stu,keyCols=[1],outCols=(1-3,7-9)) (merge-join G5 G2 G10 inner-join,+7,+1) (lookup-join G5 G10 abc@ab,keyCols=[7],outCols=(1-3,7-9))
 │    ├── [ordering: +(1|7)]
 │    │    ├── best: (merge-join G5="[ordering: +7]" G2="[ordering: +1]" G10 inner-join,+7,+1)
 │    │    └── cost: 11928.36
 │    └── []
 │         ├── best: (merge-join G5="[ordering: +7]" G2="[ordering: +1]" G10 inner-join,+7,+1)
 │         └── cost: 11928.36
 ├── G9: (scan xyz,cols=(12-14)) (scan xyz@xy,cols=(12-14)) (scan xyz@yz,cols=(12-14))
 │    ├── [ordering: +12]
 │    │    ├── best: (scan xyz@xy,cols=(12-14))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan xyz,cols=(12-14))
 │         └── cost: 1098.72
 ├── G10: (filters)
 ├── G11: (eq G14 G15)
 ├── G12: (filters G16)
 ├── G13: (eq G15 G17)
 ├── G14: (variable a)
 ├── G15: (variable s)
 ├── G16: (eq G14 G17)
 └── G17: (variable x)

# Cross joins are not reordered apart from commutation.
memo
SELECT * FROM abc, stu, xyz, pqr WHERE a = 1
----
memo (optimized, ~32KB, required=[presentation: a:1,b:2,c:3,s:7,t:8,u:9,x:12,y:13,z:14,p:18,q:19,r:20,s:21,t:22])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4)
 │    └── [presentation: a:1,b:2,c:3,s:7,t:8,u:9,x:12,y:13,z:14,p:18,q:19,r:20,s:21,t:22]
 │         ├── best: (inner-join G3 G2 G4)
 │         └── cost: 325035606.20
 ├── G2: (select G5 G6) (scan abc@ab,cols=(1-3),constrained)
 │    └── []
 │         ├── best: (scan abc@ab,cols=(1-3),constrained)
 │         └── cost: 19.09
 ├── G3: (inner-join G7 G8 G4) (inner-join G8 G7 G4)
 │    └── []
 │         ├── best: (inner-join G8 G7 G4)
 │         └── cost: 100035577.07
 ├── G4: (filters)
 ├── G5: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G6: (filters G9)
 ├── G7: (scan stu,cols=(7-9)) (scan stu@uts,cols=(7-9))
 │    └── []
 │         ├── best: (scan stu,cols=(7-9))
 │         └── cost: 10628.62
 ├── G8: (inner-join G10 G11 G4) (inner-join G11 G10 G4)
 │    └── []
 │         ├── best: (inner-join G10 G11 G4)
 │         └── cost: 12257.91
 ├── G9: (eq G12 G13)
 ├── G10: (scan xyz,cols=(12-14)) (scan xyz@xy,cols=(12-14)) (scan xyz@yz,cols=(12-14))
 │    └── []
 │         ├── best: (scan xyz,cols=(12-14))
 │         └── cost: 1098.72
 ├── G11: (scan pqr,cols=(18-22))
 │    └── []
 │         ├── best: (scan pqr,cols=(18-22))
 │         └── cost: 1129.02
 ├── G12: (variable a)
 └── G13: (const 1)

# Joins that exceed reorder_joins_limit are not reordered apart from
# commutation.
memo set=reorder_joins_limit=1
SELECT *
FROM stu, abc, xyz, pqr
WHERE u = a AND a = x AND x = p
----
memo (optimized, ~41KB, required=[presentation: s:1,t:2,u:3,a:6,b:7,c:8,x:12,y:13,z:14,p:18,q:19,r:20,s:21,t:22])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4) (merge-join G2 G3 G5 inner-join,+3,+6) (merge-join G3 G2 G5 inner-join,+6,+3) (lookup-join G3 G5 stu@uts,keyCols=[6],outCols=(1-3,6-8,12-14,18-22))
 │    └── [presentation: s:1,t:2,u:3,a:6,b:7,c:8,x:12,y:13,z:14,p:18,q:19,r:20,s:21,t:22]
 │         ├── best: (merge-join G2="[ordering: +3]" G3="[ordering: +(6|12|18)]" G5 inner-join,+3,+6)
 │         └── cost: 14216.14
 ├── G2: (scan stu,cols=(1-3)) (scan stu@uts,cols=(1-3))
 │    ├── [ordering: +3]
 │    │    ├── best: (scan stu@uts,cols=(1-3))
 │    │    └── cost: 10628.62
 │    └── []
 │         ├── best: (scan stu,cols=(1-3))
 │         └── cost: 10628.62
 ├── G3: (inner-join G6 G7 G8) (inner-join G7 G6 G8) (merge-join G6 G7 G5 inner-join,+6,+12) (merge-join G7 G6 G5 inner-join,+12,+6) (lookup-join G7 G5 abc@ab,keyCols=[12],outCols=(6-8,12-14,18-22))
 │    ├── [ordering: +(6|12|18)]
 │    │    ├── best: (merge-join G6="[ordering: +6]" G7="[ordering: +(12|18)]" G5 inner-join,+6,+12)
 │    │    └── cost: 3386.50
 │    └── []
 │         ├── best: (merge-join G6="[ordering: +6]" G7="[ordering: +(12|18)]" G5 inner-join,+6,+12)
 │         └── cost: 3386.50
 ├── G4: (filters G9)
 ├── G5: (filters)
 ├── G6: (scan abc,cols=(6-8)) (scan abc@ab,cols=(6-8)) (scan abc@bc,cols=(6-8))
 │    ├── [ordering: +6]
 │    │    ├── best: (scan abc@ab,cols=(6-8))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan abc,cols=(6-8))
 │         └── cost: 1098.72
 ├── G7: (inner-join G10 G11 G12) (inner-join G11 G10 G12) (merge-join G10 G11 G5 inner-join,+12,+18) (lookup-join G10 G5 pqr,keyCols=[12],outCols=(12-14,18-22)) (merge-join G11 G10 G5 inner-join,+18,+12) (lookup-join G11 G5 xyz@xy,keyCols=[18],outCols=(12-14,18-22))
 │    ├── [ordering: +(12|18)]
 │    │    ├── best: (merge-join G10="[ordering: +12]" G11="[ordering: +18]" G5 inner-join,+12,+18)
 │    │    └── cost: 2257.76
 │    └── []
 │         ├── best: (merge-join G10="[ordering: +12]" G11="[ordering: +18]" G5 inner-join,+12,+18)
 │         └── cost: 2257.76
 ├── G8: (filters G13)
 ├── G9: (eq G14 G15)
 ├── G10: (scan xyz,cols=(12-14)) (scan xyz@xy,cols=(12-14)) (scan xyz@yz,cols=(12-14))
 │    ├── [ordering: +12]
 │    │    ├── best: (scan xyz@xy,cols=(12-14))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan xyz,cols=(12-14))
 │         └── cost: 1098.72
 ├── G11: (scan pqr,cols=(18-22))
 │    ├── [ordering: +18]
 │    │    ├── best: (scan pqr,cols=(18-22))
 │    │    └── cost: 1129.02
 │    └── []
 │         ├── best: (scan pqr,cols=(18-22))
 │         └── cost: 1129.02
 ├── G12: (filters G16)
 ├── G13: (eq G15 G17)
 ├── G14: (variable u)
 ├── G15: (variable a)
 ├── G16: (eq G17 G18)
 ├── G17: (variable x)
 └── G18: (variable p)

# Case where an implicit filter is used in the final plan (abc.a = pqr.p in the
# merge join).
opt expect=ReorderJoins format=hide-all
SELECT *
FROM abc,
     stu,
     xyz,
     (SELECT * FROM pqr WHERE p = 5)
WHERE a = stu.s
  AND q = y
  AND stu.s = p
----
inner-join (lookup stu)
 ├── inner-join (lookup xyz@yz)
 │    ├── inner-join (lookup abc@ab)
 │    │    ├── scan pqr
 │    │    │    └── constraint: /18: [/5 - /5]
 │    │    └── filters (true)
 │    └── filters (true)
 └── filters (true)

# The apply join at the top of the tree should not be reordered. However, the
# lower-level inner join can be reordered despite the fact that it has outer
# columns.
memo expect=ReorderJoins
SELECT *
FROM abc
INNER JOIN LATERAL (
  SELECT *
  FROM (SELECT * FROM (VALUES (a+1), (b*2)) f(v))
  INNER JOIN stu
  ON s = v
)
ON a = v
----
memo (optimized, ~17KB, required=[presentation: a:1,b:2,c:3,v:7,s:8,t:9,u:10])
 ├── G1: (inner-join-apply G2 G3 G4)
 │    └── [presentation: a:1,b:2,c:3,v:7,s:8,t:9,u:10]
 │         ├── best: (inner-join-apply G2 G3 G4)
 │         └── cost: 1197.24
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G3: (inner-join G5 G6 G7) (inner-join G6 G5 G7) (lookup-join G5 G8 stu,keyCols=[7],outCols=(7-10)) (merge-join G6 G5 G8 inner-join,+8,+7)
 │    └── []
 │         ├── best: (lookup-join G5 G8 stu,keyCols=[7],outCols=(7-10))
 │         └── cost: 85.45
 ├── G4: (filters G9)
 ├── G5: (values G10 id=v1)
 │    ├── [ordering: +7]
 │    │    ├── best: (sort G5)
 │    │    └── cost: 0.14
 │    └── []
 │         ├── best: (values G10 id=v1)
 │         └── cost: 0.03
 ├── G6: (scan stu,cols=(8-10)) (scan stu@uts,cols=(8-10))
 │    ├── [ordering: +8]
 │    │    ├── best: (scan stu,cols=(8-10))
 │    │    └── cost: 10628.62
 │    └── []
 │         ├── best: (scan stu,cols=(8-10))
 │         └── cost: 10628.62
 ├── G7: (filters G11)
 ├── G8: (filters)
 ├── G9: (eq G12 G13)
 ├── G10: (scalar-list G14 G15)
 ├── G11: (eq G16 G13)
 ├── G12: (variable a)
 ├── G13: (variable column1)
 ├── G14: (tuple G17)
 ├── G15: (tuple G18)
 ├── G16: (variable s)
 ├── G17: (scalar-list G19)
 ├── G18: (scalar-list G20)
 ├── G19: (plus G12 G21)
 ├── G20: (mult G22 G23)
 ├── G21: (const 1)
 ├── G22: (variable b)
 └── G23: (const 2)

# Only commutation is possible for a single join.
exploretrace rule=ReorderJoins format=hide-all
SELECT * FROM abc, stu WHERE a = s
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── scan abc
   ├── scan stu
   └── filters
        └── a = s

New expression 1 of 1:
  inner-join (hash)
   ├── scan stu
   ├── scan abc
   └── filters
        └── a = s
----
----

# No-op case because the joins have join flags.
opt expect-not=ReorderJoins format=hide-all
SELECT *
FROM abc
INNER HASH JOIN stu
ON a = s
INNER MERGE JOIN xyz
ON s = x
----
inner-join (merge)
 ├── flags: force merge join
 ├── sort
 │    └── inner-join (hash)
 │         ├── flags: force hash join (store right side)
 │         ├── scan abc
 │         ├── scan stu
 │         └── filters
 │              └── a = s
 ├── scan xyz@xy
 └── filters (true)

# No-op case because InnerJoinApply is not matched.
opt expect-not=ReorderJoins format=hide-all
SELECT *
FROM abc
INNER JOIN LATERAL (SELECT * FROM (VALUES (a+1), (b*2)) f(v))
ON a = v
----
inner-join-apply
 ├── scan abc
 ├── values
 │    ├── (a + 1,)
 │    └── (b * 2,)
 └── filters
      └── a = column1

opt expect=ReorderJoins format=hide-all
SELECT * FROM abc
LEFT JOIN stu ON a = s
LEFT JOIN small ON a = m
----
right-join (merge)
 ├── scan stu
 ├── left-join (merge)
 │    ├── scan abc@ab
 │    ├── sort
 │    │    └── scan small
 │    └── filters (true)
 └── filters (true)

# Don't add a join between xyz and stu to the memo (because doing so would
# create a cross join).
exploretrace rule=ReorderJoins format=hide-all
SELECT *
FROM stu
INNER JOIN abc
ON s = a
INNER JOIN xyz
ON b = y
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── inner-join (hash)
   │    ├── scan stu
   │    ├── scan abc
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 1:
  inner-join (hash)
   ├── inner-join (hash)
   │    ├── scan abc
   │    ├── scan stu
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

================================================================================
ReorderJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── inner-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 3:
  inner-join (hash)
   ├── scan stu
   ├── inner-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   └── filters
        └── s = a

New expression 2 of 3:
  inner-join (hash)
   ├── inner-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   ├── scan stu
   └── filters
        └── s = a

New expression 3 of 3:
  inner-join (hash)
   ├── scan xyz
   ├── inner-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   └── filters
        └── b = y
----
----

# Left join under an inner join. Push the inner join into the left input of the
# left join.
opt expect=ReorderJoins
SELECT *
FROM
(
  SELECT *
  FROM medium
  LEFT JOIN large ON large.m = medium.m
) medium
INNER JOIN small ON medium.n = small.n
----
right-join (hash)
 ├── columns: m:1 n:2!null m:6 n:7 m:11 n:12!null
 ├── fd: (2)==(12), (12)==(2)
 ├── scan large
 │    └── columns: large.m:6 large.n:7
 ├── inner-join (hash)
 │    ├── columns: medium.m:1 medium.n:2!null small.m:11 small.n:12!null
 │    ├── fd: (2)==(12), (12)==(2)
 │    ├── scan medium
 │    │    └── columns: medium.m:1 medium.n:2
 │    ├── scan small
 │    │    └── columns: small.m:11 small.n:12
 │    └── filters
 │         └── medium.n:2 = small.n:12 [outer=(2,12), constraints=(/2: (/NULL - ]; /12: (/NULL - ]), fd=(2)==(12), (12)==(2)]
 └── filters
      └── large.m:6 = medium.m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

# Left join under an inner join. Push the inner join into the left input of the
# left join, but leave the (small.m:9 = large.n:6) OR (large.n:6 IS NULL) filter
# on a select above the join tree. It can't be pushed into the left join because
# it originated from an inner join.
opt expect=ReorderJoins
SELECT *
FROM
(
  SELECT medium.n, large.n
  FROM medium
  LEFT JOIN large ON large.m = medium.m
) result(a, b)
INNER JOIN small
ON result.a = small.n AND (small.m = result.b OR result.b IS NULL)
----
project
 ├── columns: a:2!null b:7 m:11 n:12!null
 ├── fd: (2)==(12), (12)==(2)
 └── select
      ├── columns: medium.m:1 medium.n:2!null large.m:6 large.n:7 small.m:11 small.n:12!null
      ├── fd: (2)==(12), (12)==(2)
      ├── right-join (hash)
      │    ├── columns: medium.m:1 medium.n:2!null large.m:6 large.n:7 small.m:11 small.n:12!null
      │    ├── fd: (2)==(12), (12)==(2)
      │    ├── scan large
      │    │    └── columns: large.m:6 large.n:7
      │    ├── inner-join (hash)
      │    │    ├── columns: medium.m:1 medium.n:2!null small.m:11 small.n:12!null
      │    │    ├── fd: (2)==(12), (12)==(2)
      │    │    ├── scan medium
      │    │    │    └── columns: medium.m:1 medium.n:2
      │    │    ├── scan small
      │    │    │    └── columns: small.m:11 small.n:12
      │    │    └── filters
      │    │         └── medium.n:2 = small.n:12 [outer=(2,12), constraints=(/2: (/NULL - ]; /12: (/NULL - ]), fd=(2)==(12), (12)==(2)]
      │    └── filters
      │         └── large.m:6 = medium.m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
      └── filters
           └── (small.m:11 = large.n:7) OR (large.n:7 IS NULL) [outer=(7,11)]

# Semi join under an inner join. Push the inner join into the left input of the
# semi join.
opt expect=ReorderJoins
SELECT *
FROM
(
  SELECT medium.n
  FROM medium
  WHERE EXISTS (SELECT * FROM large WHERE medium.m = large.m)
) medium
INNER JOIN small ON small.n = medium.n
----
project
 ├── columns: n:2!null m:12 n:13!null
 ├── fd: (2)==(13), (13)==(2)
 └── semi-join (hash)
      ├── columns: medium.m:1 medium.n:2!null small.m:12 small.n:13!null
      ├── fd: (2)==(13), (13)==(2)
      ├── inner-join (hash)
      │    ├── columns: medium.m:1 medium.n:2!null small.m:12 small.n:13!null
      │    ├── fd: (2)==(13), (13)==(2)
      │    ├── scan medium
      │    │    └── columns: medium.m:1 medium.n:2
      │    ├── scan small
      │    │    └── columns: small.m:12 small.n:13
      │    └── filters
      │         └── small.n:13 = medium.n:2 [outer=(2,13), constraints=(/2: (/NULL - ]; /13: (/NULL - ]), fd=(2)==(13), (13)==(2)]
      ├── scan large
      │    └── columns: large.m:6
      └── filters
           └── medium.m:1 = large.m:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

# Anti join under an inner join. Push the inner join into the left input of the
# anti join.
opt expect=ReorderJoins
SELECT *
FROM
(
  SELECT medium.n
  FROM medium
  WHERE NOT EXISTS (SELECT * FROM large WHERE medium.m <> large.m)
) medium
INNER JOIN small ON small.n = medium.n
----
project
 ├── columns: n:2!null m:12 n:13!null
 ├── fd: (2)==(13), (13)==(2)
 └── anti-join (cross)
      ├── columns: medium.m:1 medium.n:2!null small.m:12 small.n:13!null
      ├── fd: (2)==(13), (13)==(2)
      ├── inner-join (hash)
      │    ├── columns: medium.m:1 medium.n:2!null small.m:12 small.n:13!null
      │    ├── fd: (2)==(13), (13)==(2)
      │    ├── scan medium
      │    │    └── columns: medium.m:1 medium.n:2
      │    ├── scan small
      │    │    └── columns: small.m:12 small.n:13
      │    └── filters
      │         └── small.n:13 = medium.n:2 [outer=(2,13), constraints=(/2: (/NULL - ]; /13: (/NULL - ]), fd=(2)==(13), (13)==(2)]
      ├── scan large
      │    └── columns: large.m:6
      └── filters
           └── medium.m:1 != large.m:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])]

# Inner join in the right input of a left join; no reordering is valid apart
# from commutation.
exploretrace rule=ReorderJoins format=hide-all
SELECT *
FROM small
LEFT JOIN
(
  SELECT large.m
  FROM large
  INNER JOIN medium
  ON large.n = medium.n
) large
ON small.m = large.m
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  project
   └── left-join (hash)
        ├── scan small
        ├── inner-join (hash)
        │    ├── scan large
        │    ├── scan medium
        │    └── filters
        │         └── large.n = medium.n
        └── filters
             └── small.m = large.m

New expression 1 of 1:
  project
   └── left-join (hash)
        ├── scan small
        ├── inner-join (hash)
        │    ├── scan medium
        │    ├── scan large
        │    └── filters
        │         └── large.n = medium.n
        └── filters
             └── small.m = large.m

================================================================================
ReorderJoins
================================================================================
Source expression:
  project
   └── left-join (hash)
        ├── scan small
        ├── inner-join (hash)
        │    ├── scan large
        │    ├── scan medium
        │    └── filters
        │         └── large.n = medium.n
        └── filters
             └── small.m = large.m

No new expressions.
----
----

exploretrace rule=ReorderJoins format=hide-all
SELECT *
FROM
(
  SELECT medium.n, large.n
  FROM medium
  LEFT JOIN large ON large.m = medium.m
) result(a, b)
INNER JOIN small
ON result.a = small.n AND (small.m = result.b OR result.b IS NULL)
INNER JOIN xyz ON True
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  project
   └── inner-join (cross)
        ├── inner-join (hash)
        │    ├── left-join (hash)
        │    │    ├── scan medium
        │    │    ├── scan large
        │    │    └── filters
        │    │         └── large.m = medium.m
        │    ├── scan small
        │    └── filters
        │         ├── medium.n = small.n
        │         └── (small.m = large.n) OR (large.n IS NULL)
        ├── scan xyz
        └── filters (true)

No new expressions.

================================================================================
ReorderJoins
================================================================================
Source expression:
  project
   └── inner-join (cross)
        ├── inner-join (hash)
        │    ├── right-join (hash)
        │    │    ├── scan large
        │    │    ├── scan medium
        │    │    └── filters
        │    │         └── large.m = medium.m
        │    ├── scan small
        │    └── filters
        │         ├── medium.n = small.n
        │         └── (small.m = large.n) OR (large.n IS NULL)
        ├── scan xyz
        └── filters (true)

New expression 1 of 2:
  project
   └── inner-join (cross)
        ├── select
        │    ├── left-join (hash)
        │    │    ├── inner-join (hash)
        │    │    │    ├── scan medium
        │    │    │    ├── scan small
        │    │    │    └── filters
        │    │    │         └── medium.n = small.n
        │    │    ├── scan large
        │    │    └── filters
        │    │         └── large.m = medium.m
        │    └── filters
        │         └── (small.m = large.n) OR (large.n IS NULL)
        ├── scan xyz
        └── filters (true)

New expression 2 of 2:
  project
   └── inner-join (cross)
        ├── inner-join (hash)
        │    ├── scan small
        │    ├── right-join (hash)
        │    │    ├── scan large
        │    │    ├── scan medium
        │    │    └── filters
        │    │         └── large.m = medium.m
        │    └── filters
        │         ├── medium.n = small.n
        │         └── (small.m = large.n) OR (large.n IS NULL)
        ├── scan xyz
        └── filters (true)

================================================================================
ReorderJoins
================================================================================
Source expression:
  project
   └── inner-join (cross)
        ├── select
        │    ├── right-join (hash)
        │    │    ├── scan large
        │    │    ├── inner-join (hash)
        │    │    │    ├── scan medium
        │    │    │    ├── scan small
        │    │    │    └── filters
        │    │    │         └── medium.n = small.n
        │    │    └── filters
        │    │         └── large.m = medium.m
        │    └── filters
        │         └── (small.m = large.n) OR (large.n IS NULL)
        ├── scan xyz
        └── filters (true)

New expression 1 of 1:
  project
   └── inner-join (cross)
        ├── scan xyz
        ├── select
        │    ├── right-join (hash)
        │    │    ├── scan large
        │    │    ├── inner-join (hash)
        │    │    │    ├── scan medium
        │    │    │    ├── scan small
        │    │    │    └── filters
        │    │    │         └── medium.n = small.n
        │    │    └── filters
        │    │         └── large.m = medium.m
        │    └── filters
        │         └── (small.m = large.n) OR (large.n IS NULL)
        └── filters (true)
----
----

# Full join on top of a full join.
exploretrace rule=ReorderJoins format=hide-all
SELECT * FROM abc
FULL JOIN stu ON s = a
FULL JOIN xyz ON b = y
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  full-join (hash)
   ├── full-join (hash)
   │    ├── scan abc
   │    ├── scan stu
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 1:
  full-join (hash)
   ├── full-join (hash)
   │    ├── scan stu
   │    ├── scan abc
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

================================================================================
ReorderJoins
================================================================================
Source expression:
  full-join (hash)
   ├── full-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 3:
  full-join (hash)
   ├── full-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   ├── scan stu
   └── filters
        └── s = a

New expression 2 of 3:
  full-join (hash)
   ├── scan stu
   ├── full-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   └── filters
        └── s = a

New expression 3 of 3:
  full-join (hash)
   ├── scan xyz
   ├── full-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   └── filters
        └── b = y
----
----

# Full join on top of a full join. Lower join is in right input of upper join.
exploretrace rule=ReorderJoins format=hide-all
SELECT * FROM abc
FULL JOIN
(
  SELECT * FROM stu
  FULL JOIN xyz ON t = y
) f(s, t, u, x, y, z)
ON a = x
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  full-join (hash)
   ├── scan abc
   ├── full-join (hash)
   │    ├── scan stu
   │    ├── scan xyz
   │    └── filters
   │         └── t = y
   └── filters
        └── a = x

New expression 1 of 1:
  full-join (hash)
   ├── scan abc
   ├── full-join (hash)
   │    ├── scan xyz
   │    ├── scan stu
   │    └── filters
   │         └── t = y
   └── filters
        └── a = x

================================================================================
ReorderJoins
================================================================================
Source expression:
  full-join (hash)
   ├── scan abc
   ├── full-join (hash)
   │    ├── scan stu
   │    ├── scan xyz
   │    └── filters
   │         └── t = y
   └── filters
        └── a = x

New expression 1 of 3:
  full-join (hash)
   ├── full-join (hash)
   │    ├── scan stu
   │    ├── scan xyz
   │    └── filters
   │         └── t = y
   ├── scan abc
   └── filters
        └── a = x

New expression 2 of 3:
  full-join (hash)
   ├── scan stu
   ├── full-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── a = x
   └── filters
        └── t = y

New expression 3 of 3:
  full-join (hash)
   ├── full-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── a = x
   ├── scan stu
   └── filters
        └── t = y
----
----

# Left join on top of a left join.
exploretrace rule=ReorderJoins format=hide-all
SELECT * FROM abc
LEFT JOIN stu ON s = a
LEFT JOIN xyz ON b = y
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  left-join (hash)
   ├── left-join (hash)
   │    ├── scan abc
   │    ├── scan stu
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

No new expressions.

================================================================================
ReorderJoins
================================================================================
Source expression:
  left-join (hash)
   ├── right-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 1:
  left-join (hash)
   ├── left-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   ├── scan stu
   └── filters
        └── s = a
----
----

# Left join on top of a full join.
exploretrace rule=ReorderJoins format=hide-all
SELECT * FROM abc
FULL JOIN stu ON s = a
LEFT JOIN xyz ON b = y
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  left-join (hash)
   ├── full-join (hash)
   │    ├── scan abc
   │    ├── scan stu
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 1:
  left-join (hash)
   ├── full-join (hash)
   │    ├── scan stu
   │    ├── scan abc
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── b = y

================================================================================
ReorderJoins
================================================================================
Source expression:
  left-join (hash)
   ├── full-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   ├── scan xyz
   └── filters
        └── b = y

New expression 1 of 2:
  full-join (hash)
   ├── left-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   ├── scan stu
   └── filters
        └── s = a

New expression 2 of 2:
  full-join (hash)
   ├── scan stu
   ├── left-join (hash)
   │    ├── scan abc
   │    ├── scan xyz
   │    └── filters
   │         └── b = y
   └── filters
        └── s = a
----
----

# Verify that the reversed join expressions get added to the memo, and there
# are no duplicates.
memo expect=ReorderJoins
SELECT * FROM abc JOIN xyz ON a=z
----
memo (optimized, ~14KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4) (merge-join G2 G3 G5 inner-join,+1,+9) (lookup-join G3 G5 abc@ab,keyCols=[9],outCols=(1-3,7-9))
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (inner-join G2 G3 G4)
 │         └── cost: 2237.51
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    ├── [ordering: +1]
 │    │    ├── best: (scan abc@ab,cols=(1-3))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    ├── [ordering: +9]
 │    │    ├── best: (sort G3)
 │    │    └── cost: 1348.20
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable z)

memo expect=ReorderJoins
SELECT * FROM abc FULL OUTER JOIN xyz ON a=z
----
memo (optimized, ~12KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (full-join G2 G3 G4) (full-join G3 G2 G4) (merge-join G2 G3 G5 full-join,+1,+9)
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (full-join G2 G3 G4)
 │         └── cost: 2237.61
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    ├── [ordering: +1]
 │    │    ├── best: (scan abc@ab,cols=(1-3))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    ├── [ordering: +9]
 │    │    ├── best: (sort G3)
 │    │    └── cost: 1348.20
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable z)

# Verify that we swap to get the smaller side on the right.
opt expect=ReorderJoins
SELECT * FROM abc INNER JOIN xyz ON a=c WHERE b=1
----
inner-join (cross)
 ├── columns: a:1!null b:2!null c:3!null x:7 y:8 z:9
 ├── fd: ()-->(2), (1)==(3), (3)==(1)
 ├── scan xyz
 │    └── columns: x:7 y:8 z:9
 ├── select
 │    ├── columns: a:1!null b:2!null c:3!null
 │    ├── fd: ()-->(2), (1)==(3), (3)==(1)
 │    ├── scan abc@bc
 │    │    ├── columns: a:1 b:2!null c:3!null
 │    │    ├── constraint: /2/3/4: (/1/NULL - /1]
 │    │    └── fd: ()-->(2)
 │    └── filters
 │         └── a:1 = c:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 └── filters (true)

# Verify that the hash join hint prevents swapping the sides.
opt expect-not=ReorderJoins
SELECT * FROM abc INNER HASH JOIN xyz ON a=c WHERE b=1
----
inner-join (cross)
 ├── columns: a:1!null b:2!null c:3!null x:7 y:8 z:9
 ├── flags: force hash join (store right side)
 ├── fd: ()-->(2), (1)==(3), (3)==(1)
 ├── select
 │    ├── columns: a:1!null b:2!null c:3!null
 │    ├── fd: ()-->(2), (1)==(3), (3)==(1)
 │    ├── scan abc@bc
 │    │    ├── columns: a:1 b:2!null c:3!null
 │    │    ├── constraint: /2/3/4: (/1/NULL - /1]
 │    │    └── fd: ()-->(2)
 │    └── filters
 │         └── a:1 = c:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 ├── scan xyz
 │    └── columns: x:7 y:8 z:9
 └── filters (true)

opt expect=ReorderJoins
SELECT * FROM (SELECT * FROM abc WHERE b=1) FULL OUTER JOIN xyz ON a=z
----
full-join (hash)
 ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
 ├── scan xyz
 │    └── columns: x:7 y:8 z:9
 ├── scan abc@bc
 │    ├── columns: a:1 b:2!null c:3
 │    ├── constraint: /2/3/4: [/1 - /1]
 │    └── fd: ()-->(2)
 └── filters
      └── a:1 = z:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Verify that commuting works correctly when there is a lookup join hint
# (specifically that it returns the original expression and flags when applied
# twice; if it didn't, we'd see more inner-join expressions).
memo expect-not=ReorderJoins
SELECT * FROM abc INNER LOOKUP JOIN xyz ON a=x
----
memo (optimized, ~13KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (inner-join G2 G3 G4) (lookup-join G2 G5 xyz@xy,keyCols=[1],outCols=(1-3,7-9))
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (lookup-join G2 G5 xyz@xy,keyCols=[1],outCols=(1-3,7-9))
 │         └── cost: 23182.74
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable x)

# Verify that setting the join limit to 1 allows only commutation.
exploretrace rule=ReorderJoins format=hide-all set=reorder_joins_limit=1
SELECT * FROM abc
INNER JOIN stu ON s = a
INNER JOIN xyz ON s = x
----
----
================================================================================
ReorderJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── inner-join (hash)
   │    ├── scan abc
   │    ├── scan stu
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── s = x

New expression 1 of 1:
  inner-join (hash)
   ├── inner-join (hash)
   │    ├── scan stu
   │    ├── scan abc
   │    └── filters
   │         └── s = a
   ├── scan xyz
   └── filters
        └── s = x

================================================================================
ReorderJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── inner-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   ├── scan xyz
   └── filters
        └── s = x

New expression 1 of 1:
  inner-join (hash)
   ├── scan xyz
   ├── inner-join (merge)
   │    ├── scan stu
   │    ├── scan abc@ab
   │    └── filters (true)
   └── filters
        └── s = x
----
----

# --------------------------------------------------
# CommuteLeftJoin
# --------------------------------------------------

memo
SELECT * FROM abc LEFT OUTER JOIN xyz ON a=z
----
memo (optimized, ~12KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (left-join G2 G3 G4) (right-join G3 G2 G4) (merge-join G2 G3 G5 left-join,+1,+9)
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (left-join G2 G3 G4)
 │         └── cost: 2237.61
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    ├── [ordering: +1]
 │    │    ├── best: (scan abc@ab,cols=(1-3))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    ├── [ordering: +9]
 │    │    ├── best: (sort G3)
 │    │    └── cost: 1348.20
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable z)

opt
SELECT * FROM abc LEFT OUTER JOIN xyz ON a=z WHERE b=1
----
right-join (hash)
 ├── columns: a:1 b:2!null c:3 x:7 y:8 z:9
 ├── fd: ()-->(2)
 ├── scan xyz
 │    └── columns: x:7 y:8 z:9
 ├── scan abc@bc
 │    ├── columns: a:1 b:2!null c:3
 │    ├── constraint: /2/3/4: [/1 - /1]
 │    └── fd: ()-->(2)
 └── filters
      └── a:1 = z:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# --------------------------------------------------
# CommuteRightJoin
# --------------------------------------------------

memo
SELECT * FROM abc RIGHT OUTER JOIN xyz ON a=z
----
memo (optimized, ~14KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (left-join G2 G3 G4) (right-join G3 G2 G4) (lookup-join G2 G5 abc@ab,keyCols=[9],outCols=(1-3,7-9)) (merge-join G3 G2 G5 right-join,+1,+9)
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (left-join G2 G3 G4)
 │         └── cost: 2237.61
 ├── G2: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    ├── [ordering: +9]
 │    │    ├── best: (sort G2)
 │    │    └── cost: 1348.20
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G3: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    ├── [ordering: +1]
 │    │    ├── best: (scan abc@ab,cols=(1-3))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable z)

opt
SELECT * FROM (SELECT * FROM abc WHERE b=1) RIGHT OUTER JOIN xyz ON a=z
----
left-join (hash)
 ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
 ├── scan xyz
 │    └── columns: x:7 y:8 z:9
 ├── scan abc@bc
 │    ├── columns: a:1 b:2!null c:3
 │    ├── constraint: /2/3/4: [/1 - /1]
 │    └── fd: ()-->(2)
 └── filters
      └── a:1 = z:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# -----------------------------------------------------
# CommuteSemiJoin
# -----------------------------------------------------

exec-ddl
CREATE TABLE def (d INT, e INT, f INT, PRIMARY KEY (d, e));
----

exec-ddl
ALTER TABLE abc INJECT STATISTICS '[
  {
    "columns": ["a"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 100,
    "distinct_count": 100
  },
  {
    "columns": ["b"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 100,
    "distinct_count": 100
  }
]'
----

exec-ddl
ALTER TABLE def INJECT STATISTICS '[
  {
    "columns": ["d"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 10000,
    "distinct_count": 10000
  },
  {
    "columns": ["e"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 10000,
    "distinct_count": 10000
  }
]'
----

# Test the CommuteSemiJoinRule creates an appropriate inner join.
opt
SELECT * from abc WHERE EXISTS (SELECT * FROM def WHERE a=f)
----
semi-join (hash)
 ├── columns: a:1 b:2 c:3
 ├── scan abc
 │    └── columns: a:1 b:2 c:3
 ├── scan def
 │    └── columns: f:9
 └── filters
      └── a:1 = f:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Test that we don't commute a SemiJoin when the On conditions are not
# equalities. For example, in this test we have a Lt condition.
opt expect-not=CommuteSemiJoin
SELECT * from abc WHERE EXISTS (SELECT * FROM def WHERE a < e)
----
semi-join (cross)
 ├── columns: a:1 b:2 c:3
 ├── scan abc
 │    └── columns: a:1 b:2 c:3
 ├── scan def
 │    └── columns: e:8!null
 └── filters
      └── a:1 < e:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]

# Test that we commute a SemiJoin after the On conditions have been
# split into equality predicates by the SplitDisjunctionOfSemiJoinTerms rule.
opt expect=CommuteSemiJoin
SELECT * from abc WHERE EXISTS (SELECT * FROM def WHERE a=d OR c=e)
----
project
 ├── columns: a:1 b:2 c:3
 └── distinct-on
      ├── columns: a:1 b:2 c:3 rowid:4!null
      ├── grouping columns: rowid:4!null
      ├── key: (4)
      ├── fd: (4)-->(1-3)
      ├── union-all
      │    ├── columns: a:1 b:2 c:3 rowid:4!null
      │    ├── left columns: a:13 b:14 c:15 rowid:16
      │    ├── right columns: a:24 b:25 c:26 rowid:27
      │    ├── semi-join (lookup def)
      │    │    ├── columns: a:13 b:14 c:15 rowid:16!null
      │    │    ├── key columns: [13] = [19]
      │    │    ├── key: (16)
      │    │    ├── fd: (16)-->(13-15)
      │    │    ├── scan abc
      │    │    │    ├── columns: a:13 b:14 c:15 rowid:16!null
      │    │    │    ├── key: (16)
      │    │    │    └── fd: (16)-->(13-15)
      │    │    └── filters (true)
      │    └── semi-join (hash)
      │         ├── columns: a:24 b:25 c:26 rowid:27!null
      │         ├── key: (27)
      │         ├── fd: (27)-->(24-26)
      │         ├── scan abc
      │         │    ├── columns: a:24 b:25 c:26 rowid:27!null
      │         │    ├── key: (27)
      │         │    └── fd: (27)-->(24-26)
      │         ├── scan def
      │         │    └── columns: e:31!null
      │         └── filters
      │              └── c:26 = e:31 [outer=(26,31), constraints=(/26: (/NULL - ]; /31: (/NULL - ]), fd=(26)==(31), (31)==(26)]
      └── aggregations
           ├── const-agg [as=a:1, outer=(1)]
           │    └── a:1
           ├── const-agg [as=b:2, outer=(2)]
           │    └── b:2
           └── const-agg [as=c:3, outer=(3)]
                └── c:3

# Test that we don't commute a SemiJoin when the On conditions are not
# equalities. For example, in this test we have an Or condition.
opt disable=SplitDisjunctionOfJoinTerms expect-not=CommuteSemiJoin
SELECT * from abc WHERE EXISTS (SELECT * FROM def WHERE a=d OR c=e)
----
semi-join (cross)
 ├── columns: a:1 b:2 c:3
 ├── scan abc
 │    └── columns: a:1 b:2 c:3
 ├── scan def
 │    ├── columns: d:7!null e:8!null
 │    └── key: (7,8)
 └── filters
      └── (a:1 = d:7) OR (c:3 = e:8) [outer=(1,3,7,8)]

opt disable=CommuteSemiJoin format=show-all
SELECT * from abc WHERE EXISTS (SELECT * FROM def WHERE a=d AND c=e)
----
semi-join (lookup t.public.def)
 ├── columns: a:1(int) b:2(int) c:3(int)
 ├── key columns: [1 3] = [7 8]
 ├── lookup columns are key
 ├── stats: [rows=100, distinct(1)=100, null(1)=0, distinct(3)=10, null(3)=0]
 ├── cost: 539.760394
 ├── prune: (2)
 ├── interesting orderings: (+1,+2) (+2,+3)
 ├── scan t.public.abc
 │    ├── columns: t.public.abc.a:1(int) t.public.abc.b:2(int) t.public.abc.c:3(int)
 │    ├── stats: [rows=100, distinct(1)=100, null(1)=0, distinct(3)=10, null(3)=1]
 │    ├── cost: 135.72
 │    ├── prune: (1-3)
 │    ├── interesting orderings: (+1,+2) (+2,+3)
 │    └── unfiltered-cols: (1-6)
 └── filters (true)

# TODO(rytaft): See stats/join tests. Since we don't collect the stats properly
# for SemiJoins, we prefer the InnerJoin plan over the SemiJoin one more times
# than necessary.
opt format=show-all
SELECT * from abc WHERE EXISTS (SELECT * FROM def WHERE a=d AND c=e)
----
semi-join (lookup t.public.def)
 ├── columns: a:1(int) b:2(int) c:3(int)
 ├── key columns: [1 3] = [7 8]
 ├── lookup columns are key
 ├── stats: [rows=100, distinct(1)=100, null(1)=0, distinct(3)=10, null(3)=0]
 ├── cost: 539.760394
 ├── prune: (2)
 ├── interesting orderings: (+1,+2) (+2,+3)
 ├── scan t.public.abc
 │    ├── columns: t.public.abc.a:1(int) t.public.abc.b:2(int) t.public.abc.c:3(int)
 │    ├── stats: [rows=100, distinct(1)=100, null(1)=0, distinct(3)=10, null(3)=1]
 │    ├── cost: 135.72
 │    ├── prune: (1-3)
 │    ├── interesting orderings: (+1,+2) (+2,+3)
 │    └── unfiltered-cols: (1-6)
 └── filters (true)

exec-ddl
CREATE TABLE customers (id INT PRIMARY KEY, name STRING)
----

exec-ddl
CREATE TABLE orders (id INT PRIMARY KEY, cust_id INT REFERENCES customers (id), order_date DATE, INDEX (order_date) STORING (cust_id))
----

exec-ddl
ALTER TABLE customers INJECT STATISTICS '[
  {
    "columns": ["id"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 100000,
    "distinct_count": 100000
  }
]'
----

exec-ddl
ALTER TABLE orders INJECT STATISTICS '[
  {
    "columns": ["id"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 1000000,
    "distinct_count": 1000000
  },
  {
    "columns": ["cust_id"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 1000000,
    "distinct_count": 10000000
  },
  {
    "columns": ["order_date"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 1000000,
    "distinct_count": 1000000
  }
]'
----

opt disable=CommuteSemiJoin
SELECT *
FROM customers c
WHERE EXISTS(SELECT * FROM orders o WHERE o.cust_id=c.id AND o.order_date='2019-01-01')
----
semi-join (merge)
 ├── columns: id:1!null name:2
 ├── left ordering: +1
 ├── right ordering: +6
 ├── key: (1)
 ├── fd: (1)-->(2)
 ├── scan customers [as=c]
 │    ├── columns: c.id:1!null name:2
 │    ├── key: (1)
 │    ├── fd: (1)-->(2)
 │    └── ordering: +1
 ├── sort
 │    ├── columns: cust_id:6 order_date:7!null
 │    ├── fd: ()-->(7)
 │    ├── ordering: +6 opt(7) [actual: +6]
 │    └── scan orders@orders_order_date_idx [as=o]
 │         ├── columns: cust_id:6 order_date:7!null
 │         ├── constraint: /7/5: [/'2019-01-01' - /'2019-01-01']
 │         └── fd: ()-->(7)
 └── filters (true)

# The CommuteSemiJoin rule allows a much better plan because we can use
# a lookup join.
opt
SELECT *
FROM customers c
WHERE EXISTS(SELECT * FROM orders o WHERE o.cust_id=c.id AND o.order_date='2019-01-01')
----
project
 ├── columns: id:1!null name:2
 ├── key: (1)
 ├── fd: (1)-->(2)
 └── inner-join (lookup customers [as=c])
      ├── columns: c.id:1!null name:2 cust_id:6!null
      ├── key columns: [6] = [1]
      ├── lookup columns are key
      ├── key: (6)
      ├── fd: (1)-->(2), (1)==(6), (6)==(1)
      ├── distinct-on
      │    ├── columns: cust_id:6
      │    ├── grouping columns: cust_id:6
      │    ├── key: (6)
      │    └── scan orders@orders_order_date_idx [as=o]
      │         ├── columns: cust_id:6 order_date:7!null
      │         ├── constraint: /7/5: [/'2019-01-01' - /'2019-01-01']
      │         └── fd: ()-->(7)
      └── filters (true)

# --------------------------------------------------
# GenerateMergeJoins
# --------------------------------------------------

opt
SELECT * FROM abc JOIN xyz ON a=x
----
inner-join (merge)
 ├── columns: a:1!null b:2 c:3 x:7!null y:8 z:9
 ├── left ordering: +7
 ├── right ordering: +1
 ├── fd: (1)==(7), (7)==(1)
 ├── scan xyz@xy
 │    ├── columns: x:7 y:8 z:9
 │    └── ordering: +7
 ├── scan abc@ab
 │    ├── columns: a:1 b:2 c:3
 │    └── ordering: +1
 └── filters (true)

memo
SELECT * FROM abc JOIN xyz ON a=x
----
memo (optimized, ~16KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4) (merge-join G2 G3 G5 inner-join,+1,+7) (lookup-join G2 G5 xyz@xy,keyCols=[1],outCols=(1-3,7-9)) (merge-join G3 G2 G5 inner-join,+7,+1) (lookup-join G3 G5 abc@ab,keyCols=[7],outCols=(1-3,7-9))
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (merge-join G3="[ordering: +7]" G2="[ordering: +1]" G5 inner-join,+7,+1)
 │         └── cost: 1245.56
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    ├── [ordering: +1]
 │    │    ├── best: (scan abc@ab,cols=(1-3))
 │    │    └── cost: 135.72
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 135.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    ├── [ordering: +7]
 │    │    ├── best: (scan xyz@xy,cols=(7-9))
 │    │    └── cost: 1098.72
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable x)

# Verify that we don't generate merge joins if there's a hint that says otherwise.
memo
SELECT * FROM abc INNER HASH JOIN xyz ON a=x
----
memo (optimized, ~12KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (inner-join G2 G3 G4)
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (inner-join G2 G3 G4)
 │         └── cost: 1254.36
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 135.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G5)
 ├── G5: (eq G6 G7)
 ├── G6: (variable a)
 └── G7: (variable x)

# Do not generate merge joins if optimizer_merge_joins_enabled is false.
memo set=(optimizer_merge_joins_enabled=false) expect-not=GenerateMergeJoins
SELECT * FROM abc JOIN xyz ON a=x
----
memo (optimized, ~15KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4) (lookup-join G2 G5 xyz@xy,keyCols=[1],outCols=(1-3,7-9)) (lookup-join G3 G5 abc@ab,keyCols=[7],outCols=(1-3,7-9))
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (inner-join G3 G2 G4)
 │         └── cost: 1249.71
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 135.72
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters G6)
 ├── G5: (filters)
 ├── G6: (eq G7 G8)
 ├── G7: (variable a)
 └── G8: (variable x)

opt
SELECT * FROM abc JOIN xyz ON x=a
----
inner-join (merge)
 ├── columns: a:1!null b:2 c:3 x:7!null y:8 z:9
 ├── left ordering: +7
 ├── right ordering: +1
 ├── fd: (1)==(7), (7)==(1)
 ├── scan xyz@xy
 │    ├── columns: x:7 y:8 z:9
 │    └── ordering: +7
 ├── scan abc@ab
 │    ├── columns: a:1 b:2 c:3
 │    └── ordering: +1
 └── filters (true)

opt
SELECT * FROM abc JOIN xyz ON a=x AND a=x AND x=a
----
inner-join (merge)
 ├── columns: a:1!null b:2 c:3 x:7!null y:8 z:9
 ├── left ordering: +7
 ├── right ordering: +1
 ├── fd: (1)==(7), (7)==(1)
 ├── scan xyz@xy
 │    ├── columns: x:7 y:8 z:9
 │    └── ordering: +7
 ├── scan abc@ab
 │    ├── columns: a:1 b:2 c:3
 │    └── ordering: +1
 └── filters (true)

# Use constraints to force the choice of an index which doesn't help, and
# verify that we don't prefer a merge-join that has to sort both of its inputs.
opt
SELECT * FROM abc JOIN xyz ON a=x AND b=y WHERE b=1 AND y=1
----
inner-join (lookup xyz@xy)
 ├── columns: a:1!null b:2!null c:3 x:7!null y:8!null z:9
 ├── key columns: [1 2] = [7 8]
 ├── fd: ()-->(2,8), (1)==(7), (7)==(1), (2)==(8), (8)==(2)
 ├── scan abc@bc
 │    ├── columns: a:1 b:2!null c:3
 │    ├── constraint: /2/3/4: [/1 - /1]
 │    └── fd: ()-->(2)
 └── filters
      └── y:8 = 1 [outer=(8), constraints=(/8: [/1 - /1]; tight), fd=()-->(8)]

# Verify case where we generate multiple merge-joins.
memo disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight)
SELECT * FROM stu AS l JOIN stu AS r ON (l.s, l.t, l.u) = (r.s, r.t, r.u)
----
memo (optimized, ~20KB, required=[presentation: s:1,t:2,u:3,s:6,t:7,u:8])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4) (merge-join G2 G3 G5 inner-join,+1,+2,+3,+6,+7,+8) (merge-join G2 G3 G5 inner-join,+3,+2,+1,+8,+7,+6) (lookup-join G2 G5 stu [as=r],keyCols=[1 2 3],outCols=(1-3,6-8)) (lookup-join G2 G5 stu@uts [as=r],keyCols=[3 2 1],outCols=(1-3,6-8)) (merge-join G3 G2 G5 inner-join,+6,+7,+8,+1,+2,+3) (merge-join G3 G2 G5 inner-join,+8,+7,+6,+3,+2,+1) (lookup-join G3 G5 stu [as=l],keyCols=[6 7 8],outCols=(1-3,6-8)) (lookup-join G3 G5 stu@uts [as=l],keyCols=[8 7 6],outCols=(1-3,6-8))
 │    └── [presentation: s:1,t:2,u:3,s:6,t:7,u:8]
 │         ├── best: (merge-join G2="[ordering: +1,+2,+3]" G3="[ordering: +6,+7,+8]" G5 inner-join,+1,+2,+3,+6,+7,+8)
 │         └── cost: 21457.26
 ├── G2: (scan stu [as=l],cols=(1-3)) (scan stu@uts [as=l],cols=(1-3))
 │    ├── [ordering: +1,+2,+3]
 │    │    ├── best: (scan stu [as=l],cols=(1-3))
 │    │    └── cost: 10628.62
 │    ├── [ordering: +3,+2,+1]
 │    │    ├── best: (scan stu@uts [as=l],cols=(1-3))
 │    │    └── cost: 10628.62
 │    └── []
 │         ├── best: (scan stu [as=l],cols=(1-3))
 │         └── cost: 10628.62
 ├── G3: (scan stu [as=r],cols=(6-8)) (scan stu@uts [as=r],cols=(6-8))
 │    ├── [ordering: +6,+7,+8]
 │    │    ├── best: (scan stu [as=r],cols=(6-8))
 │    │    └── cost: 10628.62
 │    ├── [ordering: +8,+7,+6]
 │    │    ├── best: (scan stu@uts [as=r],cols=(6-8))
 │    │    └── cost: 10628.62
 │    └── []
 │         ├── best: (scan stu [as=r],cols=(6-8))
 │         └── cost: 10628.62
 ├── G4: (filters G6 G7 G8)
 ├── G5: (filters)
 ├── G6: (eq G9 G10)
 ├── G7: (eq G11 G12)
 ├── G8: (eq G13 G14)
 ├── G9: (variable l.s)
 ├── G10: (variable r.s)
 ├── G11: (variable l.t)
 ├── G12: (variable r.t)
 ├── G13: (variable l.u)
 └── G14: (variable r.u)

exploretrace rule=GenerateMergeJoins disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight)
SELECT * FROM stu AS l JOIN stu AS r ON (l.s, l.t, l.u) = (r.s, r.t, r.u)
----
----
================================================================================
GenerateMergeJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: s:1!null t:2!null u:3!null s:6!null t:7!null u:8!null
   ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
   ├── key: (6-8)
   ├── fd: (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3)
   ├── scan stu [as=l]
   │    ├── columns: l.s:1!null l.t:2!null l.u:3!null
   │    └── key: (1-3)
   ├── scan stu [as=r]
   │    ├── columns: r.s:6!null r.t:7!null r.u:8!null
   │    └── key: (6-8)
   └── filters
        ├── l.s:1 = r.s:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
        ├── l.t:2 = r.t:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]
        └── l.u:3 = r.u:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]

New expression 1 of 2:
  inner-join (merge)
   ├── columns: s:1!null t:2!null u:3!null s:6!null t:7!null u:8!null
   ├── left ordering: +1,+2,+3
   ├── right ordering: +6,+7,+8
   ├── key: (6-8)
   ├── fd: (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3)
   ├── scan stu [as=l]
   │    ├── columns: l.s:1!null l.t:2!null l.u:3!null
   │    ├── key: (1-3)
   │    └── ordering: +1,+2,+3
   ├── scan stu [as=r]
   │    ├── columns: r.s:6!null r.t:7!null r.u:8!null
   │    ├── key: (6-8)
   │    └── ordering: +6,+7,+8
   └── filters (true)

New expression 2 of 2:
  inner-join (merge)
   ├── columns: s:1!null t:2!null u:3!null s:6!null t:7!null u:8!null
   ├── left ordering: +3,+2,+1
   ├── right ordering: +8,+7,+6
   ├── key: (6-8)
   ├── fd: (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3)
   ├── scan stu@uts [as=l]
   │    ├── columns: l.s:1!null l.t:2!null l.u:3!null
   │    ├── key: (1-3)
   │    └── ordering: +3,+2,+1
   ├── scan stu@uts [as=r]
   │    ├── columns: r.s:6!null r.t:7!null r.u:8!null
   │    ├── key: (6-8)
   │    └── ordering: +8,+7,+6
   └── filters (true)

================================================================================
GenerateMergeJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: s:1!null t:2!null u:3!null s:6!null t:7!null u:8!null
   ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
   ├── key: (6-8)
   ├── fd: (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3)
   ├── scan stu [as=r]
   │    ├── columns: r.s:6!null r.t:7!null r.u:8!null
   │    └── key: (6-8)
   ├── scan stu [as=l]
   │    ├── columns: l.s:1!null l.t:2!null l.u:3!null
   │    └── key: (1-3)
   └── filters
        ├── l.s:1 = r.s:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
        ├── l.t:2 = r.t:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]
        └── l.u:3 = r.u:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]

New expression 1 of 2:
  inner-join (merge)
   ├── columns: s:1!null t:2!null u:3!null s:6!null t:7!null u:8!null
   ├── left ordering: +6,+7,+8
   ├── right ordering: +1,+2,+3
   ├── key: (6-8)
   ├── fd: (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3)
   ├── scan stu [as=r]
   │    ├── columns: r.s:6!null r.t:7!null r.u:8!null
   │    ├── key: (6-8)
   │    └── ordering: +6,+7,+8
   ├── scan stu [as=l]
   │    ├── columns: l.s:1!null l.t:2!null l.u:3!null
   │    ├── key: (1-3)
   │    └── ordering: +1,+2,+3
   └── filters (true)

New expression 2 of 2:
  inner-join (merge)
   ├── columns: s:1!null t:2!null u:3!null s:6!null t:7!null u:8!null
   ├── left ordering: +8,+7,+6
   ├── right ordering: +3,+2,+1
   ├── key: (6-8)
   ├── fd: (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3)
   ├── scan stu@uts [as=r]
   │    ├── columns: r.s:6!null r.t:7!null r.u:8!null
   │    ├── key: (6-8)
   │    └── ordering: +8,+7,+6
   ├── scan stu@uts [as=l]
   │    ├── columns: l.s:1!null l.t:2!null l.u:3!null
   │    ├── key: (1-3)
   │    └── ordering: +3,+2,+1
   └── filters (true)
----
----

# Add statistics to make table stu large (so that sorting abc is relatively cheap).
exec-ddl
ALTER TABLE stu INJECT STATISTICS '[
  {
    "columns": ["s"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 1000000,
    "distinct_count": 1000000
  }
]'
----

# The ordering is coming from the left side.
opt
SELECT * FROM stu LEFT OUTER JOIN abc ON (c,b,a) = (s,t,u)
----
left-join (merge)
 ├── columns: s:1!null t:2!null u:3!null a:6 b:7 c:8
 ├── left ordering: +3,+2,+1
 ├── right ordering: +6,+7,+8
 ├── scan stu@uts
 │    ├── columns: s:1!null t:2!null u:3!null
 │    ├── key: (1-3)
 │    └── ordering: +3,+2,+1
 ├── sort (segmented)
 │    ├── columns: a:6 b:7 c:8
 │    ├── ordering: +6,+7,+8
 │    └── scan abc@ab
 │         ├── columns: a:6 b:7 c:8
 │         └── ordering: +6,+7
 └── filters (true)

# The ordering is coming from the right side.
opt
SELECT * FROM abc RIGHT OUTER JOIN stu ON (c,b,a) = (s,t,u)
----
left-join (merge)
 ├── columns: a:1 b:2 c:3 s:7!null t:8!null u:9!null
 ├── left ordering: +9,+8,+7
 ├── right ordering: +1,+2,+3
 ├── scan stu@uts
 │    ├── columns: s:7!null t:8!null u:9!null
 │    ├── key: (7-9)
 │    └── ordering: +9,+8,+7
 ├── sort (segmented)
 │    ├── columns: a:1 b:2 c:3
 │    ├── ordering: +1,+2,+3
 │    └── scan abc@ab
 │         ├── columns: a:1 b:2 c:3
 │         └── ordering: +1,+2
 └── filters (true)

# In these cases, we shouldn't pick up equivalencies.
memo
SELECT * FROM abc JOIN xyz ON a=b
----
memo (optimized, ~18KB, required=[presentation: a:1,b:2,c:3,x:7,y:8,z:9])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4)
 │    └── [presentation: a:1,b:2,c:3,x:7,y:8,z:9]
 │         ├── best: (inner-join G3 G2 G4)
 │         └── cost: 1247.31
 ├── G2: (select G5 G6) (select G7 G6) (select G8 G6)
 │    └── []
 │         ├── best: (select G7 G6)
 │         └── cost: 126.05
 ├── G3: (scan xyz,cols=(7-9)) (scan xyz@xy,cols=(7-9)) (scan xyz@yz,cols=(7-9))
 │    └── []
 │         ├── best: (scan xyz,cols=(7-9))
 │         └── cost: 1098.72
 ├── G4: (filters)
 ├── G5: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 135.72
 ├── G6: (filters G9)
 ├── G7: (scan abc@ab,cols=(1-3),constrained)
 │    └── []
 │         ├── best: (scan abc@ab,cols=(1-3),constrained)
 │         └── cost: 125.02
 ├── G8: (scan abc@bc,cols=(1-3),constrained)
 │    └── []
 │         ├── best: (scan abc@bc,cols=(1-3),constrained)
 │         └── cost: 125.02
 ├── G9: (eq G10 G11)
 ├── G10: (variable a)
 └── G11: (variable b)

exec-ddl
CREATE TABLE kfloat (k FLOAT PRIMARY KEY)
----

memo
SELECT * FROM abc JOIN kfloat ON a=k
----
memo (optimized, ~14KB, required=[presentation: a:1,b:2,c:3,k:7])
 ├── G1: (inner-join G2 G3 G4) (inner-join G3 G2 G4)
 │    └── [presentation: a:1,b:2,c:3,k:7]
 │         ├── best: (inner-join G3 G2 G4)
 │         └── cost: 2198.22
 ├── G2: (scan abc,cols=(1-3)) (scan abc@ab,cols=(1-3)) (scan abc@bc,cols=(1-3))
 │    └── []
 │         ├── best: (scan abc,cols=(1-3))
 │         └── cost: 135.72
 ├── G3: (scan kfloat,cols=(7))
 │    └── []
 │         ├── best: (scan kfloat,cols=(7))
 │         └── cost: 1048.22
 ├── G4: (filters G5)
 ├── G5: (eq G6 G7)
 ├── G6: (variable a)
 └── G7: (variable k)

# We should only pick up one equivalency.
opt
SELECT * FROM abc JOIN xyz ON a=x AND a=y
----
inner-join (lookup abc@ab)
 ├── columns: a:1!null b:2 c:3 x:7!null y:8!null z:9
 ├── key columns: [7] = [1]
 ├── fd: (1)==(7,8), (7)==(1,8), (8)==(1,7)
 ├── select
 │    ├── columns: x:7!null y:8!null z:9
 │    ├── fd: (7)==(8), (8)==(7)
 │    ├── scan xyz@yz
 │    │    ├── columns: x:7 y:8!null z:9
 │    │    └── constraint: /8/9/10: (/NULL - ]
 │    └── filters
 │         └── x:7 = y:8 [outer=(7,8), constraints=(/7: (/NULL - ]; /8: (/NULL - ]), fd=(7)==(8), (8)==(7)]
 └── filters (true)

# Verify multiple merge-joins can be chained.
opt
SELECT * FROM abc JOIN xyz ON a=x AND b=y RIGHT OUTER JOIN stu ON a=s
----
left-join (merge)
 ├── columns: a:1 b:2 c:3 x:7 y:8 z:9 s:13!null t:14!null u:15!null
 ├── left ordering: +13
 ├── right ordering: +1
 ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2)
 ├── scan stu
 │    ├── columns: s:13!null t:14!null u:15!null
 │    ├── key: (13-15)
 │    └── ordering: +13
 ├── inner-join (merge)
 │    ├── columns: a:1!null b:2!null c:3 x:7!null y:8!null z:9
 │    ├── left ordering: +7,+8
 │    ├── right ordering: +1,+2
 │    ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2)
 │    ├── ordering: +(1|7) [actual: +7]
 │    ├── scan xyz@xy
 │    │    ├── columns: x:7 y:8 z:9
 │    │    └── ordering: +7,+8
 │    ├── scan abc@ab
 │    │    ├── columns: a:1 b:2 c:3
 │    │    └── ordering: +1,+2
 │    └── filters (true)
 └── filters (true)

opt
SELECT * FROM abc JOIN xyz ON a=x AND b=y RIGHT OUTER JOIN stu ON a=u AND y=t
----
left-join (merge)
 ├── columns: a:1 b:2 c:3 x:7 y:8 z:9 s:13!null t:14!null u:15!null
 ├── left ordering: +15,+14
 ├── right ordering: +1,+8
 ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2)
 ├── scan stu@uts
 │    ├── columns: s:13!null t:14!null u:15!null
 │    ├── key: (13-15)
 │    └── ordering: +15,+14
 ├── inner-join (merge)
 │    ├── columns: a:1!null b:2!null c:3 x:7!null y:8!null z:9
 │    ├── left ordering: +7,+8
 │    ├── right ordering: +1,+2
 │    ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2)
 │    ├── ordering: +(1|7),+(2|8) [actual: +7,+8]
 │    ├── scan xyz@xy
 │    │    ├── columns: x:7 y:8 z:9
 │    │    └── ordering: +7,+8
 │    ├── scan abc@ab
 │    │    ├── columns: a:1 b:2 c:3
 │    │    └── ordering: +1,+2
 │    └── filters (true)
 └── filters (true)

# Generate a merge join with two partial indexes.

exec-ddl
CREATE TABLE partial_a (a INT, INDEX (a) WHERE a > 0)
----

exec-ddl
CREATE TABLE partial_b (b INT, INDEX (b) WHERE b > 0)
----

opt
SELECT a, b FROM partial_a JOIN partial_b ON a = b WHERE a > 0 AND b > 0
----
inner-join (merge)
 ├── columns: a:1!null b:5!null
 ├── left ordering: +1
 ├── right ordering: +5
 ├── fd: (1)==(5), (5)==(1)
 ├── scan partial_a@partial_a_a_idx,partial
 │    ├── columns: a:1!null
 │    └── ordering: +1
 ├── scan partial_b@partial_b_b_idx,partial
 │    ├── columns: b:5!null
 │    └── ordering: +5
 └── filters (true)

# --------------------------------------------------
# GenerateLookupJoins
# --------------------------------------------------

exec-ddl
CREATE TABLE abcd (a INT, b INT, c INT, INDEX (a,b))
----

exec-ddl
CREATE TABLE abcde (a INT, b INT, c INT, d INT, e INT, INDEX (a,b,c))
----

# Covering case.
opt expect=GenerateLookupJoins
SELECT a,b,n,m FROM small JOIN abcd ON a=m
----
inner-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6!null b:7 n:2 m:1!null
 ├── key columns: [1] = [6]
 ├── fd: (1)==(6), (6)==(1)
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# Covering case, left-join.
opt expect=GenerateLookupJoins
SELECT a,b,n,m FROM small LEFT JOIN abcd ON a=m
----
left-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6 b:7 n:2 m:1
 ├── key columns: [1] = [6]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# Covering case, left-join, extra filter bound by index.
opt expect=GenerateLookupJoins
SELECT a,b,n,m FROM small LEFT JOIN abcd ON a=m AND b>n
----
left-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6 b:7 n:2 m:1
 ├── lookup expression
 │    └── filters
 │         ├── b:7 > n:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# Non-covering case.
opt expect=GenerateLookupJoins
SELECT * FROM small JOIN abcd ON a=m
----
inner-join (lookup abcd)
 ├── columns: m:1!null n:2 a:6!null b:7 c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7 abcd.rowid:9!null
 │    ├── key columns: [1] = [6]
 │    ├── fd: (9)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Non-covering case, left join.
opt expect=GenerateLookupJoins
SELECT * FROM small LEFT JOIN abcd ON a=m
----
left-join (lookup abcd)
 ├── columns: m:1 n:2 a:6 b:7 c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:6 b:7 abcd.rowid:9
 │    ├── key columns: [1] = [6]
 │    ├── fd: (9)-->(6,7)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Non-covering case, extra filter bound by index.
opt expect=GenerateLookupJoins
SELECT * FROM small JOIN abcd ON a=m AND b>n
----
inner-join (lookup abcd)
 ├── columns: m:1!null n:2!null a:6!null b:7!null c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2!null a:6!null b:7!null abcd.rowid:9!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > n:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Non-covering case, extra filter bound by index, left join.
opt expect=GenerateLookupJoins
SELECT * FROM small LEFT JOIN abcd ON a=m AND b>n
----
left-join (lookup abcd)
 ├── columns: m:1 n:2 a:6 b:7 c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:6 b:7 abcd.rowid:9
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > n:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Non-covering case, extra filter not bound by index.
opt expect=GenerateLookupJoins
SELECT * FROM small JOIN abcd ON a=m AND c>n
----
inner-join (lookup abcd)
 ├── columns: m:1!null n:2!null a:6!null b:7 c:8!null
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7 abcd.rowid:9!null
 │    ├── key columns: [1] = [6]
 │    ├── fd: (9)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters
      └── c:8 > n:2 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ])]

# Non-covering case, extra filter not bound by index, left join.
# In this case, we can generate lookup joins as paired-joins.
opt expect=GenerateLookupJoins
SELECT * FROM small LEFT JOIN abcd ON a=m AND c>n
----
left-join (lookup abcd)
 ├── columns: m:1 n:2 a:6 b:7 c:8
 ├── key columns: [15] = [9]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:12 b:13 abcd.rowid:15 continuation:18
 │    ├── key columns: [1] = [12]
 │    ├── first join in paired joiner; continuation column: continuation:18
 │    ├── fd: (15)-->(12,13,18)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters
      └── c:8 > n:2 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ])]


# Verify rule application when we can do a lookup join on both sides.
exploretrace rule=GenerateLookupJoins
SELECT * FROM abc JOIN xyz ON a=x AND a=y
----
----
================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: a:1!null b:2 c:3 x:7!null y:8!null z:9
   ├── fd: (1)==(7,8), (7)==(1,8), (8)==(1,7)
   ├── select
   │    ├── columns: x:7!null y:8!null z:9
   │    ├── fd: (7)==(8), (8)==(7)
   │    ├── scan xyz@yz
   │    │    ├── columns: x:7 y:8!null z:9
   │    │    └── constraint: /8/9/10: (/NULL - ]
   │    └── filters
   │         └── x:7 = y:8 [outer=(7,8), constraints=(/7: (/NULL - ]; /8: (/NULL - ]), fd=(7)==(8), (8)==(7)]
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters
        └── a:1 = x:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]

New expression 1 of 1:
  inner-join (lookup abc@ab)
   ├── columns: a:1!null b:2 c:3 x:7!null y:8!null z:9
   ├── key columns: [7] = [1]
   ├── fd: (1)==(7,8), (7)==(1,8), (8)==(1,7)
   ├── select
   │    ├── columns: x:7!null y:8!null z:9
   │    ├── fd: (7)==(8), (8)==(7)
   │    ├── scan xyz@yz
   │    │    ├── columns: x:7 y:8!null z:9
   │    │    └── constraint: /8/9/10: (/NULL - ]
   │    └── filters
   │         └── x:7 = y:8 [outer=(7,8), constraints=(/7: (/NULL - ]; /8: (/NULL - ]), fd=(7)==(8), (8)==(7)]
   └── filters (true)
----
----

# Verify rule application when we can do a lookup join on the left side.
exploretrace rule=GenerateLookupJoins
SELECT * FROM abc JOIN xyz ON a=z
----
----
================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: a:1!null b:2 c:3 x:7 y:8 z:9!null
   ├── fd: (1)==(9), (9)==(1)
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   └── filters
        └── a:1 = z:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

No new expressions.

================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: a:1!null b:2 c:3 x:7 y:8 z:9!null
   ├── fd: (1)==(9), (9)==(1)
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters
        └── a:1 = z:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

New expression 1 of 1:
  inner-join (lookup abc@ab)
   ├── columns: a:1!null b:2 c:3 x:7 y:8 z:9!null
   ├── key columns: [9] = [1]
   ├── fd: (1)==(9), (9)==(1)
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   └── filters (true)
----
----

exploretrace rule=GenerateLookupJoins
SELECT * FROM abc RIGHT JOIN xyz ON a=z
----
----
================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  left-join (hash)
   ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters
        └── a:1 = z:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

New expression 1 of 1:
  left-join (lookup abc@ab)
   ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
   ├── key columns: [9] = [1]
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   └── filters (true)
----
----

# Verify rule application when we can do a lookup join on the right side.
exploretrace rule=GenerateLookupJoins
SELECT * FROM abc JOIN xyz ON c=x
----
----
================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: a:1 b:2 c:3!null x:7!null y:8 z:9
   ├── fd: (3)==(7), (7)==(3)
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   └── filters
        └── c:3 = x:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)]

New expression 1 of 1:
  inner-join (lookup xyz@xy)
   ├── columns: a:1 b:2 c:3!null x:7!null y:8 z:9
   ├── key columns: [3] = [7]
   ├── fd: (3)==(7), (7)==(3)
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters (true)

================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  inner-join (hash)
   ├── columns: a:1 b:2 c:3!null x:7!null y:8 z:9
   ├── fd: (3)==(7), (7)==(3)
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters
        └── c:3 = x:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)]

No new expressions.
----
----

exploretrace rule=GenerateLookupJoins
SELECT * FROM abc LEFT JOIN xyz ON c=x
----
----
================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  left-join (hash)
   ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   └── filters
        └── c:3 = x:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)]

New expression 1 of 1:
  left-join (lookup xyz@xy)
   ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
   ├── key columns: [3] = [7]
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters (true)
----
----

# Verify we don't generate a lookup join.
exploretrace rule=GenerateLookupJoins
SELECT * FROM abc RIGHT JOIN xyz ON c=x
----
----
================================================================================
GenerateLookupJoins
================================================================================
Source expression:
  left-join (hash)
   ├── columns: a:1 b:2 c:3 x:7 y:8 z:9
   ├── scan xyz
   │    └── columns: x:7 y:8 z:9
   ├── scan abc
   │    └── columns: a:1 b:2 c:3
   └── filters
        └── c:3 = x:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)]

No new expressions.
----
----

# Verify we don't generate lookup joins if there is a hint that says otherwise.
memo expect-not=GenerateLookupJoins
SELECT a,b,n,m FROM small INNER HASH JOIN abcd ON a=m
----
memo (optimized, ~11KB, required=[presentation: a:6,b:7,n:2,m:1])
 ├── G1: (inner-join G2 G3 G4)
 │    └── [presentation: a:6,b:7,n:2,m:1]
 │         ├── best: (inner-join G2 G3 G4)
 │         └── cost: 1136.32
 ├── G2: (scan small,cols=(1,2))
 │    └── []
 │         ├── best: (scan small,cols=(1,2))
 │         └── cost: 39.02
 ├── G3: (scan abcd,cols=(6,7)) (scan abcd@abcd_a_b_idx,cols=(6,7))
 │    └── []
 │         ├── best: (scan abcd@abcd_a_b_idx,cols=(6,7))
 │         └── cost: 1078.52
 ├── G4: (filters G5)
 ├── G5: (eq G6 G7)
 ├── G6: (variable a)
 └── G7: (variable m)

# Lookup semi-join with index that contains all columns in the join condition.
opt expect=GenerateLookupJoins
SELECT m, n FROM small WHERE EXISTS(SELECT 1 FROM abcd WHERE m = a)
----
semi-join (lookup abcd@abcd_a_b_idx)
 ├── columns: m:1 n:2
 ├── key columns: [1] = [6]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# We can generate a lookup semi-join when the index doesn't contain all
# columns in the join condition, using paired-joins.
opt expect=GenerateLookupJoins
SELECT m, n FROM small WHERE EXISTS(SELECT 1 FROM abcd WHERE m = a AND n = c)
----
semi-join (lookup abcd)
 ├── columns: m:1 n:2
 ├── key columns: [17] = [9]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:14!null abcd.rowid:17!null continuation:20
 │    ├── key columns: [1] = [14]
 │    ├── first join in paired joiner; continuation column: continuation:20
 │    ├── fd: (17)-->(14,20), (1)==(14), (14)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters
      └── n:2 = c:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]

# Lookup anti-join with index that contains all columns in the join condition.
opt expect=GenerateLookupJoins
SELECT m, n FROM small WHERE NOT EXISTS(SELECT 1 FROM abcd WHERE m = a)
----
anti-join (lookup abcd@abcd_a_b_idx)
 ├── columns: m:1 n:2
 ├── key columns: [1] = [6]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# We can generate a lookup anti-join when the index doesn't contain all
# columns in the join condition, using paired-joins.
opt expect=GenerateLookupJoins
SELECT m, n FROM small WHERE NOT EXISTS(SELECT 1 FROM abcd WHERE m = a AND n = c)
----
anti-join (lookup abcd)
 ├── columns: m:1 n:2
 ├── key columns: [17] = [9]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:14 abcd.rowid:17 continuation:20
 │    ├── key columns: [1] = [14]
 │    ├── first join in paired joiner; continuation column: continuation:20
 │    ├── fd: (17)-->(14,20)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters
      └── n:2 = c:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]

exec-ddl
CREATE INDEX shard_a_idx ON shard (shard_a, a)
----

# Generate an inner lookup join by synthesizing an equality constraint for an
# indexed computed column.
opt expect=GenerateLookupJoins
SELECT m, k FROM small INNER LOOKUP JOIN shard ON small.m = shard.a
----
project
 ├── columns: m:1!null k:6!null
 ├── fd: (6)-->(1)
 └── inner-join (lookup shard@shard_a_idx)
      ├── columns: m:1!null k:6!null a:7!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [14 1] = [9 7]
      ├── fd: (6)-->(7), (1)==(7), (7)==(1)
      ├── project
      │    ├── columns: shard_a_eq:14 m:1
      │    ├── immutable
      │    ├── fd: (1)-->(14)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── projections
      │         └── mod(fnv32(crdb_internal.datums_to_bytes(m:1)), 3) [as=shard_a_eq:14, outer=(1), immutable]
      └── filters (true)

# Generate a left lookup join by synthesizing an equality constraint for an
# indexed computed column.
opt expect=GenerateLookupJoins
SELECT m, k FROM small LEFT LOOKUP JOIN shard ON small.m = shard.a
----
project
 ├── columns: m:1 k:6
 └── left-join (lookup shard@shard_a_idx)
      ├── columns: m:1 k:6 a:7
      ├── flags: force lookup join (into right side)
      ├── key columns: [14 1] = [9 7]
      ├── fd: (6)-->(7)
      ├── project
      │    ├── columns: shard_a_eq:14 m:1
      │    ├── immutable
      │    ├── fd: (1)-->(14)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── projections
      │         └── mod(fnv32(crdb_internal.datums_to_bytes(m:1)), 3) [as=shard_a_eq:14, outer=(1), immutable]
      └── filters (true)

# Generate a semi lookup join by synthesizing an equality constraint for an
# indexed computed column.
opt expect=GenerateLookupJoins
SELECT m FROM small WHERE EXISTS (SELECT * FROM shard WHERE small.m = shard.a)
----
semi-join (lookup shard@shard_a_idx)
 ├── columns: m:1
 ├── key columns: [15 1] = [9 7]
 ├── project
 │    ├── columns: shard_a_eq:15 m:1
 │    ├── immutable
 │    ├── fd: (1)-->(15)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── mod(fnv32(crdb_internal.datums_to_bytes(m:1)), 3) [as=shard_a_eq:15, outer=(1), immutable]
 └── filters (true)

# Generate an anti lookup join by synthesizing an equality constraint for an
# indexed computed column.
opt expect=GenerateLookupJoins
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM shard WHERE small.m = shard.a)
----
anti-join (lookup shard@shard_a_idx)
 ├── columns: m:1
 ├── key columns: [15 1] = [9 7]
 ├── project
 │    ├── columns: shard_a_eq:15 m:1
 │    ├── immutable
 │    ├── fd: (1)-->(15)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── mod(fnv32(crdb_internal.datums_to_bytes(m:1)), 3) [as=shard_a_eq:15, outer=(1), immutable]
 └── filters (true)

exec-ddl
DROP INDEX shard_a_idx
----

exec-ddl
CREATE INDEX shard_a_b_idx ON shard (shard_a_b, a, b)
----

# Generate a lookup join by synthesizing an equality constraint for an indexed
# computed column when the expression references multiple columns.
opt expect=GenerateLookupJoins
SELECT m, k FROM small INNER LOOKUP JOIN shard ON small.m = shard.a AND small.n = shard.b
----
project
 ├── columns: m:1!null k:6!null
 ├── fd: (6)-->(1)
 └── inner-join (lookup shard@shard_a_b_idx)
      ├── columns: m:1!null n:2!null k:6!null a:7!null b:8!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [14 1 2] = [10 7 8]
      ├── fd: (6)-->(7,8), (1)==(7), (7)==(1), (2)==(8), (8)==(2)
      ├── project
      │    ├── columns: shard_a_b_eq:14 m:1 n:2
      │    ├── immutable
      │    ├── fd: (1,2)-->(14)
      │    ├── scan small
      │    │    └── columns: m:1 n:2
      │    └── projections
      │         └── mod(fnv32(crdb_internal.datums_to_bytes(m:1, n:2)), 3) [as=shard_a_b_eq:14, outer=(1,2), immutable]
      └── filters (true)

exec-ddl
DROP INDEX shard_a_b_idx
----

exec-ddl
CREATE INDEX shard_b_null_idx ON shard (shard_b_null, b)
----

# Do not synthesize an equality constraint for a nullable computed column.
opt expect-not=GenerateLookupJoins
SELECT m, k FROM small INNER LOOKUP JOIN shard ON small.m = shard.b
----
project
 ├── columns: m:1!null k:6!null
 ├── fd: (6)-->(1)
 └── inner-join (hash)
      ├── columns: m:1!null k:6!null b:8!null
      ├── flags: force lookup join (into right side)
      ├── fd: (6)-->(8), (1)==(8), (8)==(1)
      ├── scan small
      │    └── columns: m:1
      ├── scan shard@shard_b_null_idx
      │    ├── columns: k:6!null b:8
      │    ├── key: (6)
      │    └── fd: (6)-->(8)
      └── filters
           └── m:1 = b:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

exec-ddl
DROP INDEX shard_b_null_idx
----

# Regression test for #59615 and #78681. Ensure that invalid lookup joins are
# not created for left, semi, and anti joins.
exec-ddl
CREATE TABLE t59615 (
  x INT NOT NULL CHECK (x in (1, 3)),
  y INT NOT NULL,
  z INT,
  PRIMARY KEY (x, y)
)
----

opt expect=GenerateLookupJoins
SELECT * FROM (VALUES (1), (2)) AS u(y) LEFT JOIN t59615 t ON u.y = t.y
----
left-join (lookup t59615 [as=t])
 ├── columns: y:1!null x:2 y:3 z:4
 ├── lookup expression
 │    └── filters
 │         ├── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)]
 │         └── column1:1 = y:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 ├── cardinality: [2 - ]
 ├── fd: (2,3)-->(4)
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── (1,)
 │    └── (2,)
 └── filters (true)

# Regression test for #78681.
opt expect=GenerateLookupJoins
SELECT * FROM (VALUES (1), (2)) AS u(y) WHERE EXISTS (
  SELECT * FROM t59615 t WHERE u.y = t.y
)
----
semi-join (lookup t59615 [as=t])
 ├── columns: y:1!null
 ├── lookup expression
 │    └── filters
 │         ├── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)]
 │         └── column1:1 = y:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 ├── cardinality: [0 - 2]
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── (1,)
 │    └── (2,)
 └── filters (true)

opt expect=GenerateLookupJoins
SELECT * FROM (VALUES (1), (2)) AS u(y) WHERE NOT EXISTS (
  SELECT * FROM t59615 t WHERE u.y = t.y
)
----
anti-join (lookup t59615 [as=t])
 ├── columns: y:1!null
 ├── lookup expression
 │    └── filters
 │         ├── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)]
 │         └── column1:1 = y:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 ├── cardinality: [0 - 2]
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── (1,)
 │    └── (2,)
 └── filters (true)

exec-ddl
CREATE TABLE lookup_expr (
  r STRING NOT NULL CHECK (r IN ('east', 'west')),
  k INT NOT NULL,
  u INT,
  v INT,
  w INT,
  x INT,
  y INT NOT NULL CHECK (y IN (10, 20)),
  z INT NOT NULL CHECK (z = 5),
  PRIMARY KEY (r, k),
  INDEX idx_x_etc (r, x, y, z, w),
  INDEX idx_u_etc (r, u, y, v, z, w),
  INDEX idx_vrw (v, r, w)
)
----

# The filters in the lookup expression should constrain the primary index.
# The left lookup join is wrapped in a Project since r was not one of the
# original output columns of the join.
opt expect=GenerateLookupJoins
SELECT t.k FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(k, v) LEFT JOIN lookup_expr t
ON q.k = t.k
----
project
 ├── columns: k:4
 ├── cardinality: [3 - ]
 └── project
      ├── columns: column1:1!null k:4
      ├── cardinality: [3 - ]
      └── left-join (lookup lookup_expr [as=t])
           ├── columns: column1:1!null r:3 k:4
           ├── lookup expression
           │    └── filters
           │         ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
           │         └── column1:1 = k:4 [outer=(1,4), constraints=(/1: (/NULL - ]; /4: (/NULL - ]), fd=(1)==(4), (4)==(1)]
           ├── cardinality: [3 - ]
           ├── values
           │    ├── columns: column1:1!null
           │    ├── cardinality: [3 - 3]
           │    ├── (1,)
           │    ├── (2,)
           │    └── (3,)
           └── filters (true)

# The filters in the lookup expression should constrain the primary index.
opt expect=GenerateLookupJoins
SELECT q.k FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(k, v) WHERE NOT EXISTS (
  SELECT * FROM lookup_expr t WHERE q.k = t.k
)
----
anti-join (lookup lookup_expr [as=t])
 ├── columns: k:1!null
 ├── lookup expression
 │    └── filters
 │         ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │         └── column1:1 = k:4 [outer=(1,4), constraints=(/1: (/NULL - ]; /4: (/NULL - ]), fd=(1)==(4), (4)==(1)]
 ├── cardinality: [0 - 3]
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [3 - 3]
 │    ├── (1,)
 │    ├── (2,)
 │    └── (3,)
 └── filters (true)

# The filters in the lookup expression should constrain all columns in
# idx_x_etc.
opt expect=GenerateLookupJoins
SELECT * FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(w, x) LEFT JOIN lookup_expr t
ON q.w = t.w AND q.x = t.x
----
left-join (lookup lookup_expr [as=t])
 ├── columns: w:1!null x:2 r:3 k:4 u:5 v:6 w:7 x:8 y:9 z:10
 ├── key columns: [3 4] = [3 4]
 ├── lookup columns are key
 ├── cardinality: [3 - ]
 ├── fd: (3,4)-->(5-9)
 ├── left-join (lookup lookup_expr@idx_x_etc [as=t])
 │    ├── columns: column1:1!null column2:2 r:3 k:4 w:7 x:8 y:9 z:10
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │         ├── y:9 IN (10, 20) [outer=(9), constraints=(/9: [/10 - /10] [/20 - /20]; tight)]
 │    │         ├── column2:2 = x:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]
 │    │         ├── "lookup_join_const_col_@10":13 = z:10 [outer=(10,13), constraints=(/10: (/NULL - ]; /13: (/NULL - ]), fd=(10)==(13), (13)==(10)]
 │    │         └── column1:1 = w:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]
 │    ├── cardinality: [3 - ]
 │    ├── fd: (3,4)-->(7-10)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@10":13!null column1:1!null column2:2
 │    │    ├── cardinality: [3 - 3]
 │    │    ├── fd: ()-->(13)
 │    │    ├── values
 │    │    │    ├── columns: column1:1!null column2:2
 │    │    │    ├── cardinality: [3 - 3]
 │    │    │    ├── (1, 10)
 │    │    │    ├── (2, 20)
 │    │    │    └── (3, NULL)
 │    │    └── projections
 │    │         └── 5 [as="lookup_join_const_col_@10":13]
 │    └── filters (true)
 └── filters (true)

# The filters in the lookup expression should constrain only the first 2 columns
# of idx_u_etc.
opt expect=GenerateLookupJoins
SELECT * FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(w, u) LEFT JOIN lookup_expr t
ON q.w = t.w AND q.u = t.u
----
left-join (lookup lookup_expr [as=t])
 ├── columns: w:1!null u:2 r:3 k:4 u:5 v:6 w:7 x:8 y:9 z:10
 ├── key columns: [3 4] = [3 4]
 ├── lookup columns are key
 ├── cardinality: [3 - ]
 ├── fd: (3,4)-->(5-9)
 ├── left-join (lookup lookup_expr@idx_u_etc [as=t])
 │    ├── columns: column1:1!null column2:2 r:3 k:4 u:5 v:6 w:7 y:9 z:10
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │         └── column2:2 = u:5 [outer=(2,5), constraints=(/2: (/NULL - ]; /5: (/NULL - ]), fd=(2)==(5), (5)==(2)]
 │    ├── cardinality: [3 - ]
 │    ├── fd: (3,4)-->(5-7,9,10)
 │    ├── values
 │    │    ├── columns: column1:1!null column2:2
 │    │    ├── cardinality: [3 - 3]
 │    │    ├── (1, 10)
 │    │    ├── (2, 20)
 │    │    └── (3, NULL)
 │    └── filters
 │         └── column1:1 = w:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]
 └── filters (true)

# Ensure that we constrain all columns in idx_vrw, not just v.
opt expect=GenerateLookupJoins
SELECT * FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(v, w) LEFT JOIN lookup_expr t
ON q.v = t.v AND q.w = t.w
----
left-join (lookup lookup_expr [as=t])
 ├── columns: v:1!null w:2 r:3 k:4 u:5 v:6 w:7 x:8 y:9 z:10
 ├── key columns: [3 4] = [3 4]
 ├── lookup columns are key
 ├── cardinality: [3 - ]
 ├── fd: (3,4)-->(5-9)
 ├── left-join (lookup lookup_expr@idx_vrw [as=t])
 │    ├── columns: column1:1!null column2:2 r:3 k:4 v:6 w:7
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │         ├── column1:1 = v:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    │         └── column2:2 = w:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]
 │    ├── cardinality: [3 - ]
 │    ├── fd: (3,4)-->(6,7)
 │    ├── values
 │    │    ├── columns: column1:1!null column2:2
 │    │    ├── cardinality: [3 - 3]
 │    │    ├── (1, 10)
 │    │    ├── (2, 20)
 │    │    └── (3, NULL)
 │    └── filters (true)
 └── filters (true)

exec-ddl
DROP INDEX idx_vrw
----

# The OR filter gets converted to an IN expression in the lookup expression
# filters.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(w, v) LEFT LOOKUP JOIN lookup_expr t
ON (t.u = 1 OR t.u = 2) AND q.v = t.v
----
left-join (lookup lookup_expr [as=t])
 ├── columns: w:1!null v:2 r:3 k:4 u:5 v:6 w:7 x:8 y:9 z:10
 ├── key columns: [3 4] = [3 4]
 ├── lookup columns are key
 ├── cardinality: [3 - ]
 ├── fd: (3,4)-->(5-9)
 ├── left-join (lookup lookup_expr@idx_u_etc [as=t])
 │    ├── columns: column1:1!null column2:2 r:3 k:4 u:5 v:6 w:7 y:9 z:10
 │    ├── flags: force lookup join (into right side)
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │         ├── u:5 IN (1, 2) [outer=(5), constraints=(/5: [/1 - /1] [/2 - /2]; tight)]
 │    │         ├── y:9 IN (10, 20) [outer=(9), constraints=(/9: [/10 - /10] [/20 - /20]; tight)]
 │    │         ├── column2:2 = v:6 [outer=(2,6), constraints=(/2: (/NULL - ]; /6: (/NULL - ]), fd=(2)==(6), (6)==(2)]
 │    │         └── "lookup_join_const_col_@10":13 = z:10 [outer=(10,13), constraints=(/10: (/NULL - ]; /13: (/NULL - ]), fd=(10)==(13), (13)==(10)]
 │    ├── cardinality: [3 - ]
 │    ├── fd: (3,4)-->(5-7,9,10)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@10":13!null column1:1!null column2:2
 │    │    ├── cardinality: [3 - 3]
 │    │    ├── fd: ()-->(13)
 │    │    ├── values
 │    │    │    ├── columns: column1:1!null column2:2
 │    │    │    ├── cardinality: [3 - 3]
 │    │    │    ├── (1, 10)
 │    │    │    ├── (2, 20)
 │    │    │    └── (3, NULL)
 │    │    └── projections
 │    │         └── 5 [as="lookup_join_const_col_@10":13]
 │    └── filters (true)
 └── filters (true)

# We can't build a lookup join with any of the indexes.
opt expect-not=GenerateLookupJoins
SELECT * FROM (VALUES (1, 10), (2, 20), (3, NULL)) AS q(w, x) LEFT JOIN lookup_expr t
ON q.w = t.w
----
right-join (hash)
 ├── columns: w:1!null x:2 r:3 k:4 u:5 v:6 w:7 x:8 y:9 z:10
 ├── cardinality: [3 - ]
 ├── fd: (3,4)-->(5-9)
 ├── scan lookup_expr [as=t]
 │    ├── columns: r:3!null k:4!null u:5 v:6 w:7 x:8 y:9!null z:10!null
 │    ├── check constraint expressions
 │    │    ├── r:3 IN ('east', 'west') [outer=(3), constraints=(/3: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │    ├── y:9 IN (10, 20) [outer=(9), constraints=(/9: [/10 - /10] [/20 - /20]; tight)]
 │    │    └── z:10 = 5 [outer=(10), constraints=(/10: [/5 - /5]; tight), fd=()-->(10)]
 │    ├── key: (3,4)
 │    └── fd: ()-->(10), (3,4)-->(5-9)
 ├── values
 │    ├── columns: column1:1!null column2:2
 │    ├── cardinality: [3 - 3]
 │    ├── (1, 10)
 │    ├── (2, 20)
 │    └── (3, NULL)
 └── filters
      └── column1:1 = w:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]

# Regression test for #59738. findConstantFilterCols should not panic when it
# finds constant filters for columns that are not in the index of the lookup
# table.
exec-ddl
CREATE TABLE t59738_ab (a INT, b INT AS (a + 10) STORED, INDEX (a))
----

exec-ddl
CREATE TABLE t59738_cd (c INT, d INT)
----

opt expect=GenerateLookupJoins
SELECT * FROM t59738_cd LEFT LOOKUP JOIN t59738_ab ON a = c AND d > 5
----
left-join (lookup t59738_ab)
 ├── columns: c:1 d:2 a:6 b:7
 ├── key columns: [8] = [8]
 ├── lookup columns are key
 ├── fd: (6)~~>(7)
 ├── left-join (lookup t59738_ab@t59738_ab_a_idx)
 │    ├── columns: c:1 d:2 a:6 t59738_ab.rowid:8
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [6]
 │    ├── fd: (8)-->(6)
 │    ├── scan t59738_cd
 │    │    └── columns: c:1 d:2
 │    └── filters
 │         └── d:2 > 5 [outer=(2), constraints=(/2: [/6 - ]; tight)]
 └── filters (true)

# Regression test for #81968. The index used for the first join in a paired left
# join should have different column IDs than the index used for the second join.
exec-ddl
CREATE TABLE items (
    id        INT NOT NULL PRIMARY KEY,
    chat_id   INT NOT NULL,
    author_id INT NOT NULL,
    deleted   BOOL,
    value     INT,
    INDEX chat_id_idx (chat_id),
    INDEX deleted_chat_id_value_idx (deleted, chat_id, value)
)
----

exec-ddl
CREATE TABLE views (
    chat_id INT NOT NULL,
    user_id INT NOT NULL,
    PRIMARY KEY (chat_id, user_id)
)
----

opt
SELECT (SELECT count(items.id)
        FROM items
        WHERE items.chat_id = views.chat_id
          AND items.author_id != views.user_id)
FROM views
WHERE chat_id = 1
  AND user_id = 1;
----
project
 ├── columns: count:13!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(13)
 ├── group-by (streaming)
 │    ├── columns: count:12!null
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(12)
 │    ├── left-join (lookup items)
 │    │    ├── columns: views.chat_id:1!null user_id:2!null items.chat_id:6 author_id:7
 │    │    ├── key columns: [14] = [5]
 │    │    ├── lookup columns are key
 │    │    ├── second join in paired joiner
 │    │    ├── fd: ()-->(1,2,6)
 │    │    ├── left-join (lookup items@chat_id_idx)
 │    │    │    ├── columns: views.chat_id:1!null user_id:2!null id:14 items.chat_id:15 continuation:21
 │    │    │    ├── key columns: [1] = [15]
 │    │    │    ├── first join in paired joiner; continuation column: continuation:21
 │    │    │    ├── key: (14)
 │    │    │    ├── fd: ()-->(1,2,15), (14)-->(21)
 │    │    │    ├── scan views
 │    │    │    │    ├── columns: views.chat_id:1!null user_id:2!null
 │    │    │    │    ├── constraint: /1/2: [/1/1 - /1/1]
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    └── fd: ()-->(1,2)
 │    │    │    └── filters (true)
 │    │    └── filters
 │    │         └── author_id:7 != user_id:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │    └── aggregations
 │         └── count [as=count:12, outer=(6)]
 │              └── items.chat_id:6
 └── projections
      └── count:12 [as=count:13, outer=(12)]

# Regression test for #85504. Do not normalize deleted = false to NOT deleted in
# lookup expression.
opt
SELECT (SELECT count(items.id)
        FROM items
        WHERE items.chat_id = views.chat_id
          AND deleted = false
          AND items.value > 0
          AND items.author_id != views.user_id)
FROM views
WHERE chat_id = 1
  AND user_id = 1
----
project
 ├── columns: count:13!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(13)
 ├── group-by (streaming)
 │    ├── columns: count:12!null
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(12)
 │    ├── left-join (lookup items)
 │    │    ├── columns: views.chat_id:1!null user_id:2!null items.chat_id:6 author_id:7 deleted:8 value:9
 │    │    ├── key columns: [14] = [5]
 │    │    ├── lookup columns are key
 │    │    ├── second join in paired joiner
 │    │    ├── fd: ()-->(1,2,6)
 │    │    ├── left-join (lookup items@deleted_chat_id_value_idx)
 │    │    │    ├── columns: views.chat_id:1!null user_id:2!null id:14 items.chat_id:15 deleted:17 value:18 continuation:23
 │    │    │    ├── lookup expression
 │    │    │    │    └── filters
 │    │    │    │         ├── value:18 > 0 [outer=(18), constraints=(/18: [/1 - ]; tight)]
 │    │    │    │         ├── "lookup_join_const_col_@8":22 = deleted:17 [outer=(17,22), constraints=(/17: (/NULL - ]; /22: (/NULL - ]), fd=(17)==(22), (22)==(17)]
 │    │    │    │         └── views.chat_id:1 = items.chat_id:15 [outer=(1,15), constraints=(/1: (/NULL - ]; /15: (/NULL - ]), fd=(1)==(15), (15)==(1)]
 │    │    │    ├── first join in paired joiner; continuation column: continuation:23
 │    │    │    ├── key: (14)
 │    │    │    ├── fd: ()-->(1,2,15,17), (14)-->(18,23)
 │    │    │    ├── project
 │    │    │    │    ├── columns: "lookup_join_const_col_@8":22!null views.chat_id:1!null user_id:2!null
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    ├── fd: ()-->(1,2,22)
 │    │    │    │    ├── scan views
 │    │    │    │    │    ├── columns: views.chat_id:1!null user_id:2!null
 │    │    │    │    │    ├── constraint: /1/2: [/1/1 - /1/1]
 │    │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    │    ├── key: ()
 │    │    │    │    │    └── fd: ()-->(1,2)
 │    │    │    │    └── projections
 │    │    │    │         └── false [as="lookup_join_const_col_@8":22]
 │    │    │    └── filters (true)
 │    │    └── filters
 │    │         └── author_id:7 != user_id:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │    └── aggregations
 │         └── count [as=count:12, outer=(6)]
 │              └── items.chat_id:6
 └── projections
      └── count:12 [as=count:13, outer=(12)]

# Don't plan a lookup join with no equalities unless the input has only one row
# or the join has a lookup hint.
opt expect-not=(GenerateLookupJoins,GenerateLookupJoinsWithFilter)
SELECT a,b,n,m FROM small JOIN abcd ON a>=m
----
inner-join (cross)
 ├── columns: a:6!null b:7 n:2 m:1!null
 ├── scan abcd@abcd_a_b_idx
 │    └── columns: a:6 b:7
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      └── a:6 >= m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])]

opt expect=GenerateLookupJoins
SELECT a,b,n,m FROM (SELECT * FROM small LIMIT 1) JOIN abcd ON a>=m
----
inner-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6!null b:7 n:2 m:1!null
 ├── lookup expression
 │    └── filters
 │         └── a:6 >= m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])]
 ├── fd: ()-->(1,2)
 ├── scan small
 │    ├── columns: m:1 n:2
 │    ├── limit: 1
 │    ├── key: ()
 │    └── fd: ()-->(1,2)
 └── filters (true)

opt expect=GenerateLookupJoins
SELECT a,b,n,m FROM small INNER LOOKUP JOIN abcd ON a>=m
----
inner-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6!null b:7 n:2 m:1!null
 ├── flags: force lookup join (into right side)
 ├── lookup expression
 │    └── filters
 │         └── a:6 >= m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# Don't use the inequality in the lookup condition when lookup joins with
# variable inequalities are disabled.
opt expect=GenerateLookupJoins set=variable_inequality_lookup_join_enabled=false
SELECT a,b,n,m FROM small LEFT JOIN abcd ON a=m AND b>n
----
left-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6 b:7 n:2 m:1
 ├── key columns: [1] = [6]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      └── b:7 > n:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]

# Don't generate a lookup join when inequality lookup joins are disabled.
opt expect-not=GenerateLookupJoins set=variable_inequality_lookup_join_enabled=false
SELECT a,b,n,m FROM (SELECT * FROM small LIMIT 1) JOIN abcd ON a>=m
----
inner-join (cross)
 ├── columns: a:6!null b:7 n:2 m:1!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
 ├── fd: ()-->(1,2)
 ├── scan abcd@abcd_a_b_idx
 │    └── columns: a:6 b:7
 ├── scan small
 │    ├── columns: m:1 n:2
 │    ├── limit: 1
 │    ├── key: ()
 │    └── fd: ()-->(1,2)
 └── filters
      └── a:6 >= m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])]

# --------------------------------------------------
# GenerateLookupJoinsWithFilter
# --------------------------------------------------
#
# The rule and cases are similar to GenerateLookupJoins, except that we have a
# filter that was pushed down to the lookup side (which needs to be pulled back
# into the ON condition).

# Covering case.
opt expect=GenerateLookupJoinsWithFilter
SELECT a,b,n,m FROM small JOIN abcd ON a=m AND b>1
----
inner-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6!null b:7!null n:2 m:1!null
 ├── lookup expression
 │    └── filters
 │         ├── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 ├── fd: (1)==(6), (6)==(1)
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# Covering case, left-join.
opt expect=GenerateLookupJoinsWithFilter
SELECT a,b,n,m FROM small LEFT JOIN abcd ON a=m AND b>1
----
left-join (lookup abcd@abcd_a_b_idx)
 ├── columns: a:6 b:7 n:2 m:1
 ├── lookup expression
 │    └── filters
 │         ├── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters (true)

# Non-covering case.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small JOIN abcd ON a=m AND b>1
----
inner-join (lookup abcd)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null abcd.rowid:9!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Non-covering case, left join.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small LEFT JOIN abcd ON a=m AND b>1
----
left-join (lookup abcd)
 ├── columns: m:1 n:2 a:6 b:7 c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:6 b:7 abcd.rowid:9
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Non-covering case, extra filter bound by index.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small JOIN abcd ON a=m AND b>n AND b>1
----
inner-join (lookup abcd)
 ├── columns: m:1!null n:2!null a:6!null b:7!null c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2!null a:6!null b:7!null abcd.rowid:9!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > n:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters
 │         └── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 └── filters (true)

# Non-covering case, extra filter bound by index, left join.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small LEFT JOIN abcd ON a=m AND b>n AND b>1
----
left-join (lookup abcd)
 ├── columns: m:1 n:2 a:6 b:7 c:8
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:6 b:7 abcd.rowid:9
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > n:2 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ])]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters
 │         └── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 └── filters (true)

# Non-covering case, extra filter not bound by index.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small JOIN abcd ON a=m AND c>n AND b>1
----
inner-join (lookup abcd)
 ├── columns: m:1!null n:2!null a:6!null b:7!null c:8!null
 ├── key columns: [9] = [9]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null abcd.rowid:9!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 > 1 [outer=(7), constraints=(/7: [/2 - ]; tight)]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (9)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters
      └── c:8 > n:2 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ])]

# Non-covering case, extra filter not bound by index, left join.
# In this case, we can generate lookup joins as paired-joins.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small LEFT JOIN abcd ON a=m AND c>n AND b>1
----
left-join (lookup abcd)
 ├── columns: m:1 n:2 a:6 b:7 c:8
 ├── key columns: [15] = [9]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:12 b:13 abcd.rowid:15 continuation:18
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:13 > 1 [outer=(13), constraints=(/13: [/2 - ]; tight)]
 │    │         └── m:1 = a:12 [outer=(1,12), constraints=(/1: (/NULL - ]; /12: (/NULL - ]), fd=(1)==(12), (12)==(1)]
 │    ├── first join in paired joiner; continuation column: continuation:18
 │    ├── fd: (15)-->(12,13,18)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters
      └── c:8 > n:2 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ])]

# Constant columns are projected and used by lookup joiner.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b=10
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8 d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8 abcde.rowid:11!null
 │    ├── key columns: [1 14] = [6 7]
 │    ├── fd: ()-->(7), (11)-->(6,8), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null m:1 n:2
 │    │    ├── fd: ()-->(14)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         └── 10 [as="lookup_join_const_col_@7":14]
 │    └── filters (true)
 └── filters (true)

# Constant columns not projected if not prefix of an index.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND c=10
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7 c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(8), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7 c:8!null abcde.rowid:11!null
 │    ├── key columns: [1] = [6]
 │    ├── fd: ()-->(8), (11)-->(6,7), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters
 │         └── c:8 = 10 [outer=(8), constraints=(/8: [/10 - /10]; tight), fd=()-->(8)]
 └── filters (true)

# Constant value projections should have the type of the constant, not the
# indexed column.
opt expect=GenerateLookupJoinsWithFilter format=show-types
SELECT * FROM (VALUES (1), (3)) v(i) INNER LOOKUP JOIN tchar ON a = i AND c = 'foo'
----
inner-join (lookup tchar)
 ├── columns: i:1(int!null) a:2(int!null) c:3(varchar!null)
 ├── flags: force lookup join (into right side)
 ├── key columns: [1 6] = [2 3]
 ├── lookup columns are key
 ├── cardinality: [0 - 2]
 ├── fd: ()-->(3), (1)==(2), (2)==(1)
 ├── project
 │    ├── columns: "lookup_join_const_col_@3":6(string!null) column1:1(int!null)
 │    ├── cardinality: [2 - 2]
 │    ├── fd: ()-->(6)
 │    ├── values
 │    │    ├── columns: column1:1(int!null)
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1,) [type=tuple{int}]
 │    │    └── (3,) [type=tuple{int}]
 │    └── projections
 │         └── 'foo' [as="lookup_join_const_col_@3":6, type=string]
 └── filters (true)

# Multiple constant columns projected and used by lookup joiner.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b=10 AND c=10
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7,8), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcde.rowid:11!null
 │    ├── key columns: [1 14 15] = [6 7 8]
 │    ├── fd: ()-->(7,8), (11)-->(6), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null "lookup_join_const_col_@8":15!null m:1 n:2
 │    │    ├── fd: ()-->(14,15)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         ├── 10 [as="lookup_join_const_col_@7":14]
 │    │         └── 10 [as="lookup_join_const_col_@8":15]
 │    └── filters (true)
 └── filters (true)

# Column constrained to multiple constants used by lookup joiner.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b IN (10, 20, 30)
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8 d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8 abcde.rowid:11!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 IN (10, 20, 30) [outer=(7), constraints=(/7: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (11)-->(6-8), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# One column constrained to multiple constants and another constrained to a
# single constant used by lookup joiner.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b IN (10, 20, 30) AND c=10
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(8), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcde.rowid:11!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 IN (10, 20, 30) [outer=(7), constraints=(/7: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │         ├── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    │         └── "lookup_join_const_col_@8":14 = c:8 [outer=(8,14), constraints=(/8: (/NULL - ]; /14: (/NULL - ]), fd=(8)==(14), (14)==(8)]
 │    ├── fd: ()-->(8), (11)-->(6,7), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@8":14!null m:1 n:2
 │    │    ├── fd: ()-->(14)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         └── 10 [as="lookup_join_const_col_@8":14]
 │    └── filters (true)
 └── filters (true)

# One column constrained to a single constant and another constrained to
# multiple constants. A project should not be added to the input for the single
# constant - the equality should be included in the the lookup expression.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b=10 AND c IN (10, 20, 30)
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcde.rowid:11!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── c:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │         ├── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    │         └── "lookup_join_const_col_@7":14 = b:7 [outer=(7,14), constraints=(/7: (/NULL - ]; /14: (/NULL - ]), fd=(7)==(14), (14)==(7)]
 │    ├── fd: ()-->(7), (11)-->(6,8), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null m:1 n:2
 │    │    ├── fd: ()-->(14)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         └── 10 [as="lookup_join_const_col_@7":14]
 │    └── filters (true)
 └── filters (true)

# Multiple columns constrained to multiple constants used by lookup joiner.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b IN (10, 20) AND c IN (30, 40)
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcde.rowid:11!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 IN (10, 20) [outer=(7), constraints=(/7: [/10 - /10] [/20 - /20]; tight)]
 │    │         ├── c:8 IN (30, 40) [outer=(8), constraints=(/8: [/30 - /30] [/40 - /40]; tight)]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (11)-->(6-8), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Filters are reduced properly as constant filters are extracted.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b=10 AND c=10 AND d=10
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9!null e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7-9), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcde.rowid:11!null
 │    ├── key columns: [1 14 15] = [6 7 8]
 │    ├── fd: ()-->(7,8), (11)-->(6), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null "lookup_join_const_col_@8":15!null m:1 n:2
 │    │    ├── fd: ()-->(14,15)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         ├── 10 [as="lookup_join_const_col_@7":14]
 │    │         └── 10 [as="lookup_join_const_col_@8":15]
 │    └── filters (true)
 └── filters
      └── d:9 = 10 [outer=(9), constraints=(/9: [/10 - /10]; tight), fd=()-->(9)]

# Non equality filters don't trigger constant projection.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcde ON a=m AND b<10
----
inner-join (lookup abcde)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8 d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcde@abcde_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8 abcde.rowid:11!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 < 10 [outer=(7), constraints=(/7: (/NULL - /9]; tight)]
 │    │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    ├── fd: (11)-->(6-8), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

# Lookup Joiner uses the constant equality columns at the same time as the explicit
# column equalities.
opt expect=GenerateLookupJoinsWithFilter
SELECT a, b, c FROM small INNER LOOKUP JOIN abcde ON m=b AND a=10 AND c=10
----
project
 ├── columns: a:6!null b:7!null c:8!null
 ├── fd: ()-->(6,8)
 └── inner-join (lookup abcde@abcde_a_b_c_idx)
      ├── columns: m:1!null a:6!null b:7!null c:8!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [14 1 15] = [6 7 8]
      ├── fd: ()-->(6,8), (1)==(7), (7)==(1)
      ├── project
      │    ├── columns: "lookup_join_const_col_@6":14!null "lookup_join_const_col_@8":15!null m:1
      │    ├── fd: ()-->(14,15)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── projections
      │         ├── 10 [as="lookup_join_const_col_@6":14]
      │         └── 10 [as="lookup_join_const_col_@8":15]
      └── filters (true)

# Projection of constant columns work with non const expressions as well.
exec-ddl
CREATE TABLE bool_col (a INT, b INT, c bool, d bool, e bool, INDEX (a,b,c))
----

# Projection of constant columns work on boolean expressions.
opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN bool_col ON a=m AND b=10 AND c=true
----
inner-join (lookup bool_col)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7,8), (1)==(6), (6)==(1)
 ├── inner-join (lookup bool_col@bool_col_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null bool_col.rowid:11!null
 │    ├── key columns: [1 14 15] = [6 7 8]
 │    ├── fd: ()-->(7,8), (11)-->(6), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null "lookup_join_const_col_@8":15!null m:1 n:2
 │    │    ├── fd: ()-->(14,15)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         ├── 10 [as="lookup_join_const_col_@7":14]
 │    │         └── true [as="lookup_join_const_col_@8":15]
 │    └── filters (true)
 └── filters (true)

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN bool_col ON a=m AND b=10 AND c
----
inner-join (lookup bool_col)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7,8), (1)==(6), (6)==(1)
 ├── inner-join (lookup bool_col@bool_col_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null bool_col.rowid:11!null
 │    ├── key columns: [1 14 15] = [6 7 8]
 │    ├── fd: ()-->(7,8), (11)-->(6), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null "lookup_join_const_col_@8":15!null m:1 n:2
 │    │    ├── fd: ()-->(14,15)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         ├── 10 [as="lookup_join_const_col_@7":14]
 │    │         └── true [as="lookup_join_const_col_@8":15]
 │    └── filters (true)
 └── filters (true)

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN bool_col ON a=m AND b=10 AND NOT c
----
inner-join (lookup bool_col)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9 e:10
 ├── key columns: [11] = [11]
 ├── lookup columns are key
 ├── fd: ()-->(7,8), (1)==(6), (6)==(1)
 ├── inner-join (lookup bool_col@bool_col_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null bool_col.rowid:11!null
 │    ├── key columns: [1 14 15] = [6 7 8]
 │    ├── fd: ()-->(7,8), (11)-->(6), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":14!null "lookup_join_const_col_@8":15!null m:1 n:2
 │    │    ├── fd: ()-->(14,15)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         ├── 10 [as="lookup_join_const_col_@7":14]
 │    │         └── false [as="lookup_join_const_col_@8":15]
 │    └── filters (true)
 └── filters (true)

# Use constant CHECK constraints to generate lookup join keys.
exec-ddl
CREATE TABLE abcd_check (a INT, b INT NOT NULL, c INT, d INT, CHECK (b IN (10, 20)), INDEX (a, b, c))
----

exec-ddl
ALTER TABLE abcd_check INJECT STATISTICS '[
  {
    "columns": ["a"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 100000,
    "distinct_count": 100000
  }
]'
----

opt expect=GenerateLookupJoins
SELECT * FROM small INNER JOIN abcd_check ON a=m
----
inner-join (lookup abcd_check)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8 d:9
 ├── key columns: [10] = [10]
 ├── lookup columns are key
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd_check@abcd_check_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8 abcd_check.rowid:10!null
 │    ├── key columns: [1] = [6]
 │    ├── fd: (10)-->(6-8), (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── filters (true)

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcd_check ON a=m AND c=30
----
inner-join (lookup abcd_check)
 ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null d:9
 ├── key columns: [10] = [10]
 ├── lookup columns are key
 ├── fd: ()-->(8), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd_check@abcd_check_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcd_check.rowid:10!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── b:7 IN (10, 20) [outer=(7), constraints=(/7: [/10 - /10] [/20 - /20]; tight)]
 │    │         ├── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 │    │         └── "lookup_join_const_col_@8":13 = c:8 [outer=(8,13), constraints=(/8: (/NULL - ]; /13: (/NULL - ]), fd=(8)==(13), (13)==(8)]
 │    ├── fd: ()-->(8), (10)-->(6,7), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@8":13!null m:1 n:2
 │    │    ├── fd: ()-->(13)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         └── 30 [as="lookup_join_const_col_@8":13]
 │    └── filters (true)
 └── filters (true)

# Use computed column expressions to generate lookup join keys.
exec-ddl
CREATE TABLE abcd_comp (a INT, b INT AS (d % 4) STORED, c INT, d INT, INDEX (a, b, c))
----

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcd_comp ON a=m AND d=5
----
inner-join (lookup abcd_comp)
 ├── columns: m:1!null n:2 a:6!null b:7 c:8 d:9!null
 ├── key columns: [10] = [10]
 ├── lookup columns are key
 ├── fd: ()-->(7,9), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd_comp@abcd_comp_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8 abcd_comp.rowid:10!null
 │    ├── key columns: [1 13] = [6 7]
 │    ├── fd: ()-->(7), (10)-->(6,8), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":13!null m:1 n:2
 │    │    ├── fd: ()-->(13)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         └── 1 [as="lookup_join_const_col_@7":13]
 │    └── filters (true)
 └── filters
      └── d:9 = 5 [outer=(9), constraints=(/9: [/5 - /5]; tight), fd=()-->(9)]

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER JOIN abcd_comp ON a=m AND d=5 AND c=30
----
inner-join (lookup abcd_comp)
 ├── columns: m:1!null n:2 a:6!null b:7 c:8!null d:9!null
 ├── key columns: [10] = [10]
 ├── lookup columns are key
 ├── fd: ()-->(7-9), (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd_comp@abcd_comp_a_b_c_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7!null c:8!null abcd_comp.rowid:10!null
 │    ├── key columns: [1 13 14] = [6 7 8]
 │    ├── fd: ()-->(7,8), (10)-->(6), (1)==(6), (6)==(1)
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@7":13!null "lookup_join_const_col_@8":14!null m:1 n:2
 │    │    ├── fd: ()-->(13,14)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── projections
 │    │         ├── 1 [as="lookup_join_const_col_@7":13]
 │    │         └── 30 [as="lookup_join_const_col_@8":14]
 │    └── filters (true)
 └── filters
      └── d:9 = 5 [outer=(9), constraints=(/9: [/5 - /5]; tight), fd=()-->(9)]

exec-ddl
CREATE TABLE t(pk INT PRIMARY KEY, col0 INT, col1 INT, col2 INT, col4 INT, UNIQUE INDEX (col2))
----

# Make sure we don't generate a lookup join with no key columns (#41676).
opt expect-not=GenerateLookupJoinsWithFilter
SELECT pk FROM t WHERE col4 = 1 AND col0 = 1 AND col2 IN (SELECT col0 FROM t WHERE col0 = 1 AND col2 IS NULL);
----
project
 ├── columns: pk:1!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1)
 └── semi-join (cross)
      ├── columns: pk:1!null col0:2!null col2:4!null col4:5!null
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1,2,4,5)
      ├── select
      │    ├── columns: pk:1!null col0:2!null col2:4!null col4:5!null
      │    ├── cardinality: [0 - 1]
      │    ├── key: ()
      │    ├── fd: ()-->(1,2,4,5)
      │    ├── index-join t
      │    │    ├── columns: pk:1!null col0:2 col2:4 col4:5
      │    │    ├── cardinality: [0 - 1]
      │    │    ├── key: ()
      │    │    ├── fd: ()-->(1,2,4,5)
      │    │    └── scan t@t_col2_key
      │    │         ├── columns: pk:1!null col2:4!null
      │    │         ├── constraint: /4: [/1 - /1]
      │    │         ├── cardinality: [0 - 1]
      │    │         ├── key: ()
      │    │         └── fd: ()-->(1,4)
      │    └── filters
      │         ├── col4:5 = 1 [outer=(5), constraints=(/5: [/1 - /1]; tight), fd=()-->(5)]
      │         └── col0:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]
      ├── select
      │    ├── columns: col0:9!null col2:11
      │    ├── lax-key: (11)
      │    ├── fd: ()-->(9,11)
      │    ├── index-join t
      │    │    ├── columns: col0:9 col2:11
      │    │    ├── lax-key: (9,11)
      │    │    ├── fd: ()-->(11), (11)~~>(9)
      │    │    └── scan t@t_col2_key
      │    │         ├── columns: pk:8!null col2:11
      │    │         ├── constraint: /11: [/NULL - /NULL]
      │    │         ├── key: (8)
      │    │         └── fd: ()-->(11), (11)~~>(8)
      │    └── filters
      │         └── col0:9 = 1 [outer=(9), constraints=(/9: [/1 - /1]; tight), fd=()-->(9)]
      └── filters (true)

# Lookup semi-join with covering index.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, n FROM small WHERE EXISTS(SELECT 1 FROM abcd WHERE m = a AND a > b)
----
semi-join (lookup abcd@abcd_a_b_idx)
 ├── columns: m:1 n:2
 ├── key columns: [1] = [6]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      └── a:6 > b:7 [outer=(6,7), constraints=(/6: (/NULL - ]; /7: (/NULL - ])]

# We can generate a lookup semi-join when the index is non-covering using
# paired-joins.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, n FROM small WHERE EXISTS(SELECT 1 FROM abcd WHERE m = a AND n = c AND a > b)
----
semi-join (lookup abcd)
 ├── columns: m:1 n:2
 ├── key columns: [17] = [9]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:14!null b:15!null abcd.rowid:17!null continuation:20
 │    ├── key columns: [1] = [14]
 │    ├── first join in paired joiner; continuation column: continuation:20
 │    ├── fd: (17)-->(14,15,20), (1)==(14), (14)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters
 │         └── a:14 > b:15 [outer=(14,15), constraints=(/14: (/NULL - ]; /15: (/NULL - ])]
 └── filters
      └── n:2 = c:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]

# Lookup anti-join with covering index.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, n FROM small WHERE NOT EXISTS(SELECT 1 FROM abcd WHERE m = a AND a > b)
----
anti-join (lookup abcd@abcd_a_b_idx)
 ├── columns: m:1 n:2
 ├── key columns: [1] = [6]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      └── a:6 > b:7 [outer=(6,7), constraints=(/6: (/NULL - ]; /7: (/NULL - ])]

# We can generate a lookup semi-join when the index is non-covering using
# paired-joins.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, n FROM small WHERE NOT EXISTS(SELECT 1 FROM abcd WHERE m = a AND n = c AND a > b)
----
anti-join (lookup abcd)
 ├── columns: m:1 n:2
 ├── key columns: [17] = [9]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:14 b:15 abcd.rowid:17 continuation:20
 │    ├── key columns: [1] = [14]
 │    ├── first join in paired joiner; continuation column: continuation:20
 │    ├── fd: (17)-->(14,15,20)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters
 │         └── a:14 > b:15 [outer=(14,15), constraints=(/14: (/NULL - ]; /15: (/NULL - ])]
 └── filters
      └── n:2 = c:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]

# Regression test for #79384. Do not generate unnecessary cross-joins with
# constant values when generating a lookup join with a lookup expression.
exec-ddl
CREATE TABLE t79384 (
  a INT,
  b INT,
  c INT,
  INDEX (a, b, c)
)
----

opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small JOIN t79384 ON b IN (1, 2, 3) AND c > 0 AND m = a
----
project
 ├── columns: m:1!null
 └── inner-join (lookup t79384@t79384_a_b_c_idx)
      ├── columns: m:1!null a:6!null b:7!null c:8!null
      ├── lookup expression
      │    └── filters
      │         ├── b:7 IN (1, 2, 3) [outer=(7), constraints=(/7: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │         ├── c:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
      │         └── m:1 = a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
      ├── fd: (1)==(6), (6)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Regression test for #80525. Do not normalize c = false to NOT c in lookup
# expression.
exec-ddl
CREATE TABLE t80525_a (a INT, INDEX (a))
----

exec-ddl
CREATE TABLE t80525_bcd (b INT, c BOOL, d INT, INDEX (b, c, d))
----

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM t80525_a INNER LOOKUP JOIN t80525_bcd ON b = a AND c = false AND d > 0
----
inner-join (lookup t80525_bcd@t80525_bcd_b_c_d_idx)
 ├── columns: a:1!null b:5!null c:6!null d:7!null
 ├── flags: force lookup join (into right side)
 ├── lookup expression
 │    └── filters
 │         ├── d:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
 │         ├── a:1 = b:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]
 │         └── "lookup_join_const_col_@6":11 = c:6 [outer=(6,11), constraints=(/6: (/NULL - ]; /11: (/NULL - ]), fd=(6)==(11), (11)==(6)]
 ├── fd: ()-->(6), (1)==(5), (5)==(1)
 ├── project
 │    ├── columns: "lookup_join_const_col_@6":11!null a:1
 │    ├── fd: ()-->(11)
 │    ├── scan t80525_a
 │    │    └── columns: a:1
 │    └── projections
 │         └── false [as="lookup_join_const_col_@6":11]
 └── filters (true)


# --------------------------------------------------
# GenerateLookupJoinsWithFilter + Partial Indexes
# --------------------------------------------------

exec-ddl
CREATE TABLE partial_tab (
    k INT PRIMARY KEY,
    i INT,
    s STRING
)
----

exec-ddl
ALTER TABLE partial_tab INJECT STATISTICS '[
  {
    "columns": ["i"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 10000,
    "distinct_count": 10000
  },
  {
    "columns": ["s"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 10000,
    "distinct_count": 50
  }
]'
----

# Storing the index predicate column.

exec-ddl
CREATE INDEX partial_idx ON partial_tab (i) STORING (s) WHERE s IN ('foo', 'bar', 'baz')
----

# Lookup inner-join with no remaining filters.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small JOIN partial_tab ON n = i WHERE s IN ('foo', 'bar', 'baz')
----
project
 ├── columns: m:1
 └── inner-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2!null i:7!null s:8!null
      ├── key columns: [2] = [7]
      ├── fd: (2)==(7), (7)==(2)
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Lookup inner-join with remaining filters.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small JOIN partial_tab ON n = i WHERE s = 'foo'
----
project
 ├── columns: m:1
 └── inner-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2!null i:7!null s:8!null
      ├── key columns: [2] = [7]
      ├── fd: ()-->(8), (2)==(7), (7)==(2)
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters
           └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]

# Lookup semi-join.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE EXISTS (SELECT 1 FROM partial_tab WHERE s = 'foo' AND n = i)
----
project
 ├── columns: m:1
 └── semi-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2
      ├── key columns: [2] = [7]
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters
           └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]

# Lookup anti-join.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT 1 FROM partial_tab WHERE s = 'foo' AND n = i)
----
project
 ├── columns: m:1
 └── anti-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2
      ├── key columns: [2] = [7]
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters
           └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]

# Do not generate a lookup join when the predicate is not implied by the filter.
opt expect-not=GenerateLookupJoinsWithFilter
SELECT m FROM small JOIN partial_tab ON n = i WHERE s = 'not_implied'
----
project
 ├── columns: m:1
 └── inner-join (hash)
      ├── columns: m:1 n:2!null i:7!null s:8!null
      ├── fd: ()-->(8), (2)==(7), (7)==(2)
      ├── select
      │    ├── columns: i:7 s:8!null
      │    ├── fd: ()-->(8)
      │    ├── scan partial_tab
      │    │    ├── columns: i:7 s:8
      │    │    └── partial index predicates
      │    │         └── partial_idx: filters
      │    │              └── s:8 IN ('bar', 'baz', 'foo') [outer=(8), constraints=(/8: [/'bar' - /'bar'] [/'baz' - /'baz'] [/'foo' - /'foo']; tight)]
      │    └── filters
      │         └── s:8 = 'not_implied' [outer=(8), constraints=(/8: [/'not_implied' - /'not_implied']; tight), fd=()-->(8)]
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters
           └── n:2 = i:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]

exec-ddl
DROP INDEX partial_idx
----

# Not storing the index predicate column.

exec-ddl
CREATE INDEX partial_idx ON partial_tab (i) WHERE s IN ('foo', 'bar', 'baz')
----

# The remaining filters are empty when the query filters exactly match the
# partial index predicate.
#
# TODO(mgartner): The outer inner-join is not necessary here and should be
# eliminated, like in EliminateIndexJoinInsideProject.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small JOIN partial_tab ON n = i WHERE s IN ('foo', 'bar', 'baz')
----
project
 ├── columns: m:1
 └── inner-join (lookup partial_tab)
      ├── columns: m:1 n:2!null i:7!null s:8!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── fd: (2)==(7), (7)==(2)
      ├── inner-join (lookup partial_tab@partial_idx,partial)
      │    ├── columns: m:1 n:2!null k:6!null i:7!null
      │    ├── key columns: [2] = [7]
      │    ├── fd: (6)-->(7), (2)==(7), (7)==(2)
      │    ├── scan small
      │    │    └── columns: m:1 n:2
      │    └── filters (true)
      └── filters (true)

# The remaining filters are not empty when the query filters imply but do not
# exactly match the partial index predicate.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small JOIN partial_tab ON n = i WHERE s = 'foo'
----
project
 ├── columns: m:1
 └── inner-join (lookup partial_tab)
      ├── columns: m:1 n:2!null i:7!null s:8!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── fd: ()-->(8), (2)==(7), (7)==(2)
      ├── inner-join (lookup partial_tab@partial_idx,partial)
      │    ├── columns: m:1 n:2!null k:6!null i:7!null
      │    ├── key columns: [2] = [7]
      │    ├── fd: (6)-->(7), (2)==(7), (7)==(2)
      │    ├── scan small
      │    │    └── columns: m:1 n:2
      │    └── filters (true)
      └── filters
           └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]

# Generate a lookup semi-join when the index does not cover "s", but the
# reference to "s" no longer exists in the filters.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE EXISTS (SELECT 1 FROM partial_tab WHERE s IN ('foo', 'bar', 'baz') AND n = i)
----
project
 ├── columns: m:1
 └── semi-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2
      ├── key columns: [2] = [7]
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# We can generate a lookup semi-join when the index does not cover "s",
# which is referenced in the remaining filter, by using paired-joins.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE EXISTS (SELECT 1 FROM partial_tab WHERE s = 'foo' AND n = i)
----
project
 ├── columns: m:1
 └── semi-join (lookup partial_tab)
      ├── columns: m:1 n:2
      ├── key columns: [13] = [6]
      ├── lookup columns are key
      ├── second join in paired joiner
      ├── inner-join (lookup partial_tab@partial_idx,partial)
      │    ├── columns: m:1 n:2!null k:13!null i:14!null continuation:18
      │    ├── key columns: [2] = [14]
      │    ├── first join in paired joiner; continuation column: continuation:18
      │    ├── fd: (13)-->(14,18), (2)==(14), (14)==(2)
      │    ├── scan small
      │    │    └── columns: m:1 n:2
      │    └── filters (true)
      └── filters
           └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]

# Generate a lookup anti-join when the index does not cover "s", but the
# reference to "s" no longer exists in the filters.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT 1 FROM partial_tab WHERE s IN ('foo', 'bar', 'baz') AND n = i)
----
project
 ├── columns: m:1
 └── anti-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2
      ├── key columns: [2] = [7]
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# We can generate a lookup anti-join when the index does not cover "s",
# which is referenced in the remaining filter, by using paired-joins.
opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT 1 FROM partial_tab WHERE s = 'foo' AND n = i)
----
project
 ├── columns: m:1
 └── anti-join (lookup partial_tab)
      ├── columns: m:1 n:2
      ├── key columns: [13] = [6]
      ├── lookup columns are key
      ├── second join in paired joiner
      ├── left-join (lookup partial_tab@partial_idx,partial)
      │    ├── columns: m:1 n:2 k:13 i:14 continuation:18
      │    ├── key columns: [2] = [14]
      │    ├── first join in paired joiner; continuation column: continuation:18
      │    ├── fd: (13)-->(14,18)
      │    ├── scan small
      │    │    └── columns: m:1 n:2
      │    └── filters (true)
      └── filters
           └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]

# A lookup semi-join on a partial index should have the same cost as a lookup
# semi-join on a non-partial index.
exec-ddl
CREATE INDEX full_idx ON partial_tab (i)
----

opt format=show-cost
SELECT m FROM small WHERE EXISTS (SELECT 1 FROM partial_tab WHERE n = i)
----
project
 ├── columns: m:1
 ├── cost: 263.356
 └── semi-join (lookup partial_tab@full_idx)
      ├── columns: m:1 n:2
      ├── key columns: [2] = [7]
      ├── cost: 263.236
      ├── scan small
      │    ├── columns: m:1 n:2
      │    └── cost: 39.02
      └── filters (true)

opt format=show-cost
SELECT m FROM small WHERE EXISTS (SELECT 1 FROM partial_tab WHERE s IN ('foo', 'bar', 'baz') AND n = i)
----
project
 ├── columns: m:1
 ├── cost: 263.356
 └── semi-join (lookup partial_tab@partial_idx,partial)
      ├── columns: m:1 n:2
      ├── key columns: [2] = [7]
      ├── cost: 263.236
      ├── scan small
      │    ├── columns: m:1 n:2
      │    └── cost: 39.02
      └── filters (true)

exec-ddl
DROP INDEX full_idx
----

exec-ddl
DROP INDEX partial_idx
----

exec-ddl
CREATE INDEX partial_idx ON partial_tab (i) WHERE s = 'foo'
----

# Generate a second inner join to lookup s even though the partial index
# predicate holds s constant.
# TODO(mgartner): The right side of the join can "produce" columns held constant
# by a partial index predicate, so the second inner-join to lookup s is not
# necessary.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, s FROM small INNER LOOKUP JOIN partial_tab ON n = i WHERE s = 'foo'
----
project
 ├── columns: m:1 s:8!null
 ├── fd: ()-->(8)
 └── inner-join (lookup partial_tab)
      ├── columns: m:1 n:2!null i:7!null s:8!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── fd: ()-->(8), (2)==(7), (7)==(2)
      ├── inner-join (lookup partial_tab@partial_idx,partial)
      │    ├── columns: m:1 n:2!null k:6!null i:7!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [2] = [7]
      │    ├── fd: (6)-->(7), (2)==(7), (7)==(2)
      │    ├── scan small
      │    │    └── columns: m:1 n:2
      │    └── filters (true)
      └── filters (true)

exec-ddl
DROP INDEX partial_idx
----

exec-ddl
CREATE INDEX i_partial ON virt (i) WHERE v1 > 0
----

# Covering case with partial index predicate that references a virtual column.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.i AND v1 > 0
----
project
 ├── columns: m:1!null v1:9!null
 ├── immutable
 ├── fd: (1)-->(9)
 └── project
      ├── columns: v1:9!null m:1!null i:7!null
      ├── immutable
      ├── fd: (7)-->(9), (1)==(7), (7)==(1)
      ├── inner-join (lookup virt@i_partial,partial)
      │    ├── columns: m:1!null i:7!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [7]
      │    ├── fd: (1)==(7), (7)==(1)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── filters (true)
      └── projections
           └── i:7 + 10 [as=v1:9, outer=(7), immutable]

# Non-covering case with partial index predicate that references a virtual
# column.
opt expect=GenerateLookupJoinsWithFilter
SELECT m, virt.j, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.i AND v1 > 0
----
project
 ├── columns: m:1!null j:8!null v1:9!null
 ├── immutable
 ├── fd: (1)-->(9)
 └── project
      ├── columns: v1:9!null m:1!null i:7!null j:8!null
      ├── immutable
      ├── fd: (7)-->(9), (1)==(7), (7)==(1)
      ├── inner-join (lookup virt)
      │    ├── columns: m:1!null i:7!null j:8!null
      │    ├── key columns: [6] = [6]
      │    ├── lookup columns are key
      │    ├── fd: (1)==(7), (7)==(1)
      │    ├── inner-join (lookup virt@i_partial,partial)
      │    │    ├── columns: m:1!null k:6!null i:7!null
      │    │    ├── flags: force lookup join (into right side)
      │    │    ├── key columns: [1] = [7]
      │    │    ├── fd: (6)-->(7), (1)==(7), (7)==(1)
      │    │    ├── scan small
      │    │    │    └── columns: m:1
      │    │    └── filters (true)
      │    └── filters (true)
      └── projections
           └── i:7 + 10 [as=v1:9, outer=(7), immutable]

exec-ddl
DROP INDEX i_partial
----

# Regression test for #125422. An equality between with two variables implies
# that either variable is not null, allowing a lookup join into a partial index
# with an IS NOT NULL predicate.
exec-ddl
CREATE TABLE t125422 (
  k INT PRIMARY KEY,
  a INT,
  b INT,
  c INT,
  INDEX idx125422 (a, b) WHERE a IS NOT NULL
)
----

opt
SELECT * FROM t125422 AS t1
LEFT LOOKUP JOIN t125422@idx125422 AS t2
ON t1.a = t2.a AND t1.b = t2.b AND t2.c = 10
----
left-join (lookup t125422 [as=t2])
 ├── columns: k:1!null a:2 b:3 c:4 k:7 a:8 b:9 c:10
 ├── key columns: [13] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── key: (1,7)
 ├── fd: (1)-->(2-4), (7)-->(8,9), (1,7)-->(10)
 ├── left-join (lookup t125422@idx125422,partial [as=t2])
 │    ├── columns: t1.k:1!null t1.a:2 t1.b:3 t1.c:4 t2.k:13 t2.a:14 t2.b:15 continuation:19
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [2 3] = [14 15]
 │    ├── first join in paired joiner; continuation column: continuation:19
 │    ├── key: (1,13)
 │    ├── fd: (1)-->(2-4), (13)-->(14,15,19)
 │    ├── scan t125422 [as=t1]
 │    │    ├── columns: t1.k:1!null t1.a:2 t1.b:3 t1.c:4
 │    │    ├── partial index predicates
 │    │    │    └── idx125422: filters
 │    │    │         └── t1.a:2 IS NOT NULL [outer=(2), constraints=(/2: (/NULL - ]; tight)]
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t2.c:10 = 10 [outer=(10), constraints=(/10: [/10 - /10]; tight), fd=()-->(10)]

# Regression test for #63735. Ensure that the constant filter which maximally
# constrains the lookup table is chosen when there is more than one option.
# Here, the CHECK constraint establishes an implicit filter that constrains the
# t63735.x column to any value in (10, 20, 30). However, the computed column
# expression does more, establishing an implicit filter that constrains the
# t63735.x column to exactly the value 30.
exec-ddl
CREATE TABLE t63735 (
  x INT NOT NULL AS (y * 2) STORED CHECK (x IN (10, 20, 30)),
  y INT NOT NULL,
  PRIMARY KEY (x, y)
)
----

opt expect=GenerateLookupJoinsWithFilter
SELECT * FROM small INNER LOOKUP JOIN t63735 ON n = y WHERE m = 5 AND n = 15
----
inner-join (lookup t63735)
 ├── columns: m:1!null n:2!null x:6!null y:7!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [10 2] = [6 7]
 ├── lookup columns are key
 ├── fd: ()-->(1,2,6,7), (2)==(7), (7)==(2)
 ├── project
 │    ├── columns: x_eq:10!null m:1!null n:2!null
 │    ├── immutable
 │    ├── fd: ()-->(1,2,10)
 │    ├── select
 │    │    ├── columns: m:1!null n:2!null
 │    │    ├── fd: ()-->(1,2)
 │    │    ├── scan small
 │    │    │    └── columns: m:1 n:2
 │    │    └── filters
 │    │         ├── n:2 = 15 [outer=(2), constraints=(/2: [/15 - /15]; tight), fd=()-->(2)]
 │    │         └── m:1 = 5 [outer=(1), constraints=(/1: [/5 - /5]; tight), fd=()-->(1)]
 │    └── projections
 │         └── n:2 * 2 [as=x_eq:10, outer=(2), immutable]
 └── filters
      └── y:7 = 15 [outer=(7), constraints=(/7: [/15 - /15]; tight), fd=()-->(7)]

# Regression test for #101844. Derived FK equalities should not be added to the
# ON filters of a lookup join.
exec-ddl
CREATE TABLE p101844 (
  r INT,
  id INT,
  PRIMARY KEY (r, id),
  UNIQUE WITHOUT INDEX (id)
)
----

exec-ddl
CREATE TABLE c101844 (
  r INT,
  p_id INT,
  id INT,
  PRIMARY KEY (r, p_id, id),
  UNIQUE INDEX c_p_id_id_key (p_id, id),
  FOREIGN KEY (r, p_id) REFERENCES p101844 (r, id)
)
----

# The derived c.r = p.r filters should not be added to the lookup join ON
# condition if they aren't used as equality columns.
opt
SELECT *
FROM p101844 p LEFT LOOKUP JOIN c101844@c_p_id_id_key c
ON c.p_id = p.id AND c.id = 1234
----
left-join (lookup c101844@c_p_id_id_key [as=c])
 ├── columns: r:1!null id:2!null r:5 p_id:6 id:7
 ├── flags: force lookup join (into right side)
 ├── key columns: [2 10] = [6 7]
 ├── lookup columns are key
 ├── key: (2)
 ├── fd: (2)-->(1,5-7), (6)-->(5)
 ├── project
 │    ├── columns: "lookup_join_const_col_@7":10!null p.r:1!null p.id:2!null
 │    ├── key: (2)
 │    ├── fd: ()-->(10), (2)-->(1)
 │    ├── scan p101844 [as=p]
 │    │    ├── columns: p.r:1!null p.id:2!null
 │    │    ├── key: (2)
 │    │    └── fd: (2)-->(1)
 │    └── projections
 │         └── 1234 [as="lookup_join_const_col_@7":10]
 └── filters (true)


# --------------------------------------------------
# GenerateLookupJoinsWithVirtualCols
# --------------------------------------------------

exec-ddl
CREATE INDEX v1 ON virt (v1)
----

exec-ddl
CREATE INDEX v2 ON virt (v2)
----

# Covering case. Join on virtual column but do not produce it.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
project
 ├── columns: m:1!null k:6!null
 ├── immutable
 ├── fd: (6)-->(1)
 └── inner-join (lookup virt@v1)
      ├── columns: m:1!null k:6!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [9]
      ├── immutable
      ├── fd: (6)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Covering case. Join on virtual column expression but do not produce it.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k FROM small INNER LOOKUP JOIN virt ON m = virt.i + 10
----
project
 ├── columns: m:1!null k:6!null
 ├── immutable
 ├── fd: (6)-->(1)
 └── inner-join (lookup virt@v1)
      ├── columns: m:1!null k:6!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [9]
      ├── immutable
      ├── fd: (6)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Covering case. Produce virtual column.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
inner-join (lookup virt@v1)
 ├── columns: m:1!null k:6!null v1:9!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Covering case. Join on virtual column expression and produce it.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.i + 10
----
inner-join (lookup virt@v1)
 ├── columns: m:1!null k:6!null v1:9!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Non-covering.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.i, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7 v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9), (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@v1)
 │    ├── columns: m:1!null k:6!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Non-covering. Join on virtual column expression.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.i, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.i + 10
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7 v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9), (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@v1)
 │    ├── columns: m:1!null k:6!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Do not generate a lookup join when the right side projects a column that is
# not a virtual computed column.
opt expect-not=GenerateLookupJoinsWithVirtualCols
SELECT m, tmp.p
FROM small INNER LOOKUP JOIN (
  SELECT v1, i + 5 AS p FROM virt
) tmp ON m = tmp.v1
----
project
 ├── columns: m:1!null p:16
 ├── immutable
 └── inner-join (hash)
      ├── columns: m:1!null v1:9!null p:16
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: p:16 v1:9
      │    ├── immutable
      │    ├── scan virt
      │    │    ├── columns: i:7
      │    │    ├── check constraint expressions
      │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    └── computed column expressions
      │    │         ├── v1:9
      │    │         │    └── i:7 + 10
      │    │         ├── v2:10
      │    │         │    └── i:7 + 100
      │    │         ├── v3:11
      │    │         │    └── i:7 + j:8
      │    │         └── v4:12
      │    │              └── i:7 + 1
      │    └── projections
      │         ├── i:7 + 5 [as=p:16, outer=(7), immutable]
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Do not generate a lookup join when all the projected virtual columns cannot
# be produced from a single index.
opt expect-not=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.v1, virt.v2 FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
inner-join (hash)
 ├── columns: m:1!null v1:9!null v2:10
 ├── flags: force lookup join (into right side)
 ├── immutable
 ├── fd: (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v1:9 v2:10
 │    ├── immutable
 │    ├── scan virt
 │    │    ├── columns: i:7
 │    │    ├── check constraint expressions
 │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │    └── computed column expressions
 │    │         ├── v1:9
 │    │         │    └── i:7 + 10
 │    │         ├── v2:10
 │    │         │    └── i:7 + 100
 │    │         ├── v3:11
 │    │         │    └── i:7 + j:8
 │    │         └── v4:12
 │    │              └── i:7 + 1
 │    └── projections
 │         ├── i:7 + 10 [as=v1:9, outer=(7), immutable]
 │         └── i:7 + 100 [as=v2:10, outer=(7), immutable]
 └── filters
      └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Left join, covering case. Join on virtual column but do not produce it.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k FROM small LEFT LOOKUP JOIN virt ON m = virt.v1
----
project
 ├── columns: m:1 k:6
 ├── immutable
 └── left-join (lookup virt@v1)
      ├── columns: m:1 k:6 v1:9
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [9]
      ├── immutable
      ├── fd: (6)-->(9)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Left join, covering case. Join on virtual column and produce it.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1
----
left-join (lookup virt@v1)
 ├── columns: m:1 k:6 v1:9
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (6)-->(9)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Left join, Non-covering.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.i, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1
----
left-join (lookup virt)
 ├── columns: m:1 i:7 v1:9
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)~~>(9)
 ├── left-join (lookup virt@v1)
 │    ├── columns: m:1 k:6 v1:9
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Generate lookup joins with virtual columns for semi-joins.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1)
----
semi-join (lookup virt@v1)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Generate lookup joins with virtual columns for anti-joins.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1)
----
anti-join (lookup virt@v1)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

exec-ddl
DROP INDEX v1
----

exec-ddl
DROP INDEX v2
----

exec-ddl
CREATE INDEX v1_v2 ON virt (v1, v2)
----

# Covering case. Single key column in index with multiple virtual columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1, virt.v2 FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
inner-join (lookup virt@v1_v2)
 ├── columns: m:1!null k:6!null v1:9!null v2:10
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (6)-->(9,10), (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Covering case. Multiple key columns in index with multiple virtual columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1, virt.v2 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND n = virt.v2
----
project
 ├── columns: m:1!null k:6!null v1:9!null v2:10!null
 ├── immutable
 ├── fd: (6)-->(9,10), (1)==(9), (9)==(1)
 └── inner-join (lookup virt@v1_v2)
      ├── columns: m:1!null n:2!null k:6!null v1:9!null v2:10!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1 2] = [9 10]
      ├── immutable
      ├── fd: (6)-->(9,10), (1)==(9), (9)==(1), (2)==(10), (10)==(2)
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Covering case. Left join with single key column in index with multiple virtual
# columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1, virt.v2 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1
----
left-join (lookup virt@v1_v2)
 ├── columns: m:1 k:6 v1:9 v2:10
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (6)-->(9,10)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Covering case. Left join with multiple key columns in index with multiple
# virtual columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1, virt.v2 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND n = virt.v2
----
project
 ├── columns: m:1 k:6 v1:9 v2:10
 ├── immutable
 ├── fd: (6)-->(9,10)
 └── left-join (lookup virt@v1_v2)
      ├── columns: m:1 n:2 k:6 v1:9 v2:10
      ├── flags: force lookup join (into right side)
      ├── key columns: [1 2] = [9 10]
      ├── immutable
      ├── fd: (6)-->(9,10)
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Semi-join with single key column in index with multiple virtual columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1)
----
semi-join (lookup virt@v1_v2)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Semi-join with multiple virtual key columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND n = virt.v2)
----
project
 ├── columns: m:1
 ├── immutable
 └── semi-join (lookup virt@v1_v2)
      ├── columns: m:1 n:2
      ├── key columns: [1 2] = [9 10]
      ├── immutable
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Anti-join with single key column in index with multiple virtual columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1)
----
anti-join (lookup virt@v1_v2)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Anti-join with multiple virtual key columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND n = virt.v2)
----
project
 ├── columns: m:1
 ├── immutable
 └── anti-join (lookup virt@v1_v2)
      ├── columns: m:1 n:2
      ├── key columns: [1 2] = [9 10]
      ├── immutable
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

exec-ddl
DROP INDEX v1_v2
----

exec-ddl
CREATE INDEX i_v1 ON virt (i, v1)
----

exec-ddl
CREATE INDEX v2_i ON virt (v2, i)
----

# Covering case. One of the lookup columns is a virtual column.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.i AND n = virt.v1
----
project
 ├── columns: m:1!null k:6!null v1:9!null
 ├── immutable
 ├── fd: (6)-->(1,9), (1)-->(9)
 └── inner-join (lookup virt@i_v1)
      ├── columns: m:1!null n:2!null k:6!null i:7!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1 2] = [7 9]
      ├── immutable
      ├── fd: (6)-->(7), (7)-->(9), (1)==(7), (7)==(1), (2)==(9), (9)==(2)
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Do not generate an inner lookup join when the filter does not reference
# virtual columns. HoistProjectFromInnerJoin and GenerateLookupJoins can produce
# a lookup join in this case.
opt expect-not=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.i
----
project
 ├── columns: m:1!null k:6!null v1:9
 ├── immutable
 ├── fd: (6)-->(1,9), (1)-->(9)
 └── project
      ├── columns: v1:9 m:1!null k:6!null i:7!null
      ├── immutable
      ├── fd: (6)-->(7), (7)-->(9), (1)==(7), (7)==(1)
      ├── inner-join (lookup virt@i_v1)
      │    ├── columns: m:1!null k:6!null i:7!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [7]
      │    ├── fd: (6)-->(7), (1)==(7), (7)==(1)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── filters (true)
      └── projections
           └── i:7 + 10 [as=v1:9, outer=(7), immutable]

# Covering case. A virtual column is the lookup column and the non-virtual
# column can be produced.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.i, virt.v2 FROM small INNER LOOKUP JOIN virt ON m = virt.v2
----
inner-join (lookup virt@v2_i)
 ├── columns: m:1!null i:7 v2:10!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [10]
 ├── immutable
 ├── fd: (7)-->(10), (1)==(10), (10)==(1)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Left join, covering case. One of the lookup columns is a virtual column.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.i AND n = virt.v1
----
project
 ├── columns: m:1 k:6 v1:9
 ├── immutable
 └── left-join (lookup virt@i_v1)
      ├── columns: m:1 n:2 k:6 i:7 v1:9
      ├── flags: force lookup join (into right side)
      ├── key columns: [1 2] = [7 9]
      ├── immutable
      ├── fd: (6)-->(7), (7)~~>(9)
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Do not generate a inner lookup join when the filter does not reference virtual
# columns. HoistProjectFromInnerJoin and GenerateLookupJoins can produce a
# lookup join in this case.
opt expect-not=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.i
----
project
 ├── columns: m:1 k:6 v1:9
 ├── immutable
 └── project
      ├── columns: v1:9 m:1 k:6 i:7
      ├── immutable
      ├── fd: (6)-->(7), (7)~~>(9)
      ├── left-join (lookup virt@i_v1)
      │    ├── columns: m:1 k:6 i:7
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [7]
      │    ├── fd: (6)-->(7)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── filters (true)
      └── projections
           └── CASE k:6 IS NULL WHEN true THEN CAST(NULL AS INT8) ELSE i:7 + 10 END [as=v1:9, outer=(6,7), immutable]

# Left join, covering case. A virtual column is the lookup column and the
# non-virtual column can be produced.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.i, virt.v2 FROM small LEFT LOOKUP JOIN virt ON m = virt.v2
----
left-join (lookup virt@v2_i)
 ├── columns: m:1 i:7 v2:10
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [10]
 ├── immutable
 ├── fd: (7)~~>(10)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

exec-ddl
DROP INDEX i_v1
----

exec-ddl
DROP INDEX v2_i
----

exec-ddl
CREATE INDEX v4_i ON virt (v4, i)
----

# Do not generate an inner lookup join when the filter does not reference
# virtual columns.
opt expect-not=GenerateLookupJoinsWithVirtualCols
SELECT m, i, v4 FROM small INNER LOOKUP JOIN virt ON m = virt.i
----
project
 ├── columns: m:1!null i:7!null v4:12
 ├── immutable
 ├── fd: (7)-->(12), (1)==(7), (7)==(1)
 ├── inner-join (lookup virt@v4_i)
 │    ├── columns: m:1!null i:7!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [16 1] = [12 7]
 │    ├── fd: (1)==(7), (7)==(1)
 │    ├── project
 │    │    ├── columns: v4_eq:16 m:1
 │    │    ├── immutable
 │    │    ├── fd: (1)-->(16)
 │    │    ├── scan small
 │    │    │    └── columns: m:1
 │    │    └── projections
 │    │         └── m:1 + 1 [as=v4_eq:16, outer=(1), immutable]
 │    └── filters (true)
 └── projections
      └── i:7 + 1 [as=v4:12, outer=(7), immutable]

# Do not generate a left lookup join when the filter does not reference virtual
# columns.
opt expect-not=GenerateLookupJoinsWithVirtualCols
SELECT m, i, v4 FROM small LEFT LOOKUP JOIN virt ON m = virt.i
----
project
 ├── columns: m:1 i:7 v4:12
 ├── immutable
 ├── fd: (7)~~>(12)
 ├── left-join (lookup virt@v4_i)
 │    ├── columns: m:1 i:7
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [16 1] = [12 7]
 │    ├── project
 │    │    ├── columns: v4_eq:16 m:1
 │    │    ├── immutable
 │    │    ├── fd: (1)-->(16)
 │    │    ├── scan small
 │    │    │    └── columns: m:1
 │    │    └── projections
 │    │         └── m:1 + 1 [as=v4_eq:16, outer=(1), immutable]
 │    └── filters (true)
 └── projections
      └── CASE i:7 IS NULL WHEN true THEN CAST(NULL AS INT8) ELSE i:7 + 1 END [as=v4:12, outer=(7), immutable]

exec-ddl
DROP INDEX v4_i
----

exec-ddl
CREATE INDEX l_v1 ON virt (l, v1)
----

exec-ddl
CREATE INDEX v2_l ON virt (v2, l)
----

# Semi-join with non-virtual and virtual key columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.l AND n = virt.v1)
----
project
 ├── columns: m:1
 ├── immutable
 └── semi-join (lookup virt@l_v1)
      ├── columns: m:1 n:2
      ├── key columns: [1 2] = [13 9]
      ├── immutable
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Semi-join with virtual and non-virtual key columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v2 AND n = virt.l)
----
project
 ├── columns: m:1
 ├── immutable
 └── semi-join (lookup virt@v2_l)
      ├── columns: m:1 n:2
      ├── key columns: [1 2] = [10 13]
      ├── immutable
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Anti-join with non-virtual and virtual key columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.l AND n = virt.v1)
----
project
 ├── columns: m:1
 ├── immutable
 └── anti-join (lookup virt@l_v1)
      ├── columns: m:1 n:2
      ├── key columns: [1 2] = [13 9]
      ├── immutable
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

# Anti-join with virtual and non-virtual key columns.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v2 AND n = virt.l)
----
project
 ├── columns: m:1
 ├── immutable
 └── anti-join (lookup virt@v2_l)
      ├── columns: m:1 n:2
      ├── key columns: [1 2] = [10 13]
      ├── immutable
      ├── scan small
      │    └── columns: m:1 n:2
      └── filters (true)

exec-ddl
DROP INDEX l_v1
----

exec-ddl
DROP INDEX v2_l
----

exec-ddl
CREATE INDEX j_v1 ON virt (j, v1)
----

# Covering case with a set of possible values based on optional filters.
opt
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
project
 ├── columns: m:1!null k:6!null v1:9!null
 ├── immutable
 ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 └── inner-join (lookup virt@j_v1)
      ├── columns: m:1!null k:6!null j:8!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── lookup expression
      │    └── filters
      │         ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
      ├── fd: (6)-->(8,9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Non-covering case with a set of possible values based on optional filters.
opt
SELECT m, virt.i, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7 k:6!null v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (6)-->(7), (7)-->(9), (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@j_v1)
 │    ├── columns: m:1!null k:6!null j:8!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 │    ├── fd: (6)-->(8,9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Left join, covering case with multiple constant values based on optional
# filters.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1
----
project
 ├── columns: m:1 k:6 v1:9
 ├── immutable
 ├── fd: (6)-->(9)
 └── left-join (lookup virt@j_v1)
      ├── columns: m:1 k:6 j:8 v1:9
      ├── flags: force lookup join (into right side)
      ├── lookup expression
      │    └── filters
      │         ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
      ├── fd: (6)-->(8,9)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Left join, non-covering case with multiple constant values based on optional
# filters.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m, virt.i, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1
----
left-join (lookup virt)
 ├── columns: m:1 i:7 k:6 v1:9
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (6)-->(7), (7)~~>(9)
 ├── left-join (lookup virt@j_v1)
 │    ├── columns: m:1 k:6 j:8 v1:9
 │    ├── flags: force lookup join (into right side)
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 │    ├── fd: (6)-->(8,9)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Semi-join with a set of possible values based on optional filters.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1)
----
semi-join (lookup virt@j_v1)
 ├── columns: m:1
 ├── lookup expression
 │    └── filters
 │         ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Anti-join with multiple constant values based on optional filters.
opt expect=GenerateLookupJoinsWithVirtualCols
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1)
----
anti-join (lookup virt@j_v1)
 ├── columns: m:1
 ├── lookup expression
 │    └── filters
 │         ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

exec-ddl
DROP INDEX j_v1
----


# --------------------------------------------------
# GenerateLookupJoinsWithVirtualColsAndFilter
# --------------------------------------------------

exec-ddl
CREATE INDEX v1_storing_i ON virt (v1) STORING (i)
----

# Covering case. Virtual column is the lookup column and there is an extra
# filter on the non-virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND i > 0
----
project
 ├── columns: m:1!null i:7!null
 ├── immutable
 ├── fd: (7)-->(1)
 └── inner-join (lookup virt@v1_storing_i)
      ├── columns: m:1!null i:7!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [9]
      ├── immutable
      ├── fd: (7)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters
           └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]

# Covering case. Join on virtual column expression with an extra filter on the
# non-virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i FROM small INNER LOOKUP JOIN virt ON m = virt.i + 10 AND i > 0
----
project
 ├── columns: m:1!null i:7!null
 ├── immutable
 ├── fd: (7)-->(1)
 └── inner-join (lookup virt@v1_storing_i)
      ├── columns: m:1!null i:7!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [9]
      ├── immutable
      ├── fd: (7)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters
           └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]

# Covering case. Virtual column is the lookup column and there is an extra
# filter on the non-virtual column, but the column is not selected. We do not
# handle this case yet.
# TODO(mgartner): We could handle this by wrapping the lookup join in an
# IndexJoin to fetch i and filter by it, then wrapping the index join in a
# Project that removes i.
opt
SELECT m, virt.k FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND i > 0
----
project
 ├── columns: m:1!null k:6!null
 ├── immutable
 ├── fd: (6)-->(1)
 └── inner-join (hash)
      ├── columns: m:1!null k:6!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v1:9!null k:6!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(9)
      │    ├── select
      │    │    ├── columns: k:6!null i:7!null
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7)
      │    │    ├── scan virt@v1_storing_i
      │    │    │    ├── columns: k:6!null i:7
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7)
      │    │    └── filters
      │    │         └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Non-covering case. Virtual column is the lookup column and there is an extra
# filter on a column not in the index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND j > 0
----
project
 ├── columns: m:1!null j:8!null
 ├── immutable
 └── inner-join (lookup virt)
      ├── columns: m:1!null j:8!null v1:9!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)==(9), (9)==(1)
      ├── inner-join (lookup virt@v1_storing_i)
      │    ├── columns: m:1!null k:6!null v1:9!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [9]
      │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── filters (true)
      └── filters
           └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]

opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND j > 0
----
inner-join (lookup virt)
 ├── columns: m:1!null j:8!null v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@v1_storing_i)
 │    ├── columns: m:1!null k:6!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters
      └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]

# Non-covering case. Join on virtual column expression with an extra filter on the
# non-virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j FROM small INNER LOOKUP JOIN virt ON m = virt.i + 10 AND j > 0
----
project
 ├── columns: m:1!null j:8!null
 ├── immutable
 └── inner-join (lookup virt)
      ├── columns: m:1!null j:8!null v1:9!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)==(9), (9)==(1)
      ├── inner-join (lookup virt@v1_storing_i)
      │    ├── columns: m:1!null k:6!null v1:9!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [9]
      │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── filters (true)
      └── filters
           └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]

# Non-covering case. Virtual column is the lookup column and there is an extra
# filter on a column not in the index, but the column is not selected. We do not
# handle this case yet.
# TODO(mgartner): We could handle this by wrapping the lookup join in an
# IndexJoin to fetch i and filter by it, then wrapping the index join in a
# Project that removes i.
opt
SELECT m, virt.k FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND j > 0
----
project
 ├── columns: m:1!null k:6!null
 ├── immutable
 ├── fd: (6)-->(1)
 └── inner-join (hash)
      ├── columns: m:1!null k:6!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v1:9 k:6!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(9)
      │    ├── select
      │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7,8)
      │    │    ├── scan virt
      │    │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    │    ├── computed column expressions
      │    │    │    │    ├── v1:9
      │    │    │    │    │    └── i:7 + 10
      │    │    │    │    ├── v2:10
      │    │    │    │    │    └── i:7 + 100
      │    │    │    │    ├── v3:11
      │    │    │    │    │    └── i:7 + j:8
      │    │    │    │    └── v4:12
      │    │    │    │         └── i:7 + 1
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7,8)
      │    │    └── filters
      │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Left join, covering case. Virtual column is the lookup column and there is an
# extra filter on the non-virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND i > 0
----
project
 ├── columns: m:1 i:7
 ├── immutable
 └── left-join (lookup virt@v1_storing_i)
      ├── columns: m:1 i:7 v1:9
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [9]
      ├── immutable
      ├── fd: (7)-->(9)
      ├── scan small
      │    └── columns: m:1
      └── filters
           └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]

# Left join, covering case. Virtual column is the lookup column and there is an
# extra filter on the non-virtual column, but the column is not selected.
# TODO(mgartner): We do not handle this case yet.
opt
SELECT m, virt.k FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND i > 0
----
project
 ├── columns: m:1 k:6
 ├── immutable
 └── left-join (hash)
      ├── columns: m:1 k:6 v1:9
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(9)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v1:9!null k:6!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(9)
      │    ├── select
      │    │    ├── columns: k:6!null i:7!null
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7)
      │    │    ├── scan virt@v1_storing_i
      │    │    │    ├── columns: k:6!null i:7
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7)
      │    │    └── filters
      │    │         └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Left join, non-covering case. Virtual column is the lookup column and there is
# an extra filter on a column not in the index.
# TODO(#90771): To plan a lookup join here, we'd need to project the virtual
# column expression after the upper join.
opt expect-not=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND j > 0
----
project
 ├── columns: m:1 j:8
 ├── immutable
 └── left-join (hash)
      ├── columns: m:1 j:8 v1:9
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v1:9 j:8!null
      │    ├── immutable
      │    ├── select
      │    │    ├── columns: i:7 j:8!null
      │    │    ├── scan virt
      │    │    │    ├── columns: i:7 j:8!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    │    └── computed column expressions
      │    │    │         ├── v1:9
      │    │    │         │    └── i:7 + 10
      │    │    │         ├── v2:10
      │    │    │         │    └── i:7 + 100
      │    │    │         ├── v3:11
      │    │    │         │    └── i:7 + j:8
      │    │    │         └── v4:12
      │    │    │              └── i:7 + 1
      │    │    └── filters
      │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Left join, non-covering case. Virtual column is the lookup column and there is
# an extra filter on a column not in the index, but the column is not selected.
# TODO(mgartner): We do not handle this case yet.
opt
SELECT m, virt.k FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND j > 0
----
project
 ├── columns: m:1 k:6
 ├── immutable
 └── left-join (hash)
      ├── columns: m:1 k:6 v1:9
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(9)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v1:9 k:6!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(9)
      │    ├── select
      │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7,8)
      │    │    ├── scan virt
      │    │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    │    ├── computed column expressions
      │    │    │    │    ├── v1:9
      │    │    │    │    │    └── i:7 + 10
      │    │    │    │    ├── v2:10
      │    │    │    │    │    └── i:7 + 100
      │    │    │    │    ├── v3:11
      │    │    │    │    │    └── i:7 + j:8
      │    │    │    │    └── v4:12
      │    │    │    │         └── i:7 + 1
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7,8)
      │    │    └── filters
      │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

exec-ddl
DROP INDEX v1_storing_i
----

exec-ddl
CREATE INDEX i_v1 ON virt (i, v1)
----

# Covering case.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON virt.i = 1 AND m = virt.v1
----
inner-join (lookup virt@i_v1)
 ├── columns: m:1!null k:6!null v1:9!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [16 1] = [7 9]
 ├── immutable
 ├── fd: ()-->(1,9), (1)==(9), (9)==(1)
 ├── project
 │    ├── columns: "lookup_join_const_col_@7":16!null m:1
 │    ├── fd: ()-->(16)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── 1 [as="lookup_join_const_col_@7":16]
 └── filters (true)

# Non-covering case.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j FROM small INNER LOOKUP JOIN virt ON virt.i = 1 AND m = virt.v1
----
project
 ├── columns: m:1!null j:8!null
 ├── immutable
 ├── fd: ()-->(1)
 └── inner-join (lookup virt)
      ├── columns: m:1!null j:8!null v1:9!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(1,9), (1)==(9), (9)==(1)
      ├── inner-join (lookup virt@i_v1)
      │    ├── columns: m:1!null k:6!null v1:9!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [16 1] = [7 9]
      │    ├── fd: ()-->(1,9), (1)==(9), (9)==(1)
      │    ├── project
      │    │    ├── columns: "lookup_join_const_col_@7":16!null m:1
      │    │    ├── fd: ()-->(16)
      │    │    ├── scan small
      │    │    │    └── columns: m:1
      │    │    └── projections
      │    │         └── 1 [as="lookup_join_const_col_@7":16]
      │    └── filters (true)
      └── filters (true)

# Covering case with a set of possible values for the leading lookup column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.v1 FROM small INNER LOOKUP JOIN virt ON virt.i IN (1, 2, 3) AND m = virt.v1
----
project
 ├── columns: m:1!null k:6!null v1:9!null
 ├── immutable
 ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 └── inner-join (lookup virt@i_v1)
      ├── columns: m:1!null k:6!null i:7!null v1:9!null
      ├── flags: force lookup join (into right side)
      ├── lookup expression
      │    └── filters
      │         ├── i:7 IN (1, 2, 3) [outer=(7), constraints=(/7: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
      ├── fd: (6)-->(7,9), (7)-->(9), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Non-covering case with a set of possible values for the leading lookup column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j, virt.v1 FROM small INNER LOOKUP JOIN virt ON virt.i IN (1, 2, 3) AND m = virt.v1
----
inner-join (lookup virt)
 ├── columns: m:1!null j:8!null v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@i_v1)
 │    ├── columns: m:1!null k:6!null i:7!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── i:7 IN (1, 2, 3) [outer=(7), constraints=(/7: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 │    ├── fd: (6)-->(7,9), (7)-->(9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Left join, covering case.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON virt.i = 1 AND m = virt.v1
----
left-join (lookup virt@i_v1)
 ├── columns: m:1 k:6 v1:9
 ├── flags: force lookup join (into right side)
 ├── key columns: [16 1] = [7 9]
 ├── immutable
 ├── project
 │    ├── columns: "lookup_join_const_col_@7":16!null m:1
 │    ├── fd: ()-->(16)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── 1 [as="lookup_join_const_col_@7":16]
 └── filters (true)

# Left join, non-covering case.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j FROM small LEFT LOOKUP JOIN virt ON virt.i = 1 AND m = virt.v1
----
project
 ├── columns: m:1 j:8
 ├── immutable
 └── left-join (lookup virt)
      ├── columns: m:1 j:8 v1:9
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── immutable
      ├── left-join (lookup virt@i_v1)
      │    ├── columns: m:1 k:6 v1:9
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [16 1] = [7 9]
      │    ├── fd: (6)-->(9)
      │    ├── project
      │    │    ├── columns: "lookup_join_const_col_@7":16!null m:1
      │    │    ├── fd: ()-->(16)
      │    │    ├── scan small
      │    │    │    └── columns: m:1
      │    │    └── projections
      │    │         └── 1 [as="lookup_join_const_col_@7":16]
      │    └── filters (true)
      └── filters (true)

# Left join, covering case with multiple constant values for the leading lookup
# column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.v1 FROM small LEFT LOOKUP JOIN virt ON virt.i IN (1, 2, 3) AND m = virt.v1
----
project
 ├── columns: m:1 k:6 v1:9
 ├── immutable
 ├── fd: (6)-->(9)
 └── left-join (lookup virt@i_v1)
      ├── columns: m:1 k:6 i:7 v1:9
      ├── flags: force lookup join (into right side)
      ├── lookup expression
      │    └── filters
      │         ├── i:7 IN (1, 2, 3) [outer=(7), constraints=(/7: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
      ├── fd: (6)-->(7,9), (7)~~>(9)
      ├── scan small
      │    └── columns: m:1
      └── filters (true)

# Left join, non-covering case with multiple constant values for the leading
# lookup column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.j, virt.v1 FROM small LEFT LOOKUP JOIN virt ON virt.i IN (1, 2, 3) AND m = virt.v1
----
left-join (lookup virt)
 ├── columns: m:1 j:8 v1:9
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── left-join (lookup virt@i_v1)
 │    ├── columns: m:1 k:6 i:7 v1:9
 │    ├── flags: force lookup join (into right side)
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── i:7 IN (1, 2, 3) [outer=(7), constraints=(/7: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 │    ├── fd: (6)-->(7,9), (7)~~>(9)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Do not generate a lookup join when the right side projects a column that is
# not a virtual computed column.
opt expect-not=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, tmp.p
FROM small INNER LOOKUP JOIN (
  SELECT v1, i, i + 5 AS p FROM virt
) tmp ON tmp.i = 1 AND m = tmp.v1
----
project
 ├── columns: m:1!null p:16!null
 ├── immutable
 ├── fd: ()-->(1,16)
 └── inner-join (hash)
      ├── columns: m:1!null v1:9!null p:16!null
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: ()-->(1,9,16), (1)==(9), (9)==(1)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: p:16!null v1:9!null
      │    ├── immutable
      │    ├── fd: ()-->(9,16)
      │    ├── scan virt@i_v1
      │    │    ├── columns: i:7!null
      │    │    ├── constraint: /7/9/6: [/1/11 - /1/11]
      │    │    └── fd: ()-->(7)
      │    └── projections
      │         ├── i:7 + 5 [as=p:16, outer=(7), immutable]
      │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
      └── filters
           └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Do not generate a lookup join when all the projected virtual columns cannot
# be produced from a single index.
opt expect-not=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1, virt.v2 FROM small INNER LOOKUP JOIN virt ON virt.i = 1 AND m = virt.v1
----
inner-join (hash)
 ├── columns: m:1!null v1:9!null v2:10!null
 ├── flags: force lookup join (into right side)
 ├── immutable
 ├── fd: ()-->(1,9,10), (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v1:9!null v2:10!null
 │    ├── immutable
 │    ├── fd: ()-->(9,10)
 │    ├── scan virt@i_v1
 │    │    ├── columns: i:7!null
 │    │    ├── constraint: /7/9/6: [/1/11 - /1/11]
 │    │    └── fd: ()-->(7)
 │    └── projections
 │         ├── i:7 + 10 [as=v1:9, outer=(7), immutable]
 │         └── i:7 + 100 [as=v2:10, outer=(7), immutable]
 └── filters
      └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

exec-ddl
CREATE INDEX l_v1 ON virt (l, v1)
----

# Generate lookup joins with virtual columns for semi-joins.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE virt.l = 1 AND m = virt.v1)
----
semi-join (lookup virt@l_v1)
 ├── columns: m:1
 ├── key columns: [17 1] = [13 9]
 ├── immutable
 ├── project
 │    ├── columns: "lookup_join_const_col_@13":17!null m:1
 │    ├── fd: ()-->(17)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── 1 [as="lookup_join_const_col_@13":17]
 └── filters (true)

# Semi-join with multiple constant values for the leading lookup column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE virt.l IN (1, 2, 3) AND m = virt.v1)
----
semi-join (lookup virt@l_v1)
 ├── columns: m:1
 ├── lookup expression
 │    └── filters
 │         ├── l:13 IN (1, 2, 3) [outer=(13), constraints=(/13: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Semi-join with multiple constant values for the leading lookup column and a
# filter on a column not in the index.
# TODO(mgartner): We do not handle this case yet.
opt
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE virt.l = 1 AND m = virt.v1 AND j > 0)
----
semi-join (hash)
 ├── columns: m:1
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v1:9
 │    ├── immutable
 │    ├── select
 │    │    ├── columns: i:7 j:8!null l:13!null
 │    │    ├── fd: ()-->(13)
 │    │    ├── index-join virt
 │    │    │    ├── columns: i:7 j:8!null l:13
 │    │    │    ├── fd: ()-->(13)
 │    │    │    └── scan virt@l_v1
 │    │    │         ├── columns: k:6!null l:13!null
 │    │    │         ├── constraint: /13/9/6: [/1 - /1]
 │    │    │         ├── key: (6)
 │    │    │         └── fd: ()-->(13)
 │    │    └── filters
 │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
 │    └── projections
 │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
 └── filters
      └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Generate lookup joins with virtual columns for anti-joins.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE virt.l = 1 AND m = virt.v1)
----
anti-join (lookup virt@l_v1)
 ├── columns: m:1
 ├── key columns: [17 1] = [13 9]
 ├── immutable
 ├── project
 │    ├── columns: "lookup_join_const_col_@13":17!null m:1
 │    ├── fd: ()-->(17)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── 1 [as="lookup_join_const_col_@13":17]
 └── filters (true)

# Anti-join with multiple constant values for the leading lookup column.
# opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
opt
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE virt.l IN (1, 2, 3) AND m = virt.v1)
----
anti-join (lookup virt@l_v1)
 ├── columns: m:1
 ├── lookup expression
 │    └── filters
 │         ├── l:13 IN (1, 2, 3) [outer=(13), constraints=(/13: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │         └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Anti-join with multiple constant values for the leading lookup column and a
# filter on a column not in the index.
# TODO(mgartner): We do not handle this case yet.
opt
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE virt.l = 2 AND m = virt.v1 AND j > 0)
----
anti-join (hash)
 ├── columns: m:1
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v1:9
 │    ├── immutable
 │    ├── select
 │    │    ├── columns: i:7 j:8!null l:13!null
 │    │    ├── fd: ()-->(13)
 │    │    ├── index-join virt
 │    │    │    ├── columns: i:7 j:8!null l:13
 │    │    │    ├── fd: ()-->(13)
 │    │    │    └── scan virt@l_v1
 │    │    │         ├── columns: k:6!null l:13!null
 │    │    │         ├── constraint: /13/9/6: [/2 - /2]
 │    │    │         ├── key: (6)
 │    │    │         └── fd: ()-->(13)
 │    │    └── filters
 │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
 │    └── projections
 │         └── i:7 + 10 [as=v1:9, outer=(7), immutable]
 └── filters
      └── m:1 = v1:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

exec-ddl
DROP INDEX l_v1
----

exec-ddl
DROP INDEX i_v1
----

exec-ddl
CREATE INDEX v1_partial ON virt (v1) WHERE k > 0
----

# Covering case with partial index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND k > 0
----
inner-join (lookup virt@v1_partial,partial)
 ├── columns: m:1!null v1:9!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Non-covering case with partial index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND k > 0
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7 v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9), (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@v1_partial,partial)
 │    ├── columns: m:1!null k:6!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Left join, covering case with partial index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND k > 0
----
left-join (lookup virt@v1_partial,partial)
 ├── columns: m:1 v1:9
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Left join, non-covering case with partial index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND k > 0
----
left-join (lookup virt)
 ├── columns: m:1 i:7 v1:9
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)~~>(9)
 ├── left-join (lookup virt@v1_partial,partial)
 │    ├── columns: m:1 k:6 v1:9
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Semi-join with partial index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND k > 0)
----
semi-join (lookup virt@v1_partial,partial)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Anti-join with partial index.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND k > 0)
----
anti-join (lookup virt@v1_partial,partial)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

exec-ddl
DROP INDEX v1_partial
----

exec-ddl
CREATE INDEX v1_partial ON virt (v1) WHERE v1 > 0
----

# Covering case with partial index predicate that references the indexed virtual
# column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND v1 > 0
----
inner-join (lookup virt@v1_partial,partial)
 ├── columns: m:1!null v1:9!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (1)==(9), (9)==(1)
 ├── select
 │    ├── columns: m:1!null
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters
 │         └── m:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
 └── filters (true)

# Non-covering case with partial index predicate that references the indexed
# virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND v1 > 0
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7!null v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9), (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@v1_partial,partial)
 │    ├── columns: m:1!null k:6!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 │    ├── select
 │    │    ├── columns: m:1!null
 │    │    ├── scan small
 │    │    │    └── columns: m:1
 │    │    └── filters
 │    │         └── m:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
 │    └── filters (true)
 └── filters (true)

# Left-join, covering case with partial index predicate that references the
# indexed virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND v1 > 0
----
left-join (lookup virt@v1_partial,partial)
 ├── columns: m:1 v1:9
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Left-join, non-covering case with partial index predicate that references the
# indexed virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND v1 > 0
----
left-join (lookup virt)
 ├── columns: m:1 i:7 v1:9
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9)
 ├── left-join (lookup virt@v1_partial,partial)
 │    ├── columns: m:1 k:6 v1:9
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Semi-join with partial index predicate that references the indexed virtual
# column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND v1 > 0)
----
semi-join (lookup virt@v1_partial,partial)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Anti-join with partial index predicate that references the indexed virtual
# column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND v1 > 0)
----
anti-join (lookup virt@v1_partial,partial)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

exec-ddl
DROP INDEX v1_partial
----

exec-ddl
CREATE INDEX v1_partial ON virt (v1) WHERE v2 > 0
----

# Covering case with partial index predicate that references another virtual
# column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND v2 > 0
----
inner-join (lookup virt@v1_partial,partial)
 ├── columns: m:1!null v1:9!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── fd: (1)==(9), (9)==(1)
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Non-covering case with partial index predicate that references another virtual
# column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i, virt.v1 FROM small INNER LOOKUP JOIN virt ON m = virt.v1 AND v2 > 0
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7!null v1:9!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9), (1)==(9), (9)==(1)
 ├── inner-join (lookup virt@v1_partial,partial)
 │    ├── columns: m:1!null k:6!null v1:9!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9), (1)==(9), (9)==(1)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Left join, covering case with partial index predicate that references another
# virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND v2 > 0
----
left-join (lookup virt@v1_partial,partial)
 ├── columns: m:1 v1:9
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Left join, non-covering case with partial index predicate that references
# another virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.i, virt.v1 FROM small LEFT LOOKUP JOIN virt ON m = virt.v1 AND v2 > 0
----
left-join (lookup virt)
 ├── columns: m:1 i:7 v1:9
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (7)-->(9)
 ├── left-join (lookup virt@v1_partial,partial)
 │    ├── columns: m:1 k:6 v1:9
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [9]
 │    ├── fd: (6)-->(9)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters (true)
 └── filters (true)

# Semi-join with partial index predicate that references another virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND v2 > 0)
----
semi-join (lookup virt@v1_partial,partial)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

# Anti-join with partial index predicate that references another virtual column.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v1 AND v2 > 0)
----
anti-join (lookup virt@v1_partial,partial)
 ├── columns: m:1
 ├── key columns: [1] = [9]
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 └── filters (true)

exec-ddl
DROP INDEX v1_partial
----

exec-ddl
CREATE INDEX v3_partial ON virt (v3) WHERE v3 > 0
----

# Covering case with a more complex partial index predicate that references the
# indexed virtual column.
opt
SELECT m, virt.k, virt.v3 FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND v3 > 0 AND m > 0
----
inner-join (lookup virt@v3_partial,partial)
 ├── columns: m:1!null k:6!null v3:11!null
 ├── flags: force lookup join (into right side)
 ├── key columns: [1] = [11]
 ├── immutable
 ├── fd: (6)-->(11), (1)==(11), (11)==(1)
 ├── select
 │    ├── columns: m:1!null
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters
 │         └── m:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
 └── filters (true)

# Non-covering case with a more complex partial index predicate that references
# the indexed virtual column.
opt
SELECT m, virt.i, virt.v3 FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND v3 > 0 AND m > 0
----
inner-join (lookup virt)
 ├── columns: m:1!null i:7 v3:11!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── fd: (1)==(11), (11)==(1)
 ├── inner-join (lookup virt@v3_partial,partial)
 │    ├── columns: m:1!null k:6!null v3:11!null
 │    ├── flags: force lookup join (into right side)
 │    ├── key columns: [1] = [11]
 │    ├── fd: (6)-->(11), (1)==(11), (11)==(1)
 │    ├── select
 │    │    ├── columns: m:1!null
 │    │    ├── scan small
 │    │    │    └── columns: m:1
 │    │    └── filters
 │    │         └── m:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
 │    └── filters (true)
 └── filters (true)

exec-ddl
DROP INDEX v3_partial
----

exec-ddl
CREATE INDEX v3_partial ON virt (v3) STORING (i) WHERE i > 0 OR j > 0 OR virt.v2 > 0 OR virt.v3 > 0
----

# We can generate a lookup join with filters remaining after partial index
# implication if the index contains columns in the remaining filter and those
# columns are output by the Project.
# projected.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.i FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND i > 0
----
project
 ├── columns: m:1!null k:6!null i:7!null
 ├── immutable
 ├── fd: (6)-->(1,7)
 └── inner-join (lookup virt@v3_partial,partial)
      ├── columns: m:1!null k:6!null i:7!null v3:11!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [11]
      ├── immutable
      ├── fd: (6)-->(7,11), (1)==(11), (11)==(1)
      ├── scan small
      │    └── columns: m:1
      └── filters
           └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]

# Left join, same case as above.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.i FROM small LEFT LOOKUP JOIN virt ON m = virt.v3 AND i > 0
----
project
 ├── columns: m:1 k:6 i:7
 ├── immutable
 ├── fd: (6)-->(7)
 └── left-join (lookup virt@v3_partial,partial)
      ├── columns: m:1 k:6 i:7 v3:11
      ├── flags: force lookup join (into right side)
      ├── key columns: [1] = [11]
      ├── immutable
      ├── fd: (6)-->(7,11)
      ├── scan small
      │    └── columns: m:1
      └── filters
           └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]

# Semi-join, same case as above.
# TODO(mgartner): We don't currently handle this case, but we should be able to.
opt
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v3 AND i > 0)
----
semi-join (hash)
 ├── columns: m:1
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v3:11!null
 │    ├── immutable
 │    ├── select
 │    │    ├── columns: i:7!null j:8!null
 │    │    ├── scan virt
 │    │    │    ├── columns: i:7 j:8!null
 │    │    │    ├── check constraint expressions
 │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │    │    ├── computed column expressions
 │    │    │    │    ├── v1:9
 │    │    │    │    │    └── i:7 + 10
 │    │    │    │    ├── v2:10
 │    │    │    │    │    └── i:7 + 100
 │    │    │    │    ├── v3:11
 │    │    │    │    │    └── i:7 + j:8
 │    │    │    │    └── v4:12
 │    │    │    │         └── i:7 + 1
 │    │    │    └── partial index predicates
 │    │    │         └── v3_partial: filters
 │    │    │              └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
 │    │    └── filters
 │    │         └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
 │    └── projections
 │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
 └── filters
      └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# Anti-join, same case as above.
# TODO(mgartner): We don't currently handle this case, but we should be able to.
opt
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v3 AND i > 0)
----
anti-join (hash)
 ├── columns: m:1
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v3:11!null
 │    ├── immutable
 │    ├── select
 │    │    ├── columns: i:7!null j:8!null
 │    │    ├── scan virt
 │    │    │    ├── columns: i:7 j:8!null
 │    │    │    ├── check constraint expressions
 │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │    │    ├── computed column expressions
 │    │    │    │    ├── v1:9
 │    │    │    │    │    └── i:7 + 10
 │    │    │    │    ├── v2:10
 │    │    │    │    │    └── i:7 + 100
 │    │    │    │    ├── v3:11
 │    │    │    │    │    └── i:7 + j:8
 │    │    │    │    └── v4:12
 │    │    │    │         └── i:7 + 1
 │    │    │    └── partial index predicates
 │    │    │         └── v3_partial: filters
 │    │    │              └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
 │    │    └── filters
 │    │         └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
 │    └── projections
 │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
 └── filters
      └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# We can generate a lookup join with filters remaining after partial index
# implication if the index does not contain columns in the remaining filter,
# as long as those columns are output by the Project.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.j FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND j > 0
----
project
 ├── columns: m:1!null k:6!null j:8!null
 ├── immutable
 ├── fd: (6)-->(1,8)
 └── inner-join (lookup virt)
      ├── columns: m:1!null k:6!null j:8!null v3:11!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (6)-->(8,11), (1)==(11), (11)==(1)
      ├── inner-join (lookup virt@v3_partial,partial)
      │    ├── columns: m:1!null k:6!null v3:11!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [11]
      │    ├── fd: (6)-->(11), (1)==(11), (11)==(1)
      │    ├── scan small
      │    │    └── columns: m:1
      │    └── filters (true)
      └── filters
           └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]

# Left join, same case as above.
# TODO(#90771): To plan a lookup join here, we'd need to project the virtual
# column expression after the upper join.
opt expect-not=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.j FROM small LEFT LOOKUP JOIN virt ON m = virt.v3 AND j > 0
----
project
 ├── columns: m:1 k:6 j:8
 ├── immutable
 ├── fd: (6)-->(8)
 └── left-join (hash)
      ├── columns: m:1 k:6 j:8 v3:11
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(8,11)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v3:11 k:6!null j:8!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(8,11)
      │    ├── select
      │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7,8)
      │    │    ├── scan virt
      │    │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    │    ├── computed column expressions
      │    │    │    │    ├── v1:9
      │    │    │    │    │    └── i:7 + 10
      │    │    │    │    ├── v2:10
      │    │    │    │    │    └── i:7 + 100
      │    │    │    │    ├── v3:11
      │    │    │    │    │    └── i:7 + j:8
      │    │    │    │    └── v4:12
      │    │    │    │         └── i:7 + 1
      │    │    │    ├── partial index predicates
      │    │    │    │    └── v3_partial: filters
      │    │    │    │         └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7,8)
      │    │    └── filters
      │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
      └── filters
           └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# Semi-join, same case as above.
# TODO(mgartner): We don't currently handle this case, but we should be able to.
opt
SELECT m FROM small WHERE EXISTS (SELECT * FROM virt WHERE m = virt.v3 AND j > 0)
----
semi-join (hash)
 ├── columns: m:1
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v3:11
 │    ├── immutable
 │    ├── select
 │    │    ├── columns: i:7 j:8!null
 │    │    ├── scan virt
 │    │    │    ├── columns: i:7 j:8!null
 │    │    │    ├── check constraint expressions
 │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │    │    ├── computed column expressions
 │    │    │    │    ├── v1:9
 │    │    │    │    │    └── i:7 + 10
 │    │    │    │    ├── v2:10
 │    │    │    │    │    └── i:7 + 100
 │    │    │    │    ├── v3:11
 │    │    │    │    │    └── i:7 + j:8
 │    │    │    │    └── v4:12
 │    │    │    │         └── i:7 + 1
 │    │    │    └── partial index predicates
 │    │    │         └── v3_partial: filters
 │    │    │              └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
 │    │    └── filters
 │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
 │    └── projections
 │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
 └── filters
      └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# Anti-join, same case as above.
# TODO(mgartner): We don't currently handle this case, but we should be able to.
opt
SELECT m FROM small WHERE NOT EXISTS (SELECT * FROM virt WHERE m = virt.v3 AND j > 0)
----
anti-join (hash)
 ├── columns: m:1
 ├── immutable
 ├── scan small
 │    └── columns: m:1
 ├── project
 │    ├── columns: v3:11
 │    ├── immutable
 │    ├── select
 │    │    ├── columns: i:7 j:8!null
 │    │    ├── scan virt
 │    │    │    ├── columns: i:7 j:8!null
 │    │    │    ├── check constraint expressions
 │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │    │    ├── computed column expressions
 │    │    │    │    ├── v1:9
 │    │    │    │    │    └── i:7 + 10
 │    │    │    │    ├── v2:10
 │    │    │    │    │    └── i:7 + 100
 │    │    │    │    ├── v3:11
 │    │    │    │    │    └── i:7 + j:8
 │    │    │    │    └── v4:12
 │    │    │    │         └── i:7 + 1
 │    │    │    └── partial index predicates
 │    │    │         └── v3_partial: filters
 │    │    │              └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
 │    │    └── filters
 │    │         └── j:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
 │    └── projections
 │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
 └── filters
      └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# We can generate a lookup join with filters remaining after partial index
# implication that contain a virtual column if the columns referenced in the
# virtual column expression are output by the Project.
opt expect=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.i, virt.j FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND v3 > 0
----
project
 ├── columns: m:1!null k:6!null i:7 j:8!null
 ├── immutable
 ├── fd: (6)-->(7,8), (7,8)-->(1)
 └── inner-join (lookup virt)
      ├── columns: m:1!null k:6!null i:7 j:8!null v3:11!null
      ├── key columns: [6] = [6]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (6)-->(7,8), (7,8)-->(11), (1)==(11), (11)==(1)
      ├── inner-join (lookup virt@v3_partial,partial)
      │    ├── columns: m:1!null k:6!null i:7 v3:11!null
      │    ├── flags: force lookup join (into right side)
      │    ├── key columns: [1] = [11]
      │    ├── fd: (6)-->(7,11), (1)==(11), (11)==(1)
      │    ├── select
      │    │    ├── columns: m:1!null
      │    │    ├── scan small
      │    │    │    └── columns: m:1
      │    │    └── filters
      │    │         └── m:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
      │    └── filters (true)
      └── filters
           └── (i:7 + j:8) > 0 [outer=(7,8), immutable]

# Left join, same case as above.
# TODO(#90771): To plan a lookup join here, we'd need to project the virtual
# column expression after the upper join.
opt expect-not=GenerateLookupJoinsWithVirtualColsAndFilter
SELECT m, virt.k, virt.i, virt.j FROM small LEFT LOOKUP JOIN virt ON m = virt.v3 AND v3 > 0
----
project
 ├── columns: m:1 k:6 i:7 j:8
 ├── immutable
 ├── fd: (6)-->(7,8)
 └── left-join (hash)
      ├── columns: m:1 k:6 i:7 j:8 v3:11
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(7,8), (7,8)-->(11)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v3:11 k:6!null i:7 j:8!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(7,8), (7,8)-->(11)
      │    ├── select
      │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    ├── immutable
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7,8)
      │    │    ├── scan virt
      │    │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    │    ├── computed column expressions
      │    │    │    │    ├── v1:9
      │    │    │    │    │    └── i:7 + 10
      │    │    │    │    ├── v2:10
      │    │    │    │    │    └── i:7 + 100
      │    │    │    │    ├── v3:11
      │    │    │    │    │    └── i:7 + j:8
      │    │    │    │    └── v4:12
      │    │    │    │         └── i:7 + 1
      │    │    │    ├── partial index predicates
      │    │    │    │    └── v3_partial: filters
      │    │    │    │         └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7,8)
      │    │    └── filters
      │    │         └── (i:7 + j:8) > 0 [outer=(7,8), immutable]
      │    └── projections
      │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
      └── filters
           └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# We cannot currently generate a lookup join with filters remaining after
# partial index implication that contain a virtual column if the columns
# referenced in the virtual column expression are not output by the Project.
# TODO(mgartner): We could handle this by wrapping the lookup join in an index
# join to fetch i and j, and filter by them, then wrapping the index join in a
# Project that removes i.
opt
SELECT m, virt.k, virt.v3 FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND v3 > 0
----
inner-join (hash)
 ├── columns: m:1!null k:6!null v3:11!null
 ├── flags: force lookup join (into right side)
 ├── immutable
 ├── fd: (6)-->(11), (1)==(11), (11)==(1)
 ├── select
 │    ├── columns: m:1!null
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── filters
 │         └── m:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
 ├── project
 │    ├── columns: v3:11 k:6!null
 │    ├── immutable
 │    ├── key: (6)
 │    ├── fd: (6)-->(11)
 │    ├── select
 │    │    ├── columns: k:6!null i:7 j:8!null
 │    │    ├── immutable
 │    │    ├── key: (6)
 │    │    ├── fd: (6)-->(7,8)
 │    │    ├── scan virt
 │    │    │    ├── columns: k:6!null i:7 j:8!null
 │    │    │    ├── check constraint expressions
 │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
 │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
 │    │    │    ├── computed column expressions
 │    │    │    │    ├── v1:9
 │    │    │    │    │    └── i:7 + 10
 │    │    │    │    ├── v2:10
 │    │    │    │    │    └── i:7 + 100
 │    │    │    │    ├── v3:11
 │    │    │    │    │    └── i:7 + j:8
 │    │    │    │    └── v4:12
 │    │    │    │         └── i:7 + 1
 │    │    │    ├── partial index predicates
 │    │    │    │    └── v3_partial: filters
 │    │    │    │         └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
 │    │    │    ├── key: (6)
 │    │    │    └── fd: (6)-->(7,8)
 │    │    └── filters
 │    │         └── (i:7 + j:8) > 0 [outer=(7,8), immutable]
 │    └── projections
 │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
 └── filters
      └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

# We cannot currently generate a lookup join with filters remaining after
# partial index implication if the columns in the remaining filter are not
# output by the Project.
# TODO(mgartner): We could handle this by wrapping the lookup join in an index
# join to fetch i and filter by it, then wrapping the IndexJoin in a Project
# that removes i.
opt
SELECT m, virt.k FROM small INNER LOOKUP JOIN virt ON m = virt.v3 AND i > 0
----
project
 ├── columns: m:1!null k:6!null
 ├── immutable
 ├── fd: (6)-->(1)
 └── inner-join (hash)
      ├── columns: m:1!null k:6!null v3:11!null
      ├── flags: force lookup join (into right side)
      ├── immutable
      ├── fd: (6)-->(11), (1)==(11), (11)==(1)
      ├── scan small
      │    └── columns: m:1
      ├── project
      │    ├── columns: v3:11!null k:6!null
      │    ├── immutable
      │    ├── key: (6)
      │    ├── fd: (6)-->(11)
      │    ├── select
      │    │    ├── columns: k:6!null i:7!null j:8!null
      │    │    ├── key: (6)
      │    │    ├── fd: (6)-->(7,8)
      │    │    ├── scan virt
      │    │    │    ├── columns: k:6!null i:7 j:8!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    ├── j:8 IN (10, 20, 30) [outer=(8), constraints=(/8: [/10 - /10] [/20 - /20] [/30 - /30]; tight)]
      │    │    │    │    └── v4:12 IN (1, 2, 3) [outer=(12), constraints=(/12: [/1 - /1] [/2 - /2] [/3 - /3]; tight)]
      │    │    │    ├── computed column expressions
      │    │    │    │    ├── v1:9
      │    │    │    │    │    └── i:7 + 10
      │    │    │    │    ├── v2:10
      │    │    │    │    │    └── i:7 + 100
      │    │    │    │    ├── v3:11
      │    │    │    │    │    └── i:7 + j:8
      │    │    │    │    └── v4:12
      │    │    │    │         └── i:7 + 1
      │    │    │    ├── partial index predicates
      │    │    │    │    └── v3_partial: filters
      │    │    │    │         └── (((i:7 > 0) OR (j:8 > 0)) OR (i:7 > -100)) OR ((i:7 + j:8) > 0) [outer=(7,8), immutable]
      │    │    │    ├── key: (6)
      │    │    │    └── fd: (6)-->(7,8)
      │    │    └── filters
      │    │         └── i:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
      │    └── projections
      │         └── i:7 + j:8 [as=v3:11, outer=(7,8), immutable]
      └── filters
           └── m:1 = v3:11 [outer=(1,11), constraints=(/1: (/NULL - ]; /11: (/NULL - ]), fd=(1)==(11), (11)==(1)]

exec-ddl
DROP INDEX v3_partial
----


# -------------------------------------------------------
# GenerateInvertedJoins + GenerateInvertedJoinsFromSelect
# -------------------------------------------------------

exec-ddl
CREATE TABLE nyc_census_blocks (
  gid serial PRIMARY KEY,
  blkid varchar(15),
  popn_total float8,
  popn_white float8,
  popn_black float8,
  popn_nativ float8,
  popn_asian float8,
  popn_other float8,
  boroname varchar(32),
  geom GEOMETRY(MULTIPOLYGON,4326),
  INVERTED INDEX nyc_census_blocks_geo_idx (geom)
)
----

exec-ddl
CREATE TABLE nyc_neighborhoods (
  gid serial PRIMARY KEY,
  boroname varchar(43),
  name varchar(64),
  geom GEOMETRY(MULTIPOLYGON,4326),
  INVERTED INDEX nyc_neighborhoods_geo_idx (geom)
)
----

exec-ddl
ALTER TABLE nyc_census_blocks INJECT STATISTICS '[
  {
    "columns": ["gid"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 38794,
    "distinct_count": 38794
  },
  {
    "columns": ["boroname"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 38794,
    "distinct_count": 5
  }
]'
----

exec-ddl
ALTER TABLE nyc_neighborhoods INJECT STATISTICS '[
  {
    "columns": ["gid"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 129,
    "distinct_count": 129
  },
  {
    "columns": ["boroname"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 129,
    "distinct_count": 5
  },
  {
    "columns": ["name"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 129,
    "distinct_count": 129
  }
]'
----

# This query calculates the population density of two different neighborhoods
# in New York City.
opt expect=GenerateInvertedJoins
SELECT
  n.name,
  Sum(c.popn_total) / (ST_Area(n.geom) / 1000000.0) AS popn_per_sqkm
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods AS n
ON ST_Intersects(n.geom, c.geom) AND c.boroname = n.boroname
WHERE n.name = 'Upper West Side'
OR n.name = 'Upper East Side'
GROUP BY n.name, n.geom
----
project
 ├── columns: name:16!null popn_per_sqkm:22
 ├── immutable
 ├── group-by (hash)
 │    ├── columns: name:16!null n.geom:17!null sum:21
 │    ├── grouping columns: name:16!null n.geom:17!null
 │    ├── immutable
 │    ├── key: (16,17)
 │    ├── fd: (16,17)-->(21)
 │    ├── inner-join (lookup nyc_census_blocks [as=c])
 │    │    ├── columns: popn_total:3 c.boroname:9!null c.geom:10!null n.boroname:15!null name:16!null n.geom:17!null
 │    │    ├── key columns: [30] = [1]
 │    │    ├── lookup columns are key
 │    │    ├── immutable
 │    │    ├── fd: (9)==(15), (15)==(9)
 │    │    ├── inner-join (inverted nyc_census_blocks@nyc_census_blocks_geo_idx,inverted [as=c])
 │    │    │    ├── columns: n.boroname:15 name:16!null n.geom:17 c.gid:30!null
 │    │    │    ├── inverted-expr
 │    │    │    │    └── st_intersects(n.geom:17, c.geom:39)
 │    │    │    ├── select
 │    │    │    │    ├── columns: n.boroname:15 name:16!null n.geom:17
 │    │    │    │    ├── scan nyc_neighborhoods [as=n]
 │    │    │    │    │    └── columns: n.boroname:15 name:16 n.geom:17
 │    │    │    │    └── filters
 │    │    │    │         └── (name:16 = 'Upper West Side') OR (name:16 = 'Upper East Side') [outer=(16), constraints=(/16: [/'Upper East Side' - /'Upper East Side'] [/'Upper West Side' - /'Upper West Side']; tight)]
 │    │    │    └── filters (true)
 │    │    └── filters
 │    │         ├── st_intersects(n.geom:17, c.geom:10) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
 │    │         └── c.boroname:9 = n.boroname:15 [outer=(9,15), constraints=(/9: (/NULL - ]; /15: (/NULL - ]), fd=(9)==(15), (15)==(9)]
 │    └── aggregations
 │         └── sum [as=sum:21, outer=(3)]
 │              └── popn_total:3
 └── projections
      └── sum:21 / (st_area(n.geom:17) / 1e+06) [as=popn_per_sqkm:22, outer=(17,21), immutable]

memo expect=GenerateInvertedJoins
SELECT
  n.name,
  Sum(c.popn_total) / (ST_Area(n.geom) / 1000000.0) AS popn_per_sqkm
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods AS n
ON ST_Intersects(n.geom, c.geom) AND c.boroname = n.boroname
WHERE n.name = 'Upper West Side'
OR n.name = 'Upper East Side'
GROUP BY n.name, n.geom
----
memo (optimized, ~38KB, required=[presentation: name:16,popn_per_sqkm:22])
 ├── G1: (project G2 G3 name)
 │    └── [presentation: name:16,popn_per_sqkm:22]
 │         ├── best: (project G2 G3 name)
 │         └── cost: 7344.90
 ├── G2: (group-by G4 G5 cols=(16,17))
 │    └── []
 │         ├── best: (group-by G4 G5 cols=(16,17))
 │         └── cost: 7344.84
 ├── G3: (projections G6)
 ├── G4: (inner-join G7 G8 G9) (inner-join G8 G7 G9) (lookup-join G10 G11 nyc_neighborhoods [as=n],keyCols=[23],outCols=(3,9,10,15-17)) (lookup-join G12 G9 nyc_census_blocks [as=c],keyCols=[30],outCols=(3,9,10,15-17))
 │    └── []
 │         ├── best: (lookup-join G12 G9 nyc_census_blocks [as=c],keyCols=[30],outCols=(3,9,10,15-17))
 │         └── cost: 7338.71
 ├── G5: (aggregations G13)
 ├── G6: (div G14 G15)
 ├── G7: (scan nyc_census_blocks [as=c],cols=(3,9,10))
 │    └── []
 │         ├── best: (scan nyc_census_blocks [as=c],cols=(3,9,10))
 │         └── cost: 43866.54
 ├── G8: (select G16 G17)
 │    └── []
 │         ├── best: (select G16 G17)
 │         └── cost: 168.07
 ├── G9: (filters G18 G19)
 ├── G10: (inverted-join G7 G20 nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
 │    └── []
 │         ├── best: (inverted-join G7 G20 nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
 │         └── cost: 921840.85
 ├── G11: (filters G18 G19 G21)
 ├── G12: (inverted-join G8 G20 nyc_census_blocks@nyc_census_blocks_geo_idx,inverted [as=c])
 │    └── []
 │         ├── best: (inverted-join G8 G20 nyc_census_blocks@nyc_census_blocks_geo_idx,inverted [as=c])
 │         └── cost: 1794.89
 ├── G13: (sum G22)
 ├── G14: (variable sum)
 ├── G15: (div G23 G24)
 ├── G16: (scan nyc_neighborhoods [as=n],cols=(15-17))
 │    └── []
 │         ├── best: (scan nyc_neighborhoods [as=n],cols=(15-17))
 │         └── cost: 166.75
 ├── G17: (filters G21)
 ├── G18: (function G25 st_intersects)
 ├── G19: (eq G26 G27)
 ├── G20: (filters)
 ├── G21: (or G28 G29)
 ├── G22: (variable popn_total)
 ├── G23: (function G30 st_area)
 ├── G24: (const 1e+06)
 ├── G25: (scalar-list G31 G32)
 ├── G26: (variable c.boroname)
 ├── G27: (variable n.boroname)
 ├── G28: (eq G33 G34)
 ├── G29: (eq G33 G35)
 ├── G30: (scalar-list G31)
 ├── G31: (variable n.geom)
 ├── G32: (variable c.geom)
 ├── G33: (variable name)
 ├── G34: (const 'Upper West Side')
 └── G35: (const 'Upper East Side')

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_DWithin(c.geom, n.geom, 50)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_dwithin(c.geom:10, n.geom:24, 50.0)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_dwithin(c.geom:10, n.geom:17, 50.0) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Same test as above, but with arguments commuted.
opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_DWithin(n.geom, c.geom, 50)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_dwithin(c.geom:10, n.geom:24, 50.0)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_dwithin(n.geom:17, c.geom:10, 50.0) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10, n.geom:24)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Same test as above, but with arguments commuted.
opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(n.geom, c.geom)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_coveredby(c.geom:10, n.geom:24)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_covers(n.geom:17, c.geom:10) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Case for ST_DWithinExclusive.
opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_DWithinExclusive(c.geom, n.geom, 50)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_dwithinexclusive(c.geom:10, n.geom:24, 50.0)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_dwithinexclusive(c.geom:10, n.geom:17, 50.0) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Commuted version of previous test.
opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_DWithinExclusive(n.geom, c.geom, 50)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_dwithin(c.geom:10, n.geom:24, 50.0)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_dwithinexclusive(n.geom:17, c.geom:10, 50.0) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Complex join filter.
opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(n.geom, c.geom)
  AND (ST_DWithin(c.geom, n.geom, 50) OR ST_Intersects(n.geom, 'LINESTRING ( 0 0, 0 2 )'::geometry))
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [61] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:61!null
      │    ├── inverted-expr
      │    │    └── st_coveredby(c.geom:10, n.geom:64) AND (st_dwithin(c.geom:10, n.geom:64, 50.0) OR st_intersects('0102000000020000000000000000000000000000000000000000000000000000000000000000000040', n.geom:64))
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           ├── st_covers(n.geom:17, c.geom:10) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
           └── st_dwithin(c.geom:10, n.geom:17, 50.0) OR st_intersects(n.geom:17, '0102000000020000000000000000000000000000000000000000000000000000000000000000000040') [outer=(10,17), immutable, constraints=(/17: (/NULL - ])]

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_DFullyWithin(c.geom, n.geom, 50)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_dfullywithin(c.geom:10, n.geom:24, 50.0)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_dfullywithin(c.geom:10, n.geom:17, 50.0) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_DFullyWithinExclusive(c.geom, n.geom, 50)
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_dfullywithinexclusive(c.geom:10, n.geom:24, 50.0)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── st_dfullywithinexclusive(c.geom:10, n.geom:17, 50.0) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

opt expect=GenerateInvertedJoinsFromSelect
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom)
AND ST_Covers('LINESTRING ( 0 0, 0 2 )', n.geom)
AND ST_Covers(c.geom, 'LINESTRING ( 0 0, 0 2 )')
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_census_blocks [as=c])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [28] = [1]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_census_blocks@nyc_census_blocks_geo_idx,inverted [as=c])
      │    ├── columns: name:16 n.geom:17!null c.gid:28!null
      │    ├── inverted-expr
      │    │    └── st_coveredby(n.geom:17, c.geom:37) AND st_coveredby('0102000000020000000000000000000000000000000000000000000000000000000000000000000040', c.geom:37)
      │    ├── immutable
      │    ├── select
      │    │    ├── columns: name:16 n.geom:17!null
      │    │    ├── immutable
      │    │    ├── index-join nyc_neighborhoods
      │    │    │    ├── columns: name:16 n.geom:17
      │    │    │    └── inverted-filter
      │    │    │         ├── columns: n.gid:14!null
      │    │    │         ├── inverted expression: /20
      │    │    │         │    ├── tight: false, unique: false
      │    │    │         │    └── union spans: ["B\x89", "B\xfd \x00\x00\x00\x00\x00\x00\x00")
      │    │    │         ├── pre-filterer expression
      │    │    │         │    └── st_covers('0102000000020000000000000000000000000000000000000000000000000000000000000000000040', n.geom:17)
      │    │    │         ├── key: (14)
      │    │    │         └── scan nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n]
      │    │    │              ├── columns: n.gid:14!null n.geom_inverted_key:20!null
      │    │    │              ├── inverted constraint: /20/14
      │    │    │              │    └── spans: ["B\x89", "B\xfd \x00\x00\x00\x00\x00\x00\x00")
      │    │    │              └── flags: force-index=nyc_neighborhoods_geo_idx
      │    │    └── filters
      │    │         └── st_covers('0102000000020000000000000000000000000000000000000000000000000000000000000000000040', n.geom:17) [outer=(17), immutable, constraints=(/17: (/NULL - ])]
      │    └── filters (true)
      └── filters
           ├── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
           └── st_covers(c.geom:10, '0102000000020000000000000000000000000000000000000000000000000000000000000000000040') [outer=(10), immutable, constraints=(/10: (/NULL - ])]

opt expect=GenerateInvertedJoinsFromSelect
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom) AND c.boroname = 'Manhattan' AND n.name LIKE 'Upper%'
----
project
 ├── columns: name:16!null boroname:9!null
 ├── immutable
 ├── fd: ()-->(9)
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9!null c.geom:10!null name:16!null n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(9)
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9!null c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10, n.geom:24)
      │    ├── fd: ()-->(9)
      │    ├── select
      │    │    ├── columns: c.boroname:9!null c.geom:10
      │    │    ├── fd: ()-->(9)
      │    │    ├── scan nyc_census_blocks [as=c]
      │    │    │    └── columns: c.boroname:9 c.geom:10
      │    │    └── filters
      │    │         └── c.boroname:9 = 'Manhattan' [outer=(9), constraints=(/9: [/'Manhattan' - /'Manhattan']; tight), fd=()-->(9)]
      │    └── filters (true)
      └── filters
           ├── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
           └── name:16 LIKE 'Upper%' [outer=(16), constraints=(/16: [/'Upper' - /'Uppes'); tight)]

# It's not possible to generate an inverted join when there is an OR with a
# non-geospatial function.
opt expect-not=GenerateInvertedJoins disable=SplitDisjunctionOfJoinTerms
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom) OR n.name LIKE c.boroname || 'Upper%'
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (cross)
      ├── columns: c.boroname:9 c.geom:10 name:16 n.geom:17
      ├── immutable
      ├── scan nyc_census_blocks [as=c]
      │    └── columns: c.boroname:9 c.geom:10
      ├── scan nyc_neighborhoods [as=n]
      │    ├── columns: name:16 n.geom:17
      │    └── flags: force-index=nyc_neighborhoods_geo_idx
      └── filters
           └── st_covers(c.geom:10, n.geom:17) OR (name:16 LIKE (c.boroname:9 || 'Upper%')) [outer=(9,10,16,17), immutable]

# Semi-joins are supported by converting them to a paired-join consisting of
# an inner inverted join followed by a left semi lookup join.
opt expect=GenerateInvertedJoins
SELECT *
FROM nyc_census_blocks AS c
WHERE EXISTS (
  SELECT * FROM nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n WHERE ST_Covers(c.geom, n.geom)
)
----
semi-join (lookup nyc_neighborhoods [as=n])
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10
 ├── key columns: [22] = [14]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-10)
 ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
 │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10 n.gid:22!null continuation:29
 │    ├── first join in paired joiner; continuation column: continuation:29
 │    ├── inverted-expr
 │    │    └── st_covers(c.geom:10, n.geom:25)
 │    ├── key: (1,22)
 │    ├── fd: (1)-->(2-10), (22)-->(29)
 │    ├── scan nyc_census_blocks [as=c]
 │    │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-10)
 │    └── filters (true)
 └── filters
      └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Anti-joins are supported by converting them to a paired-join consisting of
# a left outer inverted join followed by a left anti lookup join.

# Anti-join in which the right-hand side is a Scan.
opt expect=GenerateInvertedJoins
SELECT *
FROM nyc_census_blocks AS c
WHERE NOT EXISTS (
  SELECT * FROM nyc_neighborhoods AS n WHERE ST_Covers(c.geom, n.geom)
)
----
anti-join (lookup nyc_neighborhoods [as=n])
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10
 ├── key columns: [22] = [14]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-10)
 ├── left-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
 │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10 n.gid:22 continuation:29
 │    ├── first join in paired joiner; continuation column: continuation:29
 │    ├── inverted-expr
 │    │    └── st_covers(c.geom:10, n.geom:25)
 │    ├── key: (1,22)
 │    ├── fd: (1)-->(2-10), (22)-->(29)
 │    ├── scan nyc_census_blocks [as=c]
 │    │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-10)
 │    └── filters (true)
 └── filters
      └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Anti-join in which the right-hand side is a Scan wrapped in a Select.
# In this test, the nyc_neighborhoods.name column is needed for the predicate.
opt expect=GenerateInvertedJoinsFromSelect
SELECT boroname
FROM nyc_census_blocks AS c
WHERE NOT EXISTS (
  SELECT 1 FROM nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n WHERE ST_Covers(c.geom, n.geom)
  AND c.boroname = 'Manhattan' AND n.name LIKE 'Upper%'
)
----
project
 ├── columns: boroname:9
 ├── immutable
 └── anti-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10
      ├── key columns: [23] = [14]
      ├── lookup columns are key
      ├── second join in paired joiner
      ├── immutable
      ├── left-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:23 continuation:30
      │    ├── first join in paired joiner; continuation column: continuation:30
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10, n.geom:26)
      │    ├── fd: (23)-->(30)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters
      │         └── c.boroname:9 = 'Manhattan' [outer=(9), constraints=(/9: [/'Manhattan' - /'Manhattan']; tight), fd=()-->(9)]
      └── filters
           ├── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
           └── name:16 LIKE 'Upper%' [outer=(16), constraints=(/16: [/'Upper' - /'Uppes'); tight)]

# Left joins are supported by converting them to a paired-join consisting of a
# left outer inverted join followed by a left outer lookup join.
opt expect=GenerateInvertedJoins
SELECT *
FROM nyc_census_blocks AS c
LEFT JOIN nyc_neighborhoods AS n ON ST_Covers(c.geom, n.geom)
----
left-join (lookup nyc_neighborhoods [as=n])
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10 gid:14 boroname:15 name:16 geom:17
 ├── key columns: [21] = [14]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1,14)
 ├── fd: (1)-->(2-10), (14)-->(15-17)
 ├── left-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
 │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10 n.gid:21 continuation:28
 │    ├── first join in paired joiner; continuation column: continuation:28
 │    ├── inverted-expr
 │    │    └── st_covers(c.geom:10, n.geom:24)
 │    ├── key: (1,21)
 │    ├── fd: (1)-->(2-10), (21)-->(28)
 │    ├── scan nyc_census_blocks [as=c]
 │    │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-10)
 │    └── filters (true)
 └── filters
      └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Case with a CTE and GROUP BY wrapping the left join.
opt expect=(GenerateInvertedJoins)
WITH q AS (
  SELECT * FROM nyc_census_blocks WHERE boroname = 'Manhattan'
)
SELECT count(*), (SELECT count(*) FROM q) FROM (
  SELECT n.boroname
  FROM q
  LEFT JOIN nyc_neighborhoods AS n ON ST_Intersects(q.geom, n.geom)
) GROUP BY boroname
----
with &1 (q)
 ├── columns: count:31!null count:43
 ├── immutable
 ├── select
 │    ├── columns: nyc_census_blocks.gid:1!null nyc_census_blocks.blkid:2 nyc_census_blocks.popn_total:3 nyc_census_blocks.popn_white:4 nyc_census_blocks.popn_black:5 nyc_census_blocks.popn_nativ:6 nyc_census_blocks.popn_asian:7 nyc_census_blocks.popn_other:8 nyc_census_blocks.boroname:9!null nyc_census_blocks.geom:10
 │    ├── key: (1)
 │    ├── fd: ()-->(9), (1)-->(2-8,10)
 │    ├── scan nyc_census_blocks
 │    │    ├── columns: nyc_census_blocks.gid:1!null nyc_census_blocks.blkid:2 nyc_census_blocks.popn_total:3 nyc_census_blocks.popn_white:4 nyc_census_blocks.popn_black:5 nyc_census_blocks.popn_nativ:6 nyc_census_blocks.popn_asian:7 nyc_census_blocks.popn_other:8 nyc_census_blocks.boroname:9 nyc_census_blocks.geom:10
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-10)
 │    └── filters
 │         └── nyc_census_blocks.boroname:9 = 'Manhattan' [outer=(9), constraints=(/9: [/'Manhattan' - /'Manhattan']; tight), fd=()-->(9)]
 └── project
      ├── columns: count:43 count_rows:31!null
      ├── immutable
      ├── group-by (hash)
      │    ├── columns: n.boroname:25 count_rows:31!null
      │    ├── grouping columns: n.boroname:25
      │    ├── immutable
      │    ├── key: (25)
      │    ├── fd: (25)-->(31)
      │    ├── left-join (lookup nyc_neighborhoods [as=n])
      │    │    ├── columns: geom:23 n.boroname:25 n.geom:27
      │    │    ├── key columns: [44] = [24]
      │    │    ├── lookup columns are key
      │    │    ├── second join in paired joiner
      │    │    ├── immutable
      │    │    ├── left-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    │    │    ├── columns: geom:23 n.gid:44 continuation:51
      │    │    │    ├── first join in paired joiner; continuation column: continuation:51
      │    │    │    ├── inverted-expr
      │    │    │    │    └── st_intersects(geom:23, n.geom:47)
      │    │    │    ├── fd: (44)-->(51)
      │    │    │    ├── with-scan &1 (q)
      │    │    │    │    ├── columns: geom:23
      │    │    │    │    └── mapping:
      │    │    │    │         └──  nyc_census_blocks.geom:10 => geom:23
      │    │    │    └── filters (true)
      │    │    └── filters
      │    │         └── st_intersects(geom:23, n.geom:27) [outer=(23,27), immutable, constraints=(/23: (/NULL - ]; /27: (/NULL - ])]
      │    └── aggregations
      │         └── count-rows [as=count_rows:31]
      └── projections
           └── subquery [as=count:43, subquery]
                └── scalar-group-by
                     ├── columns: count_rows:42!null
                     ├── cardinality: [1 - 1]
                     ├── key: ()
                     ├── fd: ()-->(42)
                     ├── with-scan &1 (q)
                     │    └── mapping:
                     └── aggregations
                          └── count-rows [as=count_rows:42]

# Unable to use inverted index due to inner join on the right side.
opt
SELECT *
FROM nyc_census_blocks AS c
LEFT JOIN (
  SELECT n1.*, n2.name FROM nyc_neighborhoods n1 JOIN nyc_neighborhoods n2 ON n1.boroname LIKE n2.boroname
) AS n ON ST_Covers(c.geom, n.geom)
----
project
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10 gid:14 boroname:15 name:16 geom:17 name:23
 ├── immutable
 ├── fd: (1)-->(2-10), (14)-->(15-17)
 └── left-join (cross)
      ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10 n1.gid:14 n1.boroname:15 n1.name:16 n1.geom:17 n2.boroname:22 n2.name:23
      ├── immutable
      ├── fd: (1)-->(2-10), (14)-->(15-17)
      ├── scan nyc_census_blocks [as=c]
      │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
      │    ├── key: (1)
      │    └── fd: (1)-->(2-10)
      ├── inner-join (cross)
      │    ├── columns: n1.gid:14!null n1.boroname:15!null n1.name:16 n1.geom:17 n2.boroname:22!null n2.name:23
      │    ├── fd: (14)-->(15-17)
      │    ├── scan nyc_neighborhoods [as=n1]
      │    │    ├── columns: n1.gid:14!null n1.boroname:15 n1.name:16 n1.geom:17
      │    │    ├── key: (14)
      │    │    └── fd: (14)-->(15-17)
      │    ├── scan nyc_neighborhoods [as=n2]
      │    │    └── columns: n2.boroname:22 n2.name:23
      │    └── filters
      │         └── n1.boroname:15 LIKE n2.boroname:22 [outer=(15,22), constraints=(/15: (/NULL - ]; /22: (/NULL - ])]
      └── filters
           └── st_covers(c.geom:10, n1.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Not using inverted index due to lack of geospatial function in the ON condition.
opt
SELECT *
FROM nyc_census_blocks AS c
LEFT JOIN nyc_neighborhoods AS n ON c.boroname LIKE n.boroname
----
left-join (cross)
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10 gid:14 boroname:15 name:16 geom:17
 ├── key: (1,14)
 ├── fd: (1)-->(2-10), (14)-->(15-17)
 ├── scan nyc_census_blocks [as=c]
 │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
 │    ├── key: (1)
 │    └── fd: (1)-->(2-10)
 ├── scan nyc_neighborhoods [as=n]
 │    ├── columns: n.gid:14!null n.boroname:15 name:16 n.geom:17
 │    ├── key: (14)
 │    └── fd: (14)-->(15-17)
 └── filters
      └── c.boroname:9 LIKE n.boroname:15 [outer=(9,15), constraints=(/9: (/NULL - ]; /15: (/NULL - ])]

# Bounding box operations.
opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON c.geom::box2d && n.geom
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10 name:16 n.geom:17
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_intersects(c.geom:10::BOX2D::GEOMETRY, n.geom:24)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── c.geom:10::BOX2D && n.geom:17 [outer=(10,17), immutable]

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON c.geom::box2d ~ n.geom
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10 name:16 n.geom:17
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10::BOX2D::GEOMETRY, n.geom:24)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── c.geom:10::BOX2D ~ n.geom:17 [outer=(10,17), immutable]

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON n.geom ~ c.geom::box2d
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10 name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_coveredby(c.geom:10::BOX2D::GEOMETRY, n.geom:24)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── n.geom:17 ~ c.geom:10::BOX2D [outer=(10,17), immutable, constraints=(/17: (/NULL - ])]

opt expect=GenerateInvertedJoins
SELECT
  n.name, c.boroname
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON n.geom ~ c.geom
----
project
 ├── columns: name:16 boroname:9
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.boroname:9 c.geom:10!null name:16 n.geom:17!null
      ├── key columns: [21] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.boroname:9 c.geom:10 n.gid:21!null
      │    ├── inverted-expr
      │    │    └── st_coveredby(c.geom:10, n.geom:24)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    └── columns: c.boroname:9 c.geom:10
      │    └── filters (true)
      └── filters
           └── n.geom:17 ~ c.geom:10 [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# JSON and Array operations.
exec-ddl
CREATE TABLE json_arr1 (
  k INT PRIMARY KEY,
  i INT,
  j JSONB,
  a STRING[],
  INVERTED INDEX j_idx (j),
  INVERTED INDEX a_idx (a)
)
----

exec-ddl
CREATE TABLE json_arr2 (
  k INT PRIMARY KEY,
  l INT,
  j JSONB,
  a STRING[]
)
----

exec-ddl
CREATE TABLE json_check (
  k INT PRIMARY KEY,
  i INT NOT NULL,
  j JSONB,
  CHECK (i IN (1, 2, 3)),
  INVERTED INDEX (i, j)
)
----

exec-ddl
CREATE TABLE json_comp (
  k INT PRIMARY KEY,
  a INT,
  b INT AS (a % 4) STORED,
  j JSONB,
  INVERTED INDEX bj_idx (b, j)
)
----

exec-ddl
ALTER TABLE json_arr1 INJECT STATISTICS '[
  {
    "columns": ["j"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 1000,
    "distinct_count": 1000
  }
]'
----

exec-ddl
ALTER TABLE json_arr2 INJECT STATISTICS '[
  {
    "columns": ["j"],
    "created_at": "2018-01-01 1:00:00.00000+00:00",
    "row_count": 10,
    "distinct_count": 10
  }
]'
----

# Inner join with no additional filters.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.j @> t2.j
----
project
 ├── columns: k:1!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t1.k:1!null t1.j:3 t2.j:11
      ├── key columns: [15] = [1]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)-->(3)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:11 t1.k:15!null
      │    ├── inverted-expr
      │    │    └── t1.j:17 @> t2.j:11
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.j:11
      │    └── filters (true)
      └── filters
           └── t1.j:3 @> t2.j:11 [outer=(3,11), immutable]

# Inner join with no additional filters.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.j <@ t2.j
----
project
 ├── columns: k:1!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t1.k:1!null t1.j:3 t2.j:11
      ├── key columns: [15] = [1]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)-->(3)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:11 t1.k:15!null
      │    ├── inverted-expr
      │    │    └── t1.j:17 <@ t2.j:11
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.j:11
      │    └── filters (true)
      └── filters
           └── t1.j:3 <@ t2.j:11 [outer=(3,11), immutable]

# Inner join with no additional filters.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.a @> t2.a
----
project
 ├── columns: k:1!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t1.k:1!null t1.a:4 t2.a:12
      ├── key columns: [15] = [1]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)-->(4)
      ├── inner-join (inverted json_arr1@a_idx,inverted [as=t1])
      │    ├── columns: t2.a:12 t1.k:15!null
      │    ├── inverted-expr
      │    │    └── t1.a:18 @> t2.a:12
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.a:12
      │    └── filters (true)
      └── filters
           └── t1.a:4 @> t2.a:12 [outer=(4,12), immutable]

# Inner join with no additional filters.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.a <@ t2.a
----
project
 ├── columns: k:1!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t1.k:1!null t1.a:4 t2.a:12
      ├── key columns: [15] = [1]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)-->(4)
      ├── inner-join (inverted json_arr1@a_idx,inverted [as=t1])
      │    ├── columns: t2.a:12 t1.k:15!null
      │    ├── inverted-expr
      │    │    └── t1.a:18 <@ t2.a:12
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.a:12
      │    └── filters (true)
      └── filters
           └── t1.a:4 <@ t2.a:12 [outer=(4,12), immutable]

# Left join with no additional filters.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr2 AS t2
LEFT JOIN json_arr1 AS t1
ON t1.j @> t2.j
----
project
 ├── columns: k:7
 ├── immutable
 └── left-join (lookup json_arr1 [as=t1])
      ├── columns: t2.j:3 t1.k:7 t1.j:9
      ├── key columns: [15] = [7]
      ├── lookup columns are key
      ├── second join in paired joiner
      ├── immutable
      ├── fd: (7)-->(9)
      ├── left-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:15 continuation:23
      │    ├── first join in paired joiner; continuation column: continuation:23
      │    ├── inverted-expr
      │    │    └── t1.j:17 @> t2.j:3
      │    ├── fd: (15)-->(23)
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.j:3
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Left join with no additional filters.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr2 AS t2
LEFT JOIN json_arr1 AS t1
ON t1.j <@ t2.j
----
project
 ├── columns: k:7
 ├── immutable
 └── left-join (lookup json_arr1 [as=t1])
      ├── columns: t2.j:3 t1.k:7 t1.j:9
      ├── key columns: [15] = [7]
      ├── lookup columns are key
      ├── second join in paired joiner
      ├── immutable
      ├── fd: (7)-->(9)
      ├── left-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:15 continuation:23
      │    ├── first join in paired joiner; continuation column: continuation:23
      │    ├── inverted-expr
      │    │    └── t1.j:17 <@ t2.j:3
      │    ├── fd: (15)-->(23)
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.j:3
      │    └── filters (true)
      └── filters
           └── t1.j:9 <@ t2.j:3 [outer=(3,9), immutable]

# Left join not possible when the order of tables is switched.
opt expect-not=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
LEFT JOIN json_arr2 AS t2
ON t1.j @> t2.j
----
project
 ├── columns: k:1!null
 ├── immutable
 └── left-join (cross)
      ├── columns: t1.k:1!null t1.j:3 t2.j:11
      ├── immutable
      ├── fd: (1)-->(3)
      ├── scan json_arr1 [as=t1]
      │    ├── columns: t1.k:1!null t1.j:3
      │    ├── key: (1)
      │    └── fd: (1)-->(3)
      ├── scan json_arr2 [as=t2]
      │    └── columns: t2.j:11
      └── filters
           └── t1.j:3 @> t2.j:11 [outer=(3,11), immutable]

# Inner join with additional filter.
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.j @> t2.j AND t1.j @> '{"foo": "bar"}'::jsonb AND t2.k > 5
----
inner-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null i:2 j:3!null a:4 k:9!null l:10 j:11 a:12
 ├── key columns: [15] = [1]
 ├── lookup columns are key
 ├── immutable
 ├── key: (1,9)
 ├── fd: (1)-->(2-4), (9)-->(10-12)
 ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12 t1.k:15!null
 │    ├── inverted-expr
 │    │    └── (t1.j:17 @> t2.j:11) AND (t1.j:17 @> '{"foo": "bar"}')
 │    ├── key: (9,15)
 │    ├── fd: (9)-->(10-12)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12
 │    │    ├── constraint: /9: [/6 - ]
 │    │    ├── key: (9)
 │    │    └── fd: (9)-->(10-12)
 │    └── filters (true)
 └── filters
      ├── t1.j:3 @> t2.j:11 [outer=(3,11), immutable]
      └── t1.j:3 @> '{"foo": "bar"}' [outer=(3), immutable, constraints=(/3: (/NULL - ])]

# Inner join with additional filter.
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.j <@ t2.j AND t1.j <@ '{"foo": "bar"}'::jsonb AND t2.k > 5
----
inner-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null i:2 j:3 a:4 k:9!null l:10 j:11 a:12
 ├── key columns: [15] = [1]
 ├── lookup columns are key
 ├── immutable
 ├── key: (1,9)
 ├── fd: (1)-->(2-4), (9)-->(10-12)
 ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12 t1.k:15!null
 │    ├── inverted-expr
 │    │    └── (t1.j:17 <@ t2.j:11) AND (t1.j:17 <@ '{"foo": "bar"}')
 │    ├── key: (9,15)
 │    ├── fd: (9)-->(10-12)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12
 │    │    ├── constraint: /9: [/6 - ]
 │    │    ├── key: (9)
 │    │    └── fd: (9)-->(10-12)
 │    └── filters (true)
 └── filters
      ├── t1.j:3 <@ t2.j:11 [outer=(3,11), immutable]
      └── t1.j:3 <@ '{"foo": "bar"}' [outer=(3), immutable]

# Inner join with additional filter.
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.a @> t2.a AND t1.a @> '{"foo"}'::string[] AND t2.k > 5
----
inner-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null i:2 j:3 a:4!null k:9!null l:10 j:11 a:12
 ├── key columns: [15] = [1]
 ├── lookup columns are key
 ├── immutable
 ├── key: (1,9)
 ├── fd: (1)-->(2-4), (9)-->(10-12)
 ├── inner-join (inverted json_arr1@a_idx,inverted [as=t1])
 │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12 t1.k:15!null
 │    ├── inverted-expr
 │    │    └── (t1.a:18 @> t2.a:12) AND (t1.a:18 @> ARRAY['foo'])
 │    ├── key: (9,15)
 │    ├── fd: (9)-->(10-12)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12
 │    │    ├── constraint: /9: [/6 - ]
 │    │    ├── key: (9)
 │    │    └── fd: (9)-->(10-12)
 │    └── filters (true)
 └── filters
      ├── t1.a:4 @> t2.a:12 [outer=(4,12), immutable]
      └── t1.a:4 @> ARRAY['foo'] [outer=(4), immutable, constraints=(/4: (/NULL - ])]

# Inner join with additional filter.
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.a <@ t2.a AND t1.a <@ '{"foo"}'::string[] AND t2.k > 5
----
inner-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null i:2 j:3 a:4 k:9!null l:10 j:11 a:12
 ├── key columns: [15] = [1]
 ├── lookup columns are key
 ├── immutable
 ├── key: (1,9)
 ├── fd: (1)-->(2-4), (9)-->(10-12)
 ├── inner-join (inverted json_arr1@a_idx,inverted [as=t1])
 │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12 t1.k:15!null
 │    ├── inverted-expr
 │    │    └── (t1.a:18 <@ t2.a:12) AND (t1.a:18 <@ ARRAY['foo'])
 │    ├── key: (9,15)
 │    ├── fd: (9)-->(10-12)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:9!null l:10 t2.j:11 t2.a:12
 │    │    ├── constraint: /9: [/6 - ]
 │    │    ├── key: (9)
 │    │    └── fd: (9)-->(10-12)
 │    └── filters (true)
 └── filters
      ├── t1.a:4 <@ t2.a:12 [outer=(4,12), immutable]
      └── t1.a:4 <@ ARRAY['foo'] [outer=(4), immutable]

# Left join with additional filter.
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM json_arr2 AS t2
LEFT JOIN json_arr1 AS t1
ON t1.a @> t2.a AND t1.a @> '{"foo"}'::string[] AND t2.k > 5
----
left-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4 k:7 i:8 j:9 a:10
 ├── key columns: [15] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1,7)
 ├── fd: (1)-->(2-4), (7)-->(8-10)
 ├── left-join (inverted json_arr1@a_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:15 continuation:23
 │    ├── first join in paired joiner; continuation column: continuation:23
 │    ├── inverted-expr
 │    │    └── (t1.a:18 @> t2.a:4) AND (t1.a:18 @> ARRAY['foo'])
 │    ├── key: (1,15)
 │    ├── fd: (1)-->(2-4), (15)-->(23)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters
 │         └── t2.k:1 > 5 [outer=(1), constraints=(/1: [/6 - ]; tight)]
 └── filters
      ├── t1.a:10 @> t2.a:4 [outer=(4,10), immutable]
      └── t1.a:10 @> ARRAY['foo'] [outer=(10), immutable, constraints=(/10: (/NULL - ])]

# Left join with additional filter.
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM json_arr2 AS t2
LEFT JOIN json_arr1 AS t1
ON t1.a <@ t2.a AND t1.a <@ '{"foo"}'::string[] AND t2.k > 5
----
left-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4 k:7 i:8 j:9 a:10
 ├── key columns: [15] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1,7)
 ├── fd: (1)-->(2-4), (7)-->(8-10)
 ├── left-join (inverted json_arr1@a_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:15 continuation:23
 │    ├── first join in paired joiner; continuation column: continuation:23
 │    ├── inverted-expr
 │    │    └── (t1.a:18 <@ t2.a:4) AND (t1.a:18 <@ ARRAY['foo'])
 │    ├── key: (1,15)
 │    ├── fd: (1)-->(2-4), (15)-->(23)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters
 │         └── t2.k:1 > 5 [outer=(1), constraints=(/1: [/6 - ]; tight)]
 └── filters
      ├── t1.a:10 <@ t2.a:4 [outer=(4,10), immutable]
      └── t1.a:10 <@ ARRAY['foo'] [outer=(10), immutable]

# Semi-join.
opt expect=(GenerateInvertedJoins)
SELECT * FROM json_arr2 AS t2
WHERE EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j @> t2.j
)
----
semi-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4
 ├── key columns: [16] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:16!null continuation:24
 │    ├── first join in paired joiner; continuation column: continuation:24
 │    ├── inverted-expr
 │    │    └── t1.j:18 @> t2.j:3
 │    ├── key: (1,16)
 │    ├── fd: (1)-->(2-4), (16)-->(24)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Semi-join.
opt expect=(GenerateInvertedJoins)
SELECT * FROM json_arr2 AS t2
WHERE EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j <@ t2.j
)
----
semi-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4
 ├── key columns: [16] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:16!null continuation:24
 │    ├── first join in paired joiner; continuation column: continuation:24
 │    ├── inverted-expr
 │    │    └── t1.j:18 <@ t2.j:3
 │    ├── key: (1,16)
 │    ├── fd: (1)-->(2-4), (16)-->(24)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 <@ t2.j:3 [outer=(3,9), immutable]

# Anti-join.
opt expect=GenerateInvertedJoins
SELECT * FROM json_arr2 AS t2
WHERE NOT EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j @> t2.j
)
----
anti-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4
 ├── key columns: [16] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── left-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:16 continuation:24
 │    ├── first join in paired joiner; continuation column: continuation:24
 │    ├── inverted-expr
 │    │    └── t1.j:18 @> t2.j:3
 │    ├── key: (1,16)
 │    ├── fd: (1)-->(2-4), (16)-->(24)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Anti-join.
opt expect=GenerateInvertedJoins
SELECT * FROM json_arr2 AS t2
WHERE NOT EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j <@ t2.j
)
----
anti-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4
 ├── key columns: [16] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── left-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:16 continuation:24
 │    ├── first join in paired joiner; continuation column: continuation:24
 │    ├── inverted-expr
 │    │    └── t1.j:18 <@ t2.j:3
 │    ├── key: (1,16)
 │    ├── fd: (1)-->(2-4), (16)-->(24)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 <@ t2.j:3 [outer=(3,9), immutable]

# Tests for indexes with older descriptor versions.

exec-ddl
DROP INDEX j_idx
----

exec-ddl
DROP INDEX a_idx
----

exec-ddl index-version=1
CREATE INVERTED INDEX j_idx ON json_arr1 (j)
----

exec-ddl index-version=1
CREATE INVERTED INDEX a_idx ON json_arr1 (a)
----

# Verify that we do not plan an inverted join with array inverted indexes that
# have version SecondaryIndexFamilyFormatVersion, since they do not contain
# keys for empty arrays.
opt expect-not=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.a @> t2.a
----
project
 ├── columns: k:1!null
 ├── immutable
 └── inner-join (cross)
      ├── columns: t1.k:1!null t1.a:4 t2.a:14
      ├── immutable
      ├── fd: (1)-->(4)
      ├── scan json_arr1 [as=t1]
      │    ├── columns: t1.k:1!null t1.a:4
      │    ├── key: (1)
      │    └── fd: (1)-->(4)
      ├── scan json_arr2 [as=t2]
      │    └── columns: t2.a:14
      └── filters
           └── t1.a:4 @> t2.a:14 [outer=(4,14), immutable]

# We should still plan an inverted join with JSON inverted indexes even if the
# index version is SecondaryIndexFamilyFormatVersion.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr1 AS t1
JOIN json_arr2 AS t2
ON t1.j @> t2.j
----
project
 ├── columns: k:1!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t1.k:1!null t1.j:3 t2.j:13
      ├── key columns: [17] = [1]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)-->(3)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:13 t1.k:17!null
      │    ├── inverted-expr
      │    │    └── t1.j:19 @> t2.j:13
      │    ├── scan json_arr2 [as=t2]
      │    │    └── columns: t2.j:13
      │    └── filters (true)
      └── filters
           └── t1.j:3 @> t2.j:13 [outer=(3,13), immutable]

exec-ddl
CREATE TABLE j1 (
  k INT PRIMARY KEY,
  j JSON
)
----

exec-ddl
CREATE TABLE j2 (
  k INT PRIMARY KEY,
  j JSON,
  INVERTED INDEX j2_j_idx (j)
)
----

# Regression test for #58892. Ensure that the columns projected by the left
# lookup join are from the primary index, not the inverted index.
opt expect=GenerateInvertedJoinsFromSelect
SELECT j1.*, j2.*
FROM j1 LEFT INVERTED JOIN j2@j2_j_idx
  ON j2.j @> j1.j AND j2.j = '"foo"'
ORDER BY j1.k, j2.k
----
sort (segmented)
 ├── columns: k:1!null j:2 k:5 j:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (1,5)-->(6)
 ├── ordering: +1,+5
 └── left-join (lookup j2)
      ├── columns: j1.k:1!null j1.j:2 j2.k:5 j2.j:6
      ├── key columns: [10] = [5]
      ├── lookup columns are key
      ├── second join in paired joiner
      ├── immutable
      ├── key: (1,5)
      ├── fd: (1)-->(2), (1,5)-->(6)
      ├── ordering: +1
      ├── left-join (inverted j2@j2_j_idx,inverted)
      │    ├── columns: j1.k:1!null j1.j:2 j2.k:10 continuation:15
      │    ├── flags: force inverted join (into right side)
      │    ├── first join in paired joiner; continuation column: continuation:15
      │    ├── inverted-expr
      │    │    └── j2.j:11 @> j1.j:2
      │    ├── key: (1,10)
      │    ├── fd: (1)-->(2), (10)-->(15)
      │    ├── ordering: +1
      │    ├── scan j1
      │    │    ├── columns: j1.k:1!null j1.j:2
      │    │    ├── key: (1)
      │    │    ├── fd: (1)-->(2)
      │    │    └── ordering: +1
      │    └── filters (true)
      └── filters
           ├── j2.j:6 @> j1.j:2 [outer=(2,6), immutable]
           └── j2.j:6 = '"foo"' [outer=(6), immutable, constraints=(/6: [/'"foo"' - /'"foo"']; tight), fd=()-->(6)]

# -------------------------------------------------------
# GenerateInvertedJoins on multi-column inverted indexes
# -------------------------------------------------------

# Replace the existing inverted indexes with multi-column inverted indexes.
exec-ddl
DROP INDEX nyc_census_blocks_geo_idx
----

exec-ddl
DROP INDEX nyc_neighborhoods_geo_idx
----

exec-ddl
CREATE INVERTED INDEX nyc_neighborhoods_geo_idx ON nyc_neighborhoods (boroname, geom)
----

exec-ddl
DROP INDEX j_idx
----

exec-ddl
CREATE INVERTED INDEX j_idx ON json_arr1 (k, j)
----

exec-ddl
DROP INDEX a_idx
----

exec-ddl
CREATE INVERTED INDEX a_idx ON json_arr1 (k, a)
----

# Generate an inverted join on a geo-spatial multi-column inverted index.
opt expect=GenerateInvertedJoinsFromSelect
SELECT c.gid
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom) AND n.boroname = 'Manhattan'
----
project
 ├── columns: gid:1!null
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.gid:1!null c.geom:10!null n.boroname:15!null n.geom:17!null
      ├── key columns: [23] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(15), (1)-->(10)
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted [as=n])
      │    ├── columns: c.gid:1!null c.geom:10 n.gid:23!null n.boroname:24!null
      │    ├── prefix key columns: [22] = [24]
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10, n.geom:26)
      │    ├── key: (1,23)
      │    ├── fd: ()-->(24), (1)-->(10)
      │    ├── project
      │    │    ├── columns: "inverted_join_const_col_@15":22!null c.gid:1!null c.geom:10
      │    │    ├── key: (1)
      │    │    ├── fd: ()-->(22), (1)-->(10)
      │    │    ├── scan nyc_census_blocks [as=c]
      │    │    │    ├── columns: c.gid:1!null c.geom:10
      │    │    │    ├── key: (1)
      │    │    │    └── fd: (1)-->(10)
      │    │    └── projections
      │    │         └── 'Manhattan' [as="inverted_join_const_col_@15":22]
      │    └── filters (true)
      └── filters
           └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Generate an inverted join on an ARRAY multi-column inverted index.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.a @> t2.a AND t1.k = 500
----
project
 ├── columns: k:7!null
 ├── immutable
 ├── fd: ()-->(7)
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.a:4 t1.k:7!null t1.a:10
      ├── key columns: [21] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(7,10)
      ├── inner-join (inverted json_arr1@a_idx,inverted [as=t1])
      │    ├── columns: t2.a:4 t1.k:21!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [20] = [21]
      │    ├── inverted-expr
      │    │    └── t1.a:24 @> t2.a:4
      │    ├── fd: ()-->(21)
      │    ├── project
      │    │    ├── columns: "inverted_join_const_col_@7":20!null t2.a:4
      │    │    ├── fd: ()-->(20)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.a:4
      │    │    └── projections
      │    │         └── 500 [as="inverted_join_const_col_@7":20]
      │    └── filters (true)
      └── filters
           └── t1.a:10 @> t2.a:4 [outer=(4,10), immutable]

# Generate an inverted join on a JSON multi-column inverted index.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k = 500
----
project
 ├── columns: k:7!null
 ├── immutable
 ├── fd: ()-->(7)
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.j:3 t1.k:7!null t1.j:9
      ├── key columns: [20] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(7,9)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:20!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [19] = [20]
      │    ├── inverted-expr
      │    │    └── t1.j:22 @> t2.j:3
      │    ├── fd: ()-->(20)
      │    ├── project
      │    │    ├── columns: "inverted_join_const_col_@7":19!null t2.j:3
      │    │    ├── fd: ()-->(19)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.j:3
      │    │    └── projections
      │    │         └── 500 [as="inverted_join_const_col_@7":19]
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Generate an inverted join with remaining filters.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k = 500 AND t1.a @> '{foo}'
----
project
 ├── columns: k:7!null
 ├── immutable
 ├── fd: ()-->(7)
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.j:3 t1.k:7!null t1.j:9 t1.a:10!null
      ├── key columns: [20] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(7,9,10)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:20!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [19] = [20]
      │    ├── inverted-expr
      │    │    └── t1.j:22 @> t2.j:3
      │    ├── fd: ()-->(20)
      │    ├── project
      │    │    ├── columns: "inverted_join_const_col_@7":19!null t2.j:3
      │    │    ├── fd: ()-->(19)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.j:3
      │    │    └── projections
      │    │         └── 500 [as="inverted_join_const_col_@7":19]
      │    └── filters (true)
      └── filters
           ├── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]
           └── t1.a:10 @> ARRAY['foo'] [outer=(10), immutable, constraints=(/10: (/NULL - ])]

# Constrain a single prefix column to multiple point values.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k IN (500, 600, 700)
----
project
 ├── columns: k:7!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.j:3 t1.k:7!null t1.j:9
      ├── key columns: [20] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (7)-->(9)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:20!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [19] = [20]
      │    ├── inverted-expr
      │    │    └── t1.j:22 @> t2.j:3
      │    ├── inner-join (cross)
      │    │    ├── columns: t2.j:3 "inverted_join_const_col_@7":19!null
      │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.j:3
      │    │    ├── values
      │    │    │    ├── columns: "inverted_join_const_col_@7":19!null
      │    │    │    ├── cardinality: [3 - 3]
      │    │    │    ├── (500,)
      │    │    │    ├── (600,)
      │    │    │    └── (700,)
      │    │    └── filters (true)
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Constrain a single prefix column to a column from the other side of the join.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k = t2.k
----
project
 ├── columns: k:7!null
 ├── immutable
 ├── key: (7)
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.k:1!null t2.j:3 t1.k:7!null t1.j:9
      ├── key columns: [19] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── key: (7)
      ├── fd: (1)-->(3), (7)-->(9), (1)==(7), (7)==(1)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.k:1!null t2.j:3 t1.k:19!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [1] = [19]
      │    ├── inverted-expr
      │    │    └── t1.j:21 @> t2.j:3
      │    ├── key: (19)
      │    ├── fd: (1)-->(3), (1)==(19), (19)==(1)
      │    ├── scan json_arr2 [as=t2]
      │    │    ├── columns: t2.k:1!null t2.j:3
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(3)
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Constrain a prefix column with a CHECK constraint.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_check AS t1
ON t1.j @> t2.j
----
project
 ├── columns: k:7!null
 ├── immutable
 └── inner-join (lookup json_check [as=t1])
      ├── columns: t2.j:3 t1.k:7!null t1.j:9
      ├── key columns: [14] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (7)-->(9)
      ├── inner-join (inverted json_check@json_check_i_j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:14!null i:15!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [13] = [15]
      │    ├── inverted-expr
      │    │    └── t1.j:16 @> t2.j:3
      │    ├── fd: (14)-->(15)
      │    ├── inner-join (cross)
      │    │    ├── columns: t2.j:3 "inverted_join_const_col_@8":13!null
      │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.j:3
      │    │    ├── values
      │    │    │    ├── columns: "inverted_join_const_col_@8":13!null
      │    │    │    ├── cardinality: [3 - 3]
      │    │    │    ├── (1,)
      │    │    │    ├── (2,)
      │    │    │    └── (3,)
      │    │    └── filters (true)
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Constrain a prefix column with a computed column expression.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_comp AS t1
ON t1.j @> t2.j AND t1.a = 5
----
project
 ├── columns: k:7!null
 ├── immutable
 └── inner-join (lookup json_comp [as=t1])
      ├── columns: t2.j:3 t1.k:7!null t1.a:8!null t1.j:10
      ├── key columns: [15] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(8), (7)-->(10)
      ├── inner-join (inverted json_comp@bj_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:15!null b:17!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [14] = [17]
      │    ├── inverted-expr
      │    │    └── t1.j:18 @> t2.j:3
      │    ├── fd: ()-->(17)
      │    ├── project
      │    │    ├── columns: "inverted_join_const_col_@9":14!null t2.j:3
      │    │    ├── fd: ()-->(14)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.j:3
      │    │    └── projections
      │    │         └── 1 [as="inverted_join_const_col_@9":14]
      │    └── filters (true)
      └── filters
           ├── t1.j:10 @> t2.j:3 [outer=(3,10), immutable]
           └── t1.a:8 = 5 [outer=(8), constraints=(/8: [/5 - /5]; tight), fd=()-->(8)]

exec-ddl
DROP INDEX bj_idx
----

exec-ddl
CREATE INVERTED INDEX bj_partial_idx ON json_comp (b, j) WHERE a = 5
----

# Use the original filters, not the filters reduced during partial index
# implication, to generate computed column filters that can constrain a prefix
# column.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_comp AS t1
ON t1.j @> t2.j AND t1.a = 5
----
project
 ├── columns: k:7!null
 ├── immutable
 └── inner-join (lookup json_comp [as=t1])
      ├── columns: t2.j:3 t1.k:7!null t1.a:8!null t1.j:10
      ├── key columns: [16] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(8), (7)-->(10)
      ├── inner-join (inverted json_comp@bj_partial_idx,inverted,partial [as=t1])
      │    ├── columns: t2.j:3 t1.k:16!null b:18!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [15] = [18]
      │    ├── inverted-expr
      │    │    └── t1.j:19 @> t2.j:3
      │    ├── fd: ()-->(18)
      │    ├── project
      │    │    ├── columns: "inverted_join_const_col_@9":15!null t2.j:3
      │    │    ├── fd: ()-->(15)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    └── columns: t2.j:3
      │    │    └── projections
      │    │         └── 1 [as="inverted_join_const_col_@9":15]
      │    └── filters (true)
      └── filters
           └── t1.j:10 @> t2.j:3 [outer=(3,10), immutable]

# Do not generate an inverted join when the prefix column is not constrained.
opt expect-not=GenerateInvertedJoinsFromSelect format=hide-all
SELECT t1.k
FROM json_arr2 AS t2
JOIN json_arr1 AS t1
ON t1.j @> t2.j
----
project
 └── inner-join (cross)
      ├── scan json_arr1 [as=t1]
      ├── scan json_arr2 [as=t2]
      └── filters
           └── t1.j @> t2.j

# Do not generate an inverted join when the prefix column is constrained to a
# range.
opt expect-not=GenerateInvertedJoinsFromSelect format=hide-all
SELECT t1.k
FROM json_arr2 AS t2
JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k > 500 AND t1.k < 600
----
project
 └── inner-join (cross)
      ├── scan json_arr1 [as=t1]
      │    └── constraint: /7: [/501 - /599]
      ├── scan json_arr2 [as=t2]
      └── filters
           └── t1.j @> t2.j

exec-ddl
DROP INDEX j_idx
----

exec-ddl
CREATE INVERTED INDEX j_idx ON json_arr1 (k, i, j)
----

# Generate an inverted join when there are multiple non-inverted prefix columns.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k IN (500, 600) AND t1.i IN (3, 4)
----
project
 ├── columns: k:7!null
 ├── immutable
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.j:3 t1.k:7!null i:8!null t1.j:9
      ├── key columns: [23] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (7)-->(8,9)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.j:3 t1.k:23!null i:24!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [21 22] = [23 24]
      │    ├── inverted-expr
      │    │    └── t1.j:25 @> t2.j:3
      │    ├── fd: (23)-->(24)
      │    ├── inner-join (cross)
      │    │    ├── columns: t2.j:3 "inverted_join_const_col_@7":21!null "inverted_join_const_col_@8":22!null
      │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
      │    │    ├── inner-join (cross)
      │    │    │    ├── columns: t2.j:3 "inverted_join_const_col_@7":21!null
      │    │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
      │    │    │    ├── scan json_arr2 [as=t2]
      │    │    │    │    └── columns: t2.j:3
      │    │    │    ├── values
      │    │    │    │    ├── columns: "inverted_join_const_col_@7":21!null
      │    │    │    │    ├── cardinality: [2 - 2]
      │    │    │    │    ├── (500,)
      │    │    │    │    └── (600,)
      │    │    │    └── filters (true)
      │    │    ├── values
      │    │    │    ├── columns: "inverted_join_const_col_@8":22!null
      │    │    │    ├── cardinality: [2 - 2]
      │    │    │    ├── (3,)
      │    │    │    └── (4,)
      │    │    └── filters (true)
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Generate an inverted join when one of multiple non-inverted prefix columns is
# constrained by an equality constraint.
opt expect=GenerateInvertedJoinsFromSelect
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k = t2.k AND t1.i IN (3, 4)
----
project
 ├── columns: k:7!null
 ├── immutable
 ├── key: (7)
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.k:1!null t2.j:3 t1.k:7!null i:8!null t1.j:9
      ├── key columns: [21] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── key: (7)
      ├── fd: (1)-->(3), (7)-->(8,9), (1)==(7), (7)==(1)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.k:1!null t2.j:3 t1.k:21!null i:22!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [1 20] = [21 22]
      │    ├── inverted-expr
      │    │    └── t1.j:23 @> t2.j:3
      │    ├── fd: (1)-->(3), (21)-->(22), (1)==(21), (21)==(1)
      │    ├── inner-join (cross)
      │    │    ├── columns: t2.k:1!null t2.j:3 "inverted_join_const_col_@8":20!null
      │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
      │    │    ├── fd: (1)-->(3)
      │    │    ├── scan json_arr2 [as=t2]
      │    │    │    ├── columns: t2.k:1!null t2.j:3
      │    │    │    ├── key: (1)
      │    │    │    └── fd: (1)-->(3)
      │    │    ├── values
      │    │    │    ├── columns: "inverted_join_const_col_@8":20!null
      │    │    │    ├── cardinality: [2 - 2]
      │    │    │    ├── (3,)
      │    │    │    └── (4,)
      │    │    └── filters (true)
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Generate an inverted join when multiple non-inverted prefix columns are
# constrained by equality constraints.
opt expect=GenerateInvertedJoins
SELECT t1.k
FROM json_arr2 AS t2
INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k = t2.k AND t1.i = t2.l
----
project
 ├── columns: k:7!null
 ├── immutable
 ├── key: (7)
 └── inner-join (lookup json_arr1 [as=t1])
      ├── columns: t2.k:1!null l:2!null t2.j:3 t1.k:7!null i:8!null t1.j:9
      ├── key columns: [20] = [7]
      ├── lookup columns are key
      ├── immutable
      ├── key: (7)
      ├── fd: (1)-->(2,3), (7)-->(8,9), (1)==(7), (7)==(1), (2)==(8), (8)==(2)
      ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    ├── columns: t2.k:1!null l:2!null t2.j:3 t1.k:20!null i:21!null
      │    ├── flags: force inverted join (into right side)
      │    ├── prefix key columns: [1 2] = [20 21]
      │    ├── inverted-expr
      │    │    └── t1.j:22 @> t2.j:3
      │    ├── key: (20)
      │    ├── fd: (1)-->(2,3), (20)-->(21), (1)==(20), (20)==(1), (2)==(21), (21)==(2)
      │    ├── scan json_arr2 [as=t2]
      │    │    ├── columns: t2.k:1!null l:2 t2.j:3
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2,3)
      │    └── filters (true)
      └── filters
           └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Do not generate an inverted join when the first prefix column is not
# constrained.
opt expect-not=GenerateInvertedJoinsFromSelect format=hide-all
SELECT t1.k
FROM json_arr2 AS t2
JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.i IN (1, 2)
----
project
 └── inner-join (cross)
      ├── select
      │    ├── scan json_arr1 [as=t1]
      │    └── filters
      │         └── i IN (1, 2)
      ├── scan json_arr2 [as=t2]
      └── filters
           └── t1.j @> t2.j

# Do not generate an inverted join when the second prefix column is not
# constrained.
opt expect-not=GenerateInvertedJoinsFromSelect format=hide-all
SELECT t1.k
FROM json_arr2 AS t2
JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k IN (500, 600, 700)
----
project
 └── inner-join (cross)
      ├── scan json_arr2 [as=t2]
      ├── scan json_arr1 [as=t1]
      │    └── constraint: /7
      │         ├── [/500 - /500]
      │         ├── [/600 - /600]
      │         └── [/700 - /700]
      └── filters
           └── t1.j @> t2.j

exec-ddl
DROP INDEX j_idx
----

exec-ddl
CREATE INVERTED INDEX j_idx ON json_arr1 (i, j)
----

# Generate an inverted semi-join on a multi-column inverted index.
opt expect=GenerateInvertedJoinsFromSelect
SELECT * FROM json_arr2 AS t2
WHERE EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j @> t2.j AND t1.i IN (3, 4)
)
----
project
 ├── columns: k:1!null l:2 j:3 a:4
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 └── distinct-on
      ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
      ├── grouping columns: t2.k:1!null
      ├── immutable
      ├── key: (1)
      ├── fd: (1)-->(2-4)
      ├── inner-join (lookup json_arr1 [as=t1])
      │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 i:8!null t1.j:9
      │    ├── key columns: [23] = [7]
      │    ├── lookup columns are key
      │    ├── immutable
      │    ├── fd: (1)-->(2-4)
      │    ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
      │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:23!null i:24!null
      │    │    ├── prefix key columns: [22] = [24]
      │    │    ├── inverted-expr
      │    │    │    └── t1.j:25 @> t2.j:3
      │    │    ├── fd: (1)-->(2-4), (23)-->(24)
      │    │    ├── inner-join (cross)
      │    │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 "inverted_join_const_col_@8":22!null
      │    │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
      │    │    │    ├── fd: (1)-->(2-4)
      │    │    │    ├── scan json_arr2 [as=t2]
      │    │    │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
      │    │    │    │    ├── key: (1)
      │    │    │    │    └── fd: (1)-->(2-4)
      │    │    │    ├── values
      │    │    │    │    ├── columns: "inverted_join_const_col_@8":22!null
      │    │    │    │    ├── cardinality: [2 - 2]
      │    │    │    │    ├── (3,)
      │    │    │    │    └── (4,)
      │    │    │    └── filters (true)
      │    │    └── filters (true)
      │    └── filters
      │         └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]
      └── aggregations
           ├── const-agg [as=l:2, outer=(2)]
           │    └── l:2
           ├── const-agg [as=t2.j:3, outer=(3)]
           │    └── t2.j:3
           └── const-agg [as=t2.a:4, outer=(4)]
                └── t2.a:4

# Generate an inverted semi-join on a multi-column inverted index with the
# prefix column constrained by an equality constraint.
opt expect=GenerateInvertedJoins
SELECT * FROM json_arr2 AS t2
WHERE EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j @> t2.j AND t1.i = t2.k
)
----
semi-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4
 ├── key columns: [22] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── inner-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:22!null i:23!null continuation:36
 │    ├── prefix key columns: [1] = [23]
 │    ├── first join in paired joiner; continuation column: continuation:36
 │    ├── inverted-expr
 │    │    └── t1.j:24 @> t2.j:3
 │    ├── key: (22)
 │    ├── fd: (1)-->(2-4), (22)-->(23,36), (1)==(23), (23)==(1)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# We cannot generate an inverted anti-join in this case, due to the multiple
# possible values for t1.i.
opt expect-not=GenerateInvertedJoinsFromSelect
SELECT * FROM json_arr2 AS t2
WHERE NOT EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j @> t2.j AND t1.i IN (3, 4)
)
----
anti-join (cross)
 ├── columns: k:1!null l:2 j:3 a:4
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── scan json_arr2 [as=t2]
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    ├── key: (1)
 │    └── fd: (1)-->(2-4)
 ├── select
 │    ├── columns: i:8!null t1.j:9
 │    ├── scan json_arr1 [as=t1]
 │    │    └── columns: i:8 t1.j:9
 │    └── filters
 │         └── i:8 IN (3, 4) [outer=(8), constraints=(/8: [/3 - /3] [/4 - /4]; tight)]
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Generate an inverted anti-join on a multi-column inverted index with the
# prefix column constrained by an equality constraint.
opt expect=GenerateInvertedJoins
SELECT * FROM json_arr2 AS t2
WHERE NOT EXISTS (
  SELECT * FROM json_arr1 AS t1 WHERE t1.j @> t2.j AND t1.i = t2.k
)
----
anti-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4
 ├── key columns: [22] = [7]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-4)
 ├── left-join (inverted json_arr1@j_idx,inverted [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:22 i:23 continuation:36
 │    ├── prefix key columns: [1] = [23]
 │    ├── first join in paired joiner; continuation column: continuation:36
 │    ├── inverted-expr
 │    │    └── t1.j:24 @> t2.j:3
 │    ├── key: (1,22)
 │    ├── fd: (1)-->(2-4), (22)-->(23,36)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Regression test for #59615 and #78681. Ensure that invalid inverted joins are
# not created for left, semi, and anti joins.
exec-ddl
CREATE TABLE t59615_inv (
  x INT NOT NULL CHECK (x in (1, 3)),
  y JSON,
  z INT,
  INVERTED INDEX (x, y)
)
----

opt expect-not=GenerateInvertedJoins
SELECT * FROM (VALUES ('"a"'::jsonb), ('"b"'::jsonb)) AS u(y) LEFT JOIN t59615_inv t ON t.y @> u.y
----
right-join (cross)
 ├── columns: y:1!null x:2 y:3 z:4
 ├── cardinality: [2 - ]
 ├── immutable
 ├── scan t59615_inv [as=t]
 │    ├── columns: x:2!null y:3 z:4
 │    └── check constraint expressions
 │         └── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)]
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── ('"a"',)
 │    └── ('"b"',)
 └── filters
      └── y:3 @> column1:1 [outer=(1,3), immutable]

# Disable ConvertSemiToInnerJoin to prevent GenerateInvertedJoins from firing
# for the converted inner join. With the expect-not option, we get added
# assurance that GenerateInvertedJoins is not incorrectly firing for the
# semi-join.
opt disable=ConvertSemiToInnerJoin expect-not=GenerateInvertedJoins
SELECT * FROM (VALUES ('"a"'::jsonb), ('"b"'::jsonb)) AS u(y) WHERE EXISTS (
  SELECT * FROM t59615_inv t WHERE t.y @> u.y
)
----
semi-join (cross)
 ├── columns: y:1!null
 ├── cardinality: [0 - 2]
 ├── immutable
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── ('"a"',)
 │    └── ('"b"',)
 ├── scan t59615_inv [as=t]
 │    ├── columns: y:3
 │    └── check constraint expressions
 │         └── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)]
 └── filters
      └── y:3 @> column1:1 [outer=(1,3), immutable]

opt expect-not=GenerateInvertedJoins
SELECT * FROM (VALUES ('"a"'::jsonb), ('"b"'::jsonb)) AS u(y) WHERE NOT EXISTS (
  SELECT * FROM t59615_inv t WHERE t.y @> u.y
)
----
anti-join (cross)
 ├── columns: y:1!null
 ├── cardinality: [0 - 2]
 ├── immutable
 ├── values
 │    ├── columns: column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── ('"a"',)
 │    └── ('"b"',)
 ├── scan t59615_inv [as=t]
 │    ├── columns: y:3
 │    └── check constraint expressions
 │         └── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)]
 └── filters
      └── y:3 @> column1:1 [outer=(1,3), immutable]

# Regression test for #59859. Do not panic when the ON condition has a constant
# filter to constrain the non-inverted prefix column and a variable equality
# filter that does not constrain the prefix column.
exec-ddl
CREATE TABLE t59859_a (
  k INT PRIMARY KEY,
  j JSON
)
----

exec-ddl
CREATE TABLE t59859_b (
  k INT PRIMARY KEY,
  i INT,
  j JSON,
  INVERTED INDEX (k, j)
)
----

opt expect=GenerateInvertedJoinsFromSelect format=hide-all
SELECT a.k, b.k
FROM t59859_a AS a INNER INVERTED JOIN t59859_b AS b
ON b.k = 1 AND b.j @> a.j AND b.i = a.k
----
project
 └── inner-join (lookup t59859_b [as=b])
      ├── lookup columns are key
      ├── inner-join (inverted t59859_b@t59859_b_k_j_idx,inverted [as=b])
      │    ├── flags: force inverted join (into right side)
      │    ├── inverted-expr
      │    │    └── b.j @> a.j
      │    ├── project
      │    │    ├── scan t59859_a [as=a]
      │    │    └── projections
      │    │         └── 1
      │    └── filters (true)
      └── filters
           ├── b.j @> a.j
           └── i = a.k

# Regression test for #63735. Ensure that the constant filter which maximally
# constrains the lookup table is chosen when there is more than one option.
# Here, the CHECK constraint establishes an implicit filter that constrains the
# t63735_b.x column to any value in (10, 20, 30). However, the computed column
# expression does more, establishing an implicit filter that constrains the
# t63735_b.x column to exactly the value 30.
exec-ddl
CREATE TABLE t63735_a (
  k INT PRIMARY KEY,
  j JSON
)
----

exec-ddl
CREATE TABLE t63735_b (
  x INT NOT NULL AS (y * 2) STORED CHECK (x IN (10, 20, 30)),
  y INT NOT NULL,
  j JSON,
  PRIMARY KEY (x, y),
  INVERTED INDEX (x, y, j)
)
----

opt expect=GenerateInvertedJoinsFromSelect format=hide-all
SELECT *
FROM t63735_a AS a INNER INVERTED JOIN t63735_b AS b
ON a.k = 15 AND b.j @> a.j AND b.y = a.k
----
inner-join (lookup t63735_b [as=b])
 ├── lookup columns are key
 ├── inner-join (inverted t63735_b@t63735_b_x_y_j_idx,inverted [as=b])
 │    ├── flags: force inverted join (into right side)
 │    ├── inverted-expr
 │    │    └── b.j @> a.j
 │    ├── project
 │    │    ├── scan t63735_a [as=a]
 │    │    │    └── constraint: /1: [/15 - /15]
 │    │    └── projections
 │    │         └── 30
 │    └── filters
 │         └── y = 15
 └── filters
      └── b.j @> a.j

# -------------------------------------------------------
# GenerateInvertedJoinsFromSelect + Partial Indexes
# -------------------------------------------------------

exec-ddl
DROP INDEX nyc_neighborhoods_geo_idx
----

exec-ddl
CREATE INVERTED INDEX nyc_neighborhoods_geo_idx ON nyc_neighborhoods (geom) WHERE boroname IN ('Manhattan', 'Brooklyn')
----

exec-ddl
DROP INDEX j_idx
----

exec-ddl
CREATE INVERTED INDEX j_idx ON json_arr1 (j) WHERE k > 0 AND k <= 500
----

# Inverted inner-join with no remaining filters.
opt expect=GenerateInvertedJoinsFromSelect
SELECT c.gid
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom) AND n.boroname IN ('Manhattan', 'Brooklyn')
----
project
 ├── columns: gid:1!null
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.gid:1!null c.geom:10!null n.boroname:15!null n.geom:17!null
      ├── key columns: [23] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── fd: (1)-->(10)
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted,partial [as=n])
      │    ├── columns: c.gid:1!null c.geom:10 n.gid:23!null
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10, n.geom:26)
      │    ├── key: (1,23)
      │    ├── fd: (1)-->(10)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    ├── columns: c.gid:1!null c.geom:10
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(10)
      │    └── filters (true)
      └── filters
           └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Inverted inner-join with no remaining filters.
opt expect=GenerateInvertedJoinsFromSelect
SELECT * FROM json_arr2 AS t2 INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k > 0 AND t1.k <= 500
----
inner-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4 k:7!null i:8 j:9 a:10
 ├── key columns: [22] = [7]
 ├── lookup columns are key
 ├── immutable
 ├── key: (1,7)
 ├── fd: (1)-->(2-4), (7)-->(8-10)
 ├── inner-join (inverted json_arr1@j_idx,inverted,partial [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:22!null
 │    ├── flags: force inverted join (into right side)
 │    ├── inverted-expr
 │    │    └── t1.j:24 @> t2.j:3
 │    ├── key: (1,22)
 │    ├── fd: (1)-->(2-4)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters (true)
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Inverted inner-join with remaining filters.
opt expect=GenerateInvertedJoinsFromSelect
SELECT c.gid
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n
ON ST_Covers(c.geom, n.geom) AND n.boroname = 'Manhattan'
----
project
 ├── columns: gid:1!null
 ├── immutable
 └── inner-join (lookup nyc_neighborhoods [as=n])
      ├── columns: c.gid:1!null c.geom:10!null n.boroname:15!null n.geom:17!null
      ├── key columns: [23] = [14]
      ├── lookup columns are key
      ├── immutable
      ├── fd: ()-->(15), (1)-->(10)
      ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted,partial [as=n])
      │    ├── columns: c.gid:1!null c.geom:10 n.gid:23!null
      │    ├── inverted-expr
      │    │    └── st_covers(c.geom:10, n.geom:26)
      │    ├── key: (1,23)
      │    ├── fd: (1)-->(10)
      │    ├── scan nyc_census_blocks [as=c]
      │    │    ├── columns: c.gid:1!null c.geom:10
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(10)
      │    └── filters (true)
      └── filters
           ├── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
           └── n.boroname:15 = 'Manhattan' [outer=(15), constraints=(/15: [/'Manhattan' - /'Manhattan']; tight), fd=()-->(15)]

# Inverted inner-join with remaining filters.
opt expect=GenerateInvertedJoinsFromSelect
SELECT * FROM json_arr2 AS t2 INNER INVERTED JOIN json_arr1 AS t1
ON t1.j @> t2.j AND t1.k > 0 AND t1.k <= 400
----
inner-join (lookup json_arr1 [as=t1])
 ├── columns: k:1!null l:2 j:3 a:4 k:7!null i:8 j:9 a:10
 ├── key columns: [22] = [7]
 ├── lookup columns are key
 ├── immutable
 ├── key: (1,7)
 ├── fd: (1)-->(2-4), (7)-->(8-10)
 ├── inner-join (inverted json_arr1@j_idx,inverted,partial [as=t1])
 │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4 t1.k:22!null
 │    ├── flags: force inverted join (into right side)
 │    ├── inverted-expr
 │    │    └── t1.j:24 @> t2.j:3
 │    ├── key: (1,22)
 │    ├── fd: (1)-->(2-4)
 │    ├── scan json_arr2 [as=t2]
 │    │    ├── columns: t2.k:1!null l:2 t2.j:3 t2.a:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4)
 │    └── filters
 │         └── t1.k:22 <= 400 [outer=(22), constraints=(/22: (/NULL - /400]; tight)]
 └── filters
      └── t1.j:9 @> t2.j:3 [outer=(3,9), immutable]

# Inverted "semi-join".
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM nyc_census_blocks AS c
WHERE EXISTS (
  SELECT * FROM nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n WHERE ST_Covers(c.geom, n.geom) AND n.boroname = 'Manhattan'
)
----
semi-join (lookup nyc_neighborhoods [as=n])
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10
 ├── key columns: [24] = [14]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-10)
 ├── inner-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted,partial [as=n])
 │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10 n.gid:24!null continuation:33
 │    ├── first join in paired joiner; continuation column: continuation:33
 │    ├── inverted-expr
 │    │    └── st_covers(c.geom:10, n.geom:27)
 │    ├── key: (1,24)
 │    ├── fd: (1)-->(2-10), (24)-->(33)
 │    ├── scan nyc_census_blocks [as=c]
 │    │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-10)
 │    └── filters (true)
 └── filters
      ├── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
      └── n.boroname:15 = 'Manhattan' [outer=(15), constraints=(/15: [/'Manhattan' - /'Manhattan']; tight), fd=()-->(15)]

# Inverted "anti-join".
opt expect=GenerateInvertedJoinsFromSelect
SELECT *
FROM nyc_census_blocks AS c
WHERE NOT EXISTS (
  SELECT * FROM nyc_neighborhoods@nyc_neighborhoods_geo_idx AS n WHERE ST_Covers(c.geom, n.geom) AND n.boroname = 'Manhattan'
)
----
anti-join (lookup nyc_neighborhoods [as=n])
 ├── columns: gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 boroname:9 geom:10
 ├── key columns: [24] = [14]
 ├── lookup columns are key
 ├── second join in paired joiner
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-10)
 ├── left-join (inverted nyc_neighborhoods@nyc_neighborhoods_geo_idx,inverted,partial [as=n])
 │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10 n.gid:24 continuation:33
 │    ├── first join in paired joiner; continuation column: continuation:33
 │    ├── inverted-expr
 │    │    └── st_covers(c.geom:10, n.geom:27)
 │    ├── key: (1,24)
 │    ├── fd: (1)-->(2-10), (24)-->(33)
 │    ├── scan nyc_census_blocks [as=c]
 │    │    ├── columns: c.gid:1!null blkid:2 popn_total:3 popn_white:4 popn_black:5 popn_nativ:6 popn_asian:7 popn_other:8 c.boroname:9 c.geom:10
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-10)
 │    └── filters (true)
 └── filters
      ├── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]
      └── n.boroname:15 = 'Manhattan' [outer=(15), constraints=(/15: [/'Manhattan' - /'Manhattan']; tight), fd=()-->(15)]

# Do not generate an inverted join when the predicate is not implied by the
# filter.
opt expect-not=(GenerateInvertedJoins,GenerateInvertedJoinsFromSelect)
SELECT c.gid
FROM nyc_census_blocks AS c
JOIN nyc_neighborhoods AS n
ON ST_Covers(c.geom, n.geom) AND n.boroname = 'Queens'
----
project
 ├── columns: gid:1!null
 ├── immutable
 └── inner-join (cross)
      ├── columns: c.gid:1!null c.geom:10!null n.boroname:15!null n.geom:17!null
      ├── immutable
      ├── fd: ()-->(15), (1)-->(10)
      ├── scan nyc_census_blocks [as=c]
      │    ├── columns: c.gid:1!null c.geom:10
      │    ├── key: (1)
      │    └── fd: (1)-->(10)
      ├── select
      │    ├── columns: n.boroname:15!null n.geom:17
      │    ├── fd: ()-->(15)
      │    ├── scan nyc_neighborhoods [as=n]
      │    │    ├── columns: n.boroname:15 n.geom:17
      │    │    └── partial index predicates
      │    │         └── nyc_neighborhoods_geo_idx: filters
      │    │              └── n.boroname:15 IN ('Brooklyn', 'Manhattan') [outer=(15), constraints=(/15: [/'Brooklyn' - /'Brooklyn'] [/'Manhattan' - /'Manhattan']; tight)]
      │    └── filters
      │         └── n.boroname:15 = 'Queens' [outer=(15), constraints=(/15: [/'Queens' - /'Queens']; tight), fd=()-->(15)]
      └── filters
           └── st_covers(c.geom:10, n.geom:17) [outer=(10,17), immutable, constraints=(/10: (/NULL - ]; /17: (/NULL - ])]

# Do not generate an inverted join when the predicate is not implied by the
# filter.
opt expect-not=(GenerateInvertedJoins,GenerateInvertedJoinsFromSelect)
SELECT * FROM json_arr1 AS t1 JOIN json_arr2 AS t2
ON t1.j @> t2.j AND t1.k > 0 AND t1.k <= 1000
----
inner-join (cross)
 ├── columns: k:1!null i:2 j:3 a:4 k:16!null l:17 j:18 a:19
 ├── immutable
 ├── key: (1,16)
 ├── fd: (1)-->(2-4), (16)-->(17-19)
 ├── scan json_arr1 [as=t1]
 │    ├── columns: t1.k:1!null i:2 t1.j:3 t1.a:4
 │    ├── constraint: /1: [/1 - /1000]
 │    ├── cardinality: [0 - 1000]
 │    ├── key: (1)
 │    └── fd: (1)-->(2-4)
 ├── scan json_arr2 [as=t2]
 │    ├── columns: t2.k:16!null l:17 t2.j:18 t2.a:19
 │    ├── key: (16)
 │    └── fd: (16)-->(17-19)
 └── filters
      └── t1.j:3 @> t2.j:18 [outer=(3,18), immutable]

# -----------------------------------------------------
# ConvertSemiToInnerJoin
# -----------------------------------------------------

# This rule applies when the On conditions are not equalities. For example,
# in this test we have a Lt condition. It allows us to use a lookup join even
# though the index is not covering.
opt expect=ConvertSemiToInnerJoin
SELECT * from pqr WHERE EXISTS (SELECT * FROM zz WHERE a = 0 AND q = b AND r < c)
----
project
 ├── columns: p:1!null q:2 r:3 s:4 t:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 └── project
      ├── columns: p:1!null q:2!null r:3!null s:4 t:5
      ├── key: (1)
      ├── fd: ()-->(2), (1)-->(3-5)
      └── inner-join (lookup pqr)
           ├── columns: p:1!null q:2!null r:3!null s:4 t:5 a:8!null b:9!null c:10!null
           ├── key columns: [1] = [1]
           ├── lookup columns are key
           ├── key: (1)
           ├── fd: ()-->(2,8-10), (1)-->(3-5), (2)==(9), (9)==(2)
           ├── inner-join (lookup pqr@q)
           │    ├── columns: p:1!null q:2!null a:8!null b:9!null c:10
           │    ├── key columns: [9] = [2]
           │    ├── key: (1)
           │    ├── fd: ()-->(2,8-10), (2)==(9), (9)==(2)
           │    ├── scan zz
           │    │    ├── columns: a:8!null b:9 c:10
           │    │    ├── constraint: /8: [/0 - /0]
           │    │    ├── cardinality: [0 - 1]
           │    │    ├── key: ()
           │    │    └── fd: ()-->(8-10)
           │    └── filters (true)
           └── filters
                └── r:3 < c:10 [outer=(3,10), constraints=(/3: (/NULL - ]; /10: (/NULL - ])]

# In this test we have an Or condition.
opt expect=ConvertSemiToInnerJoin
SELECT * from pqr WHERE EXISTS (SELECT * FROM zz WHERE a = 0 AND q = b AND (p = a OR r = c))
----
project
 ├── columns: p:1!null q:2 r:3 s:4 t:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 └── project
      ├── columns: p:1!null q:2!null r:3 s:4 t:5
      ├── key: (1)
      ├── fd: ()-->(2), (1)-->(3-5)
      └── inner-join (lookup pqr)
           ├── columns: p:1!null q:2!null r:3 s:4 t:5 a:8!null b:9!null c:10
           ├── key columns: [1] = [1]
           ├── lookup columns are key
           ├── key: (1)
           ├── fd: ()-->(2,8-10), (1)-->(3-5), (2)==(9), (9)==(2)
           ├── inner-join (lookup pqr@q)
           │    ├── columns: p:1!null q:2!null a:8!null b:9!null c:10
           │    ├── key columns: [9] = [2]
           │    ├── key: (1)
           │    ├── fd: ()-->(2,8-10), (2)==(9), (9)==(2)
           │    ├── scan zz
           │    │    ├── columns: a:8!null b:9 c:10
           │    │    ├── constraint: /8: [/0 - /0]
           │    │    ├── cardinality: [0 - 1]
           │    │    ├── key: ()
           │    │    └── fd: ()-->(8-10)
           │    └── filters (true)
           └── filters
                └── (p:1 = 0) OR (r:3 = c:10) [outer=(1,3,10)]

# In this case we need to add the key back to zz since it was pruned during
# normalization.
opt expect=ConvertSemiToInnerJoin
SELECT b, c from zz WHERE EXISTS (SELECT * FROM pqr WHERE p = 0 AND q = b AND (p = c OR r = c))
----
project
 ├── columns: b:2 c:3
 ├── lax-key: (2,3)
 ├── fd: (3)~~>(2)
 └── distinct-on
      ├── columns: a:1!null b:2 c:3
      ├── grouping columns: a:1!null
      ├── internal-ordering: +1
      ├── cardinality: [0 - 2]
      ├── key: (1)
      ├── fd: (1)-->(2,3)
      ├── union-all
      │    ├── columns: a:1!null b:2 c:3
      │    ├── left columns: a:14 b:15 c:16
      │    ├── right columns: a:26 b:27 c:28
      │    ├── cardinality: [0 - 2]
      │    ├── ordering: +1
      │    ├── semi-join (lookup pqr@q)
      │    │    ├── columns: a:14!null b:15 c:16!null
      │    │    ├── key columns: [15 106] = [20 19]
      │    │    ├── lookup columns are key
      │    │    ├── cardinality: [0 - 1]
      │    │    ├── key: ()
      │    │    ├── fd: ()-->(14-16)
      │    │    ├── project
      │    │    │    ├── columns: "lookup_join_const_col_@19":106!null a:14!null b:15 c:16!null
      │    │    │    ├── cardinality: [0 - 1]
      │    │    │    ├── key: ()
      │    │    │    ├── fd: ()-->(14-16,106)
      │    │    │    ├── index-join zz
      │    │    │    │    ├── columns: a:14!null b:15 c:16!null
      │    │    │    │    ├── cardinality: [0 - 1]
      │    │    │    │    ├── key: ()
      │    │    │    │    ├── fd: ()-->(14-16)
      │    │    │    │    └── scan zz@idx_c
      │    │    │    │         ├── columns: a:14!null c:16!null
      │    │    │    │         ├── constraint: /16: [/0 - /0]
      │    │    │    │         ├── cardinality: [0 - 1]
      │    │    │    │         ├── key: ()
      │    │    │    │         └── fd: ()-->(14,16)
      │    │    │    └── projections
      │    │    │         └── 0 [as="lookup_join_const_col_@19":106]
      │    │    └── filters (true)
      │    └── project
      │         ├── columns: a:26!null b:27 c:28
      │         ├── cardinality: [0 - 1]
      │         ├── key: (26)
      │         ├── fd: (26)-->(27,28), (28)~~>(26,27)
      │         ├── ordering: +26
      │         └── project
      │              ├── columns: a:26!null b:27!null c:28!null q:32!null r:33!null
      │              ├── cardinality: [0 - 1]
      │              ├── key: ()
      │              ├── fd: ()-->(26-28,32,33), (27)==(32), (32)==(27), (28)==(33), (33)==(28)
      │              └── inner-join (lookup zz)
      │                   ├── columns: a:26!null b:27!null c:28!null p:31!null q:32!null r:33!null
      │                   ├── key columns: [26] = [26]
      │                   ├── lookup columns are key
      │                   ├── cardinality: [0 - 1]
      │                   ├── key: ()
      │                   ├── fd: ()-->(26-28,31-33), (27)==(32), (32)==(27), (28)==(33), (33)==(28)
      │                   ├── inner-join (lookup zz@idx_c)
      │                   │    ├── columns: a:26!null c:28!null p:31!null q:32 r:33!null
      │                   │    ├── key columns: [33] = [28]
      │                   │    ├── lookup columns are key
      │                   │    ├── cardinality: [0 - 1]
      │                   │    ├── key: ()
      │                   │    ├── fd: ()-->(26,28,31-33), (28)==(33), (33)==(28)
      │                   │    ├── scan pqr
      │                   │    │    ├── columns: p:31!null q:32 r:33
      │                   │    │    ├── constraint: /31: [/0 - /0]
      │                   │    │    ├── cardinality: [0 - 1]
      │                   │    │    ├── key: ()
      │                   │    │    └── fd: ()-->(31-33)
      │                   │    └── filters (true)
      │                   └── filters
      │                        └── q:32 = b:27 [outer=(27,32), constraints=(/27: (/NULL - ]; /32: (/NULL - ]), fd=(27)==(32), (32)==(27)]
      └── aggregations
           ├── const-agg [as=b:2, outer=(2)]
           │    └── b:2
           └── const-agg [as=c:3, outer=(3)]
                └── c:3

# --------------------------------------------------
# PushJoinIntoIndexJoin
# --------------------------------------------------

opt expect=PushJoinIntoIndexJoin
SELECT * FROM abc INNER JOIN (SELECT * FROM pqr ORDER BY q LIMIT 5) ON a=q
----
inner-join (lookup pqr)
 ├── columns: a:1!null b:2 c:3 p:7!null q:8!null r:9 s:10 t:11
 ├── key columns: [7] = [7]
 ├── lookup columns are key
 ├── fd: (7)-->(8-11), (1)==(8), (8)==(1)
 ├── inner-join (lookup abc@ab)
 │    ├── columns: a:1!null b:2 c:3 p:7!null q:8!null
 │    ├── key columns: [8] = [1]
 │    ├── fd: (7)-->(8), (1)==(8), (8)==(1)
 │    ├── scan pqr@q
 │    │    ├── columns: p:7!null q:8
 │    │    ├── limit: 5
 │    │    ├── key: (7)
 │    │    └── fd: (7)-->(8)
 │    └── filters (true)
 └── filters (true)

# Cross join case. The plan produced by PushJoinIntoIndexJoin isn't chosen
# because the cross join doesn't filter rows.
opt expect=PushJoinIntoIndexJoin
SELECT * FROM abc CROSS JOIN (SELECT * FROM pqr ORDER BY q LIMIT 5)
----
inner-join (cross)
 ├── columns: a:1 b:2 c:3 p:7!null q:8 r:9 s:10 t:11
 ├── fd: (7)-->(8-11)
 ├── scan abc
 │    └── columns: a:1 b:2 c:3
 ├── index-join pqr
 │    ├── columns: p:7!null q:8 r:9 s:10 t:11
 │    ├── cardinality: [0 - 5]
 │    ├── key: (7)
 │    ├── fd: (7)-->(8-11)
 │    └── scan pqr@q
 │         ├── columns: p:7!null q:8
 │         ├── limit: 5
 │         ├── key: (7)
 │         └── fd: (7)-->(8)
 └── filters (true)

# No-op case because the index join is the right input of the LeftJoin.
opt expect-not=PushJoinIntoIndexJoin
SELECT * FROM abc LEFT JOIN (SELECT * FROM pqr ORDER BY q LIMIT 5) ON a=q
----
left-join (merge)
 ├── columns: a:1 b:2 c:3 p:7 q:8 r:9 s:10 t:11
 ├── left ordering: +1
 ├── right ordering: +8
 ├── fd: (7)-->(8-11)
 ├── scan abc@ab
 │    ├── columns: a:1 b:2 c:3
 │    └── ordering: +1
 ├── index-join pqr
 │    ├── columns: p:7!null q:8 r:9 s:10 t:11
 │    ├── cardinality: [0 - 5]
 │    ├── key: (7)
 │    ├── fd: (7)-->(8-11)
 │    ├── ordering: +8
 │    └── scan pqr@q
 │         ├── columns: p:7!null q:8
 │         ├── limit: 5
 │         ├── key: (7)
 │         ├── fd: (7)-->(8)
 │         └── ordering: +8
 └── filters (true)

# No-op case because the InnerJoin has join hints.
opt expect-not=PushJoinIntoIndexJoin
SELECT * FROM (SELECT * FROM pqr ORDER BY q LIMIT 5) INNER HASH JOIN abc ON a=q
----
inner-join (hash)
 ├── columns: p:1!null q:2!null r:3 s:4 t:5 a:8!null b:9 c:10
 ├── flags: force hash join (store right side)
 ├── fd: (1)-->(2-5), (2)==(8), (8)==(2)
 ├── index-join pqr
 │    ├── columns: p:1!null q:2 r:3 s:4 t:5
 │    ├── cardinality: [0 - 5]
 │    ├── key: (1)
 │    ├── fd: (1)-->(2-5)
 │    └── scan pqr@q
 │         ├── columns: p:1!null q:2
 │         ├── limit: 5
 │         ├── key: (1)
 │         └── fd: (1)-->(2)
 ├── scan abc
 │    └── columns: a:8 b:9 c:10
 └── filters
      └── a:8 = q:2 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]

# No-op case because the ON condition references a column that doesn't come from
# the input of the index join or the right side of the InnerJoin.
opt expect-not=PushJoinIntoIndexJoin
SELECT * FROM (SELECT * FROM pqr ORDER BY q LIMIT 5) INNER JOIN abc ON a=r
----
inner-join (lookup abc@ab)
 ├── columns: p:1!null q:2 r:3!null s:4 t:5 a:8!null b:9 c:10
 ├── key columns: [3] = [8]
 ├── fd: (1)-->(2-5), (3)==(8), (8)==(3)
 ├── index-join pqr
 │    ├── columns: p:1!null q:2 r:3 s:4 t:5
 │    ├── cardinality: [0 - 5]
 │    ├── key: (1)
 │    ├── fd: (1)-->(2-5)
 │    └── scan pqr@q
 │         ├── columns: p:1!null q:2
 │         ├── limit: 5
 │         ├── key: (1)
 │         └── fd: (1)-->(2)
 └── filters (true)

# No-op case because the right side of the InnerJoin has outer columns.
opt expect-not=PushJoinIntoIndexJoin disable=(TryDecorrelateProject,HoistProjectFromInnerJoin)
SELECT *
FROM stu
INNER JOIN LATERAL (
   SELECT *
   FROM (SELECT * FROM pqr ORDER BY q LIMIT 5)
   INNER JOIN (SELECT *, a*s FROM abc)
   ON a=q
)
ON True
----
inner-join-apply
 ├── columns: s:1!null t:2!null u:3!null p:6!null q:7!null r:8 s:9 t:10 a:13!null b:14 c:15 "?column?":19
 ├── immutable
 ├── fd: (1-3,6)-->(7-10), (1-3,13)-->(19), (7)==(13), (13)==(7)
 ├── scan stu
 │    ├── columns: stu.s:1!null stu.t:2!null u:3!null
 │    └── key: (1-3)
 ├── inner-join (merge)
 │    ├── columns: p:6!null q:7!null r:8 pqr.s:9 pqr.t:10 a:13!null b:14 c:15 "?column?":19
 │    ├── left ordering: +13
 │    ├── right ordering: +7
 │    ├── outer: (1)
 │    ├── immutable
 │    ├── fd: (6)-->(7-10), (13)-->(19), (7)==(13), (13)==(7)
 │    ├── project
 │    │    ├── columns: "?column?":19 a:13 b:14 c:15
 │    │    ├── outer: (1)
 │    │    ├── immutable
 │    │    ├── fd: (13)-->(19)
 │    │    ├── ordering: +13
 │    │    ├── scan abc@ab
 │    │    │    ├── columns: a:13 b:14 c:15
 │    │    │    └── ordering: +13
 │    │    └── projections
 │    │         └── a:13 * stu.s:1 [as="?column?":19, outer=(1,13), immutable]
 │    ├── index-join pqr
 │    │    ├── columns: p:6!null q:7 r:8 pqr.s:9 pqr.t:10
 │    │    ├── cardinality: [0 - 5]
 │    │    ├── key: (6)
 │    │    ├── fd: (6)-->(7-10)
 │    │    ├── ordering: +7
 │    │    └── scan pqr@q
 │    │         ├── columns: p:6!null q:7
 │    │         ├── limit: 5
 │    │         ├── key: (6)
 │    │         ├── fd: (6)-->(7)
 │    │         └── ordering: +7
 │    └── filters (true)
 └── filters (true)

# --------------------------------------------------
# Misc
# --------------------------------------------------

# Regression test for issue where zero-column expressions could exist multiple
# times in the tree, causing collisions.
opt
SELECT 1 FROM (VALUES (1), (1)) JOIN (VALUES (1), (1), (1)) ON true
UNION ALL
SELECT 1 FROM (VALUES (1), (1), (1)) JOIN (VALUES (1), (1)) ON true
----
union-all
 ├── columns: "?column?":7!null
 ├── left columns: "?column?":3
 ├── right columns: "?column?":6
 ├── cardinality: [12 - 12]
 ├── project
 │    ├── columns: "?column?":3!null
 │    ├── cardinality: [6 - 6]
 │    ├── fd: ()-->(3)
 │    ├── inner-join (cross)
 │    │    ├── cardinality: [6 - 6]
 │    │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 │    │    ├── values
 │    │    │    ├── cardinality: [3 - 3]
 │    │    │    ├── ()
 │    │    │    ├── ()
 │    │    │    └── ()
 │    │    ├── values
 │    │    │    ├── cardinality: [2 - 2]
 │    │    │    ├── ()
 │    │    │    └── ()
 │    │    └── filters (true)
 │    └── projections
 │         └── 1 [as="?column?":3]
 └── project
      ├── columns: "?column?":6!null
      ├── cardinality: [6 - 6]
      ├── fd: ()-->(6)
      ├── inner-join (cross)
      │    ├── cardinality: [6 - 6]
      │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
      │    ├── values
      │    │    ├── cardinality: [3 - 3]
      │    │    ├── ()
      │    │    ├── ()
      │    │    └── ()
      │    ├── values
      │    │    ├── cardinality: [2 - 2]
      │    │    ├── ()
      │    │    └── ()
      │    └── filters (true)
      └── projections
           └── 1 [as="?column?":6]

memo
SELECT 1 FROM (VALUES (1), (1)) JOIN (VALUES (1), (1), (1)) ON true
UNION ALL
SELECT 1 FROM (VALUES (1), (1), (1)) JOIN (VALUES (1), (1)) ON true
----
memo (optimized, ~24KB, required=[presentation: ?column?:7])
 ├── G1: (union-all G2 G3)
 │    └── [presentation: ?column?:7]
 │         ├── best: (union-all G2 G3)
 │         └── cost: 0.82
 ├── G2: (project G4 G5)
 │    └── []
 │         ├── best: (project G4 G5)
 │         └── cost: 0.34
 ├── G3: (project G6 G5)
 │    └── []
 │         ├── best: (project G6 G5)
 │         └── cost: 0.34
 ├── G4: (inner-join G7 G8 G9) (inner-join G8 G7 G9)
 │    └── []
 │         ├── best: (inner-join G8 G7 G9)
 │         └── cost: 0.21
 ├── G5: (projections G10)
 ├── G6: (inner-join G11 G12 G9) (inner-join G12 G11 G9)
 │    └── []
 │         ├── best: (inner-join G11 G12 G9)
 │         └── cost: 0.21
 ├── G7: (values G13 id=v1)
 │    └── []
 │         ├── best: (values G13 id=v1)
 │         └── cost: 0.03
 ├── G8: (values G14 id=v2)
 │    └── []
 │         ├── best: (values G14 id=v2)
 │         └── cost: 0.04
 ├── G9: (filters)
 ├── G10: (const 1)
 ├── G11: (values G14 id=v3)
 │    └── []
 │         ├── best: (values G14 id=v3)
 │         └── cost: 0.04
 ├── G12: (values G13 id=v4)
 │    └── []
 │         ├── best: (values G13 id=v4)
 │         └── cost: 0.03
 ├── G13: (scalar-list G15 G15)
 ├── G14: (scalar-list G15 G15 G15)
 ├── G15: (tuple G16)
 └── G16: (scalar-list)

opt set=reorder_joins_limit=3
SELECT
    false
FROM
    abc AS x JOIN [INSERT INTO abc (a) SELECT 1 FROM abc RETURNING 1] JOIN abc AS y ON true ON false
----
with &1
 ├── columns: bool:30!null
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 ├── key: ()
 ├── fd: ()-->(30)
 ├── project
 │    ├── columns: "?column?":22!null
 │    ├── volatile, mutations
 │    ├── fd: ()-->(22)
 │    ├── insert abc
 │    │    ├── columns: abc.rowid:10!null
 │    │    ├── insert-mapping:
 │    │    │    ├── "?column?":19 => abc.a:7
 │    │    │    ├── b_default:20 => abc.b:8
 │    │    │    ├── b_default:20 => abc.c:9
 │    │    │    └── rowid_default:21 => abc.rowid:10
 │    │    ├── return-mapping:
 │    │    │    └── rowid_default:21 => abc.rowid:10
 │    │    ├── volatile, mutations
 │    │    └── project
 │    │         ├── columns: b_default:20 rowid_default:21 "?column?":19!null
 │    │         ├── volatile
 │    │         ├── fd: ()-->(19,20)
 │    │         ├── scan abc
 │    │         └── projections
 │    │              ├── CAST(NULL AS INT8) [as=b_default:20]
 │    │              ├── unique_rowid() [as=rowid_default:21, volatile]
 │    │              └── 1 [as="?column?":19]
 │    └── projections
 │         └── 1 [as="?column?":22]
 └── values
      ├── columns: bool:30!null
      ├── cardinality: [0 - 0]
      ├── key: ()
      └── fd: ()-->(30)

opt set=reorder_joins_limit=3
SELECT 1 FROM ((VALUES (1), (1)) JOIN ((VALUES (1), (1), (1)) JOIN (VALUES (1), (1), (1), (1)) ON true) ON true)
UNION ALL
SELECT 1 FROM ((VALUES (1), (1)) JOIN (VALUES (1), (1), (1)) ON true) JOIN (VALUES (1), (1), (1), (1)) ON true
----
union-all
 ├── columns: "?column?":9!null
 ├── left columns: "?column?":4
 ├── right columns: "?column?":8
 ├── cardinality: [48 - 48]
 ├── project
 │    ├── columns: "?column?":4!null
 │    ├── cardinality: [24 - 24]
 │    ├── fd: ()-->(4)
 │    ├── inner-join (cross)
 │    │    ├── cardinality: [24 - 24]
 │    │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 │    │    ├── inner-join (cross)
 │    │    │    ├── cardinality: [12 - 12]
 │    │    │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 │    │    │    ├── values
 │    │    │    │    ├── cardinality: [4 - 4]
 │    │    │    │    ├── ()
 │    │    │    │    ├── ()
 │    │    │    │    ├── ()
 │    │    │    │    └── ()
 │    │    │    ├── values
 │    │    │    │    ├── cardinality: [3 - 3]
 │    │    │    │    ├── ()
 │    │    │    │    ├── ()
 │    │    │    │    └── ()
 │    │    │    └── filters (true)
 │    │    ├── values
 │    │    │    ├── cardinality: [2 - 2]
 │    │    │    ├── ()
 │    │    │    └── ()
 │    │    └── filters (true)
 │    └── projections
 │         └── 1 [as="?column?":4]
 └── project
      ├── columns: "?column?":8!null
      ├── cardinality: [24 - 24]
      ├── fd: ()-->(8)
      ├── inner-join (cross)
      │    ├── cardinality: [24 - 24]
      │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
      │    ├── inner-join (cross)
      │    │    ├── cardinality: [6 - 6]
      │    │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
      │    │    ├── values
      │    │    │    ├── cardinality: [3 - 3]
      │    │    │    ├── ()
      │    │    │    ├── ()
      │    │    │    └── ()
      │    │    ├── values
      │    │    │    ├── cardinality: [2 - 2]
      │    │    │    ├── ()
      │    │    │    └── ()
      │    │    └── filters (true)
      │    ├── values
      │    │    ├── cardinality: [4 - 4]
      │    │    ├── ()
      │    │    ├── ()
      │    │    ├── ()
      │    │    └── ()
      │    └── filters (true)
      └── projections
           └── 1 [as="?column?":8]

opt
SELECT 1 FROM (VALUES (1), (1)) LEFT JOIN (VALUES (1), (1), (1)) ON random() = 0
UNION ALL
SELECT 1 FROM (VALUES (1), (1), (1)) RIGHT JOIN (VALUES (1), (1)) ON random() = 0
----
union-all
 ├── columns: "?column?":7!null
 ├── left columns: "?column?":3
 ├── right columns: "?column?":6
 ├── cardinality: [4 - 12]
 ├── volatile
 ├── project
 │    ├── columns: "?column?":3!null
 │    ├── cardinality: [2 - 6]
 │    ├── volatile
 │    ├── fd: ()-->(3)
 │    ├── left-join (cross)
 │    │    ├── cardinality: [2 - 6]
 │    │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 │    │    ├── volatile
 │    │    ├── values
 │    │    │    ├── cardinality: [2 - 2]
 │    │    │    ├── ()
 │    │    │    └── ()
 │    │    ├── select
 │    │    │    ├── cardinality: [0 - 3]
 │    │    │    ├── volatile
 │    │    │    ├── values
 │    │    │    │    ├── cardinality: [3 - 3]
 │    │    │    │    ├── ()
 │    │    │    │    ├── ()
 │    │    │    │    └── ()
 │    │    │    └── filters
 │    │    │         └── random() = 0.0 [volatile]
 │    │    └── filters (true)
 │    └── projections
 │         └── 1 [as="?column?":3]
 └── project
      ├── columns: "?column?":6!null
      ├── cardinality: [2 - 6]
      ├── volatile
      ├── fd: ()-->(6)
      ├── left-join (cross)
      │    ├── cardinality: [2 - 6]
      │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
      │    ├── volatile
      │    ├── values
      │    │    ├── cardinality: [2 - 2]
      │    │    ├── ()
      │    │    └── ()
      │    ├── select
      │    │    ├── cardinality: [0 - 3]
      │    │    ├── volatile
      │    │    ├── values
      │    │    │    ├── cardinality: [3 - 3]
      │    │    │    ├── ()
      │    │    │    ├── ()
      │    │    │    └── ()
      │    │    └── filters
      │    │         └── random() = 0.0 [volatile]
      │    └── filters (true)
      └── projections
           └── 1 [as="?column?":6]

# A multi-column IN query must be able to become a lookup join.
opt
SELECT * FROM abc WHERE (a, b) IN (SELECT m, n FROM small)
----
project
 ├── columns: a:1 b:2 c:3
 └── inner-join (lookup abc@ab)
      ├── columns: a:1!null b:2!null c:3 m:7!null n:8!null
      ├── key columns: [7 8] = [1 2]
      ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2)
      ├── distinct-on
      │    ├── columns: m:7 n:8
      │    ├── grouping columns: m:7 n:8
      │    ├── key: (7,8)
      │    └── scan small
      │         └── columns: m:7 n:8
      └── filters (true)

# --------------------------------------------------
# HoistProjectFromInnerJoin
# --------------------------------------------------

opt expect=HoistProjectFromInnerJoin
SELECT * FROM small JOIN (SELECT a, a+b FROM abcd) ON a=m
----
project
 ├── columns: m:1!null n:2 a:6!null "?column?":12
 ├── immutable
 ├── fd: (1)==(6), (6)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1!null n:2 a:6!null b:7
 │    ├── key columns: [1] = [6]
 │    ├── fd: (1)==(6), (6)==(1)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── projections
      └── a:6 + b:7 [as="?column?":12, outer=(6,7), immutable]

# The rule works the other way too, thanks to join commuting.
opt expect=HoistProjectFromInnerJoin
SELECT * FROM (SELECT a, a+b FROM abcd) JOIN small ON a=m
----
project
 ├── columns: a:1!null "?column?":7 m:8!null n:9
 ├── immutable
 ├── fd: (1)==(8), (8)==(1)
 ├── inner-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: a:1!null b:2 m:8!null n:9
 │    ├── key columns: [8] = [1]
 │    ├── fd: (1)==(8), (8)==(1)
 │    ├── scan small
 │    │    └── columns: m:8 n:9
 │    └── filters (true)
 └── projections
      └── a:1 + b:2 [as="?column?":7, outer=(1,2), immutable]

# The rule should not fire when the ON condition uses a projection.
# TODO(radu): we could inline the projection.
opt expect-not=HoistProjectFromInnerJoin
SELECT * FROM small JOIN (SELECT a, a+b FROM abcd) AS rhs(a,x) ON a=m AND x=n
----
inner-join (hash)
 ├── columns: m:1!null n:2!null a:6!null x:12!null
 ├── immutable
 ├── fd: (1)==(6), (6)==(1), (2)==(12), (12)==(2)
 ├── project
 │    ├── columns: "?column?":12 a:6
 │    ├── immutable
 │    ├── scan abcd@abcd_a_b_idx
 │    │    └── columns: a:6 b:7
 │    └── projections
 │         └── a:6 + b:7 [as="?column?":12, outer=(6,7), immutable]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      ├── a:6 = m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
      └── "?column?":12 = n:2 [outer=(2,12), constraints=(/2: (/NULL - ]; /12: (/NULL - ]), fd=(2)==(12), (12)==(2)]

# The rule should not fire when a projection is volatile.
opt expect-not=HoistProjectFromInnerJoin
SELECT * FROM small JOIN (SELECT a, a+b, random() FROM abcd) ON a=m
----
inner-join (merge)
 ├── columns: m:1!null n:2 a:6!null "?column?":12 random:13
 ├── left ordering: +6
 ├── right ordering: +1
 ├── volatile
 ├── fd: (1)==(6), (6)==(1)
 ├── project
 │    ├── columns: "?column?":12 random:13 a:6
 │    ├── volatile
 │    ├── ordering: +6
 │    ├── scan abcd@abcd_a_b_idx
 │    │    ├── columns: a:6 b:7
 │    │    └── ordering: +6
 │    └── projections
 │         ├── a:6 + b:7 [as="?column?":12, outer=(6,7), immutable]
 │         └── random() [as=random:13, volatile]
 ├── sort
 │    ├── columns: m:1 n:2
 │    ├── ordering: +1
 │    └── scan small
 │         └── columns: m:1 n:2
 └── filters (true)

# The rule should be allowed to fire when the projection is from a join if the
# session flag disable_hoist_projection_in_join_limitation is true.
opt expect=HoistProjectFromInnerJoin set=disable_hoist_projection_in_join_limitation=true
SELECT * FROM (SELECT a, a+b FROM (SELECT tab1.* from abcd tab1, abcd tab2)) JOIN small ON a=m;
----
project
 ├── columns: a:1!null "?column?":13 m:14!null n:15
 ├── immutable
 ├── fd: (1)==(14), (14)==(1)
 ├── inner-join (cross)
 │    ├── columns: tab1.a:1!null tab1.b:2 m:14!null n:15
 │    ├── fd: (1)==(14), (14)==(1)
 │    ├── scan abcd@abcd_a_b_idx [as=tab2]
 │    ├── inner-join (lookup abcd@abcd_a_b_idx [as=tab1])
 │    │    ├── columns: tab1.a:1!null tab1.b:2 m:14!null n:15
 │    │    ├── key columns: [14] = [1]
 │    │    ├── fd: (1)==(14), (14)==(1)
 │    │    ├── scan small
 │    │    │    └── columns: m:14 n:15
 │    │    └── filters (true)
 │    └── filters (true)
 └── projections
      └── tab1.a:1 + tab1.b:2 [as="?column?":13, outer=(1,2), immutable]

# The rule should not fire when the projection is from a join.
opt expect-not=HoistProjectFromInnerJoin set=disable_hoist_projection_in_join_limitation=false
SELECT * FROM (SELECT a, a+b FROM (SELECT tab1.* from abcd tab1, abcd tab2)) JOIN small ON a=m
----
inner-join (hash)
 ├── columns: a:1!null "?column?":13 m:14!null n:15
 ├── immutable
 ├── fd: (1)==(14), (14)==(1)
 ├── project
 │    ├── columns: "?column?":13 tab1.a:1
 │    ├── immutable
 │    ├── inner-join (cross)
 │    │    ├── columns: tab1.a:1 tab1.b:2
 │    │    ├── scan abcd@abcd_a_b_idx [as=tab1]
 │    │    │    └── columns: tab1.a:1 tab1.b:2
 │    │    ├── scan abcd@abcd_a_b_idx [as=tab2]
 │    │    └── filters (true)
 │    └── projections
 │         └── tab1.a:1 + tab1.b:2 [as="?column?":13, outer=(1,2), immutable]
 ├── scan small
 │    └── columns: m:14 n:15
 └── filters
      └── tab1.a:1 = m:14 [outer=(1,14), constraints=(/1: (/NULL - ]; /14: (/NULL - ]), fd=(1)==(14), (14)==(1)]

# Regression test for #79943.
exec-ddl
CREATE TABLE table1 (
col1_0 GEOGRAPHY NULL,
col1_1 TIMETZ NOT NULL,
col1_2 INTERVAL[],
col1_3 REGCLASS NULL,
col1_4 REGPROCEDURE,
col1_5 STRING AS (CASE WHEN col1_2 IS NULL THEN 'abc' ELSE 'def' END) VIRTUAL,
col1_6 STRING NULL AS (lower(CAST(col1_0 AS STRING))) VIRTUAL,
col1_7 STRING AS (CASE WHEN col1_4 IS NULL THEN 'foo' ELSE 'bar' END) VIRTUAL,
INDEX (col1_5 DESC, col1_3, col1_6 DESC, col1_4 ASC) STORING (col1_0, col1_2),
UNIQUE (col1_1 ASC, lower(CAST(col1_0 AS STRING)) DESC))
----

exec-ddl
CREATE TABLE table2 (
col2_0 FLOAT4,
col2_1 INT8[] NULL,
col2_2 OID NULL,
col2_3 FLOAT4 NOT NULL,
col2_4 DECIMAL NULL,
col2_5 INT2,
col2_6 BYTES,
col2_7 TIME,
col2_8 INT8 NULL,
col2_9 REGPROC,
col2_10 NAME,
col2_11 FLOAT4 AS (col2_3 + col2_0) STORED,
col2_12 INT8 NULL AS (col2_8 + col2_5) VIRTUAL,
col2_13 INT8 NULL AS (col2_8 + col2_5) VIRTUAL,
col2_14 INT2 AS (col2_5 + col2_8) VIRTUAL,
col2_15 INT2 AS (col2_5 + col2_8) VIRTUAL,
col2_16 FLOAT4 AS (col2_3 + col2_0) VIRTUAL,
col2_17 INT2 AS (col2_5 + col2_8) VIRTUAL)
----

# Regression test for #79943.
# HoistProjectFromInnerJoin should fire, but not fire in so many cases that
# query optimization never finishes.
check-size rule-limit=100000 group-limit=10000 suppress-report expect=HoistProjectFromInnerJoin
SELECT *
FROM
    table2 tab_69833
    JOIN table1@[0] AS tab_69836
        JOIN table1 AS tab_69837 ON (tab_69836.col1_5) = (tab_69837.col1_5)
        JOIN table1@[0] AS tab_69838 ON
                (tab_69836.col1_5) = (tab_69838.col1_6)
                AND (tab_69837.col1_6) = (tab_69838.col1_7)
                AND (tab_69837.col1_5) = (tab_69838.col1_7)
        JOIN table2@[0] AS tab_69839
            JOIN table2@[0] AS tab_69840 ON
                    (tab_69839.col2_13) = (tab_69840.col2_8) AND
                    (tab_69839.col2_13) = (tab_69840.col2_12) ON
                (tab_69837.tableoid) = (tab_69840.col2_2)
                AND (tab_69838.crdb_internal_mvcc_timestamp) = (tab_69839.col2_4)
        JOIN table2@[0] AS tab_69841 ON (tab_69839.tableoid) = (tab_69841.col2_2)
        JOIN table1 AS tab_69842
            JOIN table1 AS tab_69843 ON (tab_69842.col1_5) = (tab_69843.col1_5) ON
                (tab_69838.col1_6) = (tab_69842.col1_5) AND (tab_69837.col1_6) = (tab_69843.col1_5)
        JOIN table1 AS tab_69844 ON
                (tab_69838.col1_6) = (tab_69844.col1_5) ON
            (tab_69833.col2_2) = (tab_69838.tableoid)
            AND (tab_69833.crdb_internal_mvcc_timestamp) = (tab_69839.crdb_internal_mvcc_timestamp)
            AND (tab_69833.crdb_internal_mvcc_timestamp) = (tab_69840.crdb_internal_mvcc_timestamp)
            AND (tab_69833.col2_4) = (tab_69838.crdb_internal_mvcc_timestamp)
            AND (tab_69833.crdb_internal_mvcc_timestamp) = (tab_69838.crdb_internal_mvcc_timestamp)
ORDER BY
    tab_69844.col1_6 DESC, tab_69833.crdb_internal_mvcc_timestamp
LIMIT
    51:::INT8
----

# --------------------------------------------------
# HoistProjectFromLeftJoin
# --------------------------------------------------

# The rule should use p as the canary column.
opt expect=HoistProjectFromLeftJoin
SELECT * FROM small LEFT JOIN (SELECT p, p+q FROM pqr) ON p=m
----
project
 ├── columns: m:1 n:2 p:6 "?column?":13
 ├── immutable
 ├── fd: (6)-->(13)
 ├── left-join (lookup pqr)
 │    ├── columns: m:1 n:2 p:6 q:7
 │    ├── key columns: [1] = [6]
 │    ├── lookup columns are key
 │    ├── fd: (6)-->(7)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── projections
      └── CASE p:6 IS NULL WHEN true THEN CAST(NULL AS INT8) ELSE p:6 + q:7 END [as="?column?":13, outer=(6,7), immutable]

# The rule should use a as the canary column, because it is null-rejected by
# the ON condition.
opt expect=HoistProjectFromLeftJoin
SELECT * FROM small LEFT JOIN (SELECT a, a+b FROM abcd) ON a=m
----
project
 ├── columns: m:1 n:2 a:6 "?column?":12
 ├── immutable
 ├── left-join (lookup abcd@abcd_a_b_idx)
 │    ├── columns: m:1 n:2 a:6 b:7
 │    ├── key columns: [1] = [6]
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── projections
      └── CASE a:6 IS NULL WHEN true THEN CAST(NULL AS INT8) ELSE a:6 + b:7 END [as="?column?":12, outer=(6,7), immutable]

# The rule can use p as the canary column, because it is not-null in the right
# input.
opt expect=HoistProjectFromLeftJoin
SELECT * FROM small LEFT JOIN (SELECT q, p+q FROM pqr) ON q=m
----
project
 ├── columns: m:1 n:2 q:7 "?column?":13
 ├── immutable
 ├── left-join (lookup pqr@q)
 │    ├── columns: m:1 n:2 p:6 q:7
 │    ├── key columns: [1] = [7]
 │    ├── fd: (6)-->(7)
 │    ├── scan small
 │    │    └── columns: m:1 n:2
 │    └── filters (true)
 └── projections
      └── CASE p:6 IS NULL WHEN true THEN CAST(NULL AS INT8) ELSE p:6 + q:7 END [as="?column?":13, outer=(6,7), immutable]

# We cannot find a canary column, so the rule should not fire.
# TODO(radu): we could try adding a column to the input scan, similar to EnsureKey.
opt expect-not=HoistProjectFromLeftJoin
SELECT * FROM small LEFT JOIN (SELECT a, a+b FROM abcd) ON a=m OR m=1
----
right-join (cross)
 ├── columns: m:1 n:2 a:6 "?column?":12
 ├── immutable
 ├── project
 │    ├── columns: "?column?":12 a:6
 │    ├── immutable
 │    ├── scan abcd@abcd_a_b_idx
 │    │    └── columns: a:6 b:7
 │    └── projections
 │         └── a:6 + b:7 [as="?column?":12, outer=(6,7), immutable]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      └── (a:6 = m:1) OR (m:1 = 1) [outer=(1,6), constraints=(/1: (/NULL - ])]

# The rule should not fire when the ON condition uses a projection.
# TODO(radu): we could inline the projection.
opt expect-not=HoistProjectFromLeftJoin
SELECT * FROM small LEFT JOIN (SELECT a, a+b FROM abcd) AS rhs(a,x) ON a=m AND x=n
----
right-join (hash)
 ├── columns: m:1 n:2 a:6 x:12
 ├── immutable
 ├── project
 │    ├── columns: "?column?":12 a:6
 │    ├── immutable
 │    ├── scan abcd@abcd_a_b_idx
 │    │    └── columns: a:6 b:7
 │    └── projections
 │         └── a:6 + b:7 [as="?column?":12, outer=(6,7), immutable]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      ├── a:6 = m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
      └── "?column?":12 = n:2 [outer=(2,12), constraints=(/2: (/NULL - ]; /12: (/NULL - ]), fd=(2)==(12), (12)==(2)]

# The rule should not fire when a projection is volatile.
opt expect-not=HoistProjectFromLeftJoin
SELECT * FROM small LEFT JOIN (SELECT p, p+q, random() FROM pqr) ON p=m
----
right-join (hash)
 ├── columns: m:1 n:2 p:6 "?column?":13 random:14
 ├── volatile
 ├── fd: (6)-->(13,14)
 ├── project
 │    ├── columns: "?column?":13 random:14 p:6!null
 │    ├── volatile
 │    ├── key: (6)
 │    ├── fd: (6)-->(13,14)
 │    ├── scan pqr@q
 │    │    ├── columns: p:6!null q:7
 │    │    ├── key: (6)
 │    │    └── fd: (6)-->(7)
 │    └── projections
 │         ├── p:6 + q:7 [as="?column?":13, outer=(6,7), immutable]
 │         └── random() [as=random:14, volatile]
 ├── scan small
 │    └── columns: m:1 n:2
 └── filters
      └── p:6 = m:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

# The rule should not fire when the projection is from a join.
opt expect-not=HoistProjectFromLeftJoin
SELECT * FROM (SELECT a, a+b FROM (SELECT tab1.* from abcd tab1, abcd tab2)) LEFT OUTER JOIN small ON a=m
----
left-join (hash)
 ├── columns: a:1 "?column?":13 m:14 n:15
 ├── immutable
 ├── project
 │    ├── columns: "?column?":13 tab1.a:1
 │    ├── immutable
 │    ├── inner-join (cross)
 │    │    ├── columns: tab1.a:1 tab1.b:2
 │    │    ├── scan abcd@abcd_a_b_idx [as=tab1]
 │    │    │    └── columns: tab1.a:1 tab1.b:2
 │    │    ├── scan abcd@abcd_a_b_idx [as=tab2]
 │    │    └── filters (true)
 │    └── projections
 │         └── tab1.a:1 + tab1.b:2 [as="?column?":13, outer=(1,2), immutable]
 ├── scan small
 │    └── columns: m:14 n:15
 └── filters
      └── tab1.a:1 = m:14 [outer=(1,14), constraints=(/1: (/NULL - ]; /14: (/NULL - ]), fd=(1)==(14), (14)==(1)]

# Regression test for #79943.
# HoistProjectFromLeftJoin should fire, but not fire in so many cases that
# query optimization never finishes.
check-size rule-limit=100000 group-limit=10000 suppress-report expect=HoistProjectFromLeftJoin
SELECT *
FROM
    table2 tab_69833
    LEFT OUTER JOIN table1@[0] AS tab_69836
        LEFT OUTER JOIN table1 AS tab_69837 ON (tab_69836.col1_5) = (tab_69837.col1_5)
        LEFT OUTER JOIN table1@[0] AS tab_69838 ON
                (tab_69836.col1_5) = (tab_69838.col1_6)
                AND (tab_69837.col1_6) = (tab_69838.col1_7)
                AND (tab_69837.col1_5) = (tab_69838.col1_7)
        LEFT OUTER JOIN table2@[0] AS tab_69839
            LEFT OUTER JOIN table2@[0] AS tab_69840 ON
                    (tab_69839.col2_13) = (tab_69840.col2_8) AND
                    (tab_69839.col2_13) = (tab_69840.col2_12) ON
                (tab_69837.tableoid) = (tab_69840.col2_2)
                AND (tab_69838.crdb_internal_mvcc_timestamp) = (tab_69839.col2_4)
        LEFT OUTER JOIN table2@[0] AS tab_69841 ON (tab_69839.tableoid) = (tab_69841.col2_2)
        LEFT OUTER JOIN table1 AS tab_69842
            LEFT OUTER JOIN table1 AS tab_69843 ON (tab_69842.col1_5) = (tab_69843.col1_5) ON
                (tab_69838.col1_6) = (tab_69842.col1_5) AND (tab_69837.col1_6) = (tab_69843.col1_5)
        LEFT OUTER JOIN table1 AS tab_69844 ON
                (tab_69838.col1_6) = (tab_69844.col1_5) ON
            (tab_69833.col2_2) = (tab_69838.tableoid)
            AND (tab_69833.crdb_internal_mvcc_timestamp) = (tab_69839.crdb_internal_mvcc_timestamp)
            AND (tab_69833.crdb_internal_mvcc_timestamp) = (tab_69840.crdb_internal_mvcc_timestamp)
            AND (tab_69833.col2_4) = (tab_69838.crdb_internal_mvcc_timestamp)
            AND (tab_69833.crdb_internal_mvcc_timestamp) = (tab_69838.crdb_internal_mvcc_timestamp)
ORDER BY
    tab_69844.col1_6 DESC, tab_69833.crdb_internal_mvcc_timestamp
LIMIT
    51:::INT8
----

# --------------------------------------------------
# GenerateLocalityOptimizedAntiJoin
# --------------------------------------------------

# These tables mimic REGIONAL BY ROW tables.
exec-ddl
CREATE TABLE abc_part (
    r STRING NOT NULL CHECK (r IN ('east', 'west', 'central')),
    a INT NOT NULL,
    b INT,
    c INT,
    v INT NOT NULL AS (a + 10) VIRTUAL,
    PRIMARY KEY (r, a),
    UNIQUE WITHOUT INDEX (a),
    UNIQUE WITHOUT INDEX (b),
    UNIQUE WITHOUT INDEX (v),
    UNIQUE INDEX b_idx (r, b) PARTITION BY LIST (r) (
      PARTITION east VALUES IN (('east')),
      PARTITION west VALUES IN (('west')),
      PARTITION central VALUES IN (('central'))
    ),
    INDEX c_idx (r, c) PARTITION BY LIST (r) (
      PARTITION east VALUES IN (('east')),
      PARTITION west VALUES IN (('west')),
      PARTITION central VALUES IN (('central'))
    ),
    UNIQUE INDEX v_idx (r, v) PARTITION BY LIST (r) (
      PARTITION east VALUES IN (('east')),
      PARTITION west VALUES IN (('west')),
      PARTITION central VALUES IN (('central'))
    ),
    INDEX c_b_idx (r, c, b) PARTITION BY LIST (r) (
      PARTITION east VALUES IN (('east')),
      PARTITION west VALUES IN (('west')),
      PARTITION central VALUES IN (('central'))
    )
) PARTITION BY LIST (r) (
  PARTITION east VALUES IN (('east')),
  PARTITION west VALUES IN (('west')),
  PARTITION central VALUES IN (('central'))
)
----

exec-ddl
ALTER PARTITION "east" OF INDEX abc_part@abc_part_pkey CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX abc_part@abc_part_pkey CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX abc_part@abc_part_pkey CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

exec-ddl
ALTER PARTITION "east" OF INDEX abc_part@b_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX abc_part@b_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX abc_part@b_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

exec-ddl
ALTER PARTITION "east" OF INDEX abc_part@c_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX abc_part@c_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX abc_part@c_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

exec-ddl
ALTER PARTITION "east" OF INDEX abc_part@c_b_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX abc_part@c_b_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX abc_part@c_b_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

exec-ddl
CREATE TABLE def_part (
    r STRING NOT NULL CHECK (r IN ('east', 'west', 'central')),
    d INT NOT NULL,
    e INT REFERENCES abc_part (a),
    f INT REFERENCES abc_part (b),
    PRIMARY KEY (r, d),
    UNIQUE WITHOUT INDEX (d),
    UNIQUE WITHOUT INDEX (e),
    UNIQUE INDEX e_idx (r, e) PARTITION BY LIST (r) (
      PARTITION east VALUES IN (('east')),
      PARTITION west VALUES IN (('west')),
      PARTITION central VALUES IN (('central'))
    ),
    INDEX f_idx (r, f) PARTITION BY LIST (r) (
      PARTITION east VALUES IN (('east')),
      PARTITION west VALUES IN (('west')),
      PARTITION central VALUES IN (('central'))
    )
) PARTITION BY LIST (r) (
  PARTITION east VALUES IN (('east')),
  PARTITION west VALUES IN (('west')),
  PARTITION central VALUES IN (('central'))
)
----

exec-ddl
ALTER PARTITION "east" OF INDEX def_part@def_part_pkey CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX def_part@def_part_pkey CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX def_part@def_part_pkey CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

exec-ddl
ALTER PARTITION "east" OF INDEX def_part@e_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX def_part@e_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX def_part@e_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

exec-ddl
ALTER PARTITION "east" OF INDEX def_part@f_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=east: 2}',
  lease_preferences = '[[+region=east]]'
----

exec-ddl
ALTER PARTITION "west" OF INDEX def_part@f_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=west: 2}',
  lease_preferences = '[[+region=west]]';
----

exec-ddl
ALTER PARTITION "central" OF INDEX def_part@f_idx CONFIGURE ZONE USING
  num_voters = 5,
  voter_constraints = '{+region=central: 2}',
  lease_preferences = '[[+region=central]]';
----

# Locality optimized anti join.
opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1
----
anti-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── anti-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'east'/1 - /'east'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'west'/1 - /'west'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters (true)
 └── filters (true)

# Locality optimized anti join in different region.
opt locality=(region=west) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1
----
anti-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'east') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'east' - /'east']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: west
 ├── anti-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'west' [outer=(7), constraints=(/7: [/'west' - /'west']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: west
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: west
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'west'/1 - /'west'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'east'/1 - /'east'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters (true)
 └── filters (true)

# Different join condition.
opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE f = b) AND d = 10
----
anti-join (lookup abc_part@b_idx)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── f:4 = b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ]), fd=(4)==(9), (9)==(4)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── anti-join (lookup abc_part@b_idx)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── f:4 = b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ]), fd=(4)==(9), (9)==(4)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'east'/10 - /'east'/10]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/10 - /'central'/10]
 │    │         │    └── [/'west'/10 - /'west'/10]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters (true)
 └── filters (true)

# With an extra ON filter.
opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a AND f > b) AND d = 1
----
anti-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── anti-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'east'/1 - /'east'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'west'/1 - /'west'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters
 │         └── f:4 > b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ])]
 └── filters
      └── f:4 > b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ])]

# Optimization applies even though the scan may produce more than one row.
opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND f = 10
----
distribute
 ├── columns: r:1!null d:2!null e:3 f:4!null
 ├── key: (2)
 ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2)
 ├── distribution: east
 ├── input distribution: east
 └── anti-join (lookup abc_part)
      ├── columns: def_part.r:1!null d:2!null e:3 f:4!null
      ├── lookup expression
      │    └── filters
      │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
      │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
      ├── lookup columns are key
      ├── key: (2)
      ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2)
      ├── anti-join (lookup abc_part)
      │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null
      │    ├── lookup expression
      │    │    └── filters
      │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
      │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
      │    ├── lookup columns are key
      │    ├── key: (2)
      │    ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2)
      │    ├── index-join def_part
      │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null
      │    │    ├── key: (2)
      │    │    ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2)
      │    │    └── scan def_part@f_idx
      │    │         ├── columns: def_part.r:1!null d:2!null f:4!null
      │    │         ├── constraint: /1/4/2
      │    │         │    ├── [/'central'/10 - /'central'/10]
      │    │         │    ├── [/'east'/10 - /'east'/10]
      │    │         │    └── [/'west'/10 - /'west'/10]
      │    │         ├── key: (2)
      │    │         └── fd: ()-->(4), (2)-->(1)
      │    └── filters (true)
      └── filters (true)

# Optimization applies even though the lookup join may have more than one
# matching row.
opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE f = c) AND d = 10
----
anti-join (lookup abc_part@c_idx)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── anti-join (lookup abc_part@c_idx)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'east'/10 - /'east'/10]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/10 - /'central'/10]
 │    │         │    └── [/'west'/10 - /'west'/10]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters (true)
 └── filters (true)

# Optimization applies even though the lookup join may have more than one
# matching row and there's and ON condition.
opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE f = c AND e % a = 0) AND d = 10
----
anti-join (lookup abc_part@c_idx)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── anti-join (lookup abc_part@c_idx)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 │    ├── cardinality: [0 - 1]
 │    ├── immutable
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'east'/10 - /'east'/10]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/10 - /'central'/10]
 │    │         │    └── [/'west'/10 - /'west'/10]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters
 │         └── (e:3 % a:8) = 0 [outer=(3,8), immutable]
 └── filters
      └── (e:3 % a:8) = 0 [outer=(3,8), immutable]

# The inner anti-join must produce the projected "lookup_join_const_col" because
# it is referenced in both lookup expressions.
opt locality=(region=east)
SELECT * FROM (VALUES (2), (4)) v(i) WHERE NOT EXISTS (SELECT * FROM abc_part WHERE c = 1 AND b = i)
----
anti-join (lookup abc_part@c_b_idx)
 ├── columns: i:1!null
 ├── lookup expression
 │    └── filters
 │         ├── r:2 IN ('central', 'west') [outer=(2), constraints=(/2: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         ├── "lookup_join_const_col_@5":19 = c:5 [outer=(5,19), constraints=(/5: (/NULL - ]; /19: (/NULL - ]), fd=(5)==(19), (19)==(5)]
 │         └── column1:1 = b:4 [outer=(1,4), constraints=(/1: (/NULL - ]; /4: (/NULL - ]), fd=(1)==(4), (4)==(1)]
 ├── lookup columns are key
 ├── cardinality: [0 - 2]
 ├── distribution: east
 ├── anti-join (lookup abc_part@c_b_idx)
 │    ├── columns: column1:1!null "lookup_join_const_col_@5":19!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── r:2 = 'east' [outer=(2), constraints=(/2: [/'east' - /'east']; tight), fd=()-->(2)]
 │    │         ├── "lookup_join_const_col_@5":19 = c:5 [outer=(5,19), constraints=(/5: (/NULL - ]; /19: (/NULL - ]), fd=(5)==(19), (19)==(5)]
 │    │         └── column1:1 = b:4 [outer=(1,4), constraints=(/1: (/NULL - ]; /4: (/NULL - ]), fd=(1)==(4), (4)==(1)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 2]
 │    ├── fd: ()-->(19)
 │    ├── distribution: east
 │    ├── project
 │    │    ├── columns: "lookup_join_const_col_@5":19!null column1:1!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── fd: ()-->(19)
 │    │    ├── distribution: east
 │    │    ├── values
 │    │    │    ├── columns: column1:1!null
 │    │    │    ├── cardinality: [2 - 2]
 │    │    │    ├── distribution: east
 │    │    │    ├── (2,)
 │    │    │    └── (4,)
 │    │    └── projections
 │    │         └── 1 [as="lookup_join_const_col_@5":19]
 │    └── filters (true)
 └── filters (true)

# Optimization does not apply for semi join.
opt locality=(region=central) expect-not=GenerateLocalityOptimizedAntiJoin
SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1
----
semi-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 = 'central' [outer=(7), constraints=(/7: [/'central' - /'central']; tight), fd=()-->(7)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── remote lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('east', 'west') [outer=(7), constraints=(/7: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: central
 ├── locality-optimized-search
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: central
 │    ├── scan def_part
 │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    ├── constraint: /15/16: [/'central'/1 - /'central'/1]
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    └── fd: ()-->(15-18)
 │    └── scan def_part
 │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │         ├── constraint: /21/22
 │         │    ├── [/'east'/1 - /'east'/1]
 │         │    └── [/'west'/1 - /'west'/1]
 │         ├── cardinality: [0 - 1]
 │         ├── key: ()
 │         └── fd: ()-->(21-24)
 └── filters (true)

exec-ddl
DROP INDEX c_b_idx
----

# --------------------------------------------------
# GenerateLocalityOptimizedLookupJoin
# --------------------------------------------------

# Locality optimized inner join.
opt locality=(region=east) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part INNER JOIN abc_part ON e = a WHERE d = 1
----
project
 ├── columns: r:1!null d:2!null e:3!null f:4 r:7!null a:8!null b:9 c:10 v:11!null
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-4,7-11), (3)==(8), (8)==(3)
 ├── distribution: east
 ├── inner-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3!null f:4 abc_part.r:7!null a:8!null b:9 c:10
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── remote lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4,7-10), (3)==(8), (8)==(3)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:14 d:15 e:16 f:17
 │    │    ├── right columns: def_part.r:20 d:21 e:22 f:23
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:14!null d:15!null e:16 f:17
 │    │    │    ├── constraint: /14/15: [/'east'/1 - /'east'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(14-17)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:20!null d:21!null e:22 f:23
 │    │         ├── constraint: /20/21
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'west'/1 - /'west'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(20-23)
 │    └── filters (true)
 └── projections
      └── a:8 + 10 [as=v:11, outer=(8), immutable]

# Locality optimized left join, in a different region.
opt locality=(region=west) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part LEFT JOIN abc_part ON e = a WHERE d = 1
----
project
 ├── columns: r:1!null d:2!null e:3 f:4 r:7 a:8 b:9 c:10 v:11
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-4,7-11)
 ├── distribution: west
 ├── left-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4 abc_part.r:7 a:8 b:9 c:10
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'west' [outer=(7), constraints=(/7: [/'west' - /'west']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── remote lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 IN ('central', 'east') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'east' - /'east']; tight)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4,7-10)
 │    ├── distribution: west
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:14 d:15 e:16 f:17
 │    │    ├── right columns: def_part.r:20 d:21 e:22 f:23
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: west
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:14!null d:15!null e:16 f:17
 │    │    │    ├── constraint: /14/15: [/'west'/1 - /'west'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(14-17)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:20!null d:21!null e:22 f:23
 │    │         ├── constraint: /20/21
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'east'/1 - /'east'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(20-23)
 │    └── filters (true)
 └── projections
      └── CASE abc_part.r:7 IS NULL WHEN true THEN CAST(NULL AS INT8) ELSE a:8 + 10 END [as=v:11, outer=(7,8), immutable]

# Locality optimized semi join, in a different region.
opt locality=(region=central) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1
----
semi-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 = 'central' [outer=(7), constraints=(/7: [/'central' - /'central']; tight), fd=()-->(7)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── remote lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('east', 'west') [outer=(7), constraints=(/7: [/'east' - /'east'] [/'west' - /'west']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: central
 ├── locality-optimized-search
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: central
 │    ├── scan def_part
 │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    ├── constraint: /15/16: [/'central'/1 - /'central'/1]
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    └── fd: ()-->(15-18)
 │    └── scan def_part
 │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │         ├── constraint: /21/22
 │         │    ├── [/'east'/1 - /'east'/1]
 │         │    └── [/'west'/1 - /'west'/1]
 │         ├── cardinality: [0 - 1]
 │         ├── key: ()
 │         └── fd: ()-->(21-24)
 └── filters (true)

# Locality optimized semi join, with an extra ON filter.
opt locality=(region=west) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE e = a AND f > b) AND d = 1
----
semi-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 = 'west' [outer=(7), constraints=(/7: [/'west' - /'west']; tight), fd=()-->(7)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── remote lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'east') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'east' - /'east']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: west
 ├── locality-optimized-search
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: west
 │    ├── scan def_part
 │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    ├── constraint: /15/16: [/'west'/1 - /'west'/1]
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    └── fd: ()-->(15-18)
 │    └── scan def_part
 │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │         ├── constraint: /21/22
 │         │    ├── [/'central'/1 - /'central'/1]
 │         │    └── [/'east'/1 - /'east'/1]
 │         ├── cardinality: [0 - 1]
 │         ├── key: ()
 │         └── fd: ()-->(21-24)
 └── filters
      └── f:4 > b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ])]

# Locality optimized inner join, different join condition.
opt locality=(region=east) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part INNER JOIN abc_part ON f = b WHERE d = 10
----
project
 ├── columns: r:1!null d:2!null e:3 f:4!null r:7!null a:8!null b:9!null c:10 v:11!null
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-4,7-11), (4)==(9), (9)==(4)
 ├── distribution: east
 ├── inner-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null abc_part.r:7!null a:8!null b:9!null c:10
 │    ├── key columns: [7 8] = [7 8]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4,7-10), (4)==(9), (9)==(4)
 │    ├── distribution: east
 │    ├── inner-join (lookup abc_part@b_idx)
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null abc_part.r:7!null a:8!null b:9!null
 │    │    ├── lookup expression
 │    │    │    └── filters
 │    │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │    │         └── f:4 = b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ]), fd=(4)==(9), (9)==(4)]
 │    │    ├── remote lookup expression
 │    │    │    └── filters
 │    │    │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │    │    │         └── f:4 = b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ]), fd=(4)==(9), (9)==(4)]
 │    │    ├── lookup columns are key
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4,7-9), (4)==(9), (9)==(4)
 │    │    ├── distribution: east
 │    │    ├── locality-optimized-search
 │    │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    │    ├── left columns: def_part.r:14 d:15 e:16 f:17
 │    │    │    ├── right columns: def_part.r:20 d:21 e:22 f:23
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(1-4)
 │    │    │    ├── distribution: east
 │    │    │    ├── scan def_part
 │    │    │    │    ├── columns: def_part.r:14!null d:15!null e:16 f:17
 │    │    │    │    ├── constraint: /14/15: [/'east'/10 - /'east'/10]
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    └── fd: ()-->(14-17)
 │    │    │    └── scan def_part
 │    │    │         ├── columns: def_part.r:20!null d:21!null e:22 f:23
 │    │    │         ├── constraint: /20/21
 │    │    │         │    ├── [/'central'/10 - /'central'/10]
 │    │    │         │    └── [/'west'/10 - /'west'/10]
 │    │    │         ├── cardinality: [0 - 1]
 │    │    │         ├── key: ()
 │    │    │         └── fd: ()-->(20-23)
 │    │    └── filters (true)
 │    └── filters (true)
 └── projections
      └── a:8 + 10 [as=v:11, outer=(8), immutable]

# With an extra ON filter.
opt locality=(region=east) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part INNER JOIN abc_part ON e = a AND f > b WHERE d = 1
----
project
 ├── columns: r:1!null d:2!null e:3!null f:4!null r:7!null a:8!null b:9!null c:10 v:11!null
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-4,7-11), (3)==(8), (8)==(3)
 ├── distribution: east
 ├── inner-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3!null f:4!null abc_part.r:7!null a:8!null b:9!null c:10
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── remote lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4,7-10), (3)==(8), (8)==(3)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:14 d:15 e:16 f:17
 │    │    ├── right columns: def_part.r:20 d:21 e:22 f:23
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:14!null d:15!null e:16 f:17
 │    │    │    ├── constraint: /14/15: [/'east'/1 - /'east'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(14-17)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:20!null d:21!null e:22 f:23
 │    │         ├── constraint: /20/21
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'west'/1 - /'west'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(20-23)
 │    └── filters
 │         └── f:4 > b:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ])]
 └── projections
      └── a:8 + 10 [as=v:11, outer=(8), immutable]

# Optimization applies even though the scan may produce more than one row.
opt locality=(region=east) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part INNER JOIN abc_part ON e = a WHERE f = 10
----
distribute
 ├── columns: r:1!null d:2!null e:3!null f:4!null r:7!null a:8!null b:9 c:10 v:11!null
 ├── immutable
 ├── key: (2)
 ├── fd: ()-->(4), (2)-->(1,3), (3)-->(1,2), (8)-->(7,9-11), (9)~~>(7,8,10), (3)==(8), (8)==(3)
 ├── distribution: east
 ├── input distribution: east
 └── project
      ├── columns: v:11!null def_part.r:1!null d:2!null e:3!null f:4!null abc_part.r:7!null a:8!null b:9 c:10
      ├── immutable
      ├── key: (2)
      ├── fd: ()-->(4), (2)-->(1,3), (3)-->(1,2), (8)-->(7,9-11), (9)~~>(7,8,10), (3)==(8), (8)==(3)
      ├── inner-join (lookup abc_part)
      │    ├── columns: def_part.r:1!null d:2!null e:3!null f:4!null abc_part.r:7!null a:8!null b:9 c:10
      │    ├── lookup expression
      │    │    └── filters
      │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
      │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
      │    ├── remote lookup expression
      │    │    └── filters
      │    │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
      │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
      │    ├── lookup columns are key
      │    ├── key: (2)
      │    ├── fd: ()-->(4), (2)-->(1,3), (3)-->(1,2), (8)-->(7,9,10), (9)~~>(7,8,10), (3)==(8), (8)==(3)
      │    ├── index-join def_part
      │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null
      │    │    ├── key: (2)
      │    │    ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2)
      │    │    └── scan def_part@f_idx
      │    │         ├── columns: def_part.r:1!null d:2!null f:4!null
      │    │         ├── constraint: /1/4/2
      │    │         │    ├── [/'central'/10 - /'central'/10]
      │    │         │    ├── [/'east'/10 - /'east'/10]
      │    │         │    └── [/'west'/10 - /'west'/10]
      │    │         ├── key: (2)
      │    │         └── fd: ()-->(4), (2)-->(1)
      │    └── filters (true)
      └── projections
           └── a:8 + 10 [as=v:11, outer=(8), immutable]

# Optimization applies for a semi join even though the lookup join may have more
# than one matching row.
opt locality=(region=east) expect=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE f = c) AND d = 10
----
semi-join (lookup abc_part@c_idx)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 ├── remote lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── locality-optimized-search
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── scan def_part
 │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    ├── constraint: /15/16: [/'east'/10 - /'east'/10]
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    └── fd: ()-->(15-18)
 │    └── scan def_part
 │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │         ├── constraint: /21/22
 │         │    ├── [/'central'/10 - /'central'/10]
 │         │    └── [/'west'/10 - /'west'/10]
 │         ├── cardinality: [0 - 1]
 │         ├── key: ()
 │         └── fd: ()-->(21-24)
 └── filters (true)

# Optimization does not apply for a semi join that may have more than one
# matching row when there is an ON condition.
# NOTE: A locality optimized lookup join is possible here after the join has
# been reordered (because d is a lax key), so we prevent reordering to test this
# case and ensure the expect-not holds.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedLookupJoin set=reorder_joins_limit=0
SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE f = c AND e % a = 0) AND d = 10
----
semi-join (lookup abc_part@c_idx)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'east', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── locality-optimized-search
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── scan def_part
 │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    ├── constraint: /15/16: [/'east'/10 - /'east'/10]
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    └── fd: ()-->(15-18)
 │    └── scan def_part
 │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │         ├── constraint: /21/22
 │         │    ├── [/'central'/10 - /'central'/10]
 │         │    └── [/'west'/10 - /'west'/10]
 │         ├── cardinality: [0 - 1]
 │         ├── key: ()
 │         └── fd: ()-->(21-24)
 └── filters
      └── (e:3 % a:8) = 0 [outer=(3,8), immutable]

# Optimization does not apply for an inner join since the lookup join may have more than one
# matching row.
# NOTE: A locality optimized lookup join is possible here after the join has
# been reordered (because d is a lax key), so we prevent reordering to test this
# case and ensure the expect-not holds.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedLookupJoin set=reorder_joins_limit=0
SELECT * FROM def_part INNER JOIN abc_part ON f = c WHERE d = 10
----
project
 ├── columns: r:1!null d:2!null e:3 f:4!null r:7!null a:8!null b:9 c:10!null v:11!null
 ├── immutable
 ├── key: (8)
 ├── fd: ()-->(1-4,10), (8)-->(7,9,11), (9)~~>(7,8), (4)==(10), (10)==(4)
 ├── distribution: east
 ├── inner-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null abc_part.r:7!null a:8!null b:9 c:10!null
 │    ├── key columns: [7 8] = [7 8]
 │    ├── lookup columns are key
 │    ├── key: (8)
 │    ├── fd: ()-->(1-4,10), (8)-->(7,9), (9)~~>(7,8), (4)==(10), (10)==(4)
 │    ├── distribution: east
 │    ├── inner-join (lookup abc_part@c_idx)
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null abc_part.r:7!null a:8!null c:10!null
 │    │    ├── lookup expression
 │    │    │    └── filters
 │    │    │         ├── abc_part.r:7 IN ('central', 'east', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │    │         └── f:4 = c:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ]), fd=(4)==(10), (10)==(4)]
 │    │    ├── key: (8)
 │    │    ├── fd: ()-->(1-4,10), (8)-->(7), (4)==(10), (10)==(4)
 │    │    ├── distribution: east
 │    │    ├── locality-optimized-search
 │    │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    │    ├── left columns: def_part.r:14 d:15 e:16 f:17
 │    │    │    ├── right columns: def_part.r:20 d:21 e:22 f:23
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(1-4)
 │    │    │    ├── distribution: east
 │    │    │    ├── scan def_part
 │    │    │    │    ├── columns: def_part.r:14!null d:15!null e:16 f:17
 │    │    │    │    ├── constraint: /14/15: [/'east'/10 - /'east'/10]
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    └── fd: ()-->(14-17)
 │    │    │    └── scan def_part
 │    │    │         ├── columns: def_part.r:20!null d:21!null e:22 f:23
 │    │    │         ├── constraint: /20/21
 │    │    │         │    ├── [/'central'/10 - /'central'/10]
 │    │    │         │    └── [/'west'/10 - /'west'/10]
 │    │    │         ├── cardinality: [0 - 1]
 │    │    │         ├── key: ()
 │    │    │         └── fd: ()-->(20-23)
 │    │    └── filters (true)
 │    └── filters (true)
 └── projections
      └── a:8 + 10 [as=v:11, outer=(8), immutable]

# Optimization does not apply for anti join.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedLookupJoin
SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1
----
anti-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4
 ├── lookup expression
 │    └── filters
 │         ├── abc_part.r:7 IN ('central', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 ├── lookup columns are key
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-4)
 ├── distribution: east
 ├── anti-join (lookup abc_part)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 = 'east' [outer=(7), constraints=(/7: [/'east' - /'east']; tight), fd=()-->(7)]
 │    │         └── e:3 = a:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:15 d:16 e:17 f:18
 │    │    ├── right columns: def_part.r:21 d:22 e:23 f:24
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:15!null d:16!null e:17 f:18
 │    │    │    ├── constraint: /15/16: [/'east'/1 - /'east'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(15-18)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:21!null d:22!null e:23 f:24
 │    │         ├── constraint: /21/22
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'west'/1 - /'west'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(21-24)
 │    └── filters (true)
 └── filters (true)

# We do not currently support locality optimized lookup joins on indexes with
# virtual columns.
# NOTE: A locality optimized lookup join is possible here after the join has
# been reordered, so we prevent reordering to test this case and ensure the
# expect-not holds.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedLookupJoin set=reorder_joins_limit=0
SELECT * FROM def_part INNER JOIN abc_part ON f = v WHERE d = 1
----
inner-join (lookup abc_part)
 ├── columns: r:1!null d:2!null e:3 f:4!null r:7!null a:8!null b:9 c:10 v:11!null
 ├── key columns: [7 8] = [7 8]
 ├── lookup columns are key
 ├── immutable
 ├── key: (8)
 ├── fd: ()-->(1-4,11), (8)-->(7,9,10), (9)~~>(7,8,10), (4)==(11), (11)==(4)
 ├── distribution: east
 ├── inner-join (lookup abc_part@v_idx)
 │    ├── columns: def_part.r:1!null d:2!null e:3 f:4!null abc_part.r:7!null a:8!null v:11!null
 │    ├── lookup expression
 │    │    └── filters
 │    │         ├── abc_part.r:7 IN ('central', 'east', 'west') [outer=(7), constraints=(/7: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │         └── f:4 = v:11 [outer=(4,11), constraints=(/4: (/NULL - ]; /11: (/NULL - ]), fd=(4)==(11), (11)==(4)]
 │    ├── lookup columns are key
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-4,7,8,11), (4)==(11), (11)==(4)
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: def_part.r:1!null d:2!null e:3 f:4
 │    │    ├── left columns: def_part.r:14 d:15 e:16 f:17
 │    │    ├── right columns: def_part.r:20 d:21 e:22 f:23
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-4)
 │    │    ├── distribution: east
 │    │    ├── scan def_part
 │    │    │    ├── columns: def_part.r:14!null d:15!null e:16 f:17
 │    │    │    ├── constraint: /14/15: [/'east'/1 - /'east'/1]
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    └── fd: ()-->(14-17)
 │    │    └── scan def_part
 │    │         ├── columns: def_part.r:20!null d:21!null e:22 f:23
 │    │         ├── constraint: /20/21
 │    │         │    ├── [/'central'/1 - /'central'/1]
 │    │         │    └── [/'west'/1 - /'west'/1]
 │    │         ├── cardinality: [0 - 1]
 │    │         ├── key: ()
 │    │         └── fd: ()-->(20-23)
 │    └── filters (true)
 └── filters (true)

# illustrative examples from GH #51576
exec-ddl
CREATE TABLE metrics (
  id   SERIAL PRIMARY KEY,
  name STRING,
  INDEX name_index (name)
)
----

exec-ddl
CREATE TABLE metric_values (
  metric_id INT8,
  time      TIMESTAMPTZ,
  value     INT8,
  PRIMARY KEY (metric_id, time)
)
----

# Add some metrics to force lookup join to be chosen.
exec-ddl
ALTER TABLE metric_values INJECT STATISTICS
'[
 {
   "columns": ["metric_id"],
   "created_at": "2018-01-01 1:00:00.00000+00:00",
   "row_count": 1000,
   "distinct_count": 10
 },
 {
   "columns": ["time"],
   "created_at": "2018-01-01 1:30:00.00000+00:00",
   "row_count": 1000,
   "distinct_count": 1000
 },
 {
    "columns": ["value"],
    "created_at": "2018-01-01 1:30:00.00000+00:00",
    "row_count": 1000,
    "distinct_count": 1000
  }
]'
----

exec-ddl
ALTER TABLE metrics INJECT STATISTICS
'[
 {
   "columns": ["id"],
   "created_at": "2018-01-01 1:00:00.00000+00:00",
   "row_count": 10,
   "distinct_count": 10
 },
 {
   "columns": ["name"],
   "created_at": "2018-01-01 1:30:00.00000+00:00",
   "row_count": 10,
   "distinct_count": 10
 }
]'
----

opt expect=GenerateLookupJoinsWithFilter
SELECT *
FROM metric_values
INNER JOIN metrics
ON metric_id=id
WHERE
  time BETWEEN '2020-01-01 00:00:00+00:00' AND '2020-01-01 00:10:00+00:00' AND
  name='cpu'
----
inner-join (lookup metric_values)
 ├── columns: metric_id:1!null time:2!null value:3 id:6!null name:7!null
 ├── lookup expression
 │    └── filters
 │         ├── (time:2 >= '2020-01-01 00:00:00+00') AND (time:2 <= '2020-01-01 00:10:00+00') [outer=(2), constraints=(/2: [/'2020-01-01 00:00:00+00' - /'2020-01-01 00:10:00+00']; tight)]
 │         └── id:6 = metric_id:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 ├── key: (2,6)
 ├── fd: ()-->(7), (1,2)-->(3), (1)==(6), (6)==(1)
 ├── scan metrics@name_index
 │    ├── columns: id:6!null name:7!null
 │    ├── constraint: /7/6: [/'cpu' - /'cpu']
 │    ├── key: (6)
 │    └── fd: ()-->(7)
 └── filters (true)

opt expect=GenerateLookupJoinsWithFilter
SELECT *
FROM metric_values
INNER JOIN metrics
ON metric_id=id
WHERE
  time BETWEEN '2020-01-01 00:00:00+00:00' AND '2020-01-01 00:10:00+00:00' AND
  name IN ('cpu','mem')
----
inner-join (lookup metric_values)
 ├── columns: metric_id:1!null time:2!null value:3 id:6!null name:7!null
 ├── lookup expression
 │    └── filters
 │         ├── (time:2 >= '2020-01-01 00:00:00+00') AND (time:2 <= '2020-01-01 00:10:00+00') [outer=(2), constraints=(/2: [/'2020-01-01 00:00:00+00' - /'2020-01-01 00:10:00+00']; tight)]
 │         └── id:6 = metric_id:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 ├── key: (2,6)
 ├── fd: (1,2)-->(3), (6)-->(7), (1)==(6), (6)==(1)
 ├── scan metrics@name_index
 │    ├── columns: id:6!null name:7!null
 │    ├── constraint: /7/6
 │    │    ├── [/'cpu' - /'cpu']
 │    │    └── [/'mem' - /'mem']
 │    ├── key: (6)
 │    └── fd: (6)-->(7)
 └── filters (true)

# Lookup expressions in forms that are not supported by the execution engine
# should not be generated. In this case, a lookup expression in the form
# `time IN (...)` should be created rather than `time IN (...) AND time > ...`.
opt expect=GenerateLookupJoinsWithFilter
SELECT *
FROM metric_values
INNER JOIN metrics
ON metric_id=id
WHERE
  time IN ('2022-04-07 00:00:00+00:00', '2022-04-08 00:00:00+00:00', '2022-04-09 00:00:00+00:00') AND
  time > '2022-04-07 00:00:00+00:00' AND
  name='cpu'
----
inner-join (lookup metric_values)
 ├── columns: metric_id:1!null time:2!null value:3 id:6!null name:7!null
 ├── lookup expression
 │    └── filters
 │         ├── time:2 IN ('2022-04-08 00:00:00+00', '2022-04-09 00:00:00+00') [outer=(2), constraints=(/2: [/'2022-04-08 00:00:00+00' - /'2022-04-08 00:00:00+00'] [/'2022-04-09 00:00:00+00' - /'2022-04-09 00:00:00+00']; tight)]
 │         └── id:6 = metric_id:1 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
 ├── key: (2,6)
 ├── fd: ()-->(7), (1,2)-->(3), (1)==(6), (6)==(1)
 ├── scan metrics@name_index
 │    ├── columns: id:6!null name:7!null
 │    ├── constraint: /7/6: [/'cpu' - /'cpu']
 │    ├── key: (6)
 │    └── fd: ()-->(7)
 └── filters (true)

# We don't support turning LIKE into scans yet, test that we fall back to a
# filter.
opt expect-not=GenerateLookupJoins
SELECT *
FROM metric_values
INNER JOIN metrics
ON metric_id=id
WHERE
  time::STRING LIKE '202%' AND
  name='cpu'
----
inner-join (lookup metric_values)
 ├── columns: metric_id:1!null time:2!null value:3 id:6!null name:7!null
 ├── key columns: [6] = [1]
 ├── stable
 ├── key: (2,6)
 ├── fd: ()-->(7), (1,2)-->(3), (1)==(6), (6)==(1)
 ├── scan metrics@name_index
 │    ├── columns: id:6!null name:7!null
 │    ├── constraint: /7/6: [/'cpu' - /'cpu']
 │    ├── key: (6)
 │    └── fd: ()-->(7)
 └── filters
      └── time:2::STRING LIKE '202%' [outer=(2), stable]

opt expect=GenerateLookupJoinsWithFilter
SELECT *
FROM metrics
LEFT JOIN metric_values
ON metric_id=id
AND time BETWEEN '2020-01-01 00:00:00+00:00' AND '2020-01-01 00:10:00+00:00'
AND name='cpu'
----
left-join (lookup metric_values)
 ├── columns: id:1!null name:2 metric_id:5 time:6 value:7
 ├── lookup expression
 │    └── filters
 │         ├── (time:6 >= '2020-01-01 00:00:00+00') AND (time:6 <= '2020-01-01 00:10:00+00') [outer=(6), constraints=(/6: [/'2020-01-01 00:00:00+00' - /'2020-01-01 00:10:00+00']; tight)]
 │         └── id:1 = metric_id:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]
 ├── key: (1,5,6)
 ├── fd: (1)-->(2), (5,6)-->(7)
 ├── scan metrics
 │    ├── columns: id:1!null name:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 └── filters
      └── name:2 = 'cpu' [outer=(2), constraints=(/2: [/'cpu' - /'cpu']; tight), fd=()-->(2)]

# Regression test for #68975. Ensure that findJoinFilterRange does not panic
# from trying to access the 0-th constraint in an empty constraint set.
exec-ddl
CREATE TABLE t68975 (
    s STRING,
    i INT CHECK (false),
    INDEX (s DESC)
);
----

opt
SELECT NULL
FROM t68975 AS t1 JOIN t68975 AS t2
ON t1.s = t2.s;
----
project
 ├── columns: "?column?":11
 ├── fd: ()-->(11)
 ├── inner-join (merge)
 │    ├── columns: t1.s:1!null t2.s:6!null
 │    ├── left ordering: -1
 │    ├── right ordering: -6
 │    ├── fd: (1)==(6), (6)==(1)
 │    ├── scan t68975@t68975_s_idx [as=t1]
 │    │    ├── columns: t1.s:1
 │    │    └── ordering: -1
 │    ├── scan t68975@t68975_s_idx [as=t2]
 │    │    ├── columns: t2.s:6
 │    │    └── ordering: -6
 │    └── filters (true)
 └── projections
      └── NULL [as="?column?":11]

# Regression test for #87306. Do not plan a paired join if there are no columns
# to fetch from the RHS's primary index.
exec-ddl
CREATE TABLE t87306 (
  a INT,
  b INT,
  c INT,
  INDEX (a, c)
)
----

opt expect=GenerateLookupJoinsWithFilter
SELECT m FROM small WHERE m IN (
  SELECT c FROM t87306 WHERE a = 0 OR b IN (0) AND b > 0
)
----
semi-join (lookup t87306@t87306_a_c_idx)
 ├── columns: m:1
 ├── key columns: [13 1] = [6 8]
 ├── project
 │    ├── columns: "lookup_join_const_col_@6":13!null m:1
 │    ├── fd: ()-->(13)
 │    ├── scan small
 │    │    └── columns: m:1
 │    └── projections
 │         └── 0 [as="lookup_join_const_col_@6":13]
 └── filters (true)

# --------------------------------------------------
# SplitDisjunctionOfJoinTerms
# --------------------------------------------------
exec-ddl
DROP TABLE abc
----

exec-ddl
CREATE TABLE abc
(
    a INT,
    b INT,
    c INT,
    PRIMARY KEY(a,b),
    INDEX bc (b,c) STORING (a)
)
----

exec-ddl
ALTER TABLE abc INJECT STATISTICS '[
  {
    "columns": ["b"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 1000,
    "distinct_count": 1000
  }
]'
----

exec-ddl
ALTER TABLE xyz INJECT STATISTICS '[
  {
    "columns": ["y"],
    "created_at": "2018-05-01 1:00:00.00000+00:00",
    "row_count": 1000,
    "distinct_count": 1000
  }
]'
----

# Inner join with PK columns selected and without constraints
opt expect=SplitDisjunctionOfJoinTerms
SELECT * FROM abc INNER JOIN xyz on abc.a = xyz.x or abc.b = xyz.y
----
project
 ├── columns: a:1!null b:2!null c:3 x:6 y:7 z:8
 ├── fd: (1,2)-->(3)
 └── distinct-on
      ├── columns: a:1!null b:2!null c:3 x:6 y:7 z:8 rowid:9!null
      ├── grouping columns: a:1!null b:2!null rowid:9!null
      ├── key: (1,2,9)
      ├── fd: (1,2,9)-->(3,6-8)
      ├── union-all
      │    ├── columns: a:1!null b:2!null c:3 x:6 y:7 z:8 rowid:9!null
      │    ├── left columns: a:12 b:13 c:14 x:17 y:18 z:19 rowid:20
      │    ├── right columns: a:23 b:24 c:25 x:28 y:29 z:30 rowid:31
      │    ├── inner-join (merge)
      │    │    ├── columns: a:12!null b:13!null c:14 x:17!null y:18 z:19 rowid:20!null
      │    │    ├── left ordering: +12
      │    │    ├── right ordering: +17
      │    │    ├── key: (13,20)
      │    │    ├── fd: (12,13)-->(14), (20)-->(17-19), (12)==(17), (17)==(12)
      │    │    ├── scan abc
      │    │    │    ├── columns: a:12!null b:13!null c:14
      │    │    │    ├── key: (12,13)
      │    │    │    ├── fd: (12,13)-->(14)
      │    │    │    └── ordering: +12
      │    │    ├── scan xyz@xy
      │    │    │    ├── columns: x:17 y:18 z:19 rowid:20!null
      │    │    │    ├── key: (20)
      │    │    │    ├── fd: (20)-->(17-19)
      │    │    │    └── ordering: +17
      │    │    └── filters (true)
      │    └── inner-join (merge)
      │         ├── columns: a:23!null b:24!null c:25 x:28 y:29!null z:30 rowid:31!null
      │         ├── left ordering: +24
      │         ├── right ordering: +29
      │         ├── key: (23,31)
      │         ├── fd: (23,24)-->(25), (31)-->(28-30), (24)==(29), (29)==(24)
      │         ├── scan abc@bc
      │         │    ├── columns: a:23!null b:24!null c:25
      │         │    ├── key: (23,24)
      │         │    ├── fd: (23,24)-->(25)
      │         │    └── ordering: +24
      │         ├── scan xyz@yz
      │         │    ├── columns: x:28 y:29 z:30 rowid:31!null
      │         │    ├── key: (31)
      │         │    ├── fd: (31)-->(28-30)
      │         │    └── ordering: +29
      │         └── filters (true)
      └── aggregations
           ├── const-agg [as=c:3, outer=(3)]
           │    └── c:3
           ├── const-agg [as=x:6, outer=(6)]
           │    └── x:6
           ├── const-agg [as=y:7, outer=(7)]
           │    └── y:7
           └── const-agg [as=z:8, outer=(8)]
                └── z:8

# Inner join with PK columns selected and with constraints
opt expect=SplitDisjunctionOfJoinTerms
SELECT * FROM abc INNER JOIN xyz on abc.a = xyz.x or abc.b = xyz.y where abc.b < 1 and xyz.y > 1
----
project
 ├── columns: a:1!null b:2!null c:3 x:6 y:7!null z:8
 ├── fd: (1,2)-->(3)
 └── distinct-on
      ├── columns: a:1!null b:2!null c:3 x:6 y:7!null z:8 rowid:9!null
      ├── grouping columns: a:1!null b:2!null rowid:9!null
      ├── key: (1,2,9)
      ├── fd: (1,2,9)-->(3,6-8)
      ├── union-all
      │    ├── columns: a:1!null b:2!null c:3 x:6 y:7!null z:8 rowid:9!null
      │    ├── left columns: a:12 b:13 c:14 x:17 y:18 z:19 rowid:20
      │    ├── right columns: a:23 b:24 c:25 x:28 y:29 z:30 rowid:31
      │    ├── inner-join (hash)
      │    │    ├── columns: a:12!null b:13!null c:14 x:17!null y:18!null z:19 rowid:20!null
      │    │    ├── key: (13,20)
      │    │    ├── fd: (12,13)-->(14), (20)-->(17-19), (12)==(17), (17)==(12)
      │    │    ├── scan abc@bc
      │    │    │    ├── columns: a:12!null b:13!null c:14
      │    │    │    ├── constraint: /13/14/12: [ - /0]
      │    │    │    ├── key: (12,13)
      │    │    │    └── fd: (12,13)-->(14)
      │    │    ├── scan xyz@yz
      │    │    │    ├── columns: x:17 y:18!null z:19 rowid:20!null
      │    │    │    ├── constraint: /18/19/20: [/2 - ]
      │    │    │    ├── key: (20)
      │    │    │    └── fd: (20)-->(17-19)
      │    │    └── filters
      │    │         └── a:12 = x:17 [outer=(12,17), constraints=(/12: (/NULL - ]; /17: (/NULL - ]), fd=(12)==(17), (17)==(12)]
      │    └── inner-join (merge)
      │         ├── columns: a:23!null b:24!null c:25 x:28 y:29!null z:30 rowid:31!null
      │         ├── left ordering: +24
      │         ├── right ordering: +29
      │         ├── key: (23,31)
      │         ├── fd: (23,24)-->(25), (31)-->(28-30), (24)==(29), (29)==(24)
      │         ├── scan abc@bc
      │         │    ├── columns: a:23!null b:24!null c:25
      │         │    ├── constraint: /24/25/23: [ - /0]
      │         │    ├── key: (23,24)
      │         │    ├── fd: (23,24)-->(25)
      │         │    └── ordering: +24
      │         ├── scan xyz@yz
      │         │    ├── columns: x:28 y:29!null z:30 rowid:31!null
      │         │    ├── constraint: /29/30/31: [/2 - ]
      │         │    ├── key: (31)
      │         │    ├── fd: (31)-->(28-30)
      │         │    └── ordering: +29
      │         └── filters (true)
      └── aggregations
           ├── const-agg [as=c:3, outer=(3)]
           │    └── c:3
           ├── const-agg [as=x:6, outer=(6)]
           │    └── x:6
           ├── const-agg [as=y:7, outer=(7)]
           │    └── y:7
           └── const-agg [as=z:8, outer=(8)]
                └── z:8

# Inner join with not all PK columns selected and with constraints
opt expect=SplitDisjunctionOfJoinTerms
SELECT c, a FROM abc INNER JOIN xyz on abc.a = xyz.x or abc.b = xyz.y where abc.b < 1 and xyz.y > 1
----
project
 ├── columns: c:3 a:1!null
 └── project
      ├── columns: a:1!null b:2!null c:3 x:6 y:7!null
      ├── fd: (1,2)-->(3)
      └── distinct-on
           ├── columns: a:1!null b:2!null c:3 x:6 y:7!null rowid:9!null
           ├── grouping columns: a:1!null b:2!null rowid:9!null
           ├── key: (1,2,9)
           ├── fd: (1,2,9)-->(3,6,7)
           ├── union-all
           │    ├── columns: a:1!null b:2!null c:3 x:6 y:7!null rowid:9!null
           │    ├── left columns: a:12 b:13 c:14 x:17 y:18 rowid:20
           │    ├── right columns: a:23 b:24 c:25 x:28 y:29 rowid:31
           │    ├── inner-join (hash)
           │    │    ├── columns: a:12!null b:13!null c:14 x:17!null y:18!null rowid:20!null
           │    │    ├── key: (13,20)
           │    │    ├── fd: (12,13)-->(14), (20)-->(17,18), (12)==(17), (17)==(12)
           │    │    ├── scan abc@bc
           │    │    │    ├── columns: a:12!null b:13!null c:14
           │    │    │    ├── constraint: /13/14/12: [ - /0]
           │    │    │    ├── key: (12,13)
           │    │    │    └── fd: (12,13)-->(14)
           │    │    ├── scan xyz@yz
           │    │    │    ├── columns: x:17 y:18!null rowid:20!null
           │    │    │    ├── constraint: /18/19/20: [/2 - ]
           │    │    │    ├── key: (20)
           │    │    │    └── fd: (20)-->(17,18)
           │    │    └── filters
           │    │         └── a:12 = x:17 [outer=(12,17), constraints=(/12: (/NULL - ]; /17: (/NULL - ]), fd=(12)==(17), (17)==(12)]
           │    └── inner-join (merge)
           │         ├── columns: a:23!null b:24!null c:25 x:28 y:29!null rowid:31!null
           │         ├── left ordering: +24
           │         ├── right ordering: +29
           │         ├── key: (23,31)
           │         ├── fd: (23,24)-->(25), (31)-->(28,29), (24)==(29), (29)==(24)
           │         ├── scan abc@bc
           │         │    ├── columns: a:23!null b:24!null c:25
           │         │    ├── constraint: /24/25/23: [ - /0]
           │         │    ├── key: (23,24)
           │         │    ├── fd: (23,24)-->(25)
           │         │    └── ordering: +24
           │         ├── scan xyz@yz
           │         │    ├── columns: x:28 y:29!null rowid:31!null
           │         │    ├── constraint: /29/30/31: [/2 - ]
           │         │    ├── key: (31)
           │         │    ├── fd: (31)-->(28,29)
           │         │    └── ordering: +29
           │         └── filters (true)
           └── aggregations
                ├── const-agg [as=c:3, outer=(3)]
                │    └── c:3
                ├── const-agg [as=x:6, outer=(6)]
                │    └── x:6
                └── const-agg [as=y:7, outer=(7)]
                     └── y:7

# Inner join on compound PK columns
opt expect=SplitDisjunctionOfJoinTerms
SELECT c, a FROM abc INNER JOIN xyz on (abc.a = xyz.x and abc.b = xyz.y) or (abc.b = xyz.x and abc.a = xyz.y)
where abc.b < 1 and xyz.y > 1;
----
project
 ├── columns: c:3 a:1!null
 └── project
      ├── columns: a:1!null b:2!null c:3 x:6!null y:7!null
      ├── fd: (1,2)-->(3)
      └── distinct-on
           ├── columns: a:1!null b:2!null c:3 x:6!null y:7!null rowid:9!null
           ├── grouping columns: a:1!null b:2!null rowid:9!null
           ├── key: (1,2,9)
           ├── fd: (1,2,9)-->(3,6,7)
           ├── union-all
           │    ├── columns: a:1!null b:2!null c:3 x:6!null y:7!null rowid:9!null
           │    ├── left columns: a:12 b:13 c:14 x:17 y:18 rowid:20
           │    ├── right columns: a:23 b:24 c:25 x:28 y:29 rowid:31
           │    ├── inner-join (hash)
           │    │    ├── columns: a:12!null b:13!null c:14 x:17!null y:18!null rowid:20!null
           │    │    ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
           │    │    ├── key: (20)
           │    │    ├── fd: (12,13)-->(14), (20)-->(17,18), (12)==(17), (17)==(12), (13)==(18), (18)==(13)
           │    │    ├── scan abc@bc
           │    │    │    ├── columns: a:12!null b:13!null c:14
           │    │    │    ├── constraint: /13/14/12: [ - /0]
           │    │    │    ├── key: (12,13)
           │    │    │    └── fd: (12,13)-->(14)
           │    │    ├── scan xyz@yz
           │    │    │    ├── columns: x:17 y:18!null rowid:20!null
           │    │    │    ├── constraint: /18/19/20: [/2 - ]
           │    │    │    ├── key: (20)
           │    │    │    └── fd: (20)-->(17,18)
           │    │    └── filters
           │    │         ├── a:12 = x:17 [outer=(12,17), constraints=(/12: (/NULL - ]; /17: (/NULL - ]), fd=(12)==(17), (17)==(12)]
           │    │         └── b:13 = y:18 [outer=(13,18), constraints=(/13: (/NULL - ]; /18: (/NULL - ]), fd=(13)==(18), (18)==(13)]
           │    └── inner-join (hash)
           │         ├── columns: a:23!null b:24!null c:25 x:28!null y:29!null rowid:31!null
           │         ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
           │         ├── key: (31)
           │         ├── fd: (23,24)-->(25), (31)-->(28,29), (24)==(28), (28)==(24), (23)==(29), (29)==(23)
           │         ├── scan abc@bc
           │         │    ├── columns: a:23!null b:24!null c:25
           │         │    ├── constraint: /24/25/23: [ - /0]
           │         │    ├── key: (23,24)
           │         │    └── fd: (23,24)-->(25)
           │         ├── scan xyz@yz
           │         │    ├── columns: x:28 y:29!null rowid:31!null
           │         │    ├── constraint: /29/30/31: [/2 - ]
           │         │    ├── key: (31)
           │         │    └── fd: (31)-->(28,29)
           │         └── filters
           │              ├── b:24 = x:28 [outer=(24,28), constraints=(/24: (/NULL - ]; /28: (/NULL - ]), fd=(24)==(28), (28)==(24)]
           │              └── a:23 = y:29 [outer=(23,29), constraints=(/23: (/NULL - ]; /29: (/NULL - ]), fd=(23)==(29), (29)==(23)]
           └── aggregations
                ├── const-agg [as=c:3, outer=(3)]
                │    └── c:3
                ├── const-agg [as=x:6, outer=(6)]
                │    └── x:6
                └── const-agg [as=y:7, outer=(7)]
                     └── y:7

# Semijoin without constraints
opt expect=SplitDisjunctionOfJoinTerms
SELECT * FROM abc WHERE EXISTS (SELECT * FROM xyz WHERE abc.a = xyz.x or abc.b = xyz.y)
----
project
 ├── columns: a:1!null b:2!null c:3
 ├── key: (1,2)
 ├── fd: (1,2)-->(3)
 └── distinct-on
      ├── columns: a:1!null b:2!null c:3
      ├── grouping columns: a:1!null b:2!null
      ├── key: (1,2)
      ├── fd: (1,2)-->(3)
      ├── union-all
      │    ├── columns: a:1!null b:2!null c:3
      │    ├── left columns: a:13 b:14 c:15
      │    ├── right columns: a:24 b:25 c:26
      │    ├── project
      │    │    ├── columns: a:13!null b:14!null c:15
      │    │    ├── key: (13,14)
      │    │    ├── fd: (13,14)-->(15)
      │    │    └── inner-join (merge)
      │    │         ├── columns: a:13!null b:14!null c:15 x:18!null
      │    │         ├── left ordering: +13
      │    │         ├── right ordering: +18
      │    │         ├── key: (14,18)
      │    │         ├── fd: (13,14)-->(15), (13)==(18), (18)==(13)
      │    │         ├── scan abc
      │    │         │    ├── columns: a:13!null b:14!null c:15
      │    │         │    ├── key: (13,14)
      │    │         │    ├── fd: (13,14)-->(15)
      │    │         │    └── ordering: +13
      │    │         ├── distinct-on
      │    │         │    ├── columns: x:18
      │    │         │    ├── grouping columns: x:18
      │    │         │    ├── key: (18)
      │    │         │    ├── ordering: +18
      │    │         │    └── scan xyz@xy
      │    │         │         ├── columns: x:18
      │    │         │         └── ordering: +18
      │    │         └── filters (true)
      │    └── semi-join (merge)
      │         ├── columns: a:24!null b:25!null c:26
      │         ├── left ordering: +25
      │         ├── right ordering: +30
      │         ├── key: (24,25)
      │         ├── fd: (24,25)-->(26)
      │         ├── scan abc@bc
      │         │    ├── columns: a:24!null b:25!null c:26
      │         │    ├── key: (24,25)
      │         │    ├── fd: (24,25)-->(26)
      │         │    └── ordering: +25
      │         ├── scan xyz@yz
      │         │    ├── columns: y:30
      │         │    └── ordering: +30
      │         └── filters (true)
      └── aggregations
           └── const-agg [as=c:3, outer=(3)]
                └── c:3

# Semijoin with constraints
opt expect=SplitDisjunctionOfJoinTerms
SELECT * FROM abc WHERE EXISTS (SELECT * FROM xyz WHERE (abc.a = xyz.x or abc.b = xyz.y) and xyz.y > 1) and abc.b < 1
----
project
 ├── columns: a:1!null b:2!null c:3
 ├── key: (1,2)
 ├── fd: (1,2)-->(3)
 └── distinct-on
      ├── columns: a:1!null b:2!null c:3
      ├── grouping columns: a:1!null b:2!null
      ├── key: (1,2)
      ├── fd: (1,2)-->(3)
      ├── union-all
      │    ├── columns: a:1!null b:2!null c:3
      │    ├── left columns: a:13 b:14 c:15
      │    ├── right columns: a:24 b:25 c:26
      │    ├── project
      │    │    ├── columns: a:13!null b:14!null c:15
      │    │    ├── key: (13,14)
      │    │    ├── fd: (13,14)-->(15)
      │    │    └── inner-join (hash)
      │    │         ├── columns: a:13!null b:14!null c:15 x:18!null
      │    │         ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
      │    │         ├── key: (14,18)
      │    │         ├── fd: (13,14)-->(15), (13)==(18), (18)==(13)
      │    │         ├── scan abc@bc
      │    │         │    ├── columns: a:13!null b:14!null c:15
      │    │         │    ├── constraint: /14/15/13: [ - /0]
      │    │         │    ├── key: (13,14)
      │    │         │    └── fd: (13,14)-->(15)
      │    │         ├── distinct-on
      │    │         │    ├── columns: x:18
      │    │         │    ├── grouping columns: x:18
      │    │         │    ├── key: (18)
      │    │         │    └── scan xyz@yz
      │    │         │         ├── columns: x:18 y:19!null
      │    │         │         └── constraint: /19/20/21: [/2 - ]
      │    │         └── filters
      │    │              └── a:13 = x:18 [outer=(13,18), constraints=(/13: (/NULL - ]; /18: (/NULL - ]), fd=(13)==(18), (18)==(13)]
      │    └── semi-join (merge)
      │         ├── columns: a:24!null b:25!null c:26
      │         ├── left ordering: +25
      │         ├── right ordering: +30
      │         ├── key: (24,25)
      │         ├── fd: (24,25)-->(26)
      │         ├── scan abc@bc
      │         │    ├── columns: a:24!null b:25!null c:26
      │         │    ├── constraint: /25/26/24: [ - /0]
      │         │    ├── key: (24,25)
      │         │    ├── fd: (24,25)-->(26)
      │         │    └── ordering: +25
      │         ├── scan xyz@yz
      │         │    ├── columns: y:30!null
      │         │    ├── constraint: /30/31/32: [/2 - ]
      │         │    └── ordering: +30
      │         └── filters (true)
      └── aggregations
           └── const-agg [as=c:3, outer=(3)]
                └── c:3

# Semijoin with not all PK columns selected and with constraints
opt expect=SplitDisjunctionOfJoinTerms
SELECT c FROM abc WHERE EXISTS (SELECT * FROM xyz WHERE (abc.a = xyz.x or abc.b = xyz.y) and xyz.y > 1) and abc.b < 1
----
project
 ├── columns: c:3
 └── project
      ├── columns: a:1!null b:2!null c:3
      ├── key: (1,2)
      ├── fd: (1,2)-->(3)
      └── distinct-on
           ├── columns: a:1!null b:2!null c:3
           ├── grouping columns: a:1!null b:2!null
           ├── key: (1,2)
           ├── fd: (1,2)-->(3)
           ├── union-all
           │    ├── columns: a:1!null b:2!null c:3
           │    ├── left columns: a:13 b:14 c:15
           │    ├── right columns: a:24 b:25 c:26
           │    ├── project
           │    │    ├── columns: a:13!null b:14!null c:15
           │    │    ├── key: (13,14)
           │    │    ├── fd: (13,14)-->(15)
           │    │    └── inner-join (hash)
           │    │         ├── columns: a:13!null b:14!null c:15 x:18!null
           │    │         ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
           │    │         ├── key: (14,18)
           │    │         ├── fd: (13,14)-->(15), (13)==(18), (18)==(13)
           │    │         ├── scan abc@bc
           │    │         │    ├── columns: a:13!null b:14!null c:15
           │    │         │    ├── constraint: /14/15/13: [ - /0]
           │    │         │    ├── key: (13,14)
           │    │         │    └── fd: (13,14)-->(15)
           │    │         ├── distinct-on
           │    │         │    ├── columns: x:18
           │    │         │    ├── grouping columns: x:18
           │    │         │    ├── key: (18)
           │    │         │    └── scan xyz@yz
           │    │         │         ├── columns: x:18 y:19!null
           │    │         │         └── constraint: /19/20/21: [/2 - ]
           │    │         └── filters
           │    │              └── a:13 = x:18 [outer=(13,18), constraints=(/13: (/NULL - ]; /18: (/NULL - ]), fd=(13)==(18), (18)==(13)]
           │    └── semi-join (merge)
           │         ├── columns: a:24!null b:25!null c:26
           │         ├── left ordering: +25
           │         ├── right ordering: +30
           │         ├── key: (24,25)
           │         ├── fd: (24,25)-->(26)
           │         ├── scan abc@bc
           │         │    ├── columns: a:24!null b:25!null c:26
           │         │    ├── constraint: /25/26/24: [ - /0]
           │         │    ├── key: (24,25)
           │         │    ├── fd: (24,25)-->(26)
           │         │    └── ordering: +25
           │         ├── scan xyz@yz
           │         │    ├── columns: y:30!null
           │         │    ├── constraint: /30/31/32: [/2 - ]
           │         │    └── ordering: +30
           │         └── filters (true)
           └── aggregations
                └── const-agg [as=c:3, outer=(3)]
                     └── c:3

# Outer join must use cross join
opt expect-not=SplitDisjunctionOfJoinTerms
SELECT * FROM abc LEFT JOIN xyz on abc.a = xyz.x or abc.b = xyz.y
----
left-join (cross)
 ├── columns: a:1!null b:2!null c:3 x:6 y:7 z:8
 ├── fd: (1,2)-->(3)
 ├── scan abc
 │    ├── columns: a:1!null b:2!null c:3
 │    ├── key: (1,2)
 │    └── fd: (1,2)-->(3)
 ├── scan xyz
 │    └── columns: x:6 y:7 z:8
 └── filters
      └── (a:1 = x:6) OR (b:2 = y:7) [outer=(1,2,6,7)]

# Antijoin without constraints
opt expect=SplitDisjunctionOfAntiJoinTerms
SELECT * FROM abc WHERE NOT EXISTS (SELECT * FROM xyz WHERE abc.a = xyz.x or abc.b = xyz.y);
----
project
 ├── columns: a:1!null b:2!null c:3
 ├── key: (1,2)
 ├── fd: (1,2)-->(3)
 └── intersect-all
      ├── columns: a:1!null b:2!null c:3
      ├── left columns: a:13 b:14 c:15
      ├── right columns: a:24 b:25 c:26
      ├── key: (1,2)
      ├── fd: (1,2)-->(3)
      ├── anti-join (merge)
      │    ├── columns: a:13!null b:14!null c:15
      │    ├── left ordering: +13
      │    ├── right ordering: +18
      │    ├── key: (13,14)
      │    ├── fd: (13,14)-->(15)
      │    ├── scan abc
      │    │    ├── columns: a:13!null b:14!null c:15
      │    │    ├── key: (13,14)
      │    │    ├── fd: (13,14)-->(15)
      │    │    └── ordering: +13
      │    ├── scan xyz@xy
      │    │    ├── columns: x:18
      │    │    └── ordering: +18
      │    └── filters (true)
      └── anti-join (merge)
           ├── columns: a:24!null b:25!null c:26
           ├── left ordering: +25
           ├── right ordering: +30
           ├── key: (24,25)
           ├── fd: (24,25)-->(26)
           ├── scan abc@bc
           │    ├── columns: a:24!null b:25!null c:26
           │    ├── key: (24,25)
           │    ├── fd: (24,25)-->(26)
           │    └── ordering: +25
           ├── scan xyz@yz
           │    ├── columns: y:30
           │    └── ordering: +30
           └── filters (true)

# Antijoin with constraints
opt expect=SplitDisjunctionOfAntiJoinTerms
SELECT * FROM abc WHERE NOT EXISTS (SELECT * FROM xyz WHERE (abc.a = xyz.x or abc.b = xyz.y) and xyz.y > 1) and abc.b < 1;
----
project
 ├── columns: a:1!null b:2!null c:3
 ├── key: (1,2)
 ├── fd: (1,2)-->(3)
 └── intersect-all
      ├── columns: a:1!null b:2!null c:3
      ├── left columns: a:13 b:14 c:15
      ├── right columns: a:24 b:25 c:26
      ├── key: (1,2)
      ├── fd: (1,2)-->(3)
      ├── anti-join (hash)
      │    ├── columns: a:13!null b:14!null c:15
      │    ├── key: (13,14)
      │    ├── fd: (13,14)-->(15)
      │    ├── scan abc@bc
      │    │    ├── columns: a:13!null b:14!null c:15
      │    │    ├── constraint: /14/15/13: [ - /0]
      │    │    ├── key: (13,14)
      │    │    └── fd: (13,14)-->(15)
      │    ├── scan xyz@yz
      │    │    ├── columns: x:18 y:19!null
      │    │    └── constraint: /19/20/21: [/2 - ]
      │    └── filters
      │         └── a:13 = x:18 [outer=(13,18), constraints=(/13: (/NULL - ]; /18: (/NULL - ]), fd=(13)==(18), (18)==(13)]
      └── anti-join (merge)
           ├── columns: a:24!null b:25!null c:26
           ├── left ordering: +25
           ├── right ordering: +30
           ├── key: (24,25)
           ├── fd: (24,25)-->(26)
           ├── scan abc@bc
           │    ├── columns: a:24!null b:25!null c:26
           │    ├── constraint: /25/26/24: [ - /0]
           │    ├── key: (24,25)
           │    ├── fd: (24,25)-->(26)
           │    └── ordering: +25
           ├── scan xyz@yz
           │    ├── columns: y:30!null
           │    ├── constraint: /30/31/32: [/2 - ]
           │    └── ordering: +30
           └── filters (true)

# Regression for #79886
exec-ddl
CREATE TABLE t79886 (pk INT8 PRIMARY KEY, col0 INT8, col1 FLOAT8, col3 INT8)
----

exec-ddl
CREATE INDEX idx_tab2_0 ON t79886 (col3,col1)
----

opt
SELECT pk
  FROM t79886
 WHERE (col3 IN (SELECT col0 FROM t79886)) AND col1 IS NULL
----
project
 ├── columns: pk:1!null
 ├── key: (1)
 └── semi-join (hash)
      ├── columns: pk:1!null col1:3 col3:4
      ├── key: (1)
      ├── fd: ()-->(3), (1)-->(4)
      ├── select
      │    ├── columns: pk:1!null col1:3 col3:4
      │    ├── key: (1)
      │    ├── fd: ()-->(3), (1)-->(4)
      │    ├── scan t79886@idx_tab2_0
      │    │    ├── columns: pk:1!null col1:3 col3:4
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(3,4)
      │    └── filters
      │         └── col1:3 IS NULL [outer=(3), constraints=(/3: [/NULL - /NULL]; tight), fd=()-->(3)]
      ├── scan t79886
      │    └── columns: col0:8
      └── filters
           └── col3:4 = col0:8 [outer=(4,8), constraints=(/4: (/NULL - ]; /8: (/NULL - ]), fd=(4)==(8), (8)==(4)]

# Regression for #81649
exec-ddl
CREATE TABLE t81649 (
  col0 STRING NULL,
  col1 DECIMAL NOT NULL,
  col2 STRING NULL AS (lower(col0)) STORED,
  col3 DECIMAL NOT NULL AS (col1 + 0:::DECIMAL) VIRTUAL,
  PRIMARY KEY (col3, col1),
  UNIQUE (col3 ASC, col1 DESC) STORING (col0));
----

opt
SELECT col2 FROM t81649@t81649_col3_col1_key ORDER BY col0 ASC, col3 ASC LIMIT 2;
----
index-join t81649
 ├── columns: col2:3  [hidden: col0:1 col3:4!null]
 ├── cardinality: [0 - 2]
 ├── key: (4)
 ├── fd: (1)-->(3), (4)-->(1,3)
 ├── ordering: +1,+4
 └── top-k
      ├── columns: col0:1 col1:2!null col3:4!null
      ├── internal-ordering: +1,+(2|4)
      ├── k: 2
      ├── cardinality: [0 - 2]
      ├── key: (4)
      ├── fd: (2,4)-->(1), (2)==(4), (4)==(2)
      ├── ordering: +1,+(2|4) [actual: +1,+2]
      └── scan t81649@t81649_col3_col1_key
           ├── columns: col0:1 col1:2!null col3:4!null
           ├── flags: force-index=t81649_col3_col1_key
           ├── key: (4)
           └── fd: (2,4)-->(1), (2)==(4), (4)==(2)

# Regression test for #85353
exec-ddl
CREATE TABLE t85353 (a INT, b INT)
----

exec-ddl
CREATE TABLE u85353 (
  a INT,
  b INT,
  a_b_hash INT NOT VISIBLE NOT NULL AS (mod(fnv32(crdb_internal.datums_to_bytes(a, b)), 8)) VIRTUAL,
  b_hash INT NOT VISIBLE NOT NULL AS (mod(fnv32(crdb_internal.datums_to_bytes(b)), 8)) VIRTUAL,
  INDEX a_b_idx (a_b_hash, a, b),
  INDEX b_idx (b_hash, b),
  CHECK (a_b_hash IN (0, 1, 2, 3, 4, 5, 6, 7)),
  CHECK (b_hash IN (0, 1, 2, 3, 4, 5, 6, 7))
)
----

exec-ddl
ALTER TABLE t85353 INJECT STATISTICS
'[
 {
   "columns": ["a"],
   "created_at": "2018-01-01 1:00:00.00000+00:00",
   "row_count": 100,
   "distinct_count": 10,
   "histo_buckets": [
     {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "0"},
     {"num_eq": 0, "num_range": 100, "distinct_range": 10, "upper_bound": "10"}
   ],
   "histo_col_type": "INT"
 },
 {
   "columns": ["b"],
   "created_at": "2018-01-01 1:00:00.00000+00:00",
   "row_count": 100,
   "distinct_count": 10,
   "histo_buckets": [
     {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "0"},
     {"num_eq": 0, "num_range": 100, "distinct_range": 10, "upper_bound": "10"}
   ],
   "histo_col_type": "INT"
 }
]'
----

exec-ddl
ALTER TABLE u85353 INJECT STATISTICS
'[
 {
   "columns": ["a"],
   "created_at": "2018-01-01 1:00:00.00000+00:00",
   "row_count": 100000,
   "distinct_count": 10,
   "histo_buckets": [
     {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "0"},
     {"num_eq": 0, "num_range": 100000, "distinct_range": 10, "upper_bound": "10"}
   ],
   "histo_col_type": "INT"
 },
 {
   "columns": ["b"],
   "created_at": "2018-01-01 1:30:00.00000+00:00",
   "row_count": 100000,
   "distinct_count": 10,
   "histo_buckets": [
     {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "0"},
     {"num_eq": 0, "num_range": 100000, "distinct_range": 10, "upper_bound": "10"}
   ],
   "histo_col_type": "INT"
 },
 {
   "columns": ["a","b"],
   "created_at": "2018-01-01 1:30:00.00000+00:00",
   "row_count": 100000,
   "distinct_count": 10
 }
]'
----

# The derived equijoin condition between the hash bucket column in
# u85353@u85353_b_idx and a similar hash bucket function expression on t85353.b
# should not reduce join selectivity and cause the following to
# choose lookup join.
opt
EXPLAIN (OPT) SELECT *, a_b_hash, b_hash FROM t85353 INNER JOIN u85353@b_idx USING (b) WHERE u85353.a < 10;
----
explain
 ├── columns: info:13
 ├── mode: opt
 ├── immutable
 └── project
      ├── columns: b:2!null a:1 a:6!null a_b_hash:8 b_hash:9
      ├── immutable
      ├── fd: (2,6)-->(8), (2)-->(9)
      └── inner-join (hash)
           ├── columns: t85353.a:1 t85353.b:2!null u85353.a:6!null u85353.b:7!null a_b_hash:8 b_hash:9
           ├── immutable
           ├── fd: (6,7)-->(8), (7)-->(9), (2)==(7), (7)==(2)
           ├── project
           │    ├── columns: a_b_hash:8 b_hash:9 u85353.a:6!null u85353.b:7
           │    ├── immutable
           │    ├── fd: (6,7)-->(8), (7)-->(9)
           │    ├── select
           │    │    ├── columns: u85353.a:6!null u85353.b:7
           │    │    ├── index-join u85353
           │    │    │    ├── columns: u85353.a:6 u85353.b:7
           │    │    │    └── scan u85353@b_idx
           │    │    │         ├── columns: u85353.b:7 u85353.rowid:10!null
           │    │    │         ├── flags: force-index=b_idx
           │    │    │         ├── key: (10)
           │    │    │         └── fd: (10)-->(7)
           │    │    └── filters
           │    │         └── u85353.a:6 < 10 [outer=(6), constraints=(/6: (/NULL - /9]; tight)]
           │    └── projections
           │         ├── mod(fnv32(crdb_internal.datums_to_bytes(u85353.a:6, u85353.b:7)), 8) [as=a_b_hash:8, outer=(6,7), immutable]
           │         └── mod(fnv32(crdb_internal.datums_to_bytes(u85353.b:7)), 8) [as=b_hash:9, outer=(7), immutable]
           ├── scan t85353
           │    └── columns: t85353.a:1 t85353.b:2
           └── filters
                └── t85353.b:2 = u85353.b:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]

# The derived equijoin condition between the hash bucket column in
# u85353@u85353_a_b_idx and a similar hash bucket function expression on t85353.b
# should not reduce join selectivity and cause the following to
# choose lookup join.
opt
EXPLAIN (OPT) SELECT *, a_b_hash, b_hash FROM t85353 INNER JOIN u85353@a_b_idx USING (a,b) WHERE u85353.a < 10;
----
explain
 ├── columns: info:13
 ├── mode: opt
 ├── immutable
 └── project
      ├── columns: a:1!null b:2!null a_b_hash:8 b_hash:9
      ├── immutable
      ├── fd: (1,2)-->(8), (2)-->(9)
      └── inner-join (hash)
           ├── columns: t85353.a:1!null t85353.b:2!null u85353.a:6!null u85353.b:7!null a_b_hash:8 b_hash:9
           ├── immutable
           ├── fd: (6,7)-->(8), (7)-->(9), (1)==(6), (6)==(1), (2)==(7), (7)==(2)
           ├── project
           │    ├── columns: a_b_hash:8 b_hash:9 u85353.a:6!null u85353.b:7
           │    ├── immutable
           │    ├── fd: (6,7)-->(8), (7)-->(9)
           │    ├── scan u85353@a_b_idx
           │    │    ├── columns: u85353.a:6!null u85353.b:7
           │    │    ├── constraint: /8/6/7/10
           │    │    │    ├── (/0/NULL - /0/9]
           │    │    │    ├── (/1/NULL - /1/9]
           │    │    │    ├── (/2/NULL - /2/9]
           │    │    │    ├── (/3/NULL - /3/9]
           │    │    │    ├── (/4/NULL - /4/9]
           │    │    │    ├── (/5/NULL - /5/9]
           │    │    │    ├── (/6/NULL - /6/9]
           │    │    │    └── (/7/NULL - /7/9]
           │    │    └── flags: force-index=a_b_idx
           │    └── projections
           │         ├── mod(fnv32(crdb_internal.datums_to_bytes(u85353.a:6, u85353.b:7)), 8) [as=a_b_hash:8, outer=(6,7), immutable]
           │         └── mod(fnv32(crdb_internal.datums_to_bytes(u85353.b:7)), 8) [as=b_hash:9, outer=(7), immutable]
           ├── select
           │    ├── columns: t85353.a:1!null t85353.b:2
           │    ├── scan t85353
           │    │    └── columns: t85353.a:1 t85353.b:2
           │    └── filters
           │         └── t85353.a:1 < 10 [outer=(1), constraints=(/1: (/NULL - /9]; tight)]
           └── filters
                ├── t85353.a:1 = u85353.a:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
                └── t85353.b:2 = u85353.b:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]

### BEGIN Regression tests for issue #69617

exec-ddl set=experimental_enable_unique_without_index_constraints=true
CREATE TABLE t69617_uniq_fk_parent (
  a INT NOT NULL,
  b INT NOT NULL,
  c INT NOT NULL,
  UNIQUE WITHOUT INDEX (b),
  UNIQUE WITHOUT INDEX (c),
  UNIQUE INDEX (a,b,c),
  UNIQUE INDEX (a,c)
)
----

exec-ddl
CREATE TABLE t69617_uniq_fk_child (
  a INT NOT NULL,
  b INT NOT NULL,
  c INT NOT NULL,
  FOREIGN KEY (a, b, c) REFERENCES t69617_uniq_fk_parent (a, b, c) ON UPDATE CASCADE
)
----

exec-ddl
CREATE TABLE t69617_uniq_fk_child2 (
  a INT,
  b INT,
  c INT,
  FOREIGN KEY (a, b, c) REFERENCES t69617_uniq_fk_parent (a, b, c) ON UPDATE CASCADE
)
----

exec-ddl
CREATE TABLE t69617_uniq_fk_child3 (
  a INT,
  b INT,
  c INT,
  FOREIGN KEY (a, b, c) REFERENCES t69617_uniq_fk_parent (a, b, c) MATCH FULL ON UPDATE CASCADE
)
----

exec-ddl
CREATE TABLE t69617_other_table (
  a INT,
  b INT,
  c INT
)
----

# Predicates on columns a and b should be derived and index
# uniq_fk_parent_a_b_c_key should be used for the lookup join.
opt expect=GenerateLookupJoins
SELECT *
FROM t69617_uniq_fk_child
INNER LOOKUP JOIN t69617_uniq_fk_parent USING (b)
----
project
 ├── columns: b:2!null a:1!null c:3!null a:7!null c:9!null
 ├── fd: (9)-->(2,7), (2)-->(7,9)
 └── inner-join (lookup t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key)
      ├── columns: t69617_uniq_fk_child.a:1!null t69617_uniq_fk_child.b:2!null t69617_uniq_fk_child.c:3!null t69617_uniq_fk_parent.a:7!null t69617_uniq_fk_parent.b:8!null t69617_uniq_fk_parent.c:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1 2 3] = [7 8 9]
      ├── lookup columns are key
      ├── fd: (9)-->(7,8), (8)-->(7,9), (2)==(8), (8)==(2)
      ├── scan t69617_uniq_fk_child
      │    └── columns: t69617_uniq_fk_child.a:1!null t69617_uniq_fk_child.b:2!null t69617_uniq_fk_child.c:3!null
      └── filters (true)

# Left outer join should derive predicates.
opt expect=GenerateLookupJoins
SELECT *
FROM t69617_uniq_fk_child
LEFT OUTER LOOKUP JOIN t69617_uniq_fk_parent USING (b)
----
project
 ├── columns: b:2!null a:1!null c:3!null a:7!null c:9!null
 ├── fd: (9)-->(2,7), (2)-->(7,9)
 └── inner-join (lookup t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key)
      ├── columns: t69617_uniq_fk_child.a:1!null t69617_uniq_fk_child.b:2!null t69617_uniq_fk_child.c:3!null t69617_uniq_fk_parent.a:7!null t69617_uniq_fk_parent.b:8!null t69617_uniq_fk_parent.c:9!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [1 2 3] = [7 8 9]
      ├── lookup columns are key
      ├── fd: (9)-->(7,8), (8)-->(7,9), (2)==(8), (8)==(2)
      ├── scan t69617_uniq_fk_child
      │    └── columns: t69617_uniq_fk_child.a:1!null t69617_uniq_fk_child.b:2!null t69617_uniq_fk_child.c:3!null
      └── filters (true)

# Lateral join which utilizes lookup join should derive predicates.
opt expect=GenerateLookupJoinsWithFilter
SELECT *
FROM t69617_uniq_fk_child
JOIN LATERAL (SELECT * FROM t69617_uniq_fk_parent WHERE t69617_uniq_fk_child.b = t69617_uniq_fk_parent.b)
ON true WHERE b = 1
----
inner-join (lookup t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key)
 ├── columns: a:1!null b:2!null c:3!null a:7!null b:8!null c:9!null
 ├── key columns: [1 2 3] = [7 8 9]
 ├── lookup columns are key
 ├── fd: ()-->(2,7-9), (2)==(8), (8)==(2)
 ├── select
 │    ├── columns: t69617_uniq_fk_child.a:1!null t69617_uniq_fk_child.b:2!null t69617_uniq_fk_child.c:3!null
 │    ├── fd: ()-->(2)
 │    ├── scan t69617_uniq_fk_child
 │    │    └── columns: t69617_uniq_fk_child.a:1!null t69617_uniq_fk_child.b:2!null t69617_uniq_fk_child.c:3!null
 │    └── filters
 │         └── t69617_uniq_fk_child.b:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]
 └── filters
      └── t69617_uniq_fk_parent.b:8 = 1 [outer=(8), constraints=(/8: [/1 - /1]; tight), fd=()-->(8)]

# Outer join as the input to lookup join may derive predicates from the FK constraint.
opt expect=GenerateLookupJoins
SELECT t69617_uniq_fk_parent.* FROM
    (t69617_other_table other RIGHT OUTER JOIN t69617_uniq_fk_child child ON other.a = child.a)
INNER LOOKUP JOIN
    t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key ON child.b = t69617_uniq_fk_parent.b;
----
project
 ├── columns: a:13!null b:14!null c:15!null
 ├── fd: (15)-->(13,14), (14)-->(13,15)
 └── inner-join (lookup t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key)
      ├── columns: other.a:1 child.a:7!null child.b:8!null child.c:9!null t69617_uniq_fk_parent.a:13!null t69617_uniq_fk_parent.b:14!null t69617_uniq_fk_parent.c:15!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [7 8 9] = [13 14 15]
      ├── lookup columns are key
      ├── fd: (15)-->(13,14), (14)-->(13,15), (8)==(14), (14)==(8)
      ├── left-join (hash)
      │    ├── columns: other.a:1 child.a:7!null child.b:8!null child.c:9!null
      │    ├── scan t69617_uniq_fk_child [as=child]
      │    │    └── columns: child.a:7!null child.b:8!null child.c:9!null
      │    ├── scan t69617_other_table [as=other]
      │    │    └── columns: other.a:1
      │    └── filters
      │         └── other.a:1 = child.a:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]
      └── filters (true)

# The optimizer may not derive predicates on nullable columns from an FK
# constraint which does not use the MATCH FULL option.
opt expect-not=(GenerateLookupJoins,GenerateLookupJoinsWithFilter)
SELECT t69617_uniq_fk_parent.* FROM
    (t69617_other_table other RIGHT OUTER JOIN t69617_uniq_fk_child2 child ON other.a = child.a)
INNER LOOKUP JOIN
    t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key ON child.b = t69617_uniq_fk_parent.b;
----
project
 ├── columns: a:13!null b:14!null c:15!null
 ├── fd: (15)-->(13,14), (14)-->(13,15)
 └── inner-join (hash)
      ├── columns: other.a:1 child.a:7 child.b:8!null t69617_uniq_fk_parent.a:13!null t69617_uniq_fk_parent.b:14!null t69617_uniq_fk_parent.c:15!null
      ├── flags: force lookup join (into right side)
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
      ├── fd: (15)-->(13,14), (14)-->(13,15), (8)==(14), (14)==(8)
      ├── left-join (hash)
      │    ├── columns: other.a:1 child.a:7 child.b:8
      │    ├── scan t69617_uniq_fk_child2 [as=child]
      │    │    └── columns: child.a:7 child.b:8
      │    ├── scan t69617_other_table [as=other]
      │    │    └── columns: other.a:1
      │    └── filters
      │         └── other.a:1 = child.a:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]
      ├── scan t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key
      │    ├── columns: t69617_uniq_fk_parent.a:13!null t69617_uniq_fk_parent.b:14!null t69617_uniq_fk_parent.c:15!null
      │    ├── flags: force-index=t69617_uniq_fk_parent_a_b_c_key
      │    ├── key: (15)
      │    └── fd: (15)-->(13,14), (14)-->(13,15)
      └── filters
           └── child.b:8 = t69617_uniq_fk_parent.b:14 [outer=(8,14), constraints=(/8: (/NULL - ]; /14: (/NULL - ]), fd=(8)==(14), (14)==(8)]

# It is OK to derive predicates on nullable columns from an FK
# constraint which uses the MATCH FULL option.
opt expect=GenerateLookupJoins
SELECT t69617_uniq_fk_parent.* FROM
    (t69617_other_table other RIGHT OUTER JOIN t69617_uniq_fk_child3 child ON other.a = child.a)
INNER LOOKUP JOIN
    t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key ON child.b = t69617_uniq_fk_parent.b;
----
project
 ├── columns: a:13!null b:14!null c:15!null
 ├── fd: (15)-->(13,14), (14)-->(13,15)
 └── inner-join (lookup t69617_uniq_fk_parent@t69617_uniq_fk_parent_a_b_c_key)
      ├── columns: other.a:1 child.a:7 child.b:8!null child.c:9 t69617_uniq_fk_parent.a:13!null t69617_uniq_fk_parent.b:14!null t69617_uniq_fk_parent.c:15!null
      ├── flags: force lookup join (into right side)
      ├── key columns: [7 8 9] = [13 14 15]
      ├── lookup columns are key
      ├── fd: (15)-->(13,14), (14)-->(13,15), (8)==(14), (14)==(8)
      ├── left-join (hash)
      │    ├── columns: other.a:1 child.a:7 child.b:8 child.c:9
      │    ├── scan t69617_uniq_fk_child3 [as=child]
      │    │    └── columns: child.a:7 child.b:8 child.c:9
      │    ├── scan t69617_other_table [as=other]
      │    │    └── columns: other.a:1
      │    └── filters
      │         └── other.a:1 = child.a:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]
      └── filters (true)

### END regression tests for issue #69617

# Regression test for #89603
exec-ddl
CREATE TABLE table11 (
      col1_1 FLOAT8 NOT NULL,
      col1_2 TIMESTAMP NOT NULL,
      col1_3 BYTES NOT NULL,
      col1_4 NAME NOT NULL,
      col1_5 OID NULL,
      col1_6 JSONB NULL,
      col1_7 REGPROCEDURE NULL,
      INDEX table1_col1_3_col1_1_expr_idx (col1_3 ASC, col1_1 DESC)
  )
----

# The sort must be done as the last step instead of in between the first and
# second joins of the paired join.
opt set=testing_optimizer_random_seed=4057832385546395198 set=testing_optimizer_cost_perturbation=1.0
SELECT
        tab_17534.col1_3 AS col_52140,
        0:::OID AS col_52141,
        tab_17533.col1_7 AS col_52142,
        '\x58':::BYTES AS col_52143,
        0:::OID AS col_52144
FROM
        table11@[0] AS tab_17533
        RIGHT JOIN table11@[0] AS tab_17534
                JOIN table11@[0] AS tab_17535 ON
                                (tab_17534.crdb_internal_mvcc_timestamp) = (tab_17535.crdb_internal_mvcc_timestamp)
                                AND (tab_17534.col1_4) = (tab_17535.col1_4)
                JOIN table11@[0] AS tab_17536 ON
                                (tab_17535.col1_1) = (tab_17536.col1_1)
                                AND (tab_17535.col1_3) = (tab_17536.col1_3)
                                AND (tab_17534.col1_6) = (tab_17536.col1_6)
                JOIN table11@[0] AS tab_17537
                        JOIN table11@[0] AS tab_17538 ON
                                        (tab_17537.crdb_internal_mvcc_timestamp) = (tab_17538.crdb_internal_mvcc_timestamp) ON
                                (tab_17536.col1_2) = (tab_17538.col1_2) AND (tab_17535.col1_4) = (tab_17537.col1_4) ON
                        (tab_17533.col1_3) = (tab_17537.col1_3) AND (tab_17533.col1_6) = (tab_17536.col1_6)
ORDER BY
        tab_17535.col1_4, tab_17535.col1_3 DESC
----
project
 ├── columns: col_52140:13!null col_52141:61!null col_52142:7 col_52143:62!null col_52144:61!null  [hidden: tab_17535.col1_3:23!null tab_17535.col1_4:24!null]
 ├── immutable
 ├── fd: ()-->(61,62)
 ├── ordering: +24,-23 opt(61,62) [actual: +24,-23]
 ├── sort
 │    ├── columns: tab_17533.col1_3:3 tab_17533.col1_6:6 tab_17533.col1_7:7 tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16!null tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_2:32!null tab_17536.col1_3:33!null tab_17536.col1_6:36!null tab_17537.col1_3:43!null tab_17537.col1_4:44!null tab_17537.crdb_internal_mvcc_timestamp:49!null tab_17538.col1_2:52!null tab_17538.crdb_internal_mvcc_timestamp:59!null
 │    ├── immutable
 │    ├── fd: (19)==(29), (29)==(19), (14)==(24,44), (24)==(14,44), (44)==(14,24), (21)==(31), (31)==(21), (23)==(33), (33)==(23), (16)==(36), (36)==(16), (49)==(59), (59)==(49), (32)==(52), (52)==(32)
 │    ├── ordering: +(14|24|44),-(23|33) [actual: +14,-23]
 │    └── left-join (lookup table11 [as=tab_17533])
 │         ├── columns: tab_17533.col1_3:3 tab_17533.col1_6:6 tab_17533.col1_7:7 tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16!null tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_2:32!null tab_17536.col1_3:33!null tab_17536.col1_6:36!null tab_17537.col1_3:43!null tab_17537.col1_4:44!null tab_17537.crdb_internal_mvcc_timestamp:49!null tab_17538.col1_2:52!null tab_17538.crdb_internal_mvcc_timestamp:59!null
 │         ├── key columns: [70] = [8]
 │         ├── lookup columns are key
 │         ├── second join in paired joiner
 │         ├── immutable
 │         ├── fd: (19)==(29), (29)==(19), (14)==(24,44), (24)==(14,44), (44)==(14,24), (21)==(31), (31)==(21), (23)==(33), (33)==(23), (16)==(36), (36)==(16), (49)==(59), (59)==(49), (32)==(52), (52)==(32)
 │         ├── left-join (lookup table11@table1_col1_3_col1_1_expr_idx [as=tab_17533])
 │         │    ├── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16!null tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_2:32!null tab_17536.col1_3:33!null tab_17536.col1_6:36!null tab_17537.col1_3:43!null tab_17537.col1_4:44!null tab_17537.crdb_internal_mvcc_timestamp:49!null tab_17538.col1_2:52!null tab_17538.crdb_internal_mvcc_timestamp:59!null tab_17533.col1_3:65 tab_17533.rowid:70 continuation:73
 │         │    ├── key columns: [43] = [65]
 │         │    ├── first join in paired joiner; continuation column: continuation:73
 │         │    ├── immutable
 │         │    ├── fd: (70)-->(65,73), (19)==(29), (29)==(19), (14)==(24,44), (24)==(14,44), (44)==(14,24), (21)==(31), (31)==(21), (23)==(33), (33)==(23), (16)==(36), (36)==(16), (49)==(59), (59)==(49), (32)==(52), (52)==(32)
 │         │    ├── inner-join (hash)
 │         │    │    ├── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16!null tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_2:32!null tab_17536.col1_3:33!null tab_17536.col1_6:36!null tab_17537.col1_3:43!null tab_17537.col1_4:44!null tab_17537.crdb_internal_mvcc_timestamp:49!null tab_17538.col1_2:52!null tab_17538.crdb_internal_mvcc_timestamp:59!null
 │         │    │    ├── immutable
 │         │    │    ├── fd: (19)==(29), (29)==(19), (14)==(24,44), (24)==(14,44), (44)==(14,24), (21)==(31), (31)==(21), (23)==(33), (33)==(23), (16)==(36), (36)==(16), (49)==(59), (59)==(49), (32)==(52), (52)==(32)
 │         │    │    ├── inner-join (hash)
 │         │    │    │    ├── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16!null tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_2:32!null tab_17536.col1_3:33!null tab_17536.col1_6:36!null tab_17537.col1_3:43!null tab_17537.col1_4:44!null tab_17537.crdb_internal_mvcc_timestamp:49
 │         │    │    │    ├── multiplicity: left-rows(zero-or-more), right-rows(one-or-more)
 │         │    │    │    ├── immutable
 │         │    │    │    ├── fd: (14)==(24,44), (24)==(14,44), (44)==(14,24), (21)==(31), (31)==(21), (23)==(33), (33)==(23), (19)==(29), (29)==(19), (16)==(36), (36)==(16)
 │         │    │    │    ├── scan table11 [as=tab_17537]
 │         │    │    │    │    └── columns: tab_17537.col1_3:43!null tab_17537.col1_4:44!null tab_17537.crdb_internal_mvcc_timestamp:49
 │         │    │    │    ├── inner-join (lookup table11 [as=tab_17536])
 │         │    │    │    │    ├── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16!null tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_2:32!null tab_17536.col1_3:33!null tab_17536.col1_6:36!null
 │         │    │    │    │    ├── key columns: [38] = [38]
 │         │    │    │    │    ├── lookup columns are key
 │         │    │    │    │    ├── immutable
 │         │    │    │    │    ├── fd: (19)==(29), (29)==(19), (14)==(24), (24)==(14), (21)==(31), (31)==(21), (23)==(33), (33)==(23), (16)==(36), (36)==(16)
 │         │    │    │    │    ├── inner-join (lookup table11@table1_col1_3_col1_1_expr_idx [as=tab_17536])
 │         │    │    │    │    │    ├── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16 tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null tab_17536.col1_1:31!null tab_17536.col1_3:33!null tab_17536.rowid:38!null
 │         │    │    │    │    │    ├── key columns: [23 21] = [33 31]
 │         │    │    │    │    │    ├── immutable
 │         │    │    │    │    │    ├── fd: (38)-->(31,33), (19)==(29), (29)==(19), (14)==(24), (24)==(14), (23)==(33), (33)==(23), (21)==(31), (31)==(21)
 │         │    │    │    │    │    ├── inner-join (hash)
 │         │    │    │    │    │    │    ├── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16 tab_17534.crdb_internal_mvcc_timestamp:19!null tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29!null
 │         │    │    │    │    │    │    ├── immutable
 │         │    │    │    │    │    │    ├── fd: (19)==(29), (29)==(19), (14)==(24), (24)==(14)
 │         │    │    │    │    │    │    ├── scan table11 [as=tab_17535]
 │         │    │    │    │    │    │    │    └── columns: tab_17535.col1_1:21!null tab_17535.col1_3:23!null tab_17535.col1_4:24!null tab_17535.crdb_internal_mvcc_timestamp:29
 │         │    │    │    │    │    │    ├── scan table11 [as=tab_17534]
 │         │    │    │    │    │    │    │    └── columns: tab_17534.col1_3:13!null tab_17534.col1_4:14!null tab_17534.col1_6:16 tab_17534.crdb_internal_mvcc_timestamp:19
 │         │    │    │    │    │    │    └── filters
 │         │    │    │    │    │    │         ├── tab_17534.crdb_internal_mvcc_timestamp:19 = tab_17535.crdb_internal_mvcc_timestamp:29 [outer=(19,29), immutable, constraints=(/19: (/NULL - ]; /29: (/NULL - ]), fd=(19)==(29), (29)==(19)]
 │         │    │    │    │    │    │         └── tab_17534.col1_4:14 = tab_17535.col1_4:24 [outer=(14,24), constraints=(/14: (/NULL - ]; /24: (/NULL - ]), fd=(14)==(24), (24)==(14)]
 │         │    │    │    │    │    └── filters (true)
 │         │    │    │    │    └── filters
 │         │    │    │    │         └── tab_17534.col1_6:16 = tab_17536.col1_6:36 [outer=(16,36), immutable, constraints=(/16: (/NULL - ]; /36: (/NULL - ]), fd=(16)==(36), (36)==(16)]
 │         │    │    │    └── filters
 │         │    │    │         └── tab_17535.col1_4:24 = tab_17537.col1_4:44 [outer=(24,44), constraints=(/24: (/NULL - ]; /44: (/NULL - ]), fd=(24)==(44), (44)==(24)]
 │         │    │    ├── scan table11 [as=tab_17538]
 │         │    │    │    └── columns: tab_17538.col1_2:52!null tab_17538.crdb_internal_mvcc_timestamp:59
 │         │    │    └── filters
 │         │    │         ├── tab_17537.crdb_internal_mvcc_timestamp:49 = tab_17538.crdb_internal_mvcc_timestamp:59 [outer=(49,59), immutable, constraints=(/49: (/NULL - ]; /59: (/NULL - ]), fd=(49)==(59), (59)==(49)]
 │         │    │         └── tab_17536.col1_2:32 = tab_17538.col1_2:52 [outer=(32,52), constraints=(/32: (/NULL - ]; /52: (/NULL - ]), fd=(32)==(52), (52)==(32)]
 │         │    └── filters (true)
 │         └── filters
 │              └── tab_17533.col1_6:6 = tab_17536.col1_6:36 [outer=(6,36), immutable, constraints=(/6: (/NULL - ]; /36: (/NULL - ]), fd=(6)==(36), (36)==(6)]
 └── projections
      ├── 0 [as=col_52141:61]
      └── '\x58' [as=col_52143:62]

# ------------------------------------------------------
# GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
# ------------------------------------------------------

exec-ddl
CREATE TABLE "parent" (
  p_id INT,
  id2 INT,
  id3 INT,
  PRIMARY KEY(p_id),
  UNIQUE INDEX id2_idx(id2),
  INDEX id3_idx(id3)
) LOCALITY REGIONAL BY ROW;
----

exec-ddl
CREATE TABLE "child" (
  c_id INT PRIMARY KEY,
  c_p_id INT REFERENCES parent (p_id),
  INDEX (c_p_id),
  FAMILY (c_id, c_p_id)
) LOCALITY REGIONAL BY ROW;
----

exec-ddl
CREATE TABLE "parent3" (
  p_id INT,
  id2 INT,
  id3 INT,
  PRIMARY KEY(p_id),
  UNIQUE INDEX id2_idx(id2) STORING (id3),
  INDEX id3_idx(id3) STORING (id2)
) LOCALITY REGIONAL BY ROW;
----

exec-ddl
CREATE TABLE "child3" (
  c_id INT PRIMARY KEY,
  c_p_id INT REFERENCES parent3 (id2),
  INDEX (c_p_id),
  FAMILY (c_id, c_p_id)
) LOCALITY REGIONAL BY ROW;
----

# Join on unique column `p_id` should produce a locality-optimized-search of
# lookup joins with the first branch being a locality-optimized join.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM parent p, child c WHERE p_id = c_p_id LIMIT 1
----
limit
 ├── columns: p_id:1!null id2:2 id3:3 c_id:7!null c_p_id:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,7,8), (1)==(8), (8)==(1)
 ├── distribution: east
 ├── project
 │    ├── columns: p_id:1!null id2:2 id3:3 c_id:7!null c_p_id:8!null
 │    ├── key: (7)
 │    ├── fd: (1)-->(2,3), (2)~~>(1,3), (7)-->(8), (1)==(8), (8)==(1)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    └── locality-optimized-search
 │         ├── columns: p_id:1!null id2:2 id3:3 p.crdb_region:4!null c_id:7!null c_p_id:8!null
 │         ├── left columns: p_id:12 id2:13 id3:14 p.crdb_region:15 c_id:24 c_p_id:25
 │         ├── right columns: p_id:18 id2:19 id3:20 p.crdb_region:21 c_id:29 c_p_id:30
 │         ├── key: (7)
 │         ├── fd: (7)-->(8), (1)-->(2-4), (2)~~>(1,3,4), (1)==(8), (8)==(1)
 │         ├── limit hint: 1.00
 │         ├── distribution: east
 │         ├── project
 │         │    ├── columns: p_id:12!null id2:13 id3:14 p.crdb_region:15!null c_id:24!null c_p_id:25!null
 │         │    ├── key: (24)
 │         │    ├── fd: (24)-->(25), (12)-->(13-15), (13)~~>(12,14,15), (12)==(25), (25)==(12)
 │         │    ├── limit hint: 1.00
 │         │    └── inner-join (lookup parent [as=p])
 │         │         ├── columns: p_id:12!null id2:13 id3:14 p.crdb_region:15!null c_id:24!null c_p_id:25!null c.crdb_region:26!null
 │         │         ├── lookup expression
 │         │         │    └── filters
 │         │         │         ├── p.crdb_region:15 = 'east' [outer=(15), constraints=(/15: [/'east' - /'east']; tight), fd=()-->(15)]
 │         │         │         └── c_p_id:25 = p_id:12 [outer=(12,25), constraints=(/12: (/NULL - ]; /25: (/NULL - ]), fd=(12)==(25), (25)==(12)]
 │         │         ├── remote lookup expression
 │         │         │    └── filters
 │         │         │         ├── p.crdb_region:15 IN ('central', 'west') [outer=(15), constraints=(/15: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         │         │         └── c_p_id:25 = p_id:12 [outer=(12,25), constraints=(/12: (/NULL - ]; /25: (/NULL - ]), fd=(12)==(25), (25)==(12)]
 │         │         ├── lookup columns are key
 │         │         ├── key: (24)
 │         │         ├── fd: ()-->(26), (24)-->(25), (12)-->(13-15), (13)~~>(12,14,15), (12)==(25), (25)==(12)
 │         │         ├── limit hint: 1.00
 │         │         ├── scan child [as=c]
 │         │         │    ├── columns: c_id:24!null c_p_id:25 c.crdb_region:26!null
 │         │         │    ├── constraint: /26/24: [/'east' - /'east']
 │         │         │    ├── key: (24)
 │         │         │    └── fd: ()-->(26), (24)-->(25)
 │         │         └── filters (true)
 │         └── project
 │              ├── columns: p_id:18!null id2:19 id3:20 p.crdb_region:21!null c_id:29!null c_p_id:30!null
 │              ├── key: (29)
 │              ├── fd: (29)-->(18-21,30), (18)-->(19-21), (19)~~>(18,20,21), (18)==(30), (30)==(18)
 │              ├── limit hint: 1.00
 │              └── inner-join (lookup parent [as=p])
 │                   ├── columns: p_id:18!null id2:19 id3:20 p.crdb_region:21!null c_id:29!null c_p_id:30!null c.crdb_region:31!null
 │                   ├── lookup expression
 │                   │    └── filters
 │                   │         ├── p.crdb_region:21 IN ('central', 'east', 'west') [outer=(21), constraints=(/21: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │                   │         └── c_p_id:30 = p_id:18 [outer=(18,30), constraints=(/18: (/NULL - ]; /30: (/NULL - ]), fd=(18)==(30), (30)==(18)]
 │                   ├── lookup columns are key
 │                   ├── key: (29)
 │                   ├── fd: (29)-->(30,31), (18)-->(19-21), (19)~~>(18,20,21), (18)==(30), (30)==(18)
 │                   ├── limit hint: 1.00
 │                   ├── scan child [as=c]
 │                   │    ├── columns: c_id:29!null c_p_id:30 c.crdb_region:31!null
 │                   │    ├── constraint: /31/29
 │                   │    │    ├── [/'central' - /'central']
 │                   │    │    └── [/'west' - /'west']
 │                   │    ├── key: (29)
 │                   │    └── fd: (29)-->(30,31)
 │                   └── filters (true)
 └── 1

# Join on unique covering index column `id2` should produce a
# locality-optimized-search with a locality-optimized join.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM parent3 p, child3 c WHERE id2 = c_p_id LIMIT 1
----
limit
 ├── columns: p_id:1!null id2:2!null id3:3 c_id:7!null c_p_id:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,7,8), (2)==(8), (8)==(2)
 ├── distribution: east
 ├── project
 │    ├── columns: p_id:1!null id2:2!null id3:3 c_id:7!null c_p_id:8!null
 │    ├── key: (7)
 │    ├── fd: (1)-->(2,3), (2)-->(1,3), (7)-->(8), (2)==(8), (8)==(2)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    └── locality-optimized-search
 │         ├── columns: p_id:1!null id2:2!null id3:3 p.crdb_region:4!null c_id:7!null c_p_id:8!null
 │         ├── left columns: p_id:12 id2:13 id3:14 p.crdb_region:15 c_id:24 c_p_id:25
 │         ├── right columns: p_id:18 id2:19 id3:20 p.crdb_region:21 c_id:29 c_p_id:30
 │         ├── key: (7)
 │         ├── fd: (7)-->(8), (1)-->(2-4), (2)-->(1,3,4), (2)==(8), (8)==(2)
 │         ├── limit hint: 1.00
 │         ├── distribution: east
 │         ├── project
 │         │    ├── columns: p_id:12!null id2:13!null id3:14 p.crdb_region:15!null c_id:24!null c_p_id:25!null
 │         │    ├── key: (24)
 │         │    ├── fd: (24)-->(25), (12)-->(13-15), (13)-->(12,14,15), (13)==(25), (25)==(13)
 │         │    ├── limit hint: 1.00
 │         │    └── inner-join (lookup parent3@id2_idx [as=p])
 │         │         ├── columns: p_id:12!null id2:13!null id3:14 p.crdb_region:15!null c_id:24!null c_p_id:25!null c.crdb_region:26!null
 │         │         ├── lookup expression
 │         │         │    └── filters
 │         │         │         ├── p.crdb_region:15 = 'east' [outer=(15), constraints=(/15: [/'east' - /'east']; tight), fd=()-->(15)]
 │         │         │         └── c_p_id:25 = id2:13 [outer=(13,25), constraints=(/13: (/NULL - ]; /25: (/NULL - ]), fd=(13)==(25), (25)==(13)]
 │         │         ├── remote lookup expression
 │         │         │    └── filters
 │         │         │         ├── p.crdb_region:15 IN ('central', 'west') [outer=(15), constraints=(/15: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │         │         │         └── c_p_id:25 = id2:13 [outer=(13,25), constraints=(/13: (/NULL - ]; /25: (/NULL - ]), fd=(13)==(25), (25)==(13)]
 │         │         ├── lookup columns are key
 │         │         ├── key: (24)
 │         │         ├── fd: ()-->(26), (24)-->(25), (12)-->(13-15), (13)-->(12,14,15), (13)==(25), (25)==(13)
 │         │         ├── limit hint: 1.00
 │         │         ├── scan child3 [as=c]
 │         │         │    ├── columns: c_id:24!null c_p_id:25 c.crdb_region:26!null
 │         │         │    ├── constraint: /26/24: [/'east' - /'east']
 │         │         │    ├── key: (24)
 │         │         │    └── fd: ()-->(26), (24)-->(25)
 │         │         └── filters (true)
 │         └── project
 │              ├── columns: p_id:18!null id2:19!null id3:20 p.crdb_region:21!null c_id:29!null c_p_id:30!null
 │              ├── key: (29)
 │              ├── fd: (29)-->(18-21,30), (18)-->(19-21), (19)-->(18,20,21), (19)==(30), (30)==(19)
 │              ├── limit hint: 1.00
 │              └── inner-join (lookup parent3@id2_idx [as=p])
 │                   ├── columns: p_id:18!null id2:19!null id3:20 p.crdb_region:21!null c_id:29!null c_p_id:30!null c.crdb_region:31!null
 │                   ├── lookup expression
 │                   │    └── filters
 │                   │         ├── p.crdb_region:21 IN ('central', 'east', 'west') [outer=(21), constraints=(/21: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │                   │         └── c_p_id:30 = id2:19 [outer=(19,30), constraints=(/19: (/NULL - ]; /30: (/NULL - ]), fd=(19)==(30), (30)==(19)]
 │                   ├── lookup columns are key
 │                   ├── key: (29)
 │                   ├── fd: (29)-->(30,31), (18)-->(19-21), (19)-->(18,20,21), (19)==(30), (30)==(19)
 │                   ├── limit hint: 1.00
 │                   ├── scan child3 [as=c]
 │                   │    ├── columns: c_id:29!null c_p_id:30 c.crdb_region:31!null
 │                   │    ├── constraint: /31/29
 │                   │    │    ├── [/'central' - /'central']
 │                   │    │    └── [/'west' - /'west']
 │                   │    ├── key: (29)
 │                   │    └── fd: (29)-->(30,31)
 │                   └── filters (true)
 └── 1

# Join on unique non-covering index column `id2` should produce a
# locality-optimized-search with a locality-optimized join plus a lookup join
# to retrieve missing base table columns.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM parent p, child c WHERE id2 = c_p_id LIMIT 1
----
limit
 ├── columns: p_id:1!null id2:2!null id3:3 c_id:7!null c_p_id:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,7,8), (2)==(8), (8)==(2)
 ├── distribution: east
 ├── inner-join (lookup parent [as=p])
 │    ├── columns: p_id:1!null id2:2!null id3:3 c_id:7!null c_p_id:8!null
 │    ├── key columns: [4 1] = [4 1]
 │    ├── lookup columns are key
 │    ├── key: (7)
 │    ├── fd: (1)-->(2,3), (2)-->(1,3), (7)-->(8), (2)==(8), (8)==(2)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: p_id:1!null id2:2!null p.crdb_region:4!null c_id:7!null c_p_id:8!null
 │    │    ├── left columns: p_id:12 id2:13 p.crdb_region:15 c_id:24 c_p_id:25
 │    │    ├── right columns: p_id:18 id2:19 p.crdb_region:21 c_id:29 c_p_id:30
 │    │    ├── key: (7)
 │    │    ├── fd: (7)-->(8), (1)-->(2,4), (2)-->(1,4), (2)==(8), (8)==(2)
 │    │    ├── limit hint: 29.67
 │    │    ├── distribution: east
 │    │    ├── project
 │    │    │    ├── columns: p_id:12!null id2:13!null p.crdb_region:15!null c_id:24!null c_p_id:25!null
 │    │    │    ├── key: (24)
 │    │    │    ├── fd: (24)-->(25), (12)-->(13,15), (13)-->(12,15), (13)==(25), (25)==(13)
 │    │    │    ├── limit hint: 29.67
 │    │    │    └── inner-join (lookup parent@id2_idx [as=p])
 │    │    │         ├── columns: p_id:12!null id2:13!null p.crdb_region:15!null c_id:24!null c_p_id:25!null c.crdb_region:26!null
 │    │    │         ├── lookup expression
 │    │    │         │    └── filters
 │    │    │         │         ├── p.crdb_region:15 = 'east' [outer=(15), constraints=(/15: [/'east' - /'east']; tight), fd=()-->(15)]
 │    │    │         │         └── c_p_id:25 = id2:13 [outer=(13,25), constraints=(/13: (/NULL - ]; /25: (/NULL - ]), fd=(13)==(25), (25)==(13)]
 │    │    │         ├── remote lookup expression
 │    │    │         │    └── filters
 │    │    │         │         ├── p.crdb_region:15 IN ('central', 'west') [outer=(15), constraints=(/15: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │    │    │         │         └── c_p_id:25 = id2:13 [outer=(13,25), constraints=(/13: (/NULL - ]; /25: (/NULL - ]), fd=(13)==(25), (25)==(13)]
 │    │    │         ├── lookup columns are key
 │    │    │         ├── key: (24)
 │    │    │         ├── fd: ()-->(26), (24)-->(25), (12)-->(13,15), (13)-->(12,15), (13)==(25), (25)==(13)
 │    │    │         ├── limit hint: 29.67
 │    │    │         ├── scan child [as=c]
 │    │    │         │    ├── columns: c_id:24!null c_p_id:25 c.crdb_region:26!null
 │    │    │         │    ├── constraint: /26/24: [/'east' - /'east']
 │    │    │         │    ├── key: (24)
 │    │    │         │    └── fd: ()-->(26), (24)-->(25)
 │    │    │         └── filters (true)
 │    │    └── project
 │    │         ├── columns: p_id:18!null id2:19!null p.crdb_region:21!null c_id:29!null c_p_id:30!null
 │    │         ├── key: (29)
 │    │         ├── fd: (29)-->(18,19,21,30), (18)-->(19,21), (19)-->(18,21), (19)==(30), (30)==(19)
 │    │         ├── limit hint: 29.67
 │    │         └── inner-join (lookup parent@id2_idx [as=p])
 │    │              ├── columns: p_id:18!null id2:19!null p.crdb_region:21!null c_id:29!null c_p_id:30!null c.crdb_region:31!null
 │    │              ├── lookup expression
 │    │              │    └── filters
 │    │              │         ├── p.crdb_region:21 IN ('central', 'east', 'west') [outer=(21), constraints=(/21: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │    │              │         └── c_p_id:30 = id2:19 [outer=(19,30), constraints=(/19: (/NULL - ]; /30: (/NULL - ]), fd=(19)==(30), (30)==(19)]
 │    │              ├── lookup columns are key
 │    │              ├── key: (29)
 │    │              ├── fd: (29)-->(30,31), (18)-->(19,21), (19)-->(18,21), (19)==(30), (30)==(19)
 │    │              ├── limit hint: 29.67
 │    │              ├── scan child [as=c]
 │    │              │    ├── columns: c_id:29!null c_p_id:30 c.crdb_region:31!null
 │    │              │    ├── constraint: /31/29
 │    │              │    │    ├── [/'central' - /'central']
 │    │              │    │    └── [/'west' - /'west']
 │    │              │    ├── key: (29)
 │    │              │    └── fd: (29)-->(30,31)
 │    │              └── filters (true)
 │    └── filters (true)
 └── 1

# Join on non-unique covering index column `id2` does not currently produce
# a locality-optimized-search with a locality-optimized join plus a lookup join.
# TODO(msirek): Implement 3-branch locality-optimized search to handle this.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM parent3 p, child3 c WHERE id3 = c_p_id LIMIT 1
----
distribute
 ├── columns: p_id:1!null id2:2 id3:3!null c_id:7!null c_p_id:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,7,8), (3)==(8), (8)==(3)
 ├── distribution: east
 ├── input distribution: central,east,west
 └── limit
      ├── columns: p_id:1!null id2:2 id3:3!null c_id:7!null c_p_id:8!null
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1-3,7,8), (3)==(8), (8)==(3)
      ├── inner-join (hash)
      │    ├── columns: p_id:1!null id2:2 id3:3!null c_id:7!null c_p_id:8!null
      │    ├── key: (1,7)
      │    ├── fd: (1)-->(2,3), (2)~~>(1,3), (7)-->(8), (3)==(8), (8)==(3)
      │    ├── limit hint: 1.00
      │    ├── scan parent3 [as=p]
      │    │    ├── columns: p_id:1!null id2:2 id3:3
      │    │    ├── check constraint expressions
      │    │    │    └── p.crdb_region:4 IN ('central', 'east', 'west') [outer=(4), constraints=(/4: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2,3), (2)~~>(1,3)
      │    ├── scan child3 [as=c]
      │    │    ├── columns: c_id:7!null c_p_id:8
      │    │    ├── check constraint expressions
      │    │    │    └── c.crdb_region:9 IN ('central', 'east', 'west') [outer=(9), constraints=(/9: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │    │    ├── key: (7)
      │    │    └── fd: (7)-->(8)
      │    └── filters
      │         └── id3:3 = c_p_id:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
      └── 1

# Left outer join returns all left rows, so the hard limit can be pushed to
# the scan on `child` and locality-optimized scan is used.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM child c LEFT OUTER JOIN parent p ON p_id = c_p_id LIMIT 1
----
project
 ├── columns: c_id:1!null c_p_id:2 p_id:6 id2:7 id3:8
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1,2,6-8)
 ├── distribution: east
 └── left-join (lookup parent [as=p])
      ├── columns: c_id:1!null c_p_id:2 p_id:6 id2:7 id3:8 p.crdb_region:9
      ├── lookup expression
      │    └── filters
      │         ├── p.crdb_region:9 = 'east' [outer=(9), constraints=(/9: [/'east' - /'east']; tight), fd=()-->(9)]
      │         └── c_p_id:2 = p_id:6 [outer=(2,6), constraints=(/2: (/NULL - ]; /6: (/NULL - ]), fd=(2)==(6), (6)==(2)]
      ├── remote lookup expression
      │    └── filters
      │         ├── p.crdb_region:9 IN ('central', 'west') [outer=(9), constraints=(/9: [/'central' - /'central'] [/'west' - /'west']; tight)]
      │         └── c_p_id:2 = p_id:6 [outer=(2,6), constraints=(/2: (/NULL - ]; /6: (/NULL - ]), fd=(2)==(6), (6)==(2)]
      ├── lookup columns are key
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1,2,6-9)
      ├── distribution: east
      ├── locality-optimized-search
      │    ├── columns: c_id:1!null c_p_id:2
      │    ├── left columns: c_id:12 c_p_id:13
      │    ├── right columns: c_id:17 c_p_id:18
      │    ├── cardinality: [0 - 1]
      │    ├── key: ()
      │    ├── fd: ()-->(1,2)
      │    ├── distribution: east
      │    ├── scan child [as=c]
      │    │    ├── columns: c_id:12!null c_p_id:13
      │    │    ├── constraint: /14/12: [/'east' - /'east']
      │    │    ├── limit: 1
      │    │    ├── key: ()
      │    │    └── fd: ()-->(12,13)
      │    └── scan child [as=c]
      │         ├── columns: c_id:17!null c_p_id:18
      │         ├── constraint: /19/17
      │         │    ├── [/'central' - /'central']
      │         │    └── [/'west' - /'west']
      │         ├── limit: 1
      │         ├── key: ()
      │         └── fd: ()-->(17,18)
      └── filters (true)

# Join on unique non-covering index column `id2` should produce a
# locality-optimized-search with a locality-optimized semijoin.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM child c WHERE c_p_id IN (SELECT id2 FROM parent p) LIMIT 1
----
limit
 ├── columns: c_id:1!null c_p_id:2
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1,2)
 ├── distribution: east
 ├── locality-optimized-search
 │    ├── columns: c_id:1!null c_p_id:2
 │    ├── left columns: c_id:37 c_p_id:38
 │    ├── right columns: c_id:42 c_p_id:43
 │    ├── key: (1)
 │    ├── fd: (1)-->(2)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    ├── project
 │    │    ├── columns: c_id:37!null c_p_id:38
 │    │    ├── key: (37)
 │    │    ├── fd: (37)-->(38)
 │    │    ├── limit hint: 1.00
 │    │    └── semi-join (lookup parent@id2_idx [as=p])
 │    │         ├── columns: c_id:37!null c_p_id:38 c.crdb_region:39!null
 │    │         ├── lookup expression
 │    │         │    └── filters
 │    │         │         ├── p.crdb_region:50 = 'east' [outer=(50), constraints=(/50: [/'east' - /'east']; tight), fd=()-->(50)]
 │    │         │         └── c_p_id:38 = id2:48 [outer=(38,48), constraints=(/38: (/NULL - ]; /48: (/NULL - ]), fd=(38)==(48), (48)==(38)]
 │    │         ├── remote lookup expression
 │    │         │    └── filters
 │    │         │         ├── p.crdb_region:50 IN ('central', 'west') [outer=(50), constraints=(/50: [/'central' - /'central'] [/'west' - /'west']; tight)]
 │    │         │         └── c_p_id:38 = id2:48 [outer=(38,48), constraints=(/38: (/NULL - ]; /48: (/NULL - ]), fd=(38)==(48), (48)==(38)]
 │    │         ├── lookup columns are key
 │    │         ├── key: (37)
 │    │         ├── fd: ()-->(39), (37)-->(38)
 │    │         ├── limit hint: 1.00
 │    │         ├── scan child [as=c]
 │    │         │    ├── columns: c_id:37!null c_p_id:38 c.crdb_region:39!null
 │    │         │    ├── constraint: /39/37: [/'east' - /'east']
 │    │         │    ├── key: (37)
 │    │         │    └── fd: ()-->(39), (37)-->(38)
 │    │         └── filters (true)
 │    └── project
 │         ├── columns: c_id:42!null c_p_id:43
 │         ├── key: (42)
 │         ├── fd: (42)-->(43)
 │         ├── limit hint: 1.00
 │         └── semi-join (lookup parent@id2_idx [as=p])
 │              ├── columns: c_id:42!null c_p_id:43 c.crdb_region:44!null
 │              ├── lookup expression
 │              │    └── filters
 │              │         ├── p.crdb_region:56 IN ('central', 'east', 'west') [outer=(56), constraints=(/56: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
 │              │         └── c_p_id:43 = id2:54 [outer=(43,54), constraints=(/43: (/NULL - ]; /54: (/NULL - ]), fd=(43)==(54), (54)==(43)]
 │              ├── lookup columns are key
 │              ├── key: (42)
 │              ├── fd: (42)-->(43,44)
 │              ├── limit hint: 1.00
 │              ├── scan child [as=c]
 │              │    ├── columns: c_id:42!null c_p_id:43 c.crdb_region:44!null
 │              │    ├── constraint: /44/42
 │              │    │    ├── [/'central' - /'central']
 │              │    │    └── [/'west' - /'west']
 │              │    ├── key: (42)
 │              │    └── fd: (42)-->(43,44)
 │              └── filters (true)
 └── 1

# Join on unique non-covering index column `id2` should not produce a
# locality-optimized-search with a locality-optimized antijoin as this rewrite
# does not support antijoin.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLocalityOptimizedJoin
SELECT * FROM child c WHERE c_p_id NOT IN (SELECT id2 FROM parent p) LIMIT 1
----
distribute
 ├── columns: c_id:1!null c_p_id:2
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1,2)
 ├── distribution: east
 ├── input distribution: central,east,west
 └── limit
      ├── columns: c_id:1!null c_p_id:2
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1,2)
      ├── anti-join (cross)
      │    ├── columns: c_id:1!null c_p_id:2
      │    ├── key: (1)
      │    ├── fd: (1)-->(2)
      │    ├── limit hint: 1.00
      │    ├── scan child [as=c]
      │    │    ├── columns: c_id:1!null c_p_id:2
      │    │    ├── check constraint expressions
      │    │    │    └── c.crdb_region:3 IN ('central', 'east', 'west') [outer=(3), constraints=(/3: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    ├── scan parent@id2_idx [as=p]
      │    │    ├── columns: id2:7
      │    │    └── lax-key: (7)
      │    └── filters
      │         └── (c_p_id:2 = id2:7) IS NOT false [outer=(2,7)]
      └── 1

# --------------------------------------------
# GenerateLocalityOptimizedSearchOfLookupJoins
# --------------------------------------------

exec-ddl
CREATE TABLE "parent2" (
  p_id INT,
  id2 INT,
  id3 INT,
  PRIMARY KEY(p_id),
  UNIQUE INDEX id2_idx(id2),
  INDEX id3_idx(id3)
) LOCALITY REGIONAL BY TABLE IN "east";
----

exec-ddl
CREATE TABLE "child2" (
  c_id INT PRIMARY KEY,
  c_p_id INT REFERENCES parent2 (p_id),
  v INT NOT NULL,
  INDEX (c_p_id),
  INDEX (v) STORING (c_id, c_p_id),
  FAMILY (c_id, c_p_id)
) LOCALITY REGIONAL BY ROW;
----

# Join on unique column `id2` doesn't produce a locality-optimized-search of
# lookup joins currently because the `p.crdb_region = 'east'` term causes a
# projection to be added on top of the scan of `child`, causing the
# `IsRegionalByRowTableScanOrSelect` check to fail.
# TODO(msirek): Enable unbounded locality-optimized scan with a limit hint
# or teach `GenerateLocalityOptimizedSearchOfLookupJoins` to handle
# projections in the input table.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM parent p, child c WHERE id2 = c_p_id AND p.crdb_region = 'east' LIMIT 1
----
project
 ├── columns: p_id:1!null id2:2!null id3:3 c_id:7!null c_p_id:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,7,8), (2)==(8), (8)==(2)
 ├── distribution: east
 └── limit
      ├── columns: p_id:1!null id2:2!null id3:3 p.crdb_region:4!null c_id:7!null c_p_id:8!null
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1-4,7,8), (2)==(8), (8)==(2)
      ├── distribution: east
      ├── project
      │    ├── columns: p_id:1!null id2:2!null id3:3 p.crdb_region:4!null c_id:7!null c_p_id:8!null
      │    ├── key: (7)
      │    ├── fd: ()-->(4), (1)-->(2,3), (2)-->(1,3), (7)-->(8), (2)==(8), (8)==(2)
      │    ├── limit hint: 1.00
      │    ├── distribution: east
      │    └── inner-join (lookup child@child_crdb_region_c_p_id_idx [as=c])
      │         ├── columns: p_id:1!null id2:2!null id3:3 p.crdb_region:4!null c_id:7!null c_p_id:8!null c.crdb_region:9!null
      │         ├── lookup expression
      │         │    └── filters
      │         │         ├── c.crdb_region:9 IN ('central', 'east', 'west') [outer=(9), constraints=(/9: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │         │         └── id2:2 = c_p_id:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)]
      │         ├── key: (7)
      │         ├── fd: ()-->(4), (1)-->(2,3), (2)-->(1,3), (7)-->(8,9), (2)==(8), (8)==(2)
      │         ├── limit hint: 1.00
      │         ├── distribution: east
      │         ├── scan parent [as=p]
      │         │    ├── columns: p_id:1!null id2:2 id3:3 p.crdb_region:4!null
      │         │    ├── constraint: /4/1: [/'east' - /'east']
      │         │    ├── key: (1)
      │         │    ├── fd: ()-->(4), (1)-->(2,3), (2)~~>(1,3)
      │         │    ├── limit hint: 10.00
      │         │    └── distribution: east
      │         └── filters (true)
      └── 1

# Join on unique column `id2` should produce a locality-optimized-search of
# lookup joins plus a lookup join to retrieve missing base table columns since
# RBT table parent2 is in the local region.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM parent2 p, child2 c WHERE id2 = c_p_id LIMIT 1
----
limit
 ├── columns: p_id:1!null id2:2!null id3:3 c_id:6!null c_p_id:7!null v:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,6-8), (2)==(7), (7)==(2)
 ├── distribution: east
 ├── inner-join (lookup parent2 [as=p])
 │    ├── columns: p_id:1!null id2:2!null id3:3 c_id:6!null c_p_id:7!null v:8!null
 │    ├── key columns: [1] = [1]
 │    ├── lookup columns are key
 │    ├── key: (6)
 │    ├── fd: (1)-->(2,3), (2)-->(1,3), (6)-->(7,8), (2)==(7), (7)==(2)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: p_id:1!null id2:2!null c_id:6!null c_p_id:7!null v:8!null
 │    │    ├── left columns: p_id:12 id2:13 c_id:22 c_p_id:23 v:24
 │    │    ├── right columns: p_id:17 id2:18 c_id:28 c_p_id:29 v:30
 │    │    ├── key: (6)
 │    │    ├── fd: (6)-->(7,8), (1)-->(2), (2)-->(1), (2)==(7), (7)==(2)
 │    │    ├── limit hint: 100.00
 │    │    ├── distribution: east
 │    │    ├── project
 │    │    │    ├── columns: p_id:12!null id2:13!null c_id:22!null c_p_id:23!null v:24!null
 │    │    │    ├── key: (22)
 │    │    │    ├── fd: (22)-->(23,24), (12)-->(13), (13)-->(12), (13)==(23), (23)==(13)
 │    │    │    ├── limit hint: 100.00
 │    │    │    └── inner-join (lookup parent2@id2_idx [as=p])
 │    │    │         ├── columns: p_id:12!null id2:13!null c_id:22!null c_p_id:23!null v:24!null crdb_region:25!null
 │    │    │         ├── key columns: [23] = [13]
 │    │    │         ├── lookup columns are key
 │    │    │         ├── key: (22)
 │    │    │         ├── fd: ()-->(25), (22)-->(23,24), (12)-->(13), (13)-->(12), (13)==(23), (23)==(13)
 │    │    │         ├── limit hint: 100.00
 │    │    │         ├── scan child2 [as=c]
 │    │    │         │    ├── columns: c_id:22!null c_p_id:23 v:24!null crdb_region:25!null
 │    │    │         │    ├── constraint: /25/22: [/'east' - /'east']
 │    │    │         │    ├── key: (22)
 │    │    │         │    └── fd: ()-->(25), (22)-->(23,24)
 │    │    │         └── filters (true)
 │    │    └── project
 │    │         ├── columns: p_id:17!null id2:18!null c_id:28!null c_p_id:29!null v:30!null
 │    │         ├── key: (28)
 │    │         ├── fd: (28)-->(17,18,29,30), (17)-->(18), (18)-->(17), (18)==(29), (29)==(18)
 │    │         ├── limit hint: 100.00
 │    │         └── inner-join (lookup parent2@id2_idx [as=p])
 │    │              ├── columns: p_id:17!null id2:18!null c_id:28!null c_p_id:29!null v:30!null crdb_region:31!null
 │    │              ├── key columns: [29] = [18]
 │    │              ├── lookup columns are key
 │    │              ├── key: (28)
 │    │              ├── fd: (28)-->(29-31), (17)-->(18), (18)-->(17), (18)==(29), (29)==(18)
 │    │              ├── limit hint: 100.00
 │    │              ├── scan child2 [as=c]
 │    │              │    ├── columns: c_id:28!null c_p_id:29 v:30!null crdb_region:31!null
 │    │              │    ├── constraint: /31/28
 │    │              │    │    ├── [/'central' - /'central']
 │    │              │    │    └── [/'west' - /'west']
 │    │              │    ├── key: (28)
 │    │              │    └── fd: (28)-->(29-31)
 │    │              └── filters (true)
 │    └── filters (true)
 └── 1

# Join on non-unique column `id3` should produce a locality-optimized-search of
# lookup joins, plus a lookup join to retrieve missing base table columns since
# RBT table parent2 is in the local region.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM parent2 p, child2 c WHERE id3 = c_p_id LIMIT 1
----
limit
 ├── columns: p_id:1!null id2:2 id3:3!null c_id:6!null c_p_id:7!null v:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,6-8), (3)==(7), (7)==(3)
 ├── distribution: east
 ├── inner-join (lookup parent2 [as=p])
 │    ├── columns: p_id:1!null id2:2 id3:3!null c_id:6!null c_p_id:7!null v:8!null
 │    ├── key columns: [1] = [1]
 │    ├── lookup columns are key
 │    ├── key: (1,6)
 │    ├── fd: (1)-->(2,3), (2)~~>(1,3), (6)-->(7,8), (3)==(7), (7)==(3)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: p_id:1!null id3:3!null c_id:6!null c_p_id:7!null v:8!null
 │    │    ├── left columns: p_id:12 id3:14 c_id:22 c_p_id:23 v:24
 │    │    ├── right columns: p_id:17 id3:19 c_id:28 c_p_id:29 v:30
 │    │    ├── key: (1,6)
 │    │    ├── fd: (6)-->(7,8), (1)-->(3), (3)==(7), (7)==(3)
 │    │    ├── limit hint: 100.00
 │    │    ├── distribution: east
 │    │    ├── project
 │    │    │    ├── columns: p_id:12!null id3:14!null c_id:22!null c_p_id:23!null v:24!null
 │    │    │    ├── key: (12,22)
 │    │    │    ├── fd: (22)-->(23,24), (12)-->(14), (14)==(23), (23)==(14)
 │    │    │    ├── limit hint: 100.00
 │    │    │    └── inner-join (lookup parent2@id3_idx [as=p])
 │    │    │         ├── columns: p_id:12!null id3:14!null c_id:22!null c_p_id:23!null v:24!null crdb_region:25!null
 │    │    │         ├── key columns: [23] = [14]
 │    │    │         ├── key: (12,22)
 │    │    │         ├── fd: ()-->(25), (22)-->(23,24), (12)-->(14), (14)==(23), (23)==(14)
 │    │    │         ├── limit hint: 100.00
 │    │    │         ├── scan child2 [as=c]
 │    │    │         │    ├── columns: c_id:22!null c_p_id:23 v:24!null crdb_region:25!null
 │    │    │         │    ├── constraint: /25/22: [/'east' - /'east']
 │    │    │         │    ├── key: (22)
 │    │    │         │    └── fd: ()-->(25), (22)-->(23,24)
 │    │    │         └── filters (true)
 │    │    └── project
 │    │         ├── columns: p_id:17!null id3:19!null c_id:28!null c_p_id:29!null v:30!null
 │    │         ├── key: (17,28)
 │    │         ├── fd: (28)-->(19,29,30), (17)-->(19), (19)==(29), (29)==(19)
 │    │         ├── limit hint: 100.00
 │    │         └── inner-join (lookup parent2@id3_idx [as=p])
 │    │              ├── columns: p_id:17!null id3:19!null c_id:28!null c_p_id:29!null v:30!null crdb_region:31!null
 │    │              ├── key columns: [29] = [19]
 │    │              ├── key: (17,28)
 │    │              ├── fd: (28)-->(29-31), (17)-->(19), (19)==(29), (29)==(19)
 │    │              ├── limit hint: 100.00
 │    │              ├── scan child2 [as=c]
 │    │              │    ├── columns: c_id:28!null c_p_id:29 v:30!null crdb_region:31!null
 │    │              │    ├── constraint: /31/28
 │    │              │    │    ├── [/'central' - /'central']
 │    │              │    │    └── [/'west' - /'west']
 │    │              │    ├── key: (28)
 │    │              │    ├── fd: (28)-->(29-31)
 │    │              │    └── limit hint: 20.00
 │    │              └── filters (true)
 │    └── filters (true)
 └── 1

# Join on unique non-covering index column `id2` should produce a
# locality-optimized-search of lookup semijoins.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM child2 c WHERE c_p_id IN (SELECT id2 FROM parent2 p) LIMIT 1
----
limit
 ├── columns: c_id:1!null c_p_id:2 v:3!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3)
 ├── distribution: east
 ├── locality-optimized-search
 │    ├── columns: c_id:1!null c_p_id:2 v:3!null
 │    ├── left columns: c_id:13 c_p_id:14 v:15
 │    ├── right columns: c_id:19 c_p_id:20 v:21
 │    ├── key: (1)
 │    ├── fd: (1)-->(2,3)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    ├── project
 │    │    ├── columns: c_id:13!null c_p_id:14 v:15!null
 │    │    ├── key: (13)
 │    │    ├── fd: (13)-->(14,15)
 │    │    ├── limit hint: 1.00
 │    │    └── semi-join (lookup parent2@id2_idx [as=p])
 │    │         ├── columns: c_id:13!null c_p_id:14 v:15!null crdb_region:16!null
 │    │         ├── key columns: [14] = [26]
 │    │         ├── lookup columns are key
 │    │         ├── key: (13)
 │    │         ├── fd: ()-->(16), (13)-->(14,15)
 │    │         ├── limit hint: 1.00
 │    │         ├── scan child2 [as=c]
 │    │         │    ├── columns: c_id:13!null c_p_id:14 v:15!null crdb_region:16!null
 │    │         │    ├── constraint: /16/13: [/'east' - /'east']
 │    │         │    ├── key: (13)
 │    │         │    ├── fd: ()-->(16), (13)-->(14,15)
 │    │         │    └── limit hint: 10.00
 │    │         └── filters (true)
 │    └── project
 │         ├── columns: c_id:19!null c_p_id:20 v:21!null
 │         ├── key: (19)
 │         ├── fd: (19)-->(20,21)
 │         ├── limit hint: 1.00
 │         └── semi-join (lookup parent2@id2_idx [as=p])
 │              ├── columns: c_id:19!null c_p_id:20 v:21!null crdb_region:22!null
 │              ├── key columns: [20] = [31]
 │              ├── lookup columns are key
 │              ├── key: (19)
 │              ├── fd: (19)-->(20-22)
 │              ├── limit hint: 1.00
 │              ├── scan child2 [as=c]
 │              │    ├── columns: c_id:19!null c_p_id:20 v:21!null crdb_region:22!null
 │              │    ├── constraint: /22/19
 │              │    │    ├── [/'central' - /'central']
 │              │    │    └── [/'west' - /'west']
 │              │    ├── key: (19)
 │              │    ├── fd: (19)-->(20-22)
 │              │    └── limit hint: 20.00
 │              └── filters (true)
 └── 1

# Join on unique non-covering index column `id2` should not produce a
# locality-optimized-search of lookup antijoins as this rewrite does not support
# antijoin.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM child2 c WHERE c_p_id NOT IN (SELECT id2 FROM parent2 p) LIMIT 1
----
distribute
 ├── columns: c_id:1!null c_p_id:2 v:3!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3)
 ├── distribution: east
 ├── input distribution: central,east,west
 └── limit
      ├── columns: c_id:1!null c_p_id:2 v:3!null
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1-3)
      ├── anti-join (cross)
      │    ├── columns: c_id:1!null c_p_id:2 v:3!null
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,3)
      │    ├── limit hint: 1.00
      │    ├── scan child2 [as=c]
      │    │    ├── columns: c_id:1!null c_p_id:2 v:3!null
      │    │    ├── check constraint expressions
      │    │    │    └── crdb_region:4 IN ('central', 'east', 'west') [outer=(4), constraints=(/4: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2,3)
      │    ├── scan parent2@id2_idx [as=p]
      │    │    ├── columns: id2:8
      │    │    └── lax-key: (8)
      │    └── filters
      │         └── (c_p_id:2 = id2:8) IS NOT false [outer=(2,8)]
      └── 1

# Paired lookup joins not supported for this optimization.
# TODO(msirek): Support unbounded locality-optimized scan with a limit hint or
# teach this optimization to handle paired joins.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM child2 c WHERE c_p_id IN (SELECT id2 FROM parent2 p WHERE c_id = id3) LIMIT 1
----
distribute
 ├── columns: c_id:1!null c_p_id:2 v:3!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3)
 ├── distribution: east
 ├── input distribution: central,east,west
 └── limit
      ├── columns: c_id:1!null c_p_id:2 v:3!null
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(1-3)
      ├── semi-join (lookup parent2 [as=p])
      │    ├── columns: c_id:1!null c_p_id:2 v:3!null
      │    ├── key columns: [13] = [7]
      │    ├── lookup columns are key
      │    ├── second join in paired joiner
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,3)
      │    ├── limit hint: 1.00
      │    ├── inner-join (lookup parent2@id2_idx [as=p])
      │    │    ├── columns: c_id:1!null c_p_id:2!null v:3!null p_id:13!null id2:14!null continuation:18
      │    │    ├── key columns: [2] = [14]
      │    │    ├── lookup columns are key
      │    │    ├── first join in paired joiner; continuation column: continuation:18
      │    │    ├── key: (1)
      │    │    ├── fd: (1)-->(2,3), (13)-->(14,18), (14)-->(13), (2)==(14), (14)==(2)
      │    │    ├── limit hint: 100.00
      │    │    ├── scan child2 [as=c]
      │    │    │    ├── columns: c_id:1!null c_p_id:2 v:3!null
      │    │    │    ├── check constraint expressions
      │    │    │    │    └── crdb_region:4 IN ('central', 'east', 'west') [outer=(4), constraints=(/4: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │    │    │    ├── key: (1)
      │    │    │    ├── fd: (1)-->(2,3)
      │    │    │    └── limit hint: 200.00
      │    │    └── filters (true)
      │    └── filters
      │         └── c_id:1 = id3:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]
      └── 1

# Locality-optimized-search lookup joins doesn't handle projections in the
# input.
# TODO(msirek): Support unbounded locality-optimized scan with a limit hint or
# teach this optimization to handle projections.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM parent2 p, child2 c WHERE id3 = c_p_id+1 LIMIT 1
----
distribute
 ├── columns: p_id:1!null id2:2 id3:3!null c_id:6!null c_p_id:7 v:8!null
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-3,6-8)
 ├── distribution: east
 ├── input distribution: central,east,west
 └── project
      ├── columns: p_id:1!null id2:2 id3:3!null c_id:6!null c_p_id:7 v:8!null
      ├── cardinality: [0 - 1]
      ├── immutable
      ├── key: ()
      ├── fd: ()-->(1-3,6-8)
      └── limit
           ├── columns: p_id:1!null id2:2 id3:3!null c_id:6!null c_p_id:7 v:8!null column12:12!null
           ├── cardinality: [0 - 1]
           ├── immutable
           ├── key: ()
           ├── fd: ()-->(1-3,6-8,12), (3)==(12), (12)==(3)
           ├── inner-join (hash)
           │    ├── columns: p_id:1!null id2:2 id3:3!null c_id:6!null c_p_id:7 v:8!null column12:12!null
           │    ├── immutable
           │    ├── key: (1,6)
           │    ├── fd: (1)-->(2,3), (2)~~>(1,3), (6)-->(7,8), (7)-->(12), (3)==(12), (12)==(3)
           │    ├── limit hint: 1.00
           │    ├── scan parent2 [as=p]
           │    │    ├── columns: p_id:1!null id2:2 id3:3
           │    │    ├── key: (1)
           │    │    └── fd: (1)-->(2,3), (2)~~>(1,3)
           │    ├── project
           │    │    ├── columns: column12:12 c_id:6!null c_p_id:7 v:8!null
           │    │    ├── immutable
           │    │    ├── key: (6)
           │    │    ├── fd: (6)-->(7,8), (7)-->(12)
           │    │    ├── scan child2 [as=c]
           │    │    │    ├── columns: c_id:6!null c_p_id:7 v:8!null
           │    │    │    ├── check constraint expressions
           │    │    │    │    └── crdb_region:9 IN ('central', 'east', 'west') [outer=(9), constraints=(/9: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
           │    │    │    ├── key: (6)
           │    │    │    └── fd: (6)-->(7,8)
           │    │    └── projections
           │    │         └── c_p_id:7 + 1 [as=column12:12, outer=(7), immutable]
           │    └── filters
           │         └── id3:3 = column12:12 [outer=(3,12), constraints=(/3: (/NULL - ]; /12: (/NULL - ]), fd=(3)==(12), (12)==(3)]
           └── 1

# Joins with a correlated filter support this optimization.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM child2 c WHERE c_p_id IN (SELECT id2 FROM parent2 p WHERE c_id > id3) LIMIT 1
----
limit
 ├── columns: c_id:1!null c_p_id:2 v:3!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3)
 ├── distribution: east
 ├── project
 │    ├── columns: c_id:1!null c_p_id:2 v:3!null
 │    ├── key: (1)
 │    ├── fd: (1)-->(2,3)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    └── project
 │         ├── columns: c_id:1!null c_p_id:2!null v:3!null
 │         ├── key: (1)
 │         ├── fd: (1)-->(2,3)
 │         ├── limit hint: 1.00
 │         ├── distribution: east
 │         └── inner-join (lookup parent2 [as=p])
 │              ├── columns: c_id:1!null c_p_id:2!null v:3!null id2:8!null id3:9!null
 │              ├── key columns: [7] = [7]
 │              ├── lookup columns are key
 │              ├── key: (1)
 │              ├── fd: (1)-->(2,3,8,9), (8)-->(9), (2)==(8), (8)==(2)
 │              ├── limit hint: 1.00
 │              ├── distribution: east
 │              ├── locality-optimized-search
 │              │    ├── columns: c_id:1!null c_p_id:2!null v:3!null p_id:7!null id2:8!null
 │              │    ├── left columns: c_id:19 c_p_id:20 v:21 p_id:31 id2:32
 │              │    ├── right columns: c_id:25 c_p_id:26 v:27 p_id:36 id2:37
 │              │    ├── key: (1)
 │              │    ├── fd: (1)-->(2,3), (7)-->(8), (8)-->(7), (2)==(8), (8)==(2)
 │              │    ├── limit hint: 100.00
 │              │    ├── distribution: east
 │              │    ├── project
 │              │    │    ├── columns: c_id:19!null c_p_id:20!null v:21!null p_id:31!null id2:32!null
 │              │    │    ├── key: (19)
 │              │    │    ├── fd: (19)-->(20,21), (31)-->(32), (32)-->(31), (20)==(32), (32)==(20)
 │              │    │    ├── limit hint: 100.00
 │              │    │    └── inner-join (lookup parent2@id2_idx [as=p])
 │              │    │         ├── columns: c_id:19!null c_p_id:20!null v:21!null crdb_region:22!null p_id:31!null id2:32!null
 │              │    │         ├── key columns: [20] = [32]
 │              │    │         ├── lookup columns are key
 │              │    │         ├── key: (19)
 │              │    │         ├── fd: ()-->(22), (19)-->(20,21), (31)-->(32), (32)-->(31), (20)==(32), (32)==(20)
 │              │    │         ├── limit hint: 100.00
 │              │    │         ├── scan child2 [as=c]
 │              │    │         │    ├── columns: c_id:19!null c_p_id:20 v:21!null crdb_region:22!null
 │              │    │         │    ├── constraint: /22/19: [/'east' - /'east']
 │              │    │         │    ├── key: (19)
 │              │    │         │    └── fd: ()-->(22), (19)-->(20,21)
 │              │    │         └── filters (true)
 │              │    └── project
 │              │         ├── columns: c_id:25!null c_p_id:26!null v:27!null p_id:36!null id2:37!null
 │              │         ├── key: (25)
 │              │         ├── fd: (25)-->(26,27,36,37), (36)-->(37), (37)-->(36), (26)==(37), (37)==(26)
 │              │         ├── limit hint: 100.00
 │              │         └── inner-join (lookup parent2@id2_idx [as=p])
 │              │              ├── columns: c_id:25!null c_p_id:26!null v:27!null crdb_region:28!null p_id:36!null id2:37!null
 │              │              ├── key columns: [26] = [37]
 │              │              ├── lookup columns are key
 │              │              ├── key: (25)
 │              │              ├── fd: (25)-->(26-28), (36)-->(37), (37)-->(36), (26)==(37), (37)==(26)
 │              │              ├── limit hint: 100.00
 │              │              ├── scan child2 [as=c]
 │              │              │    ├── columns: c_id:25!null c_p_id:26 v:27!null crdb_region:28!null
 │              │              │    ├── constraint: /28/25
 │              │              │    │    ├── [/'central' - /'central']
 │              │              │    │    └── [/'west' - /'west']
 │              │              │    ├── key: (25)
 │              │              │    └── fd: (25)-->(26-28)
 │              │              └── filters (true)
 │              └── filters
 │                   └── c_id:1 > id3:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ])]
 └── 1

exec-ddl
CREATE TABLE "parent4" (
  p_id INT,
  id2 INT,
  id3 INT,
  geom GEOMETRY(MULTIPOLYGON,4326),
  INVERTED INDEX nyc_census_blocks_geo_idx (geom),
  PRIMARY KEY(p_id),
  INDEX id2_id3_idx(id2)
) LOCALITY REGIONAL BY TABLE IN "east";
----

exec-ddl
CREATE TABLE "child4" (
  c_id INT PRIMARY KEY,
  c_p_id INT REFERENCES parent4 (p_id),
  geom GEOMETRY(MULTIPOLYGON,4326),
  INVERTED INDEX nyc_neighborhoods_geo_idx (geom),
  INDEX (c_p_id),
  FAMILY (c_id, c_p_id)
) LOCALITY REGIONAL BY ROW;
----

# Inverted join should not fire GenerateLocalityOptimizedSearchOfLookupJoins.
opt locality=(region=east) expect-not=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM child4 c, parent4 p WHERE ST_Intersects(p.geom, c.geom) LIMIT 1
----
distribute
 ├── columns: c_id:1!null c_p_id:2 geom:3!null p_id:8!null id2:9 id3:10 geom:11!null
 ├── cardinality: [0 - 1]
 ├── immutable
 ├── key: ()
 ├── fd: ()-->(1-3,8-11)
 ├── distribution: east
 ├── input distribution: central,east,west
 └── limit
      ├── columns: c_id:1!null c_p_id:2 c.geom:3!null p_id:8!null id2:9 id3:10 p.geom:11!null
      ├── cardinality: [0 - 1]
      ├── immutable
      ├── key: ()
      ├── fd: ()-->(1-3,8-11)
      ├── inner-join (lookup parent4 [as=p])
      │    ├── columns: c_id:1!null c_p_id:2 c.geom:3!null p_id:8!null id2:9 id3:10 p.geom:11!null
      │    ├── key columns: [15] = [8]
      │    ├── lookup columns are key
      │    ├── immutable
      │    ├── key: (1,8)
      │    ├── fd: (1)-->(2,3), (8)-->(9-11)
      │    ├── limit hint: 1.00
      │    ├── inner-join (inverted parent4@nyc_census_blocks_geo_idx,inverted [as=p])
      │    │    ├── columns: c_id:1!null c_p_id:2 c.geom:3 p_id:15!null
      │    │    ├── inverted-expr
      │    │    │    └── st_intersects(c.geom:3, p.geom:18)
      │    │    ├── key: (1,15)
      │    │    ├── fd: (1)-->(2,3)
      │    │    ├── limit hint: 100.00
      │    │    ├── scan child4 [as=c]
      │    │    │    ├── columns: c_id:1!null c_p_id:2 c.geom:3
      │    │    │    ├── check constraint expressions
      │    │    │    │    └── crdb_region:4 IN ('central', 'east', 'west') [outer=(4), constraints=(/4: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)]
      │    │    │    ├── key: (1)
      │    │    │    └── fd: (1)-->(2,3)
      │    │    └── filters (true)
      │    └── filters
      │         └── st_intersects(p.geom:11, c.geom:3) [outer=(3,11), immutable, constraints=(/3: (/NULL - ]; /11: (/NULL - ])]
      └── 1

# Regression test for #114393 - don't overwrite local with remote filters.
opt locality=(region=east) expect=GenerateLocalityOptimizedSearchOfLookupJoins
SELECT * FROM parent2 p, child2 c WHERE id2 = c_p_id AND c.v = 1 LIMIT 1
----
limit
 ├── columns: p_id:1!null id2:2!null id3:3 c_id:6!null c_p_id:7!null v:8!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1-3,6-8), (2)==(7), (7)==(2)
 ├── distribution: east
 ├── inner-join (lookup parent2 [as=p])
 │    ├── columns: p_id:1!null id2:2!null id3:3 c_id:6!null c_p_id:7!null v:8!null
 │    ├── key columns: [1] = [1]
 │    ├── lookup columns are key
 │    ├── key: (6)
 │    ├── fd: ()-->(8), (1)-->(2,3), (2)-->(1,3), (6)-->(7), (2)==(7), (7)==(2)
 │    ├── limit hint: 1.00
 │    ├── distribution: east
 │    ├── locality-optimized-search
 │    │    ├── columns: p_id:1!null id2:2!null c_id:6!null c_p_id:7!null v:8!null
 │    │    ├── left columns: p_id:13 id2:14 c_id:23 c_p_id:24 v:25
 │    │    ├── right columns: p_id:18 id2:19 c_id:29 c_p_id:30 v:31
 │    │    ├── key: (6)
 │    │    ├── fd: ()-->(8), (6)-->(7), (1)-->(2), (2)-->(1), (2)==(7), (7)==(2)
 │    │    ├── limit hint: 9.89
 │    │    ├── distribution: east
 │    │    ├── project
 │    │    │    ├── columns: p_id:13!null id2:14!null c_id:23!null c_p_id:24!null v:25!null
 │    │    │    ├── key: (23)
 │    │    │    ├── fd: ()-->(25), (23)-->(24), (13)-->(14), (14)-->(13), (14)==(24), (24)==(14)
 │    │    │    ├── limit hint: 9.89
 │    │    │    └── inner-join (lookup parent2@id2_idx [as=p])
 │    │    │         ├── columns: p_id:13!null id2:14!null c_id:23!null c_p_id:24!null v:25!null crdb_region:26!null
 │    │    │         ├── key columns: [24] = [14]
 │    │    │         ├── lookup columns are key
 │    │    │         ├── key: (23)
 │    │    │         ├── fd: ()-->(25,26), (23)-->(24), (13)-->(14), (14)-->(13), (14)==(24), (24)==(14)
 │    │    │         ├── limit hint: 9.89
 │    │    │         ├── scan child2@child2_crdb_region_v_idx [as=c]
 │    │    │         │    ├── columns: c_id:23!null c_p_id:24 v:25!null crdb_region:26!null
 │    │    │         │    ├── constraint: /26/25/23: [/'east'/1 - /'east'/1]
 │    │    │         │    ├── key: (23)
 │    │    │         │    └── fd: ()-->(25,26), (23)-->(24)
 │    │    │         └── filters (true)
 │    │    └── project
 │    │         ├── columns: p_id:18!null id2:19!null c_id:29!null c_p_id:30!null v:31!null
 │    │         ├── key: (29)
 │    │         ├── fd: ()-->(31), (29)-->(18,19,30), (18)-->(19), (19)-->(18), (19)==(30), (30)==(19)
 │    │         ├── limit hint: 9.89
 │    │         └── inner-join (lookup parent2@id2_idx [as=p])
 │    │              ├── columns: p_id:18!null id2:19!null c_id:29!null c_p_id:30!null v:31!null crdb_region:32!null
 │    │              ├── key columns: [30] = [19]
 │    │              ├── lookup columns are key
 │    │              ├── key: (29)
 │    │              ├── fd: ()-->(31), (29)-->(30,32), (18)-->(19), (19)-->(18), (19)==(30), (30)==(19)
 │    │              ├── limit hint: 9.89
 │    │              ├── scan child2@child2_crdb_region_v_idx [as=c]
 │    │              │    ├── columns: c_id:29!null c_p_id:30 v:31!null crdb_region:32!null
 │    │              │    ├── constraint: /32/31/29
 │    │              │    │    ├── [/'central'/1 - /'central'/1]
 │    │              │    │    └── [/'west'/1 - /'west'/1]
 │    │              │    ├── key: (29)
 │    │              │    └── fd: ()-->(31), (29)-->(30,32)
 │    │              └── filters (true)
 │    └── filters (true)
 └── 1
