import file=tpcc_schema
----

exec-ddl
CREATE TABLE a (k INT PRIMARY KEY, i INT, f FLOAT NOT NULL, s STRING, j JSON)
----

exec-ddl
CREATE TABLE t.b (x INT PRIMARY KEY, y INT)
----

exec-ddl
CREATE TABLE c (x INT PRIMARY KEY, y INT NOT NULL REFERENCES a(k), z INT NOT NULL, UNIQUE (x,z))
----

exec-ddl
CREATE TABLE d (x INT PRIMARY KEY, y INT NOT NULL, z INT NOT NULL, FOREIGN KEY (y,z) REFERENCES c(x,z))
----

exec-ddl
CREATE TABLE xy (x INT PRIMARY KEY, y INT)
----

exec-ddl
CREATE TABLE uv (u INT PRIMARY KEY, v INT)
----

exec-ddl
CREATE TABLE booleans (a BOOL, b BOOL, c BOOL, d BOOL, e BOOL)
----

exec-ddl
CREATE TABLE comp (i INT, c INT AS (i + 10) STORED, v INT AS (abs(i)) VIRTUAL)
----

norm
SELECT * FROM a INNER JOIN b ON a.s='foo' OR b.y<10
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── (s:4 = 'foo') OR (y:9 < 10) [outer=(4,9)]

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

norm
SELECT * FROM a RIGHT JOIN b ON k=x
----
left-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: (8)-->(1-5,9), (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# --------------------------------------------------
# SimplifyJoinFilters
# --------------------------------------------------

norm expect=SimplifyJoinFilters
SELECT * FROM a INNER JOIN xy ON x=1 OR NULL
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: ()-->(8,9), (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null y:9
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(8,9)
 │    ├── scan xy
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── x:8 = 1 [outer=(8), constraints=(/8: [/1 - /1]; tight), fd=()-->(8)]
 └── filters (true)

norm expect=SimplifyJoinFilters
SELECT * FROM a INNER JOIN xy ON (k=x AND i=y) AND true AND (f=3.5 AND s='foo')
----
inner-join (hash)
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5 x:8!null y:9!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: ()-->(3,4), (1)-->(2,5), (8)-->(9), (1)==(8), (8)==(1), (2)==(9), (9)==(2)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4!null j:5
 │    ├── key: (1)
 │    ├── fd: ()-->(3,4), (1)-->(2,5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         ├── f:3 = 3.5 [outer=(3), constraints=(/3: [/3.5 - /3.5]; tight), fd=()-->(3)]
 │         └── s:4 = 'foo' [outer=(4), constraints=(/4: [/'foo' - /'foo']; tight), fd=()-->(4)]
 ├── scan xy
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      └── i:2 = y:9 [outer=(2,9), constraints=(/2: (/NULL - ]; /9: (/NULL - ]), fd=(2)==(9), (9)==(2)]

norm expect-not=SimplifyJoinFilters
SELECT * FROM a INNER JOIN xy ON x=1 OR k=1
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan xy
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── (x:8 = 1) OR (k:1 = 1) [outer=(1,8)]

# Inner join case. Simplify IS True.
norm expect=SimplifyJoinFilters
SELECT * FROM xy INNER JOIN booleans ON (a AND x > 0) IS True
----
inner-join (cross)
 ├── columns: x:1!null y:2 a:5!null b:6 c:7 d:8 e:9
 ├── fd: ()-->(5), (1)-->(2)
 ├── select
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    ├── fd: (1)-->(2)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    └── filters
 │         └── x:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]
 ├── select
 │    ├── columns: a:5!null b:6 c:7 d:8 e:9
 │    ├── fd: ()-->(5)
 │    ├── scan booleans
 │    │    └── columns: a:5 b:6 c:7 d:8 e:9
 │    └── filters
 │         └── a:5 [outer=(5), constraints=(/5: [/true - /true]; tight), fd=()-->(5)]
 └── filters (true)

# Inner join case. Simplify is False.
norm expect=SimplifyJoinFilters
SELECT * FROM xy INNER JOIN booleans ON (a AND x > 0) IS False
----
inner-join (cross)
 ├── columns: x:1!null y:2 a:5 b:6 c:7 d:8 e:9
 ├── fd: (1)-->(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan booleans
 │    └── columns: a:5 b:6 c:7 d:8 e:9
 └── filters
      └── (NOT a:5) OR (x:1 <= 0) [outer=(1,5)]

# Left join case. Simplify IS True.
norm expect=SimplifyJoinFilters
SELECT * FROM xy LEFT JOIN booleans ON (a AND x > 0) IS True
----
left-join (cross)
 ├── columns: x:1!null y:2 a:5 b:6 c:7 d:8 e:9
 ├── fd: (1)-->(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── select
 │    ├── columns: a:5!null b:6 c:7 d:8 e:9
 │    ├── fd: ()-->(5)
 │    ├── scan booleans
 │    │    └── columns: a:5 b:6 c:7 d:8 e:9
 │    └── filters
 │         └── a:5 [outer=(5), constraints=(/5: [/true - /true]; tight), fd=()-->(5)]
 └── filters
      └── x:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]

# Full join case. Simplify IS True.
norm expect=SimplifyJoinFilters
SELECT * FROM xy FULL JOIN booleans ON (a AND x > 0) IS True
----
full-join (cross)
 ├── columns: x:1 y:2 a:5 b:6 c:7 d:8 e:9
 ├── fd: (1)-->(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan booleans
 │    └── columns: a:5 b:6 c:7 d:8 e:9
 └── filters
      ├── a:5 [outer=(5), constraints=(/5: [/true - /true]; tight), fd=()-->(5)]
      └── x:1 > 0 [outer=(1), constraints=(/1: [/1 - ]; tight)]

# Do not simplify IS NOT (when inputs are nullable).
norm expect-not=SimplifyJoinFilters
SELECT * FROM xy INNER JOIN booleans ON (a AND x > 0) IS NOT True
----
inner-join (cross)
 ├── columns: x:1!null y:2 a:5 b:6 c:7 d:8 e:9
 ├── fd: (1)-->(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan booleans
 │    └── columns: a:5 b:6 c:7 d:8 e:9
 └── filters
      └── (a:5 AND (x:1 > 0)) IS NOT true [outer=(1,5)]

# Do not simplify the IS because the right argument is Null (vs True or False).
norm expect-not=SimplifyJoinFilters
SELECT * FROM xy INNER JOIN booleans ON (a AND x > 0) IS Null
----
inner-join (cross)
 ├── columns: x:1!null y:2 a:5 b:6 c:7 d:8 e:9
 ├── fd: (1)-->(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan booleans
 │    └── columns: a:5 b:6 c:7 d:8 e:9
 └── filters
      └── (a:5 AND (x:1 > 0)) IS NULL [outer=(1,5)]

# Regression test for #54717. SimplifyJoinFilters should not simplify
# contradictions. Doing so can split the filter expressions into two
# FiltersItems such that they are no longer considered a contradiction, and
# DetectJoinContradiction does not fire.
norm expect=DetectJoinContradiction expect-not=SimplifyJoinFilters
SELECT * FROM a LEFT JOIN b ON k<1 AND k>2
----
left-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: (1)-->(2-5,8,9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── values
 │    ├── columns: x:8!null y:9!null
 │    ├── cardinality: [0 - 0]
 │    ├── key: ()
 │    └── fd: ()-->(8,9)
 └── filters (true)

# --------------------------------------------------
# DetectJoinContradiction
# --------------------------------------------------

norm expect=DetectJoinContradiction
SELECT * FROM a INNER JOIN b ON (k<1 AND k>2) OR (k<4 AND k>5)
----
values
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5!null x:8!null y:9!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1-5,8,9)

norm expect=DetectJoinContradiction
SELECT * FROM a LEFT JOIN b ON (k<1 AND k>2) OR (k<4 AND k>5)
----
left-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: (1)-->(2-5,8,9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── values
 │    ├── columns: x:8!null y:9!null
 │    ├── cardinality: [0 - 0]
 │    ├── key: ()
 │    └── fd: ()-->(8,9)
 └── filters (true)

norm expect=DetectJoinContradiction
SELECT * FROM a INNER JOIN xy ON NULL
----
values
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5!null x:8!null y:9!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1-5,8,9)

norm expect=DetectJoinContradiction
SELECT * FROM a FULL JOIN b ON i=5 AND ((k<1 AND k>2) OR (k<4 AND k>5)) AND s='foo'
----
full-join (cross)
 ├── columns: k:1 i:2 f:3 s:4 j:5 x:8 y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── false [constraints=(contradiction; tight)]

# --------------------------------------------------
# PushFilterIntoJoinLeft
# --------------------------------------------------
norm expect=PushFilterIntoJoinLeft
SELECT * FROM a INNER JOIN b ON a.k=b.x AND a.s='foo'
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4!null j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: ()-->(4), (1)-->(2,3,5), (8)-->(9), (1)==(8), (8)==(1)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4!null j:5
 │    ├── key: (1)
 │    ├── fd: ()-->(4), (1)-->(2,3,5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── s:4 = 'foo' [outer=(4), constraints=(/4: [/'foo' - /'foo']; tight), fd=()-->(4)]
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# LEFT JOIN should not push down conditions to left side of join.
norm expect-not=PushFilterIntoJoinLeft
SELECT * FROM a LEFT JOIN b ON a.k=b.x AND a.i=1
----
left-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── key: (1)
 ├── fd: (1)-->(2-5,8,9), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      └── i:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]

# Semi-join case.
norm expect=PushFilterIntoJoinLeft
SELECT * FROM a WHERE EXISTS(SELECT * FROM b WHERE x=k AND s='foo')
----
semi-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4!null j:5
 ├── key: (1)
 ├── fd: ()-->(4), (1)-->(2,3,5)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4!null j:5
 │    ├── key: (1)
 │    ├── fd: ()-->(4), (1)-->(2,3,5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── s:4 = 'foo' [outer=(4), constraints=(/4: [/'foo' - /'foo']; tight), fd=()-->(4)]
 ├── scan b
 │    ├── columns: x:8!null
 │    └── key: (8)
 └── filters
      └── x:8 = k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Do not push anti-join conditions into left input.
norm expect-not=PushFilterIntoJoinLeft
SELECT * FROM a WHERE NOT EXISTS(SELECT * FROM b WHERE x=k AND s='foo')
----
anti-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null
 │    └── key: (8)
 └── filters
      ├── x:8 = k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      └── s:4 = 'foo' [outer=(4), constraints=(/4: [/'foo' - /'foo']; tight), fd=()-->(4)]

# --------------------------------------------------
# PushFilterIntoJoinRight
# --------------------------------------------------
norm expect=PushFilterIntoJoinRight
SELECT * FROM b INNER JOIN a ON b.x=a.k AND a.s='foo'
----
inner-join (hash)
 ├── columns: x:1!null y:2 k:5!null i:6 f:7!null s:8!null j:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (5)
 ├── fd: ()-->(8), (1)-->(2), (5)-->(6,7,9), (1)==(5), (5)==(1)
 ├── scan b
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── select
 │    ├── columns: k:5!null i:6 f:7!null s:8!null j:9
 │    ├── key: (5)
 │    ├── fd: ()-->(8), (5)-->(6,7,9)
 │    ├── scan a
 │    │    ├── columns: k:5!null i:6 f:7!null s:8 j:9
 │    │    ├── key: (5)
 │    │    └── fd: (5)-->(6-9)
 │    └── filters
 │         └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]
 └── filters
      └── x:1 = k:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

norm expect=PushFilterIntoJoinRight
SELECT * FROM b LEFT JOIN a ON (a.i<0 OR a.i>10) AND b.y=1 AND a.s='foo' AND b.x=a.k
----
left-join (hash)
 ├── columns: x:1!null y:2 k:5 i:6 f:7 s:8 j:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── key: (1)
 ├── fd: (1)-->(2,5-9), (5)-->(6,7,9)
 ├── scan b
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── select
 │    ├── columns: k:5!null i:6!null f:7!null s:8!null j:9
 │    ├── key: (5)
 │    ├── fd: ()-->(8), (5)-->(6,7,9)
 │    ├── scan a
 │    │    ├── columns: k:5!null i:6 f:7!null s:8 j:9
 │    │    ├── key: (5)
 │    │    └── fd: (5)-->(6-9)
 │    └── filters
 │         ├── (i:6 < 0) OR (i:6 > 10) [outer=(6), constraints=(/6: (/NULL - /-1] [/11 - ]; tight)]
 │         └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]
 └── filters
      ├── y:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]
      └── x:1 = k:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

# RIGHT JOIN should not push down conditions to right side of join.
norm expect-not=PushFilterIntoJoinRight
SELECT * FROM b RIGHT JOIN a ON b.x=a.k AND a.i=1
----
left-join (hash)
 ├── columns: x:1 y:2 k:5!null i:6 f:7!null s:8 j:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── key: (5)
 ├── fd: (5)-->(1,2,6-9), (1)-->(2)
 ├── scan a
 │    ├── columns: k:5!null i:6 f:7!null s:8 j:9
 │    ├── key: (5)
 │    └── fd: (5)-->(6-9)
 ├── scan b
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 └── filters
      ├── x:1 = k:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]
      └── i:6 = 1 [outer=(6), constraints=(/6: [/1 - /1]; tight), fd=()-->(6)]

# Semi-join case.
norm expect=PushFilterIntoJoinRight
SELECT * FROM a WHERE EXISTS(SELECT * FROM b WHERE x=k AND y>10)
----
semi-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null y:9!null
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── y:9 > 10 [outer=(9), constraints=(/9: [/11 - ]; tight)]
 └── filters
      └── x:8 = k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Anti-join case.
norm expect=PushFilterIntoJoinRight
SELECT * FROM a WHERE NOT EXISTS(SELECT * FROM b WHERE x=k AND y>10)
----
anti-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null y:9!null
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── y:9 > 10 [outer=(9), constraints=(/9: [/11 - ]; tight)]
 └── filters
      └── x:8 = k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# -------------------------------------------------------------------------------
# PushFilterIntoJoinLeftAndRight + MapFilterIntoJoinLeft + MapFilterIntoJoinRight
# -------------------------------------------------------------------------------

# Can push to both sides with inner join.
norm expect=(MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT * FROM a INNER JOIN b ON a.k=b.x AND a.k + b.y > 5 AND b.x * a.i = 3
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── immutable
 ├── key: (8)
 ├── fd: (1)-->(2-5), (8)-->(9), (1)==(8), (8)==(1)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(2-5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── (k:1 * i:2) = 3 [outer=(1,2), immutable]
 ├── select
 │    ├── columns: x:8!null y:9
 │    ├── immutable
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── (x:8 + y:9) > 5 [outer=(8,9), immutable]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Multiple equivalent columns.
norm expect=MapFilterIntoJoinLeft
SELECT * FROM a INNER JOIN b ON a.k=b.x AND a.i=b.x AND a.i=b.y AND a.f + b.y::FLOAT > 5 AND a.s || b.x::STRING = 'foo1'
----
inner-join (hash)
 ├── columns: k:1!null i:2!null f:3!null s:4 j:5 x:8!null y:9!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── immutable
 ├── key: (8)
 ├── fd: (1)-->(3-5), (1)==(2,8,9), (2)==(1,8,9), (8)==(1,2,9), (9)==(1,2,8)
 ├── select
 │    ├── columns: k:1!null i:2!null f:3!null s:4 j:5
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(3-5), (1)==(2), (2)==(1)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         ├── (f:3 + k:1::FLOAT8) > 5.0 [outer=(1,3), immutable]
 │         ├── (s:4 || k:1::STRING) = 'foo1' [outer=(1,4), immutable]
 │         └── k:1 = i:2 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 ├── select
 │    ├── columns: x:8!null y:9!null
 │    ├── key: (8)
 │    ├── fd: (8)==(9), (9)==(8)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── x:8 = y:9 [outer=(8,9), constraints=(/8: (/NULL - ]; /9: (/NULL - ]), fd=(8)==(9), (9)==(8)]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Can push to both sides with semi-join.
norm expect=(MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT * FROM a WHERE EXISTS(
  SELECT * FROM b WHERE a.k=b.x AND a.k + b.y > 5 AND b.x * a.i = 3
)
----
semi-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(2-5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── (k:1 * i:2) = 3 [outer=(1,2), immutable]
 ├── select
 │    ├── columns: x:8!null y:9
 │    ├── immutable
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── (x:8 + y:9) > 5 [outer=(8,9), immutable]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

norm expect=PushFilterIntoJoinLeftAndRight
SELECT * FROM a WHERE EXISTS(
  SELECT * FROM b WHERE a.k=b.x AND a.k > 5 AND b.x IN (3, 7, 10)
)
----
semi-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── cardinality: [0 - 2]
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── cardinality: [0 - 2]
 │    ├── key: (1)
 │    ├── fd: (1)-->(2-5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── (k:1 > 5) AND (k:1 IN (3, 7, 10)) [outer=(1), constraints=(/1: [/7 - /7] [/10 - /10]; tight)]
 ├── select
 │    ├── columns: x:8!null
 │    ├── cardinality: [0 - 2]
 │    ├── key: (8)
 │    ├── scan b
 │    │    ├── columns: x:8!null
 │    │    └── key: (8)
 │    └── filters
 │         └── (x:8 IN (3, 7, 10)) AND (x:8 > 5) [outer=(8), constraints=(/8: [/7 - /7] [/10 - /10]; tight)]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Can only push to right side with left join.
norm expect=MapFilterIntoJoinRight expect-not=PushFilterIntoJoinLeftAndRight
SELECT * FROM a LEFT JOIN b ON a.k=b.x AND a.k + b.y > 5 AND b.x * a.i = 3
----
left-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-5,8,9), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null y:9
 │    ├── immutable
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── (x:8 + y:9) > 5 [outer=(8,9), immutable]
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      └── (x:8 * i:2) = 3 [outer=(2,8), immutable]

norm expect=MapFilterIntoJoinRight expect-not=PushFilterIntoJoinLeftAndRight
SELECT * FROM a LEFT JOIN b ON a.k=b.x AND a.k > 5 AND b.x IN (3, 7, 10)
----
left-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── key: (1)
 ├── fd: (1)-->(2-5,8,9), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null y:9
 │    ├── cardinality: [0 - 2]
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── (x:8 IN (3, 7, 10)) AND (x:8 > 5) [outer=(8), constraints=(/8: [/7 - /7] [/10 - /10]; tight)]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Cannot push with full join.
norm expect-not=(PushFilterIntoJoinLeftAndRight,MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT * FROM a FULL JOIN b ON a.k=b.x AND a.k + b.y > 5 AND b.x * a.i = 3
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── immutable
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      ├── (k:1 + y:9) > 5 [outer=(1,9), immutable]
      └── (x:8 * i:2) = 3 [outer=(2,8), immutable]

norm expect-not=(PushFilterIntoJoinLeftAndRight,MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT * FROM a FULL JOIN b ON a.k=b.x AND a.k > 5 AND b.x IN (3, 7, 10)
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      ├── k:1 > 5 [outer=(1), constraints=(/1: [/6 - ]; tight)]
      └── x:8 IN (3, 7, 10) [outer=(8), constraints=(/8: [/3 - /3] [/7 - /7] [/10 - /10]; tight)]

# Can only push to right side with anti-join.
norm expect=MapFilterIntoJoinRight expect-not=PushFilterIntoJoinLeftAndRight
SELECT * FROM a WHERE NOT EXISTS(
  SELECT * FROM b WHERE a.k=b.x AND a.k + b.y > 5 AND b.x * a.i = 3
)
----
anti-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null y:9
 │    ├── immutable
 │    ├── key: (8)
 │    ├── fd: (8)-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── (x:8 + y:9) > 5 [outer=(8,9), immutable]
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      └── (x:8 * i:2) = 3 [outer=(2,8), immutable]

norm expect=MapFilterIntoJoinRight expect-not=PushFilterIntoJoinLeftAndRight
SELECT * FROM a WHERE NOT EXISTS(
  SELECT * FROM b WHERE a.k=b.x AND a.k > 5 AND b.x IN (3, 7, 10)
)
----
anti-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: x:8!null
 │    ├── cardinality: [0 - 2]
 │    ├── key: (8)
 │    ├── scan b
 │    │    ├── columns: x:8!null
 │    │    └── key: (8)
 │    └── filters
 │         └── (x:8 IN (3, 7, 10)) AND (x:8 > 5) [outer=(8), constraints=(/8: [/7 - /7] [/10 - /10]; tight)]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Works with a non-correlated subquery.
norm expect=MapFilterIntoJoinLeft
SELECT * FROM a JOIN b ON a.k = b.x AND b.x * a.i = (SELECT min(b.x) FROM b)
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── immutable
 ├── key: (8)
 ├── fd: (1)-->(2-5), (8)-->(9), (1)==(8), (8)==(1)
 ├── select
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(2-5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── eq [outer=(1,2), immutable, subquery]
 │              ├── k:1 * i:2
 │              └── subquery
 │                   └── scalar-group-by
 │                        ├── columns: min:16
 │                        ├── cardinality: [1 - 1]
 │                        ├── key: ()
 │                        ├── fd: ()-->(16)
 │                        ├── scan b
 │                        │    ├── columns: x:12!null
 │                        │    └── key: (12)
 │                        └── aggregations
 │                             └── min [as=min:16, outer=(12)]
 │                                  └── x:12
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Optimization does not apply with correlated suqueries.
norm expect-not=(PushFilterIntoJoinLeftAndRight,MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT * FROM a JOIN b ON a.k = b.x AND b.x * a.i = (SELECT a.k * b.y FROM b)
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── immutable
 ├── key: (8)
 ├── fd: (1)-->(2-5), (1,8)-->(9), (1)==(8), (8)==(1)
 └── inner-join-apply
      ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9 "?column?":16
      ├── immutable
      ├── key: (8)
      ├── fd: (1)-->(2-5), (1,8)-->(9,16), (8)-->(16), (1)==(8), (8)==(1)
      ├── scan a
      │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
      │    ├── key: (1)
      │    └── fd: (1)-->(2-5)
      ├── ensure-distinct-on
      │    ├── columns: x:8!null y:9 "?column?":16
      │    ├── grouping columns: x:8!null
      │    ├── error: "more than one row returned by a subquery used as an expression"
      │    ├── outer: (1)
      │    ├── immutable
      │    ├── key: (8)
      │    ├── fd: (8)-->(9,16)
      │    ├── left-join (cross)
      │    │    ├── columns: x:8!null y:9 "?column?":16
      │    │    ├── outer: (1)
      │    │    ├── immutable
      │    │    ├── fd: (8)-->(9)
      │    │    ├── scan b
      │    │    │    ├── columns: x:8!null y:9
      │    │    │    ├── key: (8)
      │    │    │    └── fd: (8)-->(9)
      │    │    ├── project
      │    │    │    ├── columns: "?column?":16
      │    │    │    ├── outer: (1)
      │    │    │    ├── immutable
      │    │    │    ├── scan b
      │    │    │    │    └── columns: y:13
      │    │    │    └── projections
      │    │    │         └── k:1 * y:13 [as="?column?":16, outer=(1,13), immutable]
      │    │    └── filters (true)
      │    └── aggregations
      │         ├── const-agg [as=y:9, outer=(9)]
      │         │    └── y:9
      │         └── const-agg [as="?column?":16, outer=(16)]
      │              └── "?column?":16
      └── filters
           ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
           └── "?column?":16 = (x:8 * i:2) [outer=(2,8,16), immutable, constraints=(/16: (/NULL - ]), fd=(2,8)-->(16)]

# Ensure that we do not map filters for types with composite key encoding.
norm expect-not=(PushFilterIntoJoinLeftAndRight,MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT *
FROM (VALUES (1.0), (2.0)) AS t1(x), (VALUES (1.00), (2.00)) AS t2(y) WHERE x=y AND x::text = '1.0'
----
inner-join (hash)
 ├── columns: x:1!null y:2!null
 ├── cardinality: [0 - 4]
 ├── immutable
 ├── fd: (1)==(2), (2)==(1)
 ├── select
 │    ├── columns: column1:1!null
 │    ├── cardinality: [0 - 2]
 │    ├── immutable
 │    ├── values
 │    │    ├── columns: column1:1!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1.0,)
 │    │    └── (2.0,)
 │    └── filters
 │         └── column1:1::STRING = '1.0' [outer=(1), immutable]
 ├── values
 │    ├── columns: column1:2!null
 │    ├── cardinality: [2 - 2]
 │    ├── (1.00,)
 │    └── (2.00,)
 └── filters
      └── column1:1 = column1:2 [outer=(1,2), immutable, constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]

# Remap composite variables if filters are not composite sensitive.
norm expect=PushFilterIntoJoinLeftAndRight
SELECT *
FROM (VALUES (1.0), (2.0)) AS t1(x), (VALUES (1.00), (2.00)) AS t2(y) WHERE x=y AND x >= 1
----
inner-join (hash)
 ├── columns: x:1!null y:2!null
 ├── cardinality: [0 - 4]
 ├── immutable
 ├── fd: (1)==(2), (2)==(1)
 ├── select
 │    ├── columns: column1:1!null
 │    ├── cardinality: [0 - 2]
 │    ├── immutable
 │    ├── values
 │    │    ├── columns: column1:1!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1.0,)
 │    │    └── (2.0,)
 │    └── filters
 │         └── column1:1 >= 1 [outer=(1), immutable, constraints=(/1: [/1 - ]; tight)]
 ├── select
 │    ├── columns: column1:2!null
 │    ├── cardinality: [0 - 2]
 │    ├── immutable
 │    ├── values
 │    │    ├── columns: column1:2!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1.00,)
 │    │    └── (2.00,)
 │    └── filters
 │         └── column1:2 >= 1 [outer=(2), immutable, constraints=(/2: [/1 - ]; tight)]
 └── filters
      └── column1:1 = column1:2 [outer=(1,2), immutable, constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]

# Optimization does not apply if equality is only on one side.
norm expect-not=(PushFilterIntoJoinLeftAndRight,MapFilterIntoJoinLeft,MapFilterIntoJoinRight)
SELECT * FROM a INNER JOIN b ON b.y=b.x AND a.k=a.i AND a.k + b.y > 5 AND b.x * a.i = 3
----
inner-join (cross)
 ├── columns: k:1!null i:2!null f:3!null s:4 j:5 x:8!null y:9!null
 ├── immutable
 ├── key: (1,8)
 ├── fd: (1)-->(3-5), (1)==(2), (2)==(1), (8)==(9), (9)==(8)
 ├── select
 │    ├── columns: k:1!null i:2!null f:3!null s:4 j:5
 │    ├── key: (1)
 │    ├── fd: (1)-->(3-5), (1)==(2), (2)==(1)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── k:1 = i:2 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 ├── select
 │    ├── columns: x:8!null y:9!null
 │    ├── key: (8)
 │    ├── fd: (8)==(9), (9)==(8)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── y:9 = x:8 [outer=(8,9), constraints=(/8: (/NULL - ]; /9: (/NULL - ]), fd=(8)==(9), (9)==(8)]
 └── filters
      ├── (k:1 + y:9) > 5 [outer=(1,9), immutable]
      └── (x:8 * i:2) = 3 [outer=(2,8), immutable]

# Ensure that MapFilterIntoJoinRight doesn't cause cycle with decorrelation.
norm expect=MapFilterIntoJoinRight
SELECT
(
    SELECT b.x
    FROM (SELECT b.* FROM b FULL OUTER JOIN b AS b2 ON c.x=5) AS b, a
    WHERE a.k=b.x AND a.k+b.x < 5
)
FROM c
----
project
 ├── columns: x:21
 ├── immutable
 ├── ensure-distinct-on
 │    ├── columns: c.x:1!null b.x:6
 │    ├── grouping columns: c.x:1!null
 │    ├── error: "more than one row returned by a subquery used as an expression"
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(6)
 │    ├── left-join-apply
 │    │    ├── columns: c.x:1!null b.x:6 k:14
 │    │    ├── immutable
 │    │    ├── fd: (6)==(14), (14)==(6)
 │    │    ├── scan c
 │    │    │    ├── columns: c.x:1!null
 │    │    │    └── key: (1)
 │    │    ├── inner-join (hash)
 │    │    │    ├── columns: b.x:6!null k:14!null
 │    │    │    ├── outer: (1)
 │    │    │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
 │    │    │    ├── immutable
 │    │    │    ├── fd: (6)==(14), (14)==(6)
 │    │    │    ├── left-join (cross)
 │    │    │    │    ├── columns: b.x:6!null
 │    │    │    │    ├── outer: (1)
 │    │    │    │    ├── scan b
 │    │    │    │    │    ├── columns: b.x:6!null
 │    │    │    │    │    └── key: (6)
 │    │    │    │    ├── scan b [as=b2]
 │    │    │    │    └── filters
 │    │    │    │         └── c.x:1 = 5 [outer=(1), constraints=(/1: [/5 - /5]; tight), fd=()-->(1)]
 │    │    │    ├── select
 │    │    │    │    ├── columns: k:14!null
 │    │    │    │    ├── immutable
 │    │    │    │    ├── key: (14)
 │    │    │    │    ├── scan a
 │    │    │    │    │    ├── columns: k:14!null
 │    │    │    │    │    └── key: (14)
 │    │    │    │    └── filters
 │    │    │    │         └── (k:14 + k:14) < 5 [outer=(14), immutable]
 │    │    │    └── filters
 │    │    │         └── k:14 = b.x:6 [outer=(6,14), constraints=(/6: (/NULL - ]; /14: (/NULL - ]), fd=(6)==(14), (14)==(6)]
 │    │    └── filters (true)
 │    └── aggregations
 │         └── const-agg [as=b.x:6, outer=(6)]
 │              └── b.x:6
 └── projections
      └── b.x:6 [as=x:21, outer=(6)]

# Ensure that MapFilterIntoJoinLeft doesn't cause cycle with decorrelation.
norm expect=MapFilterIntoJoinLeft
SELECT
(
    SELECT b.x FROM a, (SELECT b.* FROM b FULL OUTER JOIN b AS b2 ON c.x=5) AS b
    WHERE a.k=b.x AND a.k+b.x < 5
)
FROM c
----
project
 ├── columns: x:21
 ├── immutable
 ├── ensure-distinct-on
 │    ├── columns: c.x:1!null b.x:13
 │    ├── grouping columns: c.x:1!null
 │    ├── error: "more than one row returned by a subquery used as an expression"
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(13)
 │    ├── left-join-apply
 │    │    ├── columns: c.x:1!null k:6 b.x:13
 │    │    ├── immutable
 │    │    ├── fd: (6)==(13), (13)==(6)
 │    │    ├── scan c
 │    │    │    ├── columns: c.x:1!null
 │    │    │    └── key: (1)
 │    │    ├── left-join (cross)
 │    │    │    ├── columns: k:6!null b.x:13!null
 │    │    │    ├── outer: (1)
 │    │    │    ├── immutable
 │    │    │    ├── fd: (6)==(13), (13)==(6)
 │    │    │    ├── inner-join (hash)
 │    │    │    │    ├── columns: k:6!null b.x:13!null
 │    │    │    │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    │    │    │    ├── immutable
 │    │    │    │    ├── key: (13)
 │    │    │    │    ├── fd: (6)==(13), (13)==(6)
 │    │    │    │    ├── select
 │    │    │    │    │    ├── columns: k:6!null
 │    │    │    │    │    ├── immutable
 │    │    │    │    │    ├── key: (6)
 │    │    │    │    │    ├── scan a
 │    │    │    │    │    │    ├── columns: k:6!null
 │    │    │    │    │    │    └── key: (6)
 │    │    │    │    │    └── filters
 │    │    │    │    │         └── (k:6 + k:6) < 5 [outer=(6), immutable]
 │    │    │    │    ├── scan b
 │    │    │    │    │    ├── columns: b.x:13!null
 │    │    │    │    │    └── key: (13)
 │    │    │    │    └── filters
 │    │    │    │         └── k:6 = b.x:13 [outer=(6,13), constraints=(/6: (/NULL - ]; /13: (/NULL - ]), fd=(6)==(13), (13)==(6)]
 │    │    │    ├── scan b [as=b2]
 │    │    │    └── filters
 │    │    │         └── c.x:1 = 5 [outer=(1), constraints=(/1: [/5 - /5]; tight), fd=()-->(1)]
 │    │    └── filters (true)
 │    └── aggregations
 │         └── const-agg [as=b.x:13, outer=(13)]
 │              └── b.x:13
 └── projections
      └── b.x:13 [as=x:21, outer=(13)]

exec-ddl
CREATE TABLE t1 (a DATE)
----

exec-ddl
CREATE TABLE t2 (b TIMESTAMPTZ)
----

# Make sure that we do not create invalid filters due to substituting columns
# with different types.
norm
SELECT * FROM t1, t2 WHERE a = b AND age(b, TIMESTAMPTZ '2017-01-01') > INTERVAL '1 day'
----
inner-join (cross)
 ├── columns: a:1!null b:5!null
 ├── stable
 ├── fd: (1)==(5), (5)==(1)
 ├── scan t1
 │    └── columns: a:1
 ├── select
 │    ├── columns: b:5
 │    ├── immutable
 │    ├── scan t2
 │    │    └── columns: b:5
 │    └── filters
 │         └── age(b:5, '2017-01-01 00:00:00+00') > '1 day' [outer=(5), immutable]
 └── filters
      └── a:1 = b:5 [outer=(1,5), stable, constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

# Regression for issue 28818. Try to trigger undetectable cycle between the
# PushFilterIntoJoinLeftAndRight and TryDecorrelateSelect rules.
norm
SELECT 1
FROM a
WHERE EXISTS (
    SELECT 1
    FROM xy
    INNER JOIN uv
    ON EXISTS (
        SELECT 1
        FROM b
        WHERE a.s >= 'foo'
        LIMIT 10
    )
    WHERE
        (SELECT s FROM a) = 'foo'
)
----
project
 ├── columns: "?column?":34!null
 ├── fd: ()-->(34)
 ├── semi-join (cross)
 │    ├── columns: s:4!null
 │    ├── select
 │    │    ├── columns: s:4!null
 │    │    ├── scan a
 │    │    │    └── columns: s:4
 │    │    └── filters
 │    │         └── s:4 >= 'foo' [outer=(4), constraints=(/4: [/'foo' - ]; tight)]
 │    ├── inner-join (cross)
 │    │    ├── inner-join (cross)
 │    │    │    ├── select
 │    │    │    │    ├── scan xy
 │    │    │    │    └── filters
 │    │    │    │         └── eq [subquery]
 │    │    │    │              ├── subquery
 │    │    │    │              │    └── max1-row
 │    │    │    │              │         ├── columns: s:28
 │    │    │    │              │         ├── error: "more than one row returned by a subquery used as an expression"
 │    │    │    │              │         ├── cardinality: [0 - 1]
 │    │    │    │              │         ├── key: ()
 │    │    │    │              │         ├── fd: ()-->(28)
 │    │    │    │              │         └── scan a
 │    │    │    │              │              └── columns: s:28
 │    │    │    │              └── 'foo'
 │    │    │    ├── select
 │    │    │    │    ├── scan uv
 │    │    │    │    └── filters
 │    │    │    │         └── eq [subquery]
 │    │    │    │              ├── subquery
 │    │    │    │              │    └── max1-row
 │    │    │    │              │         ├── columns: s:28
 │    │    │    │              │         ├── error: "more than one row returned by a subquery used as an expression"
 │    │    │    │              │         ├── cardinality: [0 - 1]
 │    │    │    │              │         ├── key: ()
 │    │    │    │              │         ├── fd: ()-->(28)
 │    │    │    │              │         └── scan a
 │    │    │    │              │              └── columns: s:28
 │    │    │    │              └── 'foo'
 │    │    │    └── filters (true)
 │    │    ├── select
 │    │    │    ├── scan b
 │    │    │    └── filters
 │    │    │         └── eq [subquery]
 │    │    │              ├── subquery
 │    │    │              │    └── max1-row
 │    │    │              │         ├── columns: s:28
 │    │    │              │         ├── error: "more than one row returned by a subquery used as an expression"
 │    │    │              │         ├── cardinality: [0 - 1]
 │    │    │              │         ├── key: ()
 │    │    │              │         ├── fd: ()-->(28)
 │    │    │              │         └── scan a
 │    │    │              │              └── columns: s:28
 │    │    │              └── 'foo'
 │    │    └── filters (true)
 │    └── filters (true)
 └── projections
      └── 1 [as="?column?":34]

# Regression for issue 36137. Try to trigger undetectable cycle between the
# PushFilterIntoJoinLeftAndRight and TryDecorrelateSelect rules.
norm
SELECT * FROM a JOIN b ON a.k = b.x
WHERE (a.k = b.x) OR (a.k IN (SELECT 5 FROM b WHERE x = y));
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: (1)-->(2-5), (8)-->(9), (1)==(8), (8)==(1)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      └── or [outer=(1,8), correlated-subquery]
           ├── k:1 = x:8
           └── any: eq
                ├── project
                │    ├── columns: "?column?":16!null
                │    ├── fd: ()-->(16)
                │    ├── select
                │    │    ├── columns: x:12!null y:13!null
                │    │    ├── key: (12)
                │    │    ├── fd: (12)==(13), (13)==(12)
                │    │    ├── scan b
                │    │    │    ├── columns: x:12!null y:13
                │    │    │    ├── key: (12)
                │    │    │    └── fd: (12)-->(13)
                │    │    └── filters
                │    │         └── x:12 = y:13 [outer=(12,13), constraints=(/12: (/NULL - ]; /13: (/NULL - ]), fd=(12)==(13), (13)==(12)]
                │    └── projections
                │         └── 5 [as="?column?":16]
                └── k:1

# Regression test for #43039. Use transitive equalities for filter inference.
norm expect=PushFilterIntoJoinLeftAndRight
SELECT
    *
FROM
    a
    JOIN b ON a.k = b.x
    JOIN c ON b.x = c.x
    JOIN d ON c.x = d.x
    JOIN xy ON d.x = xy.x
WHERE
    a.k = 3;
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9 x:12!null y:13!null z:14!null x:17!null y:18!null z:19!null x:22!null y:23
 ├── cardinality: [0 - 1]
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: ()
 ├── fd: ()-->(1-5,8,9,12-14,17-19,22,23), (1)==(8,12,17,22), (8)==(1,12,17,22), (12)==(1,8,17,22), (17)==(1,8,12,22), (22)==(1,8,12,17)
 ├── inner-join (hash)
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5 b.x:8!null b.y:9 c.x:12!null c.y:13!null c.z:14!null d.x:17!null d.y:18!null d.z:19!null
 │    ├── cardinality: [0 - 1]
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    ├── key: ()
 │    ├── fd: ()-->(1-5,8,9,12-14,17-19), (1)==(8,12,17), (8)==(1,12,17), (12)==(1,8,17), (17)==(1,8,12)
 │    ├── inner-join (hash)
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5 b.x:8!null b.y:9 c.x:12!null c.y:13!null c.z:14!null
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(1-5,8,9,12-14), (1)==(8,12), (8)==(1,12), (12)==(1,8)
 │    │    ├── inner-join (hash)
 │    │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5 b.x:8!null b.y:9
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(1-5,8,9), (1)==(8), (8)==(1)
 │    │    │    ├── select
 │    │    │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    ├── fd: ()-->(1-5)
 │    │    │    │    ├── scan a
 │    │    │    │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    │    │    │    ├── key: (1)
 │    │    │    │    │    └── fd: (1)-->(2-5)
 │    │    │    │    └── filters
 │    │    │    │         └── k:1 = 3 [outer=(1), constraints=(/1: [/3 - /3]; tight), fd=()-->(1)]
 │    │    │    ├── select
 │    │    │    │    ├── columns: b.x:8!null b.y:9
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    ├── fd: ()-->(8,9)
 │    │    │    │    ├── scan b
 │    │    │    │    │    ├── columns: b.x:8!null b.y:9
 │    │    │    │    │    ├── key: (8)
 │    │    │    │    │    └── fd: (8)-->(9)
 │    │    │    │    └── filters
 │    │    │    │         └── b.x:8 = 3 [outer=(8), constraints=(/8: [/3 - /3]; tight), fd=()-->(8)]
 │    │    │    └── filters
 │    │    │         └── k:1 = b.x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
 │    │    ├── select
 │    │    │    ├── columns: c.x:12!null c.y:13!null c.z:14!null
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(12-14)
 │    │    │    ├── scan c
 │    │    │    │    ├── columns: c.x:12!null c.y:13!null c.z:14!null
 │    │    │    │    ├── key: (12)
 │    │    │    │    └── fd: (12)-->(13,14)
 │    │    │    └── filters
 │    │    │         └── c.x:12 = 3 [outer=(12), constraints=(/12: [/3 - /3]; tight), fd=()-->(12)]
 │    │    └── filters
 │    │         └── b.x:8 = c.x:12 [outer=(8,12), constraints=(/8: (/NULL - ]; /12: (/NULL - ]), fd=(8)==(12), (12)==(8)]
 │    ├── select
 │    │    ├── columns: d.x:17!null d.y:18!null d.z:19!null
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(17-19)
 │    │    ├── scan d
 │    │    │    ├── columns: d.x:17!null d.y:18!null d.z:19!null
 │    │    │    ├── key: (17)
 │    │    │    └── fd: (17)-->(18,19)
 │    │    └── filters
 │    │         └── d.x:17 = 3 [outer=(17), constraints=(/17: [/3 - /3]; tight), fd=()-->(17)]
 │    └── filters
 │         └── c.x:12 = d.x:17 [outer=(12,17), constraints=(/12: (/NULL - ]; /17: (/NULL - ]), fd=(12)==(17), (17)==(12)]
 ├── select
 │    ├── columns: xy.x:22!null xy.y:23
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(22,23)
 │    ├── scan xy
 │    │    ├── columns: xy.x:22!null xy.y:23
 │    │    ├── key: (22)
 │    │    └── fd: (22)-->(23)
 │    └── filters
 │         └── xy.x:22 = 3 [outer=(22), constraints=(/22: [/3 - /3]; tight), fd=()-->(22)]
 └── filters
      └── d.x:17 = xy.x:22 [outer=(17,22), constraints=(/17: (/NULL - ]; /22: (/NULL - ]), fd=(17)==(22), (22)==(17)]

# Regression test for #46151. Do not push down a filter with a correlated
# subquery.
norm expect-not=PushFilterIntoJoinLeftAndRight
SELECT (SELECT i_name FROM item LIMIT 1)
  FROM history INNER JOIN order_line ON h_data = ol_dist_info
 WHERE (
        EXISTS(
            SELECT *
              FROM history
             WHERE h_data IS NOT NULL AND ol_dist_info IS NOT NULL
        )
       )
    OR (SELECT ol_i_id FROM order_line LIMIT 1) IS NOT NULL;
----
project
 ├── columns: i_name:57
 ├── inner-join (hash)
 │    ├── columns: h_data:9!null ol_dist_info:21!null ol_i_id:39 exists:49!null
 │    ├── fd: (9)==(21), (21)==(9)
 │    ├── scan history
 │    │    └── columns: h_data:9
 │    ├── select
 │    │    ├── columns: ol_dist_info:21 ol_i_id:39 exists:49!null
 │    │    ├── left-join (cross)
 │    │    │    ├── columns: ol_dist_info:21 ol_i_id:39 exists:49!null
 │    │    │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 │    │    │    ├── project
 │    │    │    │    ├── columns: exists:49!null ol_dist_info:21
 │    │    │    │    ├── group-by (hash)
 │    │    │    │    │    ├── columns: ol_o_id:12!null ol_d_id:13!null ol_w_id:14!null ol_number:15!null ol_dist_info:21 canary_agg:48
 │    │    │    │    │    ├── grouping columns: ol_o_id:12!null ol_d_id:13!null ol_w_id:14!null ol_number:15!null
 │    │    │    │    │    ├── key: (12-15)
 │    │    │    │    │    ├── fd: (12-15)-->(21,48)
 │    │    │    │    │    ├── left-join (cross)
 │    │    │    │    │    │    ├── columns: ol_o_id:12!null ol_d_id:13!null ol_w_id:14!null ol_number:15!null ol_dist_info:21 rowid:24 h_data:32
 │    │    │    │    │    │    ├── fd: (12-15)-->(21)
 │    │    │    │    │    │    ├── scan order_line
 │    │    │    │    │    │    │    ├── columns: ol_o_id:12!null ol_d_id:13!null ol_w_id:14!null ol_number:15!null ol_dist_info:21
 │    │    │    │    │    │    │    ├── key: (12-15)
 │    │    │    │    │    │    │    └── fd: (12-15)-->(21)
 │    │    │    │    │    │    ├── select
 │    │    │    │    │    │    │    ├── columns: rowid:24!null h_data:32!null
 │    │    │    │    │    │    │    ├── scan history
 │    │    │    │    │    │    │    │    └── columns: rowid:24!null h_data:32
 │    │    │    │    │    │    │    └── filters
 │    │    │    │    │    │    │         └── h_data:32 IS NOT NULL [outer=(32), constraints=(/32: (/NULL - ]; tight)]
 │    │    │    │    │    │    └── filters
 │    │    │    │    │    │         └── ol_dist_info:21 IS NOT NULL [outer=(21), constraints=(/21: (/NULL - ]; tight)]
 │    │    │    │    │    └── aggregations
 │    │    │    │    │         ├── const-not-null-agg [as=canary_agg:48, outer=(24)]
 │    │    │    │    │         │    └── rowid:24
 │    │    │    │    │         └── const-agg [as=ol_dist_info:21, outer=(21)]
 │    │    │    │    │              └── ol_dist_info:21
 │    │    │    │    └── projections
 │    │    │    │         └── canary_agg:48 IS NOT NULL [as=exists:49, outer=(48)]
 │    │    │    ├── limit
 │    │    │    │    ├── columns: ol_i_id:39!null
 │    │    │    │    ├── cardinality: [0 - 1]
 │    │    │    │    ├── key: ()
 │    │    │    │    ├── fd: ()-->(39)
 │    │    │    │    ├── scan order_line
 │    │    │    │    │    ├── columns: ol_i_id:39!null
 │    │    │    │    │    └── limit hint: 1.00
 │    │    │    │    └── 1
 │    │    │    └── filters (true)
 │    │    └── filters
 │    │         └── exists:49 OR (ol_i_id:39 IS NOT NULL) [outer=(39,49)]
 │    └── filters
 │         └── h_data:9 = ol_dist_info:21 [outer=(9,21), constraints=(/9: (/NULL - ]; /21: (/NULL - ]), fd=(9)==(21), (21)==(9)]
 └── projections
      └── subquery [as=i_name:57, subquery]
           └── limit
                ├── columns: item.i_name:52
                ├── cardinality: [0 - 1]
                ├── key: ()
                ├── fd: ()-->(52)
                ├── scan item
                │    ├── columns: item.i_name:52
                │    └── limit hint: 1.00
                └── 1

# ---------------------------------
# MapEqualityIntoJoinLeftAndRight
# ---------------------------------

norm expect=MapEqualityIntoJoinLeftAndRight
SELECT * FROM (SELECT a.k AS a_k, b.x AS b_x FROM a, b) JOIN (SELECT c.x AS c_x, d.x AS d_x FROM c, d)
ON a_k = c_x AND c_x = b_x AND b_x = d_x
----
inner-join (hash)
 ├── columns: a_k:1!null b_x:8!null c_x:12!null d_x:17!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (17)
 ├── fd: (1)==(8,12,17), (8)==(1,12,17), (12)==(1,8,17), (17)==(1,8,12)
 ├── inner-join (hash)
 │    ├── columns: k:1!null b.x:8!null
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    ├── key: (8)
 │    ├── fd: (1)==(8), (8)==(1)
 │    ├── scan a
 │    │    ├── columns: k:1!null
 │    │    └── key: (1)
 │    ├── scan b
 │    │    ├── columns: b.x:8!null
 │    │    └── key: (8)
 │    └── filters
 │         └── k:1 = b.x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
 ├── inner-join (hash)
 │    ├── columns: c.x:12!null d.x:17!null
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    ├── key: (17)
 │    ├── fd: (12)==(17), (17)==(12)
 │    ├── scan c
 │    │    ├── columns: c.x:12!null
 │    │    └── key: (12)
 │    ├── scan d
 │    │    ├── columns: d.x:17!null
 │    │    └── key: (17)
 │    └── filters
 │         └── c.x:12 = d.x:17 [outer=(12,17), constraints=(/12: (/NULL - ]; /17: (/NULL - ]), fd=(12)==(17), (17)==(12)]
 └── filters
      └── k:1 = c.x:12 [outer=(1,12), constraints=(/1: (/NULL - ]; /12: (/NULL - ]), fd=(1)==(12), (12)==(1)]

norm expect=MapEqualityIntoJoinLeftAndRight
SELECT * FROM (SELECT b.x AS b_x, c.x AS c_x FROM b, c), d WHERE b_x=d.x AND c_x=d.x
----
inner-join (hash)
 ├── columns: b_x:1!null c_x:5!null x:10!null y:11!null z:12!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (10)
 ├── fd: (10)-->(11,12), (1)==(5,10), (5)==(1,10), (10)==(1,5)
 ├── inner-join (hash)
 │    ├── columns: b.x:1!null c.x:5!null
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    ├── key: (5)
 │    ├── fd: (1)==(5), (5)==(1)
 │    ├── scan b
 │    │    ├── columns: b.x:1!null
 │    │    └── key: (1)
 │    ├── scan c
 │    │    ├── columns: c.x:5!null
 │    │    └── key: (5)
 │    └── filters
 │         └── b.x:1 = c.x:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]
 ├── scan d
 │    ├── columns: d.x:10!null d.y:11!null d.z:12!null
 │    ├── key: (10)
 │    └── fd: (10)-->(11,12)
 └── filters
      └── b.x:1 = d.x:10 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)]

norm expect=MapEqualityIntoJoinLeftAndRight
SELECT * FROM b, c, d WHERE b.x=c.x AND b.x=d.x
----
inner-join (hash)
 ├── columns: x:1!null y:2 x:5!null y:6!null z:7!null x:10!null y:11!null z:12!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (10)
 ├── fd: (1)-->(2), (5)-->(6,7), (10)-->(11,12), (1)==(5,10), (5)==(1,10), (10)==(1,5)
 ├── scan b
 │    ├── columns: b.x:1!null b.y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── inner-join (hash)
 │    ├── columns: c.x:5!null c.y:6!null c.z:7!null d.x:10!null d.y:11!null d.z:12!null
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 │    ├── key: (10)
 │    ├── fd: (5)-->(6,7), (10)-->(11,12), (5)==(10), (10)==(5)
 │    ├── scan c
 │    │    ├── columns: c.x:5!null c.y:6!null c.z:7!null
 │    │    ├── key: (5)
 │    │    └── fd: (5)-->(6,7)
 │    ├── scan d
 │    │    ├── columns: d.x:10!null d.y:11!null d.z:12!null
 │    │    ├── key: (10)
 │    │    └── fd: (10)-->(11,12)
 │    └── filters
 │         └── c.x:5 = d.x:10 [outer=(5,10), constraints=(/5: (/NULL - ]; /10: (/NULL - ]), fd=(5)==(10), (10)==(5)]
 └── filters
      └── b.x:1 = c.x:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

norm expect=MapEqualityIntoJoinLeftAndRight
SELECT * FROM c INNER JOIN d ON c.x = d.x AND d.x = c.y AND c.y = d.y AND d.y = c.z AND c.z = d.z AND d.z = c.x
----
inner-join (hash)
 ├── columns: x:1!null y:2!null z:3!null x:6!null y:7!null z:8!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (6)
 ├── fd: (1)==(2,3,6-8), (2)==(1,3,6-8), (3)==(1,2,6-8), (6)==(1-3,7,8), (7)==(1-3,6,8), (8)==(1-3,6,7)
 ├── select
 │    ├── columns: c.x:1!null c.y:2!null c.z:3!null
 │    ├── key: (1)
 │    ├── fd: (1)==(2,3), (2)==(1,3), (3)==(1,2)
 │    ├── scan c
 │    │    ├── columns: c.x:1!null c.y:2!null c.z:3!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2,3)
 │    └── filters
 │         ├── c.x:1 = c.y:2 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 │         └── c.x:1 = c.z:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 ├── select
 │    ├── columns: d.x:6!null d.y:7!null d.z:8!null
 │    ├── key: (6)
 │    ├── fd: (6)==(7,8), (7)==(6,8), (8)==(6,7)
 │    ├── scan d
 │    │    ├── columns: d.x:6!null d.y:7!null d.z:8!null
 │    │    ├── key: (6)
 │    │    └── fd: (6)-->(7,8)
 │    └── filters
 │         ├── d.x:6 = d.y:7 [outer=(6,7), constraints=(/6: (/NULL - ]; /7: (/NULL - ]), fd=(6)==(7), (7)==(6)]
 │         └── d.x:6 = d.z:8 [outer=(6,8), constraints=(/6: (/NULL - ]; /8: (/NULL - ]), fd=(6)==(8), (8)==(6)]
 └── filters
      └── c.x:1 = d.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

norm expect=MapEqualityIntoJoinLeftAndRight
SELECT * from c, d WHERE c.x = c.y AND c.x = d.x AND c.y = d.y;
----
inner-join (hash)
 ├── columns: x:1!null y:2!null z:3!null x:6!null y:7!null z:8!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (6)
 ├── fd: (1)-->(3), (6)-->(8), (1)==(2,6,7), (2)==(1,6,7), (6)==(1,2,7), (7)==(1,2,6)
 ├── select
 │    ├── columns: c.x:1!null c.y:2!null c.z:3!null
 │    ├── key: (1)
 │    ├── fd: (1)-->(3), (1)==(2), (2)==(1)
 │    ├── scan c
 │    │    ├── columns: c.x:1!null c.y:2!null c.z:3!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2,3)
 │    └── filters
 │         └── c.x:1 = c.y:2 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 ├── select
 │    ├── columns: d.x:6!null d.y:7!null d.z:8!null
 │    ├── key: (6)
 │    ├── fd: (6)-->(8), (6)==(7), (7)==(6)
 │    ├── scan d
 │    │    ├── columns: d.x:6!null d.y:7!null d.z:8!null
 │    │    ├── key: (6)
 │    │    └── fd: (6)-->(7,8)
 │    └── filters
 │         └── d.x:6 = d.y:7 [outer=(6,7), constraints=(/6: (/NULL - ]; /7: (/NULL - ]), fd=(6)==(7), (7)==(6)]
 └── filters
      └── c.x:1 = d.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

norm expect=MapEqualityIntoJoinLeftAndRight
SELECT * FROM c, d WHERE c.x = d.x AND d.x = c.y AND c.y = d.y
----
inner-join (hash)
 ├── columns: x:1!null y:2!null z:3!null x:6!null y:7!null z:8!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (6)
 ├── fd: (1)-->(3), (6)-->(8), (1)==(2,6,7), (2)==(1,6,7), (6)==(1,2,7), (7)==(1,2,6)
 ├── select
 │    ├── columns: c.x:1!null c.y:2!null c.z:3!null
 │    ├── key: (1)
 │    ├── fd: (1)-->(3), (1)==(2), (2)==(1)
 │    ├── scan c
 │    │    ├── columns: c.x:1!null c.y:2!null c.z:3!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2,3)
 │    └── filters
 │         └── c.x:1 = c.y:2 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 ├── select
 │    ├── columns: d.x:6!null d.y:7!null d.z:8!null
 │    ├── key: (6)
 │    ├── fd: (6)-->(8), (6)==(7), (7)==(6)
 │    ├── scan d
 │    │    ├── columns: d.x:6!null d.y:7!null d.z:8!null
 │    │    ├── key: (6)
 │    │    └── fd: (6)-->(7,8)
 │    └── filters
 │         └── d.x:6 = d.y:7 [outer=(6,7), constraints=(/6: (/NULL - ]; /7: (/NULL - ]), fd=(6)==(7), (7)==(6)]
 └── filters
      └── c.x:1 = d.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

exec-ddl
create table aa (a int, a1 int, a2 int)
----

exec-ddl
create table bb (b int, b1 int, b2 int)
----

exec-ddl
create table cc (c int, c1 int, c2 int)
----

norm expect=MapEqualityIntoJoinLeftAndRight
select * from aa, bb where a2 = b and b = a and a = b1 and b1 = a1
----
inner-join (hash)
 ├── columns: a:1!null a1:2!null a2:3!null b:7!null b1:8!null b2:9
 ├── fd: (1)==(2,3,7,8), (2)==(1,3,7,8), (3)==(1,2,7,8), (7)==(1-3,8), (8)==(1-3,7)
 ├── select
 │    ├── columns: a:1!null a1:2!null a2:3!null
 │    ├── fd: (1)==(2,3), (2)==(1,3), (3)==(1,2)
 │    ├── scan aa
 │    │    └── columns: a:1 a1:2 a2:3
 │    └── filters
 │         ├── a:1 = a1:2 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 │         └── a:1 = a2:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 ├── select
 │    ├── columns: b:7!null b1:8!null b2:9
 │    ├── fd: (7)==(8), (8)==(7)
 │    ├── scan bb
 │    │    └── columns: b:7 b1:8 b2:9
 │    └── filters
 │         └── b:7 = b1:8 [outer=(7,8), constraints=(/7: (/NULL - ]; /8: (/NULL - ]), fd=(7)==(8), (8)==(7)]
 └── filters
      └── a:1 = b:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]

# --------------------------------------------------
# PushFilterIntoJoinLeft + PushFilterIntoJoinRight
# --------------------------------------------------

norm expect=(PushFilterIntoJoinLeft,PushFilterIntoJoinRight)
SELECT * FROM a INNER JOIN b ON a.k=b.x AND a.i=1 AND b.y=1
----
inner-join (hash)
 ├── columns: k:1!null i:2!null f:3!null s:4 j:5 x:8!null y:9!null
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: ()-->(2,9), (1)-->(3-5), (1)==(8), (8)==(1)
 ├── select
 │    ├── columns: k:1!null i:2!null f:3!null s:4 j:5
 │    ├── key: (1)
 │    ├── fd: ()-->(2), (1)-->(3-5)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    └── filters
 │         └── i:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]
 ├── select
 │    ├── columns: x:8!null y:9!null
 │    ├── key: (8)
 │    ├── fd: ()-->(9)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── y:9 = 1 [outer=(9), constraints=(/9: [/1 - /1]; tight), fd=()-->(9)]
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# FULL JOIN should not push down conditions to either side of join.
norm expect-not=(PushFilterIntoJoinLeft,PushFilterIntoJoinRight)
SELECT * FROM a FULL JOIN b ON a.k=b.x AND a.i=1 AND b.y=1
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 x:8 y:9
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      ├── i:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]
      └── y:9 = 1 [outer=(9), constraints=(/9: [/1 - /1]; tight), fd=()-->(9)]

# Nested semi/anti-join case.
norm expect=PushFilterIntoJoinRight
SELECT * FROM b
WHERE EXISTS
(
    SELECT * FROM a WHERE k=x AND s='foo' AND NOT EXISTS(SELECT * FROM a WHERE i=10 AND y>100)
)
----
semi-join-apply
 ├── columns: x:1!null y:2
 ├── key: (1)
 ├── fd: (1)-->(2)
 ├── scan b
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── anti-join (cross)
 │    ├── columns: k:5!null s:8!null
 │    ├── outer: (2)
 │    ├── key: (5)
 │    ├── fd: ()-->(8)
 │    ├── select
 │    │    ├── columns: k:5!null s:8!null
 │    │    ├── key: (5)
 │    │    ├── fd: ()-->(8)
 │    │    ├── scan a
 │    │    │    ├── columns: k:5!null s:8
 │    │    │    ├── key: (5)
 │    │    │    └── fd: (5)-->(8)
 │    │    └── filters
 │    │         └── s:8 = 'foo' [outer=(8), constraints=(/8: [/'foo' - /'foo']; tight), fd=()-->(8)]
 │    ├── select
 │    │    ├── columns: i:13!null
 │    │    ├── fd: ()-->(13)
 │    │    ├── scan a
 │    │    │    └── columns: i:13
 │    │    └── filters
 │    │         └── i:13 = 10 [outer=(13), constraints=(/13: [/10 - /10]; tight), fd=()-->(13)]
 │    └── filters
 │         └── y:2 > 100 [outer=(2), constraints=(/2: [/101 - ]; tight)]
 └── filters
      └── k:5 = x:1 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

# --------------------------------------------------
# SimplifyLeftJoin + SimplifyRightJoin
# --------------------------------------------------
norm expect=SimplifyLeftJoin
SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12
 ├── key: (8)
 ├── fd: (8)-->(9-12), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── projections
      ├── a2.k:8 [as=a.k:1, outer=(8)]
      ├── a2.i:9 [as=a.i:2, outer=(9)]
      ├── a2.f:10 [as=a.f:3, outer=(10)]
      ├── a2.s:11 [as=a.s:4, outer=(11)]
      └── a2.j:12 [as=a.j:5, outer=(12)]

# Right side has partial rows, so only right-join can be simplified.
norm expect=SimplifyRightJoin
SELECT * FROM a FULL JOIN (SELECT * FROM a WHERE k>0) AS a2 ON a.k=a2.k
----
left-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8 i:9 f:10 s:11 j:12
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── key: (1)
 ├── fd: (1)-->(2-5,8-12), (8)-->(9-12)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── select
 │    ├── columns: k:8!null i:9 f:10!null s:11 j:12
 │    ├── key: (8)
 │    ├── fd: (8)-->(9-12)
 │    ├── scan a
 │    │    ├── columns: k:8!null i:9 f:10!null s:11 j:12
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9-12)
 │    └── filters
 │         └── k:8 > 0 [outer=(8), constraints=(/8: [/1 - ]; tight)]
 └── filters
      └── k:1 = k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Multiple equality conditions, with duplicates and reversed columns.
norm expect=SimplifyLeftJoin
SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k AND a.k=a2.k AND a2.f=a.f
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12
 ├── key: (8)
 ├── fd: (8)-->(9-12), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── projections
      ├── a2.k:8 [as=a.k:1, outer=(8)]
      ├── a2.i:9 [as=a.i:2, outer=(9)]
      ├── a2.f:10 [as=a.f:3, outer=(10)]
      ├── a2.s:11 [as=a.s:4, outer=(11)]
      └── a2.j:12 [as=a.j:5, outer=(12)]

# Input contains Project operator.
norm expect=SimplifyLeftJoin
SELECT * FROM (SELECT length(s), f FROM a) AS a FULL JOIN a AS a2 ON a.f=a2.f
----
inner-join (hash)
 ├── columns: length:8 f:3!null k:9!null i:10 f:11!null s:12 j:13
 ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 ├── immutable
 ├── fd: (9)-->(10-13), (3)==(11), (11)==(3)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:9!null a2.i:10 a2.f:11!null a2.s:12 a2.j:13
 │    ├── key: (9)
 │    └── fd: (9)-->(10-13)
 ├── project
 │    ├── columns: length:8 a.f:3!null
 │    ├── immutable
 │    ├── scan a
 │    │    └── columns: a.f:3!null a.s:4
 │    └── projections
 │         └── length(a.s:4) [as=length:8, outer=(4), immutable]
 └── filters
      └── a.f:3 = a2.f:11 [outer=(3,11), constraints=(/3: (/NULL - ]; /11: (/NULL - ]), fd=(3)==(11), (11)==(3)]

# Multiple join levels.
norm expect=SimplifyLeftJoin
SELECT * FROM a FULL JOIN (SELECT * FROM a INNER JOIN a AS a2 ON a.k=a2.k) AS a2 ON a.f=a2.f
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12 k:15!null i:16 f:17!null s:18 j:19
 ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 ├── key: (1,8)
 ├── fd: (8)-->(9-12), (1)-->(2-5), (8)==(15), (15)==(8), (9)==(16), (16)==(9), (3)==(10,17), (10)==(3,17), (17)==(3,10), (11)==(18), (18)==(11), (12)==(19), (19)==(12)
 ├── project
 │    ├── columns: a2.k:15!null a2.i:16 a2.f:17!null a2.s:18 a2.j:19 a.k:8!null a.i:9 a.f:10!null a.s:11 a.j:12
 │    ├── key: (8)
 │    ├── fd: (8)-->(9-12), (8)==(15), (15)==(8), (9)==(16), (16)==(9), (10)==(17), (17)==(10), (11)==(18), (18)==(11), (12)==(19), (19)==(12)
 │    ├── scan a
 │    │    ├── columns: a.k:8!null a.i:9 a.f:10!null a.s:11 a.j:12
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9-12)
 │    └── projections
 │         ├── a.k:8 [as=a2.k:15, outer=(8)]
 │         ├── a.i:9 [as=a2.i:16, outer=(9)]
 │         ├── a.f:10 [as=a2.f:17, outer=(10)]
 │         ├── a.s:11 [as=a2.s:18, outer=(11)]
 │         └── a.j:12 [as=a2.j:19, outer=(12)]
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 └── filters
      └── a.f:3 = a.f:10 [outer=(3,10), constraints=(/3: (/NULL - ]; /10: (/NULL - ]), fd=(3)==(10), (10)==(3)]

# Left joins on a foreign key turn into inner joins.
norm expect=SimplifyLeftJoin
SELECT *
FROM c
LEFT OUTER JOIN a
ON c.y = a.k
----
inner-join (hash)
 ├── columns: x:1!null y:2!null z:3!null k:6!null i:7 f:8!null s:9 j:10
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: (1)-->(2,3), (6)-->(7-10), (2)==(6), (6)==(2)
 ├── scan c
 │    ├── columns: x:1!null y:2!null z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters
      └── y:2 = k:6 [outer=(2,6), constraints=(/2: (/NULL - ]; /6: (/NULL - ]), fd=(2)==(6), (6)==(2)]

# Left joins on a multiple-column foreign key turn into inner joins.
norm expect=SimplifyLeftJoin
SELECT *
FROM d
LEFT OUTER JOIN c
ON d.z = c.z
AND d.y = c.x
----
inner-join (hash)
 ├── columns: x:1!null y:2!null z:3!null x:6!null y:7!null z:8!null
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: (1)-->(2,3), (6)-->(7,8), (3)==(8), (8)==(3), (2)==(6), (6)==(2)
 ├── scan d
 │    ├── columns: d.x:1!null d.y:2!null d.z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan c
 │    ├── columns: c.x:6!null c.y:7!null c.z:8!null
 │    ├── key: (6)
 │    └── fd: (6)-->(7,8)
 └── filters
      ├── d.z:3 = c.z:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]
      └── d.y:2 = c.x:6 [outer=(2,6), constraints=(/2: (/NULL - ]; /6: (/NULL - ]), fd=(2)==(6), (6)==(2)]

# Left join on a part of a foreign key turns into an inner join.
norm expect=SimplifyLeftJoin
SELECT *
FROM d
LEFT OUTER JOIN c
ON d.z = c.z
----
inner-join (hash)
 ├── columns: x:1!null y:2!null z:3!null x:6!null y:7!null z:8!null
 ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
 ├── key: (1,6)
 ├── fd: (1)-->(2,3), (6)-->(7,8), (3)==(8), (8)==(3)
 ├── scan d
 │    ├── columns: d.x:1!null d.y:2!null d.z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan c
 │    ├── columns: c.x:6!null c.y:7!null c.z:8!null
 │    ├── key: (6)
 │    └── fd: (6)-->(7,8)
 └── filters
      └── d.z:3 = c.z:8 [outer=(3,8), constraints=(/3: (/NULL - ]; /8: (/NULL - ]), fd=(3)==(8), (8)==(3)]

# Cross join case. The presence of a not-null foreign key implies that there
# will be at least one right row when there is at least one left row, so left
# rows will always be matched at least once.
norm expect=SimplifyLeftJoin
SELECT *
FROM d
LEFT OUTER JOIN c
ON True
----
inner-join (cross)
 ├── columns: x:1!null y:2!null z:3!null x:6!null y:7!null z:8!null
 ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
 ├── key: (1,6)
 ├── fd: (1)-->(2,3), (6)-->(7,8)
 ├── scan d
 │    ├── columns: d.x:1!null d.y:2!null d.z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan c
 │    ├── columns: c.x:6!null c.y:7!null c.z:8!null
 │    ├── key: (6)
 │    └── fd: (6)-->(7,8)
 └── filters (true)

norm expect=SimplifyRightJoin
SELECT * FROM (SELECT count(*) FROM b) FULL JOIN a ON True
----
left-join (cross)
 ├── columns: count:5!null k:6 i:7 f:8 s:9 j:10
 ├── cardinality: [1 - ]
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (6)
 ├── fd: ()-->(5), (6)-->(7-10)
 ├── scalar-group-by
 │    ├── columns: count_rows:5!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(5)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:5]
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters (true)

# Full-join.
norm expect=SimplifyRightJoin
SELECT * FROM (SELECT count(*) FROM b) FULL JOIN a ON True
----
left-join (cross)
 ├── columns: count:5!null k:6 i:7 f:8 s:9 j:10
 ├── cardinality: [1 - ]
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (6)
 ├── fd: ()-->(5), (6)-->(7-10)
 ├── scalar-group-by
 │    ├── columns: count_rows:5!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(5)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:5]
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters (true)

# Full-join.
norm expect=SimplifyRightJoin
SELECT * FROM (SELECT count(*) FROM b) FULL JOIN a ON True
----
left-join (cross)
 ├── columns: count:5!null k:6 i:7 f:8 s:9 j:10
 ├── cardinality: [1 - ]
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (6)
 ├── fd: ()-->(5), (6)-->(7-10)
 ├── scalar-group-by
 │    ├── columns: count_rows:5!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(5)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:5]
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters (true)

norm expect=SimplifyLeftJoin
SELECT * FROM a LEFT JOIN (SELECT count(*) FROM b) ON True
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 count:12!null
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: ()-->(12), (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scalar-group-by
 │    ├── columns: count_rows:12!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(12)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:12]
 └── filters (true)

# Full-join.
norm expect=SimplifyLeftJoin
SELECT * FROM a FULL JOIN (SELECT count(*) FROM b) ON True
----
left-join (cross)
 ├── columns: k:1 i:2 f:3 s:4 j:5 count:12!null
 ├── cardinality: [1 - ]
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (1)
 ├── fd: ()-->(12), (1)-->(2-5)
 ├── scalar-group-by
 │    ├── columns: count_rows:12!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(12)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:12]
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 └── filters (true)

# Left-join-apply.
norm expect=SimplifyLeftJoin
SELECT * FROM a WHERE (SELECT sum(column1) FROM (VALUES (k), (1))) = 1
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 └── select
      ├── columns: k:1!null i:2 f:3!null s:4 j:5 sum:9!null
      ├── immutable
      ├── key: (1)
      ├── fd: ()-->(9), (1)-->(2-5)
      ├── group-by (hash)
      │    ├── columns: k:1!null i:2 f:3!null s:4 j:5 sum:9
      │    ├── grouping columns: k:1!null
      │    ├── key: (1)
      │    ├── fd: (1)-->(2-5,9)
      │    ├── inner-join-apply
      │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5 column1:8
      │    │    ├── fd: (1)-->(2-5)
      │    │    ├── scan a
      │    │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
      │    │    │    ├── key: (1)
      │    │    │    └── fd: (1)-->(2-5)
      │    │    ├── values
      │    │    │    ├── columns: column1:8
      │    │    │    ├── outer: (1)
      │    │    │    ├── cardinality: [2 - 2]
      │    │    │    ├── (k:1,)
      │    │    │    └── (1,)
      │    │    └── filters (true)
      │    └── aggregations
      │         ├── sum [as=sum:9, outer=(8)]
      │         │    └── column1:8
      │         ├── const-agg [as=i:2, outer=(2)]
      │         │    └── i:2
      │         ├── const-agg [as=f:3, outer=(3)]
      │         │    └── f:3
      │         ├── const-agg [as=s:4, outer=(4)]
      │         │    └── s:4
      │         └── const-agg [as=j:5, outer=(5)]
      │              └── j:5
      └── filters
           └── sum:9 = 1 [outer=(9), immutable, constraints=(/9: [/1 - /1]; tight), fd=()-->(9)]

# Don't simplify left join
norm expect-not=SimplifyRightJoin
SELECT * FROM (SELECT count(*) FROM b) LEFT JOIN a ON True
----
left-join (cross)
 ├── columns: count:5!null k:6 i:7 f:8 s:9 j:10
 ├── cardinality: [1 - ]
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (6)
 ├── fd: ()-->(5), (6)-->(7-10)
 ├── scalar-group-by
 │    ├── columns: count_rows:5!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(5)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:5]
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters (true)

# Don't simplify right join
norm expect-not=SimplifyLeftJoin
SELECT * FROM a RIGHT JOIN (SELECT count(*) FROM b) ON True
----
left-join (cross)
 ├── columns: k:1 i:2 f:3 s:4 j:5 count:12!null
 ├── cardinality: [1 - ]
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (1)
 ├── fd: ()-->(12), (1)-->(2-5)
 ├── scalar-group-by
 │    ├── columns: count_rows:12!null
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(12)
 │    ├── scan b
 │    └── aggregations
 │         └── count-rows [as=count_rows:12]
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 └── filters (true)

# Can't simplify: joins on non-foreign keys.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT *
FROM c
LEFT OUTER JOIN a
ON c.z = a.k
----
left-join (hash)
 ├── columns: x:1!null y:2!null z:3!null k:6 i:7 f:8 s:9 j:10
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: (1)-->(2,3,6-10), (6)-->(7-10)
 ├── scan c
 │    ├── columns: x:1!null y:2!null z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters
      └── z:3 = k:6 [outer=(3,6), constraints=(/3: (/NULL - ]; /6: (/NULL - ]), fd=(3)==(6), (6)==(3)]

# Can't simplify: joins on non-foreign keys still in foreign key index.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT *
FROM c
LEFT OUTER JOIN a
ON c.x = a.k
----
left-join (hash)
 ├── columns: x:1!null y:2!null z:3!null k:6 i:7 f:8 s:9 j:10
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 ├── key: (1)
 ├── fd: (1)-->(2,3,6-10), (6)-->(7-10)
 ├── scan c
 │    ├── columns: x:1!null y:2!null z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters
      └── x:1 = k:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]

# Can't simplify: non-equality condition.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a FULL JOIN a AS a2 ON a.k<a2.k
----
full-join (cross)
 ├── columns: k:1 i:2 f:3 s:4 j:5 k:8 i:9 f:10 s:11 j:12
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9-12)
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── filters
      └── a.k:1 < a2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]

# Can't simplify: non-join equality condition.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a FULL JOIN a AS a2 ON a.f=1 AND a.f=a2.f
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 k:8 i:9 f:10 s:11 j:12
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9-12)
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── filters
      ├── a.f:3 = 1.0 [outer=(3), constraints=(/3: [/1.0 - /1.0]; tight), fd=()-->(3)]
      └── a.f:3 = a2.f:10 [outer=(3,10), constraints=(/3: (/NULL - ]; /10: (/NULL - ]), fd=(3)==(10), (10)==(3)]

# Can't simplify: non-null column.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a FULL JOIN a AS a2 ON a.s=a2.s
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 k:8 i:9 f:10 s:11 j:12
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9-12)
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── filters
      └── a.s:4 = a2.s:11 [outer=(4,11), constraints=(/4: (/NULL - ]; /11: (/NULL - ]), fd=(4)==(11), (11)==(4)]

# Can't simplify: equality column that is synthesized.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a FULL JOIN (SELECT k+1 AS k FROM a) AS a2 ON a.k=a2.k
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 k:15
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── immutable
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: a.k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── project
 │    ├── columns: k:15!null
 │    ├── immutable
 │    ├── scan a
 │    │    ├── columns: a.k:8!null
 │    │    └── key: (8)
 │    └── projections
 │         └── a.k:8 + 1 [as=k:15, outer=(8), immutable]
 └── filters
      └── a.k:1 = k:15 [outer=(1,15), constraints=(/1: (/NULL - ]; /15: (/NULL - ]), fd=(1)==(15), (15)==(1)]

# Can't simplify: equality condition with different column ordinals.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.f
----
full-join (cross)
 ├── columns: k:1 i:2 f:3 s:4 j:5 k:8 i:9 f:10 s:11 j:12
 ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9-12)
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── filters
      └── a.k:1 = a2.f:10 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)]

# Can't simplify: one equality condition has columns from same side of join.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k AND a.i=a.f AND a2.i=a2.f
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 k:8 i:9 f:10 s:11 j:12
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9-12)
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── filters
      ├── a.k:1 = a2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
      ├── a.i:2 = a.f:3 [outer=(2,3), constraints=(/2: (/NULL - ]; /3: (/NULL - ]), fd=(2)==(3), (3)==(2)]
      └── a2.i:9 = a2.f:10 [outer=(9,10), constraints=(/9: (/NULL - ]; /10: (/NULL - ]), fd=(9)==(10), (10)==(9)]

# Can't simplify: equality conditions have columns from different tables.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM (SELECT * FROM a, b) AS a FULL JOIN a AS a2 ON a.k=a2.k AND a.x=a2.k
----
full-join (hash)
 ├── columns: k:1 i:2 f:3 s:4 j:5 x:8 y:9 k:12 i:13 f:14 s:15 j:16
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── key: (1,8,12)
 ├── fd: (1)-->(2-5), (8)-->(9), (12)-->(13-16)
 ├── inner-join (cross)
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5 x:8!null y:9
 │    ├── key: (1,8)
 │    ├── fd: (1)-->(2-5), (8)-->(9)
 │    ├── scan a
 │    │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5)
 │    ├── scan b
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters (true)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:12!null a2.i:13 a2.f:14!null a2.s:15 a2.j:16
 │    ├── key: (12)
 │    └── fd: (12)-->(13-16)
 └── filters
      ├── a.k:1 = a2.k:12 [outer=(1,12), constraints=(/1: (/NULL - ]; /12: (/NULL - ]), fd=(1)==(12), (12)==(1)]
      └── x:8 = a2.k:12 [outer=(8,12), constraints=(/8: (/NULL - ]; /12: (/NULL - ]), fd=(8)==(12), (12)==(8)]

# Can't simplify: The a2.x column is not part of unfilteredCols.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT * FROM a LEFT JOIN (SELECT * FROM a, b) AS a2 ON a.k=a2.x
----
left-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8 i:9 f:10 s:11 j:12 x:15 y:16
 ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-one)
 ├── key: (1,8,15)
 ├── fd: (1)-->(2-5), (8)-->(9-12), (15)-->(16)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── inner-join (cross)
 │    ├── columns: k:8!null i:9 f:10!null s:11 j:12 x:15!null y:16
 │    ├── key: (8,15)
 │    ├── fd: (8)-->(9-12), (15)-->(16)
 │    ├── scan a
 │    │    ├── columns: k:8!null i:9 f:10!null s:11 j:12
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9-12)
 │    ├── scan b
 │    │    ├── columns: x:15!null y:16
 │    │    ├── key: (15)
 │    │    └── fd: (15)-->(16)
 │    └── filters (true)
 └── filters
      └── k:1 = x:15 [outer=(1,15), constraints=(/1: (/NULL - ]; /15: (/NULL - ]), fd=(1)==(15), (15)==(1)]

# Can't simplify if IGNORE_FOREIGN_KEYS hint is passed.
norm expect-not=(SimplifyRightJoin,SimplifyLeftJoin)
SELECT *
FROM c@{IGNORE_FOREIGN_KEYS}
LEFT OUTER JOIN a
ON c.y = a.k
----
left-join (hash)
 ├── columns: x:1!null y:2!null z:3!null k:6 i:7 f:8 s:9 j:10
 ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 ├── key: (1)
 ├── fd: (1)-->(2,3,6-10), (6)-->(7-10)
 ├── scan c
 │    ├── columns: x:1!null y:2!null z:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 ├── scan a
 │    ├── columns: k:6!null i:7 f:8!null s:9 j:10
 │    ├── key: (6)
 │    └── fd: (6)-->(7-10)
 └── filters
      └── y:2 = k:6 [outer=(2,6), constraints=(/2: (/NULL - ]; /6: (/NULL - ]), fd=(2)==(6), (6)==(2)]

# --------------------------------------------------
# EliminateSemiJoin
# --------------------------------------------------
norm expect=EliminateSemiJoin
SELECT * FROM a WHERE EXISTS(SELECT count(*) FROM b WHERE x=k)
----
scan a
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 └── fd: (1)-->(2-5)

norm expect=EliminateSemiJoin
SELECT * FROM a WHERE EXISTS(VALUES (k))
----
scan a
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 └── fd: (1)-->(2-5)

# Foreign key case.
norm expect=EliminateSemiJoin
SELECT * FROM c WHERE EXISTS (SELECT * FROM a WHERE k = y)
----
scan c
 ├── columns: x:1!null y:2!null z:3!null
 ├── key: (1)
 └── fd: (1)-->(2,3)

# Self-join case.
norm expect=EliminateSemiJoin
SELECT * FROM a a1 WHERE EXISTS (SELECT * FROM a a2 WHERE a1.k = a2.k)
----
scan a [as=a1]
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 └── fd: (1)-->(2-5)

# No-op case because rows from a are not guaranteed to be matched on the join
# filters.
norm expect-not=EliminateSemiJoin
SELECT * FROM a WHERE EXISTS (SELECT * FROM c WHERE k = y)
----
semi-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 ├── fd: (1)-->(2-5)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan c
 │    └── columns: y:9!null
 └── filters
      └── k:1 = y:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# --------------------------------------------------
# SimplifyZeroCardinalitySemiJoin
# --------------------------------------------------
# TODO(justin): figure out if there's a good way to make this still apply.
norm disable=(SimplifyZeroCardinalityGroup,EliminateExistsZeroRows) expect=SimplifyZeroCardinalitySemiJoin
SELECT * FROM a WHERE EXISTS(SELECT * FROM (VALUES (k)) OFFSET 1)
----
values
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1-5)

# --------------------------------------------------
# EliminateAntiJoin
# --------------------------------------------------
# TODO(justin): figure out if there's a good way to make this still apply.
norm disable=(SimplifyZeroCardinalityGroup,EliminateExistsZeroRows) expect=EliminateAntiJoin
SELECT * FROM a WHERE NOT EXISTS(SELECT * FROM (VALUES (k)) OFFSET 1)
----
scan a
 ├── columns: k:1!null i:2 f:3!null s:4 j:5
 ├── key: (1)
 └── fd: (1)-->(2-5)

# --------------------------------------------------
# SimplifyZeroCardinalityAntiJoin
# --------------------------------------------------
norm expect=SimplifyZeroCardinalityAntiJoin
SELECT * FROM a WHERE NOT EXISTS(SELECT count(*) FROM b WHERE x=k)
----
values
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1-5)

norm expect=SimplifyZeroCardinalityAntiJoin
SELECT * FROM a WHERE NOT EXISTS(VALUES (k))
----
values
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1-5)

# --------------------------------------------------
# EliminateJoinNoColsLeft
# --------------------------------------------------
norm expect=EliminateJoinNoColsLeft
SELECT unnest(ARRAY[[1,2,3],[4,5]])
----
values
 ├── columns: unnest:1!null
 ├── cardinality: [2 - 2]
 ├── (ARRAY[1,2,3],)
 └── (ARRAY[4,5],)

# --------------------------------------------------
# EliminateJoinNoColsRight
# --------------------------------------------------
norm expect=EliminateJoinNoColsRight
SELECT * FROM xy WHERE EXISTS(SELECT generate_series(x, 10))
----
group-by (hash)
 ├── columns: x:1!null y:2
 ├── grouping columns: x:1!null
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2)
 ├── project-set
 │    ├── columns: x:1!null y:2 generate_series:5
 │    ├── immutable
 │    ├── fd: (1)-->(2)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    └── zip
 │         └── generate_series(x:1, 10) [outer=(1), immutable]
 └── aggregations
      └── const-agg [as=y:2, outer=(2)]
           └── y:2

# --------------------------------------------------
# HoistJoinProjectRight
#   InnerJoinApply and LeftJoinApply tested by TryDecorrelateLimitOne tests.
# --------------------------------------------------

# Inner-join case.
norm expect=HoistJoinProjectRight
SELECT * FROM a INNER JOIN (SELECT x FROM b WHERE y=10) ON x=k
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null
 ├── key: (8)
 ├── fd: (1)-->(2-5), (1)==(8), (8)==(1)
 └── inner-join (hash)
      ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9!null
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
      ├── key: (8)
      ├── fd: ()-->(9), (1)-->(2-5), (1)==(8), (8)==(1)
      ├── scan a
      │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
      │    ├── key: (1)
      │    └── fd: (1)-->(2-5)
      ├── select
      │    ├── columns: x:8!null y:9!null
      │    ├── key: (8)
      │    ├── fd: ()-->(9)
      │    ├── scan b
      │    │    ├── columns: x:8!null y:9
      │    │    ├── key: (8)
      │    │    └── fd: (8)-->(9)
      │    └── filters
      │         └── y:9 = 10 [outer=(9), constraints=(/9: [/10 - /10]; tight), fd=()-->(9)]
      └── filters
           └── x:8 = k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Left-join case.
norm expect=HoistJoinProjectRight
SELECT * FROM a LEFT JOIN (SELECT x FROM b WHERE y=10) ON x=k
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8
 ├── key: (1)
 ├── fd: (1)-->(2-5,8)
 └── left-join (hash)
      ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8 y:9
      ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      ├── key: (1)
      ├── fd: (1)-->(2-5,8,9)
      ├── scan a
      │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
      │    ├── key: (1)
      │    └── fd: (1)-->(2-5)
      ├── select
      │    ├── columns: x:8!null y:9!null
      │    ├── key: (8)
      │    ├── fd: ()-->(9)
      │    ├── scan b
      │    │    ├── columns: x:8!null y:9
      │    │    ├── key: (8)
      │    │    └── fd: (8)-->(9)
      │    └── filters
      │         └── y:9 = 10 [outer=(9), constraints=(/9: [/10 - /10]; tight), fd=()-->(9)]
      └── filters
           └── x:8 = k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# --------------------------------------------------
# HoistJoinProjectLeft
# --------------------------------------------------

# Inner-join case.
norm expect=HoistJoinProjectLeft
SELECT * FROM (SELECT x FROM b WHERE y=10) INNER JOIN a ON x=k
----
project
 ├── columns: x:1!null k:5!null i:6 f:7!null s:8 j:9
 ├── key: (5)
 ├── fd: (5)-->(6-9), (1)==(5), (5)==(1)
 └── inner-join (hash)
      ├── columns: x:1!null y:2!null k:5!null i:6 f:7!null s:8 j:9
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
      ├── key: (5)
      ├── fd: ()-->(2), (5)-->(6-9), (1)==(5), (5)==(1)
      ├── select
      │    ├── columns: x:1!null y:2!null
      │    ├── key: (1)
      │    ├── fd: ()-->(2)
      │    ├── scan b
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── filters
      │         └── y:2 = 10 [outer=(2), constraints=(/2: [/10 - /10]; tight), fd=()-->(2)]
      ├── scan a
      │    ├── columns: k:5!null i:6 f:7!null s:8 j:9
      │    ├── key: (5)
      │    └── fd: (5)-->(6-9)
      └── filters
           └── x:1 = k:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

# Left-join case.
norm expect=HoistJoinProjectLeft
SELECT * FROM (SELECT x FROM b WHERE y=10) LEFT JOIN a ON x=k
----
project
 ├── columns: x:1!null k:5 i:6 f:7 s:8 j:9
 ├── key: (1)
 ├── fd: (5)-->(6-9), (1)-->(5-9)
 └── left-join (hash)
      ├── columns: x:1!null y:2!null k:5 i:6 f:7 s:8 j:9
      ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      ├── key: (1)
      ├── fd: ()-->(2), (5)-->(6-9), (1)-->(5-9)
      ├── select
      │    ├── columns: x:1!null y:2!null
      │    ├── key: (1)
      │    ├── fd: ()-->(2)
      │    ├── scan b
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── filters
      │         └── y:2 = 10 [outer=(2), constraints=(/2: [/10 - /10]; tight), fd=()-->(2)]
      ├── scan a
      │    ├── columns: k:5!null i:6 f:7!null s:8 j:9
      │    ├── key: (5)
      │    └── fd: (5)-->(6-9)
      └── filters
           └── x:1 = k:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

# --------------------------------------------------
# SimplifyJoinNotNullEquality
# --------------------------------------------------
norm expect=SimplifyJoinNotNullEquality disable=SimplifyJoinFilters
SELECT * FROM a INNER JOIN b ON (a.k=b.x) IS True
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: (1)-->(2-5), (8)-->(9), (1)==(8), (8)==(1)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

norm expect=SimplifyJoinNotNullEquality disable=SimplifyJoinFilters
SELECT * FROM a INNER JOIN b ON (a.k=b.x) IS False
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── k:1 != x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]

norm expect=SimplifyJoinNotNullEquality
SELECT * FROM a INNER JOIN b ON (a.k=b.x) IS Null
----
values
 ├── columns: k:1!null i:2!null f:3!null s:4!null j:5!null x:8!null y:9!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1-5,8,9)

norm expect=SimplifyJoinNotNullEquality
SELECT * FROM a INNER JOIN b ON (a.k=b.x) IS NOT True
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── k:1 != x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]

norm expect=SimplifyJoinNotNullEquality
SELECT * FROM a INNER JOIN b ON (a.k=b.x) IS NOT False
----
inner-join (hash)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (8)
 ├── fd: (1)-->(2-5), (8)-->(9), (1)==(8), (8)==(1)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      └── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

norm expect=SimplifyJoinNotNullEquality
SELECT * FROM a INNER JOIN b ON (a.k=b.x) IS NOT Null
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters (true)

# Simply multiple conditions, with other conditions present as well.
norm expect=SimplifyJoinNotNullEquality
SELECT *
FROM (SELECT * FROM a WHERE i>0) AS a
INNER JOIN (SELECT x, y, y+1 AS z FROM b WHERE y>10) AS b
ON a.f>=b.z::float AND (a.k=b.x) IS True AND a.f>=b.z::float AND (a.i=b.y) IS NOT False
----
project
 ├── columns: k:1!null i:2!null f:3!null s:4 j:5 x:8!null y:9!null z:12!null
 ├── immutable
 ├── key: (8)
 ├── fd: (1)-->(2-5), (8)-->(9), (9)-->(12), (1)==(8), (8)==(1), (2)==(9), (9)==(2)
 └── inner-join (hash)
      ├── columns: k:1!null i:2!null f:3!null s:4 j:5 x:8!null y:9!null z:12!null column13:13!null column14:14!null
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
      ├── immutable
      ├── key: (8)
      ├── fd: (1)-->(2-5), (8)-->(9), (9)-->(12), (12)-->(13,14), (1)==(8), (8)==(1), (2)==(9), (9)==(2)
      ├── select
      │    ├── columns: k:1!null i:2!null f:3!null s:4 j:5
      │    ├── key: (1)
      │    ├── fd: (1)-->(2-5)
      │    ├── scan a
      │    │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2-5)
      │    └── filters
      │         └── i:2 > 0 [outer=(2), constraints=(/2: [/1 - ]; tight)]
      ├── project
      │    ├── columns: column14:14!null column13:13!null x:8!null y:9!null z:12!null
      │    ├── immutable
      │    ├── key: (8)
      │    ├── fd: (8)-->(9), (9)-->(12), (12)-->(13,14)
      │    ├── project
      │    │    ├── columns: z:12!null x:8!null y:9!null
      │    │    ├── immutable
      │    │    ├── key: (8)
      │    │    ├── fd: (8)-->(9), (9)-->(12)
      │    │    ├── select
      │    │    │    ├── columns: x:8!null y:9!null
      │    │    │    ├── key: (8)
      │    │    │    ├── fd: (8)-->(9)
      │    │    │    ├── scan b
      │    │    │    │    ├── columns: x:8!null y:9
      │    │    │    │    ├── key: (8)
      │    │    │    │    └── fd: (8)-->(9)
      │    │    │    └── filters
      │    │    │         └── y:9 > 10 [outer=(9), constraints=(/9: [/11 - ]; tight)]
      │    │    └── projections
      │    │         └── y:9 + 1 [as=z:12, outer=(9), immutable]
      │    └── projections
      │         ├── z:12::FLOAT8 [as=column14:14, outer=(12), immutable]
      │         └── z:12::FLOAT8 [as=column13:13, outer=(12), immutable]
      └── filters
           ├── k:1 = x:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
           ├── i:2 = y:9 [outer=(2,9), constraints=(/2: (/NULL - ]; /9: (/NULL - ]), fd=(2)==(9), (9)==(2)]
           ├── f:3 >= column13:13 [outer=(3,13), constraints=(/3: (/NULL - ]; /13: (/NULL - ])]
           └── f:3 >= column14:14 [outer=(3,14), constraints=(/3: (/NULL - ]; /14: (/NULL - ])]

# Don't trigger rule when one of the variables is nullable.
norm expect-not=SimplifyJoinNotNullEquality
SELECT * FROM a INNER JOIN b ON (a.k=b.y) IS Null AND (a.i=b.x) IS NOT False
----
inner-join (cross)
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 x:8!null y:9
 ├── key: (1,8)
 ├── fd: (1)-->(2-5), (8)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 ├── scan b
 │    ├── columns: x:8!null y:9
 │    ├── key: (8)
 │    └── fd: (8)-->(9)
 └── filters
      ├── (k:1 = y:9) IS NULL [outer=(1,9)]
      └── (i:2 = x:8) IS NOT false [outer=(2,8)]

# --------------------------------------------------
# ExtractJoinComparisons
# --------------------------------------------------

norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y=u
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2,5,6), (5)-->(6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
      ├── immutable
      ├── key: (1)
      ├── fd: (1)-->(2,9), (5)-->(6), (5)==(9), (9)==(5)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           └── column9:9 = u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ]), fd=(5)==(9), (9)==(5)]

norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON u=x+y
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2,5,6), (5)-->(6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
      ├── immutable
      ├── key: (1)
      ├── fd: (1)-->(2,9), (5)-->(6), (5)==(9), (9)==(5)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           └── column9:9 = u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ]), fd=(5)==(9), (9)==(5)]

norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x=u+v
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (5)
 ├── fd: (1)-->(2), (5)-->(1,2,6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
      ├── immutable
      ├── key: (5)
      ├── fd: (1)-->(2), (5)-->(6,9), (1)==(9), (9)==(1)
      ├── scan xy
      │    ├── columns: x:1!null y:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      ├── project
      │    ├── columns: column9:9 u:5!null v:6
      │    ├── immutable
      │    ├── key: (5)
      │    ├── fd: (5)-->(6,9)
      │    ├── scan uv
      │    │    ├── columns: u:5!null v:6
      │    │    ├── key: (5)
      │    │    └── fd: (5)-->(6)
      │    └── projections
      │         └── u:5 + v:6 [as=column9:9, outer=(5,6), immutable]
      └── filters
           └── x:1 = column9:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON u+v=x
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (5)
 ├── fd: (1)-->(2), (5)-->(1,2,6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
      ├── immutable
      ├── key: (5)
      ├── fd: (1)-->(2), (5)-->(6,9), (1)==(9), (9)==(1)
      ├── scan xy
      │    ├── columns: x:1!null y:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      ├── project
      │    ├── columns: column9:9 u:5!null v:6
      │    ├── immutable
      │    ├── key: (5)
      │    ├── fd: (5)-->(6,9)
      │    ├── scan uv
      │    │    ├── columns: u:5!null v:6
      │    │    ├── key: (5)
      │    │    └── fd: (5)-->(6)
      │    └── projections
      │         └── u:5 + v:6 [as=column9:9, outer=(5,6), immutable]
      └── filters
           └── x:1 = column9:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y=u+v
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null column10:10!null
      ├── immutable
      ├── key: (1,5)
      ├── fd: (1)-->(2,9), (5)-->(6,10), (9)==(10), (10)==(9)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── project
      │    ├── columns: column10:10 u:5!null v:6
      │    ├── immutable
      │    ├── key: (5)
      │    ├── fd: (5)-->(6,10)
      │    ├── scan uv
      │    │    ├── columns: u:5!null v:6
      │    │    ├── key: (5)
      │    │    └── fd: (5)-->(6)
      │    └── projections
      │         └── u:5 + v:6 [as=column10:10, outer=(5,6), immutable]
      └── filters
           └── column9:9 = column10:10 [outer=(9,10), constraints=(/9: (/NULL - ]; /10: (/NULL - ]), fd=(9)==(10), (10)==(9)]

# Multiple extractable equalities.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y=u AND x=u+v AND x*y+1=u*v+2
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (5)
 ├── fd: (1)-->(2,5,6), (5)-->(1,2,6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null column10:10!null column11:11!null column12:12!null
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
      ├── immutable
      ├── key: (5)
      ├── fd: (1)-->(2,9,11), (5)-->(6,10,12), (5)==(9), (9)==(5), (1)==(10), (10)==(1), (11)==(12), (12)==(11)
      ├── project
      │    ├── columns: column11:11 column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9,11)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         ├── (x:1 * y:2) + 1 [as=column11:11, outer=(1,2), immutable]
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── project
      │    ├── columns: column12:12 column10:10 u:5!null v:6
      │    ├── immutable
      │    ├── key: (5)
      │    ├── fd: (5)-->(6,10,12)
      │    ├── scan uv
      │    │    ├── columns: u:5!null v:6
      │    │    ├── key: (5)
      │    │    └── fd: (5)-->(6)
      │    └── projections
      │         ├── (u:5 * v:6) + 2 [as=column12:12, outer=(5,6), immutable]
      │         └── u:5 + v:6 [as=column10:10, outer=(5,6), immutable]
      └── filters
           ├── column9:9 = u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ]), fd=(5)==(9), (9)==(5)]
           ├── x:1 = column10:10 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)]
           └── column11:11 = column12:12 [outer=(11,12), constraints=(/11: (/NULL - ]; /12: (/NULL - ]), fd=(11)==(12), (12)==(11)]

# An extractable equality with another expression.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y=u AND x+u=v
----
project
 ├── columns: x:1!null y:2 u:5!null v:6!null
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(2,5,6), (5)-->(6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6!null column9:9!null
      ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
      ├── immutable
      ├── key: (1)
      ├── fd: (1)-->(2,6,9), (5)-->(6), (5)==(9), (9)==(5)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           ├── v:6 = (x:1 + u:5) [outer=(1,5,6), immutable, constraints=(/6: (/NULL - ]), fd=(1,5)-->(6)]
           └── column9:9 = u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ]), fd=(5)==(9), (9)==(5)]

# Computed columns with matching expressions should be reused as projection
# columns.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN comp ON x=i+10 AND y=abs(i)
----
inner-join (hash)
 ├── columns: x:1!null y:2!null i:5 c:6!null v:7!null
 ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
 ├── immutable
 ├── fd: (1)-->(2), (5)-->(6,7), (1)==(6), (6)==(1), (2)==(7), (7)==(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── project
 │    ├── columns: v:7 i:5 c:6
 │    ├── immutable
 │    ├── fd: (5)-->(6,7)
 │    ├── scan comp
 │    │    ├── columns: i:5 c:6
 │    │    ├── computed column expressions
 │    │    │    ├── c:6
 │    │    │    │    └── i:5 + 10
 │    │    │    └── v:7
 │    │    │         └── abs(i:5)
 │    │    └── fd: (5)-->(6)
 │    └── projections
 │         └── abs(i:5) [as=v:7, outer=(5), immutable]
 └── filters
      ├── x:1 = c:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
      └── y:2 = v:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]

# Computed columns with matching expressions should be reused as projection
# columns.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN comp ON i+10=x AND abs(i)=y
----
inner-join (hash)
 ├── columns: x:1!null y:2!null i:5 c:6!null v:7!null
 ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
 ├── immutable
 ├── fd: (1)-->(2), (5)-->(6,7), (1)==(6), (6)==(1), (2)==(7), (7)==(2)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── project
 │    ├── columns: v:7 i:5 c:6
 │    ├── immutable
 │    ├── fd: (5)-->(6,7)
 │    ├── scan comp
 │    │    ├── columns: i:5 c:6
 │    │    ├── computed column expressions
 │    │    │    ├── c:6
 │    │    │    │    └── i:5 + 10
 │    │    │    └── v:7
 │    │    │         └── abs(i:5)
 │    │    └── fd: (5)-->(6)
 │    └── projections
 │         └── abs(i:5) [as=v:7, outer=(5), immutable]
 └── filters
      ├── x:1 = c:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)]
      └── y:2 = v:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)]

# Cases with non-extractable equality.
norm expect-not=ExtractJoinComparisons
SELECT * FROM xy FULL OUTER JOIN uv ON x=u
----
full-join (hash)
 ├── columns: x:1 y:2 u:5 v:6
 ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan uv
 │    ├── columns: u:5!null v:6
 │    ├── key: (5)
 │    └── fd: (5)-->(6)
 └── filters
      └── x:1 = u:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]

norm expect-not=ExtractJoinComparisons
SELECT * FROM xy FULL OUTER JOIN uv ON x+y=1
----
full-join (cross)
 ├── columns: x:1 y:2 u:5 v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan uv
 │    ├── columns: u:5!null v:6
 │    ├── key: (5)
 │    └── fd: (5)-->(6)
 └── filters
      └── (x:1 + y:2) = 1 [outer=(1,2), immutable]

norm expect-not=ExtractJoinComparisons
SELECT * FROM xy FULL OUTER JOIN uv ON 1=u+v
----
full-join (cross)
 ├── columns: x:1 y:2 u:5 v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan uv
 │    ├── columns: u:5!null v:6
 │    ├── key: (5)
 │    └── fd: (5)-->(6)
 └── filters
      └── (u:5 + v:6) = 1 [outer=(5,6), immutable]

norm expect-not=ExtractJoinComparisons
SELECT * FROM xy INNER JOIN uv ON (SELECT k FROM a WHERE i=x)=u
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── key: (1,5)
 ├── fd: (1)-->(2), (1,5)-->(6)
 └── inner-join-apply
      ├── columns: x:1!null y:2 u:5!null v:6 k:9
      ├── key: (1,5)
      ├── fd: (1)-->(2), (1,5)-->(6,9), (5)==(9), (9)==(5)
      ├── scan xy
      │    ├── columns: x:1!null y:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      ├── ensure-distinct-on
      │    ├── columns: u:5!null v:6 k:9
      │    ├── grouping columns: u:5!null
      │    ├── error: "more than one row returned by a subquery used as an expression"
      │    ├── outer: (1)
      │    ├── key: (5)
      │    ├── fd: (5)-->(6,9)
      │    ├── left-join (cross)
      │    │    ├── columns: u:5!null v:6 k:9 i:10
      │    │    ├── outer: (1)
      │    │    ├── key: (5,9)
      │    │    ├── fd: (5)-->(6), (9)-->(10)
      │    │    ├── scan uv
      │    │    │    ├── columns: u:5!null v:6
      │    │    │    ├── key: (5)
      │    │    │    └── fd: (5)-->(6)
      │    │    ├── scan a
      │    │    │    ├── columns: k:9!null i:10
      │    │    │    ├── key: (9)
      │    │    │    └── fd: (9)-->(10)
      │    │    └── filters
      │    │         └── i:10 = x:1 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)]
      │    └── aggregations
      │         ├── const-agg [as=v:6, outer=(6)]
      │         │    └── v:6
      │         └── const-agg [as=k:9, outer=(9)]
      │              └── k:9
      └── filters
           └── u:5 = k:9 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ]), fd=(5)==(9), (9)==(5)]

norm expect-not=ExtractJoinComparisons
SELECT * FROM xy INNER JOIN uv ON x=(SELECT k FROM a WHERE i=u)
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── key: (5)
 ├── fd: (1)-->(2), (5)-->(1,2,6)
 └── inner-join (hash)
      ├── columns: x:1!null y:2 u:5!null v:6 k:9!null
      ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
      ├── key: (5)
      ├── fd: (1)-->(2), (5)-->(6,9), (1)==(9), (9)==(1)
      ├── scan xy
      │    ├── columns: x:1!null y:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      ├── ensure-distinct-on
      │    ├── columns: u:5!null v:6 k:9
      │    ├── grouping columns: u:5!null
      │    ├── error: "more than one row returned by a subquery used as an expression"
      │    ├── key: (5)
      │    ├── fd: (5)-->(6,9)
      │    ├── left-join (hash)
      │    │    ├── columns: u:5!null v:6 k:9 i:10
      │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-one)
      │    │    ├── key: (5,9)
      │    │    ├── fd: (5)-->(6), (9)-->(10)
      │    │    ├── scan uv
      │    │    │    ├── columns: u:5!null v:6
      │    │    │    ├── key: (5)
      │    │    │    └── fd: (5)-->(6)
      │    │    ├── scan a
      │    │    │    ├── columns: k:9!null i:10
      │    │    │    ├── key: (9)
      │    │    │    └── fd: (9)-->(10)
      │    │    └── filters
      │    │         └── i:10 = u:5 [outer=(5,10), constraints=(/5: (/NULL - ]; /10: (/NULL - ]), fd=(5)==(10), (10)==(5)]
      │    └── aggregations
      │         ├── const-agg [as=v:6, outer=(6)]
      │         │    └── v:6
      │         └── const-agg [as=k:9, outer=(9)]
      │              └── k:9
      └── filters
           └── x:1 = k:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ]), fd=(1)==(9), (9)==(1)]

# Don't extract equalities where one side is an expression with no outer cols
# (#44746). This is a rare case where we can't constant fold because the
# function call errors out.
norm expect-not=ExtractJoinComparisons
SELECT * FROM xy FULL JOIN uv ON (substring('', ')') = '') = (u > 0)
----
full-join (cross)
 ├── columns: x:1 y:2 u:5 v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 ├── scan uv
 │    ├── columns: u:5!null v:6
 │    ├── key: (5)
 │    └── fd: (5)-->(6)
 └── filters
      └── (substring('', ')') = '') = (u:5 > 0) [outer=(5), immutable]

# Case with < operator.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y<u
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 └── inner-join (cross)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── immutable
      ├── key: (1,5)
      ├── fd: (1)-->(2,9), (5)-->(6)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           └── column9:9 < u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ])]

# Case with <= operator.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y<=u
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 └── inner-join (cross)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── immutable
      ├── key: (1,5)
      ├── fd: (1)-->(2,9), (5)-->(6)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           └── column9:9 <= u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ])]

# Case with > operator.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y>u
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 └── inner-join (cross)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── immutable
      ├── key: (1,5)
      ├── fd: (1)-->(2,9), (5)-->(6)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           └── column9:9 > u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ])]

# Case with >= operator.
norm expect=ExtractJoinComparisons
SELECT * FROM xy JOIN uv ON x+y>=u
----
project
 ├── columns: x:1!null y:2 u:5!null v:6
 ├── immutable
 ├── key: (1,5)
 ├── fd: (1)-->(2), (5)-->(6)
 └── inner-join (cross)
      ├── columns: x:1!null y:2 u:5!null v:6 column9:9!null
      ├── immutable
      ├── key: (1,5)
      ├── fd: (1)-->(2,9), (5)-->(6)
      ├── project
      │    ├── columns: column9:9 x:1!null y:2
      │    ├── immutable
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,9)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── projections
      │         └── x:1 + y:2 [as=column9:9, outer=(1,2), immutable]
      ├── scan uv
      │    ├── columns: u:5!null v:6
      │    ├── key: (5)
      │    └── fd: (5)-->(6)
      └── filters
           └── column9:9 >= u:5 [outer=(5,9), constraints=(/5: (/NULL - ]; /9: (/NULL - ])]

# --------------------------------------------------
# LeftAssociateJoinsLeft,  LeftAssociateJoinsRight,
# RightAssociateJoinsLeft,  RightAssociateJoinsRight
# --------------------------------------------------

# Common use case.
norm expect=RightAssociateJoinsLeft
SELECT * FROM xy, a, uv WHERE x=u AND y=k
----
inner-join (hash)
 ├── columns: x:1!null y:2!null k:5!null i:6 f:7!null s:8 j:9 u:12!null v:13
 ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one)
 ├── key: (12)
 ├── fd: (1)-->(2), (5)-->(6-9), (12)-->(13), (2)==(5), (5)==(2), (1)==(12), (12)==(1)
 ├── inner-join (hash)
 │    ├── columns: x:1!null y:2!null k:5!null i:6 f:7!null s:8 j:9
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
 │    ├── key: (1)
 │    ├── fd: (1)-->(2), (5)-->(6-9), (2)==(5), (5)==(2)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    ├── scan a
 │    │    ├── columns: k:5!null i:6 f:7!null s:8 j:9
 │    │    ├── key: (5)
 │    │    └── fd: (5)-->(6-9)
 │    └── filters
 │         └── y:2 = k:5 [outer=(2,5), constraints=(/2: (/NULL - ]; /5: (/NULL - ]), fd=(2)==(5), (5)==(2)]
 ├── scan uv
 │    ├── columns: u:12!null v:13
 │    ├── key: (12)
 │    └── fd: (12)-->(13)
 └── filters
      └── x:1 = u:12 [outer=(1,12), constraints=(/1: (/NULL - ]; /12: (/NULL - ]), fd=(1)==(12), (12)==(1)]

# LeftAssociateJoinsLeft case with one association operation.
norm expect=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER JOIN a
ON k=x
----
inner-join (cross)
 ├── scan uv
 ├── inner-join (hash)
 │    ├── scan xy
 │    ├── scan a
 │    └── filters
 │         └── k = x
 └── filters (true)

# LeftAssociateJoinsRight case with one association operation.
norm expect=LeftAssociateJoinsRight format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER JOIN a
ON i=u
----
inner-join (cross)
 ├── scan xy
 ├── inner-join (hash)
 │    ├── scan uv
 │    ├── scan a
 │    └── filters
 │         └── i = u
 └── filters (true)

# RightAssociateJoinsLeft case with one association operation.
norm expect=RightAssociateJoinsLeft format=hide-all
SELECT * FROM a
INNER JOIN (SELECT * FROM xy INNER JOIN uv ON True)
ON k=x
----
inner-join (cross)
 ├── inner-join (hash)
 │    ├── scan a
 │    ├── scan xy
 │    └── filters
 │         └── k = x
 ├── scan uv
 └── filters (true)

# RightAssociateJoinsRight case with one association operation.
norm expect=RightAssociateJoinsRight format=hide-all
SELECT * FROM a
INNER JOIN (SELECT * FROM xy INNER JOIN uv ON True)
ON i=u
----
inner-join (cross)
 ├── inner-join (hash)
 │    ├── scan a
 │    ├── scan uv
 │    └── filters
 │         └── i = u
 ├── scan xy
 └── filters (true)

# LeftAssociateJoinsLeft case with multiple filters referencing the left input.
norm expect=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER JOIN a
ON k=x AND i=y
----
inner-join (cross)
 ├── scan uv
 ├── inner-join (hash)
 │    ├── scan xy
 │    ├── scan a
 │    └── filters
 │         ├── k = x
 │         └── i = y
 └── filters (true)

# LeftAssociateJoinsLeft case with multiple filters referencing both inputs.
norm expect=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER JOIN a
ON k=x AND i=u
----
inner-join (hash)
 ├── scan uv
 ├── inner-join (hash)
 │    ├── scan xy
 │    ├── scan a
 │    └── filters
 │         └── k = x
 └── filters
      └── i = u

# Case with multiple associations. Also worst-case scenario, since 9 operations
# have to be performed with 6 joins in the tree.
norm expect=(LeftAssociateJoinsLeft,LeftAssociateJoinsRight) format=hide-all
SELECT *
FROM xy
INNER JOIN xy AS xy1 ON True
INNER JOIN xy AS xy2 ON True
INNER JOIN xy AS xy3 ON True
INNER JOIN uv AS uv1 ON xy.x=uv1.u
INNER JOIN uv AS uv2 ON xy.x=uv2.u
INNER JOIN uv AS uv3 ON xy.x=uv3.u
----
inner-join (cross)
 ├── scan xy [as=xy3]
 ├── inner-join (cross)
 │    ├── scan xy [as=xy2]
 │    ├── inner-join (cross)
 │    │    ├── scan xy [as=xy1]
 │    │    ├── inner-join (hash)
 │    │    │    ├── inner-join (hash)
 │    │    │    │    ├── inner-join (hash)
 │    │    │    │    │    ├── scan xy
 │    │    │    │    │    ├── scan uv [as=uv1]
 │    │    │    │    │    └── filters
 │    │    │    │    │         └── xy.x = uv1.u
 │    │    │    │    ├── scan uv [as=uv2]
 │    │    │    │    └── filters
 │    │    │    │         └── xy.x = uv2.u
 │    │    │    ├── scan uv [as=uv3]
 │    │    │    └── filters
 │    │    │         └── xy.x = uv3.u
 │    │    └── filters (true)
 │    └── filters (true)
 └── filters (true)

# No-op case because the lower join isn't a cross join.
norm expect-not=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON u=x)
INNER JOIN a
ON k=x
----
inner-join (hash)
 ├── inner-join (hash)
 │    ├── scan xy
 │    ├── scan uv
 │    └── filters
 │         └── u = x
 ├── scan a
 └── filters
      └── k = x

# No-op case because the upper join is a cross join.
norm expect-not=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER JOIN a
ON True
----
inner-join (cross)
 ├── inner-join (cross)
 │    ├── scan xy
 │    ├── scan uv
 │    └── filters (true)
 ├── scan a
 └── filters (true)

# No-op case because the lower join has join hints.
norm expect-not=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER MERGE JOIN uv ON True)
INNER JOIN a
ON k=x
----
inner-join (hash)
 ├── inner-join (cross)
 │    ├── flags: force merge join
 │    ├── scan xy
 │    ├── scan uv
 │    └── filters (true)
 ├── scan a
 └── filters
      └── k = x

# No-op case because the upper join has join hints.
norm expect-not=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER MERGE JOIN a
ON k=x
----
inner-join (hash)
 ├── flags: force merge join
 ├── inner-join (cross)
 │    ├── scan xy
 │    ├── scan uv
 │    └── filters (true)
 ├── scan a
 └── filters
      └── k = x

# No-op case for LeftAssociateJoinsLeft because the upper filter is bound by the
# lower join's right input (instead of left).
norm expect-not=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM (SELECT * FROM xy INNER JOIN uv ON True)
INNER JOIN a
ON i=u
----
inner-join (cross)
 ├── scan xy
 ├── inner-join (hash)
 │    ├── scan uv
 │    ├── scan a
 │    └── filters
 │         └── i = u
 └── filters (true)

# No-op case for LeftAssociateJoinsLeft because the lower join is the right
# input of the upper join.
norm expect-not=LeftAssociateJoinsLeft format=hide-all
SELECT * FROM a
INNER JOIN (SELECT * FROM xy INNER JOIN uv ON True)
ON k=x
----
inner-join (cross)
 ├── inner-join (hash)
 │    ├── scan a
 │    ├── scan xy
 │    └── filters
 │         └── k = x
 ├── scan uv
 └── filters (true)

# --------------------------------------------------
# RemoveJoinNotNullCondition
# --------------------------------------------------

# Left join case.
norm expect=RemoveJoinNotNullCondition
SELECT * FROM a LEFT JOIN a AS a2 ON a.k=a2.k AND a.f=a.f AND a2.f=a2.f
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12
 ├── key: (1)
 ├── fd: (1)-->(2-5), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5)
 ├── scan a
 │    ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 └── projections
      ├── a.k:1 [as=a2.k:8, outer=(1)]
      ├── a.i:2 [as=a2.i:9, outer=(2)]
      ├── a.f:3 [as=a2.f:10, outer=(3)]
      ├── a.s:4 [as=a2.s:11, outer=(4)]
      └── a.j:5 [as=a2.j:12, outer=(5)]

# Full join case.
norm expect=RemoveJoinNotNullCondition
SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k AND a.f=a.f AND a2.f=a2.f
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12
 ├── key: (8)
 ├── fd: (8)-->(9-12), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5)
 ├── scan a [as=a2]
 │    ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12
 │    ├── key: (8)
 │    └── fd: (8)-->(9-12)
 └── projections
      ├── a2.k:8 [as=a.k:1, outer=(8)]
      ├── a2.i:9 [as=a.i:2, outer=(9)]
      ├── a2.f:10 [as=a.f:3, outer=(10)]
      ├── a2.s:11 [as=a.s:4, outer=(11)]
      └── a2.j:12 [as=a.j:5, outer=(12)]

# No-op case because i is nullable.
norm expect-not=RemoveJoinNotNullCondition
SELECT i FROM a
FULL JOIN xy ON k < y AND i IS NOT NULL
----
project
 ├── columns: i:2
 └── full-join (cross)
      ├── columns: k:1 i:2 y:9
      ├── fd: (1)-->(2)
      ├── scan a
      │    ├── columns: k:1!null i:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      ├── scan xy
      │    └── columns: y:9
      └── filters
           ├── k:1 < y:9 [outer=(1,9), constraints=(/1: (/NULL - ]; /9: (/NULL - ])]
           └── i:2 IS NOT NULL [outer=(2), constraints=(/2: (/NULL - ]; tight)]

# No-op case because i+k can be NULL.
norm expect-not=RemoveJoinNotNullCondition
SELECT k FROM a
FULL JOIN xy ON i+k IS NOT NULL
----
project
 ├── columns: k:1
 ├── immutable
 └── full-join (cross)
      ├── columns: k:1 i:2
      ├── immutable
      ├── fd: (1)-->(2)
      ├── scan a
      │    ├── columns: k:1!null i:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      ├── scan xy
      └── filters
           └── (i:2 + k:1) IS NOT NULL [outer=(1,2), immutable]

# --------------------------------------------------
# ProjectInnerJoinValues
# --------------------------------------------------
norm expect=ProjectInnerJoinValues
SELECT (SELECT CASE WHEN ord.approved THEN 'Approved' ELSE '---' END)
FROM (VALUES (1, true), (2, false)) ord(id, approved)
----
project
 ├── columns: case:4!null
 ├── cardinality: [2 - 2]
 ├── values
 │    ├── columns: column2:2!null
 │    ├── cardinality: [2 - 2]
 │    ├── (true,)
 │    └── (false,)
 └── projections
      └── CASE WHEN column2:2 THEN 'Approved' ELSE '---' END [as=case:4, outer=(2)]

norm expect=ProjectInnerJoinValues
SELECT k, i - (SELECT f::int + i) FROM a
----
project
 ├── columns: k:1!null "?column?":9
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(9)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2,3)
 └── projections
      └── i:2 - (i:2 + f:3::INT8) [as="?column?":9, outer=(2,3), immutable]

# Correlated subquery with 0 rows.
norm expect-not=ProjectInnerJoinValues
SELECT k, i - (SELECT f::int LIMIT 0) FROM a
----
project
 ├── columns: k:1!null "?column?":9
 ├── immutable
 ├── key: (1)
 ├── fd: (1)-->(9)
 ├── left-join-apply
 │    ├── columns: k:1!null i:2 a.f:3!null f:8
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(2,3,8)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2 a.f:3!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2,3)
 │    ├── limit
 │    │    ├── columns: f:8
 │    │    ├── outer: (3)
 │    │    ├── cardinality: [0 - 0]
 │    │    ├── immutable
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(8)
 │    │    ├── values
 │    │    │    ├── columns: f:8
 │    │    │    ├── outer: (3)
 │    │    │    ├── cardinality: [1 - 1]
 │    │    │    ├── immutable
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(8)
 │    │    │    ├── limit hint: 1.00
 │    │    │    └── (a.f:3::INT8,)
 │    │    └── 0
 │    └── filters (true)
 └── projections
      └── i:2 - f:8 [as="?column?":9, outer=(2,8), immutable]

# Inner join with single-row values.
norm expect=ProjectInnerJoinValues
SELECT a.k, vals.value
FROM a INNER JOIN (SELECT 1, 9) vals(k, value) ON a.k = vals.k
----
project
 ├── columns: k:1!null value:9!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1,9)
 ├── select
 │    ├── columns: k:1!null
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1)
 │    ├── scan a
 │    │    ├── columns: k:1!null
 │    │    └── key: (1)
 │    └── filters
 │         └── k:1 = 1 [outer=(1), constraints=(/1: [/1 - /1]; tight), fd=()-->(1)]
 └── projections
      └── 9 [as="?column?":9]

# Inner join with zero-row values.
norm expect-not=ProjectInnerJoinValues
SELECT a.k, vals.value
FROM a INNER JOIN (SELECT 1, 9 LIMIT 0) vals(k, value) ON a.k = vals.k
----
values
 ├── columns: k:1!null value:9!null
 ├── cardinality: [0 - 0]
 ├── key: ()
 └── fd: ()-->(1,9)

# Inner join with multi-row values.
norm expect-not=ProjectInnerJoinValues
SELECT a.k, vals.value
FROM a
INNER JOIN (SELECT 1, 10 UNION SELECT 2, 20) vals(k, value)
ON a.k = vals.k
----
project
 ├── columns: k:1!null value:13!null
 ├── cardinality: [0 - 2]
 ├── key: (1,13)
 └── inner-join (hash)
      ├── columns: k:1!null "?column?":12!null "?column?":13!null
      ├── cardinality: [0 - 2]
      ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one)
      ├── key: (12,13)
      ├── fd: (1)==(12), (12)==(1)
      ├── scan a
      │    ├── columns: k:1!null
      │    └── key: (1)
      ├── union
      │    ├── columns: "?column?":12!null "?column?":13!null
      │    ├── left columns: "?column?":8 "?column?":9
      │    ├── right columns: "?column?":10 "?column?":11
      │    ├── cardinality: [1 - 2]
      │    ├── key: (12,13)
      │    ├── values
      │    │    ├── columns: "?column?":8!null "?column?":9!null
      │    │    ├── cardinality: [1 - 1]
      │    │    ├── key: ()
      │    │    ├── fd: ()-->(8,9)
      │    │    └── (1, 10)
      │    └── values
      │         ├── columns: "?column?":10!null "?column?":11!null
      │         ├── cardinality: [1 - 1]
      │         ├── key: ()
      │         ├── fd: ()-->(10,11)
      │         └── (2, 20)
      └── filters
           └── k:1 = "?column?":12 [outer=(1,12), constraints=(/1: (/NULL - ]; /12: (/NULL - ]), fd=(1)==(12), (12)==(1)]

# Lateral inner join with single-row values.
norm expect=ProjectInnerJoinValues
SELECT *
FROM a
INNER JOIN LATERAL (VALUES (a.k, a.k + 1, a.s, 10)) vals(c1, c2, c3, c4)
ON a.k = c1
----
project
 ├── columns: k:1!null i:2 f:3!null s:4 j:5 c1:8!null c2:9!null c3:10 c4:11!null
 ├── immutable
 ├── key: (1)
 ├── fd: ()-->(11), (1)-->(2-5,9), (1)==(8), (8)==(1), (4)==(10), (10)==(4)
 ├── scan a
 │    ├── columns: k:1!null i:2 f:3!null s:4 j:5
 │    ├── key: (1)
 │    └── fd: (1)-->(2-5)
 └── projections
      ├── k:1 [as=column1:8, outer=(1)]
      ├── k:1 + 1 [as=column2:9, outer=(1), immutable]
      ├── s:4 [as=column3:10, outer=(4)]
      └── 10 [as=column4:11]

# Lateral inner join with hidden single-row values (passed through for sort).
norm expect=ProjectInnerJoinValues
SELECT a.k
FROM a
INNER JOIN LATERAL (VALUES (a.k, a.k + 1, a.s, 10)) vals(c1, c2, c3, c4)
ON a.k = c1
ORDER BY c3 DESC
----
sort
 ├── columns: k:1!null  [hidden: column3:10]
 ├── key: (1)
 ├── fd: (1)-->(10)
 ├── ordering: -10
 └── project
      ├── columns: column3:10 k:1!null
      ├── key: (1)
      ├── fd: (1)-->(10)
      ├── scan a
      │    ├── columns: k:1!null s:4
      │    ├── key: (1)
      │    └── fd: (1)-->(4)
      └── projections
           └── s:4 [as=column3:10, outer=(4)]
