exec-ddl
CREATE TABLE a
(
    k INT PRIMARY KEY,
    i INT NOT NULL,
    f FLOAT,
    s STRING NOT NULL,
    j JSON,
    UNIQUE INDEX si_idx (s DESC, i) STORING (j),
    UNIQUE INDEX fi_idx (f, i)
)
----

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

exec-ddl
CREATE TABLE fks
(
    k INT PRIMARY KEY,
    v INT,
    r1 INT NOT NULL REFERENCES xy(x),
    r2 INT REFERENCES xy(x)
)
----

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

exec-ddl
CREATE TABLE uvwz
(
    u INT NOT NULL,
    v INT NOT NULL,
    w INT NOT NULL,
    z INT NOT NULL,

    UNIQUE INDEX (u,v),
    UNIQUE INDEX (v,w)
)
----

exec-ddl
CREATE TABLE s (
    s STRING PRIMARY KEY
)
----

exec-ddl
CREATE TABLE nullablecols (
    c1 INT,
    c2 INT,
    c3 INT,
    UNIQUE (c1),
    UNIQUE (c2,c3)
)
----

exec-ddl
CREATE TABLE xyzbs
(
  x INT PRIMARY KEY,
  y INT,
  z INT NOT NULL,
  b BOOL NOT NULL,
  s TEXT,
  INDEX (y),
  INDEX (s)
)
----

# --------------------------------------------------
# ConvertGroupByToDistinct
# --------------------------------------------------
norm expect=ConvertGroupByToDistinct
SELECT s, f FROM a GROUP BY s, f
----
distinct-on
 ├── columns: s:4!null f:3
 ├── grouping columns: f:3 s:4!null
 ├── key: (3,4)
 └── scan a
      └── columns: f:3 s:4!null

# Group by not converted to DistinctOn because it has an aggregation.
norm expect-not=ConvertGroupByToDistinct
SELECT s, f, sum(f) FROM a GROUP BY s, f
----
group-by (hash)
 ├── columns: s:4!null f:3 sum:8
 ├── grouping columns: f:3 s:4!null
 ├── key: (3,4)
 ├── fd: (3,4)-->(8)
 ├── scan a
 │    └── columns: f:3 s:4!null
 └── aggregations
      └── sum [as=sum:8, outer=(3)]
           └── f:3

# --------------------------------------------------
# EliminateJoinUnderGroupByLeft
# --------------------------------------------------

# Simple DistinctOn case with a LeftJoin.
norm expect=EliminateJoinUnderGroupByLeft
SELECT DISTINCT ON (x) x, y FROM xy LEFT JOIN fks ON x=v
----
scan xy
 ├── columns: x:1!null y:2
 ├── key: (1)
 └── fd: (1)-->(2)

# RightJoin case. The RightJoin is turned into a LeftJoin, so
# EliminateJoinUnderGroupByLeft matches it.
norm expect=EliminateJoinUnderGroupByLeft
SELECT DISTINCT ON (x) x, y FROM fks RIGHT JOIN xy ON x=v
----
scan xy
 ├── columns: x:7!null y:8
 ├── key: (7)
 └── fd: (7)-->(8)

# InnerJoin case. The Values operator in the join guarantees cardinality of at
# least one, so rows from the left input are guaranteed to be included in the
# join at least once.
norm expect=EliminateJoinUnderGroupByLeft
SELECT k, max(r1) FROM fks INNER JOIN (SELECT * FROM (VALUES (1), (2)) f(t)) ON True GROUP BY k
----
group-by (hash)
 ├── columns: k:1!null max:8!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(8)
 ├── scan fks
 │    ├── columns: k:1!null r1:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(3)
 └── aggregations
      └── max [as=max:8, outer=(3)]
           └── r1:3

# Case with ScalarGroupBy with a sum aggregate that doesn't ignore duplicates.
# The join can be eliminated because r1 is a foreign key referencing x, which
# implies that the rows of fks are not being duplicated by the join.
norm expect=EliminateJoinUnderGroupByLeft disable=EliminateJoinUnderProjectLeft
SELECT sum(k) FROM fks LEFT JOIN xy ON x=r1
----
scalar-group-by
 ├── columns: sum:11
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(11)
 ├── scan fks
 │    ├── columns: k:1!null
 │    └── key: (1)
 └── aggregations
      └── sum [as=sum:11, outer=(1)]
           └── k:1

# LeftJoin case with possible duplicate rows. The rule can fire because the
# output of the max aggregate is not affected by duplicate rows.
norm expect=EliminateJoinUnderGroupByLeft
SELECT x, max(y) FROM xy LEFT JOIN fks ON True GROUP BY x
----
group-by (hash)
 ├── columns: x:1!null max:11
 ├── grouping columns: x:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 └── aggregations
      └── max [as=max:11, outer=(2)]
           └── y:2

# LeftJoin case with a not-null foreign key equality filter and a sum aggregate.
norm expect=EliminateJoinUnderGroupByLeft disable=EliminateJoinUnderProjectLeft
SELECT k, sum(r1) FROM fks LEFT JOIN xy ON x=r1 GROUP BY k
----
group-by (hash)
 ├── columns: k:1!null sum:11!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── scan fks
 │    ├── columns: k:1!null r1:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(3)
 └── aggregations
      └── sum [as=sum:11, outer=(3)]
           └── r1:3

# The LeftJoin guarantees that all left rows will be included in the output, and
# since k is a key column, no rows from xy will be duplicated. Therefore the sum
# aggregate will not be affected by join removal.
norm expect=EliminateJoinUnderGroupByLeft disable=EliminateJoinUnderProjectLeft
SELECT x, sum(y) FROM xy LEFT JOIN fks ON x=k GROUP BY x
----
group-by (hash)
 ├── columns: x:1!null sum:11
 ├── grouping columns: x:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 └── aggregations
      └── sum [as=sum:11, outer=(2)]
           └── y:2

# The LeftJoin guarantees that all left rows will be included in the output, and
# since r2 is a foreign key referencing x, it is guaranteed that no left rows
# will be matched more than once. Therefore, the sum aggregate will be
# unaffected by join removal.
norm expect=EliminateJoinUnderGroupByLeft disable=EliminateJoinUnderProjectLeft
SELECT k, sum(r1) FROM fks LEFT JOIN xy ON x=r2 GROUP BY k
----
group-by (hash)
 ├── columns: k:1!null sum:11!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── scan fks
 │    ├── columns: k:1!null r1:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(3)
 └── aggregations
      └── sum [as=sum:11, outer=(3)]
           └── r1:3

# InnerJoin case. Because r1 is a non-null foreign key that references x, the
# join output is guaranteed to include every left row exactly once.
norm expect=EliminateJoinUnderGroupByLeft disable=EliminateJoinUnderProjectLeft
SELECT k, sum(r1) FROM fks INNER JOIN xy ON x=r1 GROUP BY k
----
group-by (hash)
 ├── columns: k:1!null sum:11!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── scan fks
 │    ├── columns: k:1!null r1:3!null
 │    ├── key: (1)
 │    └── fd: (1)-->(3)
 └── aggregations
      └── sum [as=sum:11, outer=(3)]
           └── r1:3

# Case with an ordering on left columns.
norm expect=EliminateJoinUnderGroupByLeft
SELECT max(y) FROM xy LEFT JOIN fks ON True GROUP BY x ORDER BY x
----
group-by (streaming)
 ├── columns: max:11  [hidden: x:1!null]
 ├── grouping columns: x:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── ordering: +1
 ├── scan xy
 │    ├── columns: x:1!null y:2
 │    ├── key: (1)
 │    ├── fd: (1)-->(2)
 │    └── ordering: +1
 └── aggregations
      └── max [as=max:11, outer=(2)]
           └── y:2

# Cross join case where neither the foreign key column nor its referenced column
# are output columns.
norm expect=EliminateJoinUnderGroupByLeft
SELECT DISTINCT ON (k) k, v FROM fks INNER JOIN xy ON True
----
scan fks
 ├── columns: k:1!null v:2
 ├── key: (1)
 └── fd: (1)-->(2)

# No-op case because the InnerJoin will return no rows if fks is empty.
norm expect-not=EliminateJoinUnderGroupByLeft
SELECT DISTINCT ON (x) x, y FROM xy INNER JOIN fks ON True
----
distinct-on
 ├── columns: x:1!null y:2
 ├── grouping columns: x:1!null
 ├── key: (1)
 ├── fd: (1)-->(2)
 ├── inner-join (cross)
 │    ├── columns: x:1!null y:2
 │    ├── fd: (1)-->(2)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    ├── scan fks
 │    └── filters (true)
 └── aggregations
      └── first-agg [as=y:2, outer=(2)]
           └── y:2

# No-op case because the DistinctOn is using columns from the right input.
norm expect-not=EliminateJoinUnderGroupByLeft
SELECT DISTINCT ON (x) y, k FROM xy LEFT JOIN fks ON True
----
distinct-on
 ├── columns: y:2 k:5  [hidden: x:1!null]
 ├── grouping columns: x:1!null
 ├── key: (1)
 ├── fd: (1)-->(2,5)
 ├── left-join (cross)
 │    ├── columns: x:1!null y:2 k:5
 │    ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 │    ├── key: (1,5)
 │    ├── fd: (1)-->(2)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    ├── scan fks
 │    │    ├── columns: k:5!null
 │    │    └── key: (5)
 │    └── filters (true)
 └── aggregations
      ├── first-agg [as=y:2, outer=(2)]
      │    └── y:2
      └── first-agg [as=k:5, outer=(5)]
           └── k:5

# No-op case because an InnerJoin on true may create duplicate rows that will
# affect the output of the sum on r1.
norm expect-not=EliminateJoinUnderGroupByLeft
SELECT k, sum(r1) FROM fks INNER JOIN xy ON True GROUP BY k
----
group-by (hash)
 ├── columns: k:1!null sum:11!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── inner-join (cross)
 │    ├── columns: k:1!null r1:3!null
 │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-more)
 │    ├── fd: (1)-->(3)
 │    ├── scan fks
 │    │    ├── columns: k:1!null r1:3!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(3)
 │    ├── scan xy
 │    └── filters (true)
 └── aggregations
      └── sum [as=sum:11, outer=(3)]
           └── r1:3

# No-op case with a foreign key equality filter and a sum aggregate. No-op
# because r2 is nullable and therefore the InnerJoin may filter out rows.
norm expect-not=EliminateJoinUnderGroupByLeft
SELECT k, sum(r1) FROM fks INNER JOIN xy ON x=r2 GROUP BY k
----
group-by (hash)
 ├── columns: k:1!null sum:11!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(11)
 ├── inner-join (hash)
 │    ├── columns: k:1!null r1:3!null r2:4!null x:7!null
 │    ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more)
 │    ├── key: (1)
 │    ├── fd: (1)-->(3,4), (4)==(7), (7)==(4)
 │    ├── scan fks
 │    │    ├── columns: k:1!null r1:3!null r2:4
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(3,4)
 │    ├── scan xy
 │    │    ├── columns: x:7!null
 │    │    └── key: (7)
 │    └── filters
 │         └── x:7 = r2:4 [outer=(4,7), constraints=(/4: (/NULL - ]; /7: (/NULL - ]), fd=(4)==(7), (7)==(4)]
 └── aggregations
      └── sum [as=sum:11, outer=(3)]
           └── r1:3

# No-op case because the ordering includes a column from the right input.
norm expect-not=EliminateJoinUnderGroupByLeft
SELECT x, max(y) FROM xy LEFT JOIN fks ON True GROUP BY x, k ORDER BY x, k
----
group-by (streaming)
 ├── columns: x:1!null max:11  [hidden: k:5]
 ├── grouping columns: x:1!null k:5
 ├── key: (1,5)
 ├── fd: (1,5)-->(11)
 ├── ordering: +1,+5
 ├── sort
 │    ├── columns: x:1!null y:2 k:5
 │    ├── key: (1,5)
 │    ├── fd: (1)-->(2)
 │    ├── ordering: +1,+5
 │    └── left-join (cross)
 │         ├── columns: x:1!null y:2 k:5
 │         ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
 │         ├── key: (1,5)
 │         ├── fd: (1)-->(2)
 │         ├── scan xy
 │         │    ├── columns: x:1!null y:2
 │         │    ├── key: (1)
 │         │    └── fd: (1)-->(2)
 │         ├── scan fks
 │         │    ├── columns: k:5!null
 │         │    └── key: (5)
 │         └── filters (true)
 └── aggregations
      └── max [as=max:11, outer=(2)]
           └── y:2

# --------------------------------------------------
# EliminateJoinUnderGroupByRight
# --------------------------------------------------

# InnerJoin case.
norm expect=EliminateJoinUnderGroupByRight disable=EliminateJoinUnderProjectRight
SELECT k, sum(r1) FROM xy INNER JOIN fks ON x = r1 GROUP BY k
----
group-by (hash)
 ├── columns: k:5!null sum:11!null
 ├── grouping columns: k:5!null
 ├── key: (5)
 ├── fd: (5)-->(11)
 ├── scan fks
 │    ├── columns: k:5!null r1:7!null
 │    ├── key: (5)
 │    └── fd: (5)-->(7)
 └── aggregations
      └── sum [as=sum:11, outer=(7)]
           └── r1:7

# No-op case because columns from the right side of a left join are being used.
norm expect-not=EliminateJoinUnderGroupByRight
SELECT max(r1) FROM xy LEFT JOIN fks ON x = r1
----
scalar-group-by
 ├── columns: max:11
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(11)
 ├── left-join (hash)
 │    ├── columns: x:1!null r1:7
 │    ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one)
 │    ├── scan xy
 │    │    ├── columns: x:1!null
 │    │    └── key: (1)
 │    ├── scan fks
 │    │    └── columns: r1:7!null
 │    └── filters
 │         └── x:1 = r1:7 [outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)]
 └── aggregations
      └── max [as=max:11, outer=(7)]
           └── r1:7

# --------------------------------------------------
# EliminateDistinct
# --------------------------------------------------
norm expect=EliminateDistinct
SELECT DISTINCT k FROM a
----
scan a
 ├── columns: k:1!null
 └── key: (1)

norm expect=EliminateDistinct
SELECT DISTINCT s, i FROM a
----
scan a
 ├── columns: s:4!null i:2!null
 └── key: (2,4)

norm expect=EliminateDistinct
SELECT DISTINCT ON (s, i) k, i, f FROM a
----
scan a
 ├── columns: k:1!null i:2!null f:3
 ├── key: (1)
 └── fd: (1)-->(2,3), (2,3)~~>(1)

# Strict superset of key.
norm expect=EliminateDistinct
SELECT DISTINCT s, i, f FROM a
----
scan a
 ├── columns: s:4!null i:2!null f:3
 ├── key: (2,4)
 └── fd: (2,4)-->(3), (2,3)~~>(4)

# Distinct not eliminated because columns aren't superset of any weak key.
norm expect-not=EliminateDistinct
SELECT DISTINCT i FROM a
----
distinct-on
 ├── columns: i:2!null
 ├── grouping columns: i:2!null
 ├── key: (2)
 └── scan a
      └── columns: i:2!null

# Distinct not eliminated despite a unique index on (f, i) because f is nullable.
norm expect-not=EliminateDistinct
SELECT DISTINCT f, i FROM a
----
distinct-on
 ├── columns: f:3 i:2!null
 ├── grouping columns: i:2!null f:3
 ├── key: (2,3)
 └── scan a
      ├── columns: i:2!null f:3
      └── lax-key: (2,3)

# Regression test for #40295. Ensure that the DistinctOn is replaced with a
# Project operator to keep the correct number of output columns.
exec-ddl
CREATE TABLE table0 (col0 REGTYPE);
----

exec-ddl
CREATE TABLE table1 (col0 REGCLASS, col1 REGTYPE, col2 INT4);
----

norm expect=EliminateDistinct
SELECT
  (
    SELECT
      t1.col2
    FROM
      table1 AS t1
    JOIN table0 AS t0 ON
        t1.col1 = t0.col0
        AND t1.col0 = t0.col0
    GROUP BY
      t1.col2
    HAVING
      NULL
  );
----
values
 ├── columns: col2:11
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(11)
 └── tuple
      └── subquery
           └── values
                ├── columns: t1.col2:3!null
                ├── cardinality: [0 - 0]
                ├── key: ()
                └── fd: ()-->(3)

# EnsureDistinctOn case.
# EliminateMax1Row is disabled to ensure that an EnsureDistinctOn operator is
# created.
norm expect=EliminateDistinct disable=EliminateMax1Row
SELECT (SELECT y FROM xy WHERE x=k AND k=5) FROM a
----
project
 ├── columns: y:12
 ├── left-join (cross)
 │    ├── columns: k:1!null x:8 xy.y:9
 │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 │    ├── key: (1)
 │    ├── fd: (1)-->(8,9)
 │    ├── scan a
 │    │    ├── columns: k:1!null
 │    │    └── key: (1)
 │    ├── select
 │    │    ├── columns: x:8!null xy.y:9
 │    │    ├── cardinality: [0 - 1]
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(8,9)
 │    │    ├── scan xy
 │    │    │    ├── columns: x:8!null xy.y:9
 │    │    │    ├── key: (8)
 │    │    │    └── fd: (8)-->(9)
 │    │    └── filters
 │    │         └── x:8 = 5 [outer=(8), constraints=(/8: [/5 - /5]; tight), fd=()-->(8)]
 │    └── filters
 │         └── k:1 = 5 [outer=(1), constraints=(/1: [/5 - /5]; tight), fd=()-->(1)]
 └── projections
      └── xy.y:9 [as=y:12, outer=(9)]

# --------------------------------------------------
# EliminateUpsertDistinct
# --------------------------------------------------

# Case with non-null conflict column.
norm expect=EliminateUpsertDistinct
INSERT INTO xy VALUES (1, 2) ON CONFLICT (x) DO UPDATE SET x = 100
----
upsert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── canary column: xy.x:7
 ├── fetch columns: xy.x:7 y:8
 ├── insert-mapping:
 │    ├── column1:5 => xy.x:1
 │    └── column2:6 => y:2
 ├── update-mapping:
 │    └── upsert_x:12 => xy.x:1
 ├── input binding: &1
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 ├── project
 │    ├── columns: upsert_x:12!null column1:5!null column2:6!null xy.x:7 y:8
 │    ├── cardinality: [1 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(5-8,12)
 │    ├── left-join (cross)
 │    │    ├── columns: column1:5!null column2:6!null xy.x:7 y:8
 │    │    ├── cardinality: [1 - 1]
 │    │    ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one)
 │    │    ├── key: ()
 │    │    ├── fd: ()-->(5-8)
 │    │    ├── values
 │    │    │    ├── columns: column1:5!null column2:6!null
 │    │    │    ├── cardinality: [1 - 1]
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(5,6)
 │    │    │    └── (1, 2)
 │    │    ├── select
 │    │    │    ├── columns: xy.x:7!null y:8
 │    │    │    ├── cardinality: [0 - 1]
 │    │    │    ├── key: ()
 │    │    │    ├── fd: ()-->(7,8)
 │    │    │    ├── scan xy
 │    │    │    │    ├── columns: xy.x:7!null y:8
 │    │    │    │    ├── flags: avoid-full-scan
 │    │    │    │    ├── key: (7)
 │    │    │    │    └── fd: (7)-->(8)
 │    │    │    └── filters
 │    │    │         └── xy.x:7 = 1 [outer=(7), constraints=(/7: [/1 - /1]; tight), fd=()-->(7)]
 │    │    └── filters (true)
 │    └── projections
 │         └── CASE WHEN xy.x:7 IS NULL THEN column1:5 ELSE 100 END [as=upsert_x:12, outer=(5,7)]
 └── f-k-checks
      ├── f-k-checks-item: fks(r1) -> xy(x)
      │    └── semi-join (hash)
      │         ├── columns: x:14
      │         ├── cardinality: [0 - 1]
      │         ├── key: ()
      │         ├── fd: ()-->(14)
      │         ├── except-all
      │         │    ├── columns: x:14
      │         │    ├── left columns: x:14
      │         │    ├── right columns: x:15
      │         │    ├── cardinality: [0 - 1]
      │         │    ├── key: ()
      │         │    ├── fd: ()-->(14)
      │         │    ├── with-scan &1
      │         │    │    ├── columns: x:14
      │         │    │    ├── mapping:
      │         │    │    │    └──  xy.x:7 => x:14
      │         │    │    ├── cardinality: [1 - 1]
      │         │    │    ├── key: ()
      │         │    │    └── fd: ()-->(14)
      │         │    └── with-scan &1
      │         │         ├── columns: x:15!null
      │         │         ├── mapping:
      │         │         │    └──  upsert_x:12 => x:15
      │         │         ├── cardinality: [1 - 1]
      │         │         ├── key: ()
      │         │         └── fd: ()-->(15)
      │         ├── scan fks
      │         │    ├── columns: r1:18!null
      │         │    └── flags: avoid-full-scan
      │         └── filters
      │              └── x:14 = r1:18 [outer=(14,18), constraints=(/14: (/NULL - ]; /18: (/NULL - ]), fd=(14)==(18), (18)==(14)]
      └── f-k-checks-item: fks(r2) -> xy(x)
           └── semi-join (hash)
                ├── columns: x:22
                ├── cardinality: [0 - 1]
                ├── key: ()
                ├── fd: ()-->(22)
                ├── except-all
                │    ├── columns: x:22
                │    ├── left columns: x:22
                │    ├── right columns: x:23
                │    ├── cardinality: [0 - 1]
                │    ├── key: ()
                │    ├── fd: ()-->(22)
                │    ├── with-scan &1
                │    │    ├── columns: x:22
                │    │    ├── mapping:
                │    │    │    └──  xy.x:7 => x:22
                │    │    ├── cardinality: [1 - 1]
                │    │    ├── key: ()
                │    │    └── fd: ()-->(22)
                │    └── with-scan &1
                │         ├── columns: x:23!null
                │         ├── mapping:
                │         │    └──  upsert_x:12 => x:23
                │         ├── cardinality: [1 - 1]
                │         ├── key: ()
                │         └── fd: ()-->(23)
                ├── scan fks
                │    ├── columns: r2:27
                │    └── flags: avoid-full-scan
                └── filters
                     └── x:22 = r2:27 [outer=(22,27), constraints=(/22: (/NULL - ]; /27: (/NULL - ]), fd=(22)==(27), (27)==(22)]

# Case with DO NOTHING.
norm expect=EliminateUpsertDistinct
INSERT INTO xy VALUES (1, 2) ON CONFLICT (x) DO NOTHING
----
insert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── column1:5 => x:1
 │    └── column2:6 => y:2
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── anti-join (cross)
      ├── columns: column1:5!null column2:6!null
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(5,6)
      ├── values
      │    ├── columns: column1:5!null column2:6!null
      │    ├── cardinality: [1 - 1]
      │    ├── key: ()
      │    ├── fd: ()-->(5,6)
      │    └── (1, 2)
      ├── select
      │    ├── columns: x:7!null
      │    ├── cardinality: [0 - 1]
      │    ├── key: ()
      │    ├── fd: ()-->(7)
      │    ├── scan xy
      │    │    ├── columns: x:7!null
      │    │    ├── flags: avoid-full-scan
      │    │    └── key: (7)
      │    └── filters
      │         └── x:7 = 1 [outer=(7), constraints=(/7: [/1 - /1]; tight), fd=()-->(7)]
      └── filters (true)

# Case with UPSERT.
norm expect=EliminateUpsertDistinct
UPSERT INTO nullablecols VALUES (1, 2, 2)
----
upsert nullablecols
 ├── arbiter indexes: nullablecols_pkey
 ├── columns: <none>
 ├── canary column: rowid:14
 ├── fetch columns: c1:11 c2:12 c3:13 rowid:14
 ├── insert-mapping:
 │    ├── column1:7 => c1:1
 │    ├── column2:8 => c2:2
 │    ├── column3:9 => c3:3
 │    └── rowid_default:10 => rowid:4
 ├── update-mapping:
 │    ├── column1:7 => c1:1
 │    ├── column2:8 => c2:2
 │    └── column3:9 => c3:3
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── left-join (hash)
      ├── columns: column1:7!null column2:8!null column3:9!null rowid_default:10 c1:11 c2:12 c3:13 rowid:14
      ├── cardinality: [1 - 1]
      ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      ├── volatile
      ├── key: ()
      ├── fd: ()-->(7-14)
      ├── values
      │    ├── columns: column1:7!null column2:8!null column3:9!null rowid_default:10
      │    ├── cardinality: [1 - 1]
      │    ├── volatile
      │    ├── key: ()
      │    ├── fd: ()-->(7-10)
      │    └── (1, 2, 2, unique_rowid())
      ├── scan nullablecols
      │    ├── columns: c1:11 c2:12 c3:13 rowid:14!null
      │    ├── flags: avoid-full-scan
      │    ├── key: (14)
      │    └── fd: (14)-->(11-13), (11)~~>(12-14), (12,13)~~>(11,14)
      └── filters
           └── rowid_default:10 = rowid:14 [outer=(10,14), constraints=(/10: (/NULL - ]; /14: (/NULL - ]), fd=(10)==(14), (14)==(10)]

# Case with a multi-row data source with a strict key.
norm expect=EliminateUpsertDistinct
INSERT INTO xy SELECT k, i FROM a ON CONFLICT (x) DO UPDATE SET x = xy.x + 100
----
upsert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── canary column: xy.x:12
 ├── fetch columns: xy.x:12 y:13
 ├── insert-mapping:
 │    ├── a.k:5 => xy.x:1
 │    └── i:6 => y:2
 ├── update-mapping:
 │    └── upsert_x:17 => xy.x:1
 ├── input binding: &1
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 ├── project
 │    ├── columns: upsert_x:17 a.k:5!null i:6!null xy.x:12 y:13
 │    ├── immutable
 │    ├── key: (5)
 │    ├── fd: (5)-->(6,12,13,17), (12)-->(13)
 │    ├── left-join (hash)
 │    │    ├── columns: a.k:5!null i:6!null xy.x:12 y:13
 │    │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 │    │    ├── key: (5)
 │    │    ├── fd: (5)-->(6,12,13), (12)-->(13)
 │    │    ├── scan a
 │    │    │    ├── columns: a.k:5!null i:6!null
 │    │    │    ├── key: (5)
 │    │    │    └── fd: (5)-->(6)
 │    │    ├── scan xy
 │    │    │    ├── columns: xy.x:12!null y:13
 │    │    │    ├── flags: avoid-full-scan
 │    │    │    ├── key: (12)
 │    │    │    └── fd: (12)-->(13)
 │    │    └── filters
 │    │         └── a.k:5 = xy.x:12 [outer=(5,12), constraints=(/5: (/NULL - ]; /12: (/NULL - ]), fd=(5)==(12), (12)==(5)]
 │    └── projections
 │         └── CASE WHEN xy.x:12 IS NULL THEN a.k:5 ELSE xy.x:12 + 100 END [as=upsert_x:17, outer=(5,12), immutable]
 └── f-k-checks
      ├── f-k-checks-item: fks(r1) -> xy(x)
      │    └── semi-join (hash)
      │         ├── columns: x:19
      │         ├── key: (19)
      │         ├── except
      │         │    ├── columns: x:19
      │         │    ├── left columns: x:19
      │         │    ├── right columns: x:20
      │         │    ├── key: (19)
      │         │    ├── with-scan &1
      │         │    │    ├── columns: x:19
      │         │    │    └── mapping:
      │         │    │         └──  xy.x:12 => x:19
      │         │    └── with-scan &1
      │         │         ├── columns: x:20
      │         │         └── mapping:
      │         │              └──  upsert_x:17 => x:20
      │         ├── scan fks
      │         │    ├── columns: r1:23!null
      │         │    └── flags: avoid-full-scan
      │         └── filters
      │              └── x:19 = r1:23 [outer=(19,23), constraints=(/19: (/NULL - ]; /23: (/NULL - ]), fd=(19)==(23), (23)==(19)]
      └── f-k-checks-item: fks(r2) -> xy(x)
           └── semi-join (hash)
                ├── columns: x:27
                ├── key: (27)
                ├── except
                │    ├── columns: x:27
                │    ├── left columns: x:27
                │    ├── right columns: x:28
                │    ├── key: (27)
                │    ├── with-scan &1
                │    │    ├── columns: x:27
                │    │    └── mapping:
                │    │         └──  xy.x:12 => x:27
                │    └── with-scan &1
                │         ├── columns: x:28
                │         └── mapping:
                │              └──  upsert_x:17 => x:28
                ├── scan fks
                │    ├── columns: r2:32
                │    └── flags: avoid-full-scan
                └── filters
                     └── x:27 = r2:32 [outer=(27,32), constraints=(/27: (/NULL - ]; /32: (/NULL - ]), fd=(27)==(32), (32)==(27)]

# Case with a multi-row data source with a lax key.
# TODO(#75070): the UpsertDistinctOn isn't removed in this case because the lax
# key for nullablecols isn't fully simplified.
norm
INSERT INTO xy SELECT c1, c2 FROM nullablecols ON CONFLICT (x) DO UPDATE SET x = xy.x + 100
----
upsert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── canary column: xy.x:11
 ├── fetch columns: xy.x:11 y:12
 ├── insert-mapping:
 │    ├── c1:5 => xy.x:1
 │    └── c2:6 => y:2
 ├── update-mapping:
 │    └── upsert_x:16 => xy.x:1
 ├── input binding: &1
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 ├── project
 │    ├── columns: upsert_x:16 c1:5 c2:6 xy.x:11 y:12
 │    ├── immutable
 │    ├── lax-key: (5,11)
 │    ├── fd: (5)~~>(6), (11)-->(12), (5,11)-->(16)
 │    ├── left-join (hash)
 │    │    ├── columns: c1:5 c2:6 xy.x:11 y:12
 │    │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 │    │    ├── lax-key: (5,11)
 │    │    ├── fd: (5)~~>(6), (11)-->(12)
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: c1:5 c2:6
 │    │    │    ├── grouping columns: c1:5
 │    │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
 │    │    │    ├── lax-key: (5)
 │    │    │    ├── fd: (5)~~>(6)
 │    │    │    ├── scan nullablecols
 │    │    │    │    ├── columns: c1:5 c2:6
 │    │    │    │    ├── lax-key: (5,6)
 │    │    │    │    └── fd: (5)~~>(6)
 │    │    │    └── aggregations
 │    │    │         └── first-agg [as=c2:6, outer=(6)]
 │    │    │              └── c2:6
 │    │    ├── scan xy
 │    │    │    ├── columns: xy.x:11!null y:12
 │    │    │    ├── flags: avoid-full-scan
 │    │    │    ├── key: (11)
 │    │    │    └── fd: (11)-->(12)
 │    │    └── filters
 │    │         └── c1:5 = xy.x:11 [outer=(5,11), constraints=(/5: (/NULL - ]; /11: (/NULL - ]), fd=(5)==(11), (11)==(5)]
 │    └── projections
 │         └── CASE WHEN xy.x:11 IS NULL THEN c1:5 ELSE xy.x:11 + 100 END [as=upsert_x:16, outer=(5,11), immutable]
 └── f-k-checks
      ├── f-k-checks-item: fks(r1) -> xy(x)
      │    └── semi-join (hash)
      │         ├── columns: x:18
      │         ├── key: (18)
      │         ├── except
      │         │    ├── columns: x:18
      │         │    ├── left columns: x:18
      │         │    ├── right columns: x:19
      │         │    ├── key: (18)
      │         │    ├── with-scan &1
      │         │    │    ├── columns: x:18
      │         │    │    └── mapping:
      │         │    │         └──  xy.x:11 => x:18
      │         │    └── with-scan &1
      │         │         ├── columns: x:19
      │         │         └── mapping:
      │         │              └──  upsert_x:16 => x:19
      │         ├── scan fks
      │         │    ├── columns: r1:22!null
      │         │    └── flags: avoid-full-scan
      │         └── filters
      │              └── x:18 = r1:22 [outer=(18,22), constraints=(/18: (/NULL - ]; /22: (/NULL - ]), fd=(18)==(22), (22)==(18)]
      └── f-k-checks-item: fks(r2) -> xy(x)
           └── semi-join (hash)
                ├── columns: x:26
                ├── key: (26)
                ├── except
                │    ├── columns: x:26
                │    ├── left columns: x:26
                │    ├── right columns: x:27
                │    ├── key: (26)
                │    ├── with-scan &1
                │    │    ├── columns: x:26
                │    │    └── mapping:
                │    │         └──  xy.x:11 => x:26
                │    └── with-scan &1
                │         ├── columns: x:27
                │         └── mapping:
                │              └──  upsert_x:16 => x:27
                ├── scan fks
                │    ├── columns: r2:31
                │    └── flags: avoid-full-scan
                └── filters
                     └── x:26 = r2:31 [outer=(26,31), constraints=(/26: (/NULL - ]; /31: (/NULL - ]), fd=(26)==(31), (31)==(26)]

# No-op because a, b is not a lax key over abc.
norm expect-not=EliminateUpsertDistinct
INSERT INTO xy SELECT a, b FROM abc ON CONFLICT (x) DO UPDATE SET x = xy.x + 100
----
upsert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── canary column: xy.x:10
 ├── fetch columns: xy.x:10 y:11
 ├── insert-mapping:
 │    ├── a:5 => xy.x:1
 │    └── b:6 => y:2
 ├── update-mapping:
 │    └── upsert_x:15 => xy.x:1
 ├── input binding: &1
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 ├── project
 │    ├── columns: upsert_x:15 a:5!null b:6!null xy.x:10 y:11
 │    ├── immutable
 │    ├── key: (5)
 │    ├── fd: (5)-->(6,10,11,15), (10)-->(11)
 │    ├── left-join (hash)
 │    │    ├── columns: a:5!null b:6!null xy.x:10 y:11
 │    │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 │    │    ├── key: (5)
 │    │    ├── fd: (5)-->(6,10,11), (10)-->(11)
 │    │    ├── ensure-upsert-distinct-on
 │    │    │    ├── columns: a:5!null b:6!null
 │    │    │    ├── grouping columns: a:5!null
 │    │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
 │    │    │    ├── key: (5)
 │    │    │    ├── fd: (5)-->(6)
 │    │    │    ├── scan abc
 │    │    │    │    └── columns: a:5!null b:6!null
 │    │    │    └── aggregations
 │    │    │         └── first-agg [as=b:6, outer=(6)]
 │    │    │              └── b:6
 │    │    ├── scan xy
 │    │    │    ├── columns: xy.x:10!null y:11
 │    │    │    ├── flags: avoid-full-scan
 │    │    │    ├── key: (10)
 │    │    │    └── fd: (10)-->(11)
 │    │    └── filters
 │    │         └── a:5 = xy.x:10 [outer=(5,10), constraints=(/5: (/NULL - ]; /10: (/NULL - ]), fd=(5)==(10), (10)==(5)]
 │    └── projections
 │         └── CASE WHEN xy.x:10 IS NULL THEN a:5 ELSE xy.x:10 + 100 END [as=upsert_x:15, outer=(5,10), immutable]
 └── f-k-checks
      ├── f-k-checks-item: fks(r1) -> xy(x)
      │    └── semi-join (hash)
      │         ├── columns: x:17
      │         ├── key: (17)
      │         ├── except
      │         │    ├── columns: x:17
      │         │    ├── left columns: x:17
      │         │    ├── right columns: x:18
      │         │    ├── key: (17)
      │         │    ├── with-scan &1
      │         │    │    ├── columns: x:17
      │         │    │    └── mapping:
      │         │    │         └──  xy.x:10 => x:17
      │         │    └── with-scan &1
      │         │         ├── columns: x:18
      │         │         └── mapping:
      │         │              └──  upsert_x:15 => x:18
      │         ├── scan fks
      │         │    ├── columns: r1:21!null
      │         │    └── flags: avoid-full-scan
      │         └── filters
      │              └── x:17 = r1:21 [outer=(17,21), constraints=(/17: (/NULL - ]; /21: (/NULL - ]), fd=(17)==(21), (21)==(17)]
      └── f-k-checks-item: fks(r2) -> xy(x)
           └── semi-join (hash)
                ├── columns: x:25
                ├── key: (25)
                ├── except
                │    ├── columns: x:25
                │    ├── left columns: x:25
                │    ├── right columns: x:26
                │    ├── key: (25)
                │    ├── with-scan &1
                │    │    ├── columns: x:25
                │    │    └── mapping:
                │    │         └──  xy.x:10 => x:25
                │    └── with-scan &1
                │         ├── columns: x:26
                │         └── mapping:
                │              └──  upsert_x:15 => x:26
                ├── scan fks
                │    ├── columns: r2:30
                │    └── flags: avoid-full-scan
                └── filters
                     └── x:25 = r2:30 [outer=(25,30), constraints=(/25: (/NULL - ]; /30: (/NULL - ]), fd=(25)==(30), (30)==(25)]

# --------------------------------------------------
# EliminateGroupBy
# --------------------------------------------------

# Eliminate the GroupBy when the aggregate functions produce the same column as
# their input column.
exprnorm expect=EliminateGroupBy
(GroupBy
    (Scan [ (Table "a") (Cols "k,i,f,s") ])
    [
        (AggregationsItem (ConstAgg (Var "i")) (LookupColumn "i"))
        (AggregationsItem (ConstNotNullAgg (Var "f")) (LookupColumn "f"))
        (AggregationsItem (AnyNotNullAgg (Var "s")) (LookupColumn "s"))
    ]
    [ (GroupingCols "k") ]
)
----
scan a
 ├── columns: k:1!null i:2!null f:3 s:4!null
 ├── key: (1)
 └── fd: (1)-->(2-4), (2,4)-->(1,3), (2,3)~~>(1,4)

# Eliminate the GroupBy when the aggregate functions produce a new column.
exprnorm expect=EliminateGroupBy
(GroupBy
    (Scan [ (Table "a") (Cols "k,i,f,s") ])
    [
        (AggregationsItem (ConstAgg (Var "i")) (NewColumn "i_agg" "int"))
        (AggregationsItem (ConstNotNullAgg (Var "f")) (NewColumn "f_agg" "float"))
        (AggregationsItem (AnyNotNullAgg (Var "s")) (NewColumn "s_agg" "string"))
    ]
    [ (GroupingCols "k") ]
)
----
project
 ├── columns: i_agg:8!null f_agg:9 s_agg:10!null k:1!null
 ├── key: (1)
 ├── fd: (1)-->(8-10), (8,10)-->(1,9), (8,9)~~>(1,10)
 ├── scan a
 │    ├── columns: k:1!null i:2!null f:3 s:4!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2-4), (2,4)-->(1,3), (2,3)~~>(1,4)
 └── projections
      ├── i:2 [as=i_agg:8, outer=(2)]
      ├── f:3 [as=f_agg:9, outer=(3)]
      └── s:4 [as=s_agg:10, outer=(4)]

# Eliminate the GroupBy when there is a compound key.
exprnorm expect=EliminateGroupBy
(GroupBy
    (Scan [ (Table "a") (Cols "i,f,s,j") ])
    [
        (AggregationsItem (ConstAgg (Var "f")) (LookupColumn "f"))
        (AggregationsItem (ConstNotNullAgg (Var "j")) (NewColumn "j_agg" "json"))
    ]
    [ (GroupingCols "i,s") ]
)
----
project
 ├── columns: j_agg:8 i:2!null f:3 s:4!null
 ├── key: (2,4)
 ├── fd: (2,4)-->(3,8), (2,3)~~>(4,8)
 ├── scan a
 │    ├── columns: i:2!null f:3 s:4!null j:5
 │    ├── key: (2,4)
 │    └── fd: (2,4)-->(3,5), (2,3)~~>(4,5)
 └── projections
      └── j:5 [as=j_agg:8, outer=(5)]

# Eliminate unnecessary GroupBy generated during join decorrelation.
norm expect=EliminateGroupBy
SELECT CASE WHEN EXISTS (
    SELECT 1 FROM a WHERE k = x AND i = 1
) THEN 'Y' ELSE 'N' END
FROM xy
WHERE y = 2
----
project
 ├── columns: case:14!null
 ├── left-join (hash)
 │    ├── columns: x:1!null y:2!null k:5 i:6
 │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
 │    ├── key: (1)
 │    ├── fd: ()-->(2), (1)-->(5,6)
 │    ├── select
 │    │    ├── columns: x:1!null y:2!null
 │    │    ├── key: (1)
 │    │    ├── fd: ()-->(2)
 │    │    ├── scan xy
 │    │    │    ├── columns: x:1!null y:2
 │    │    │    ├── key: (1)
 │    │    │    └── fd: (1)-->(2)
 │    │    └── filters
 │    │         └── y:2 = 2 [outer=(2), constraints=(/2: [/2 - /2]; tight), fd=()-->(2)]
 │    ├── select
 │    │    ├── columns: k:5!null i:6!null
 │    │    ├── key: (5)
 │    │    ├── fd: ()-->(6)
 │    │    ├── scan a
 │    │    │    ├── columns: k:5!null i:6!null
 │    │    │    ├── key: (5)
 │    │    │    └── fd: (5)-->(6)
 │    │    └── filters
 │    │         └── i:6 = 1 [outer=(6), constraints=(/6: [/1 - /1]; tight), fd=()-->(6)]
 │    └── filters
 │         └── k:5 = x:1 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]
 └── projections
      └── CASE WHEN k:5 IS NOT NULL THEN 'Y' ELSE 'N' END [as=case:14, outer=(5)]

# Don't eliminate the GroupBy if there is an aggregate function other than
# ConstAgg, ConstNotNullAgg, or AnyNotNullAgg.
exprnorm expect-not=EliminateGroupBy
(GroupBy
    (Scan [ (Table "a") (Cols "k,i,f,s") ])
    [
        (AggregationsItem (Sum (Var "i")) (NewColumn "sum" "int"))
        (AggregationsItem (ConstNotNullAgg (Var "f")) (NewColumn "f_agg" "float"))
        (AggregationsItem (AnyNotNullAgg (Var "s")) (NewColumn "s_agg" "string"))
    ]
    [ (GroupingCols "k") ]
)
----
group-by (hash)
 ├── columns: k:1!null sum:8!null f_agg:9 s_agg:10!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(8-10)
 ├── scan a
 │    ├── columns: k:1!null i:2!null f:3 s:4!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2-4), (2,4)-->(1,3), (2,3)~~>(1,4)
 └── aggregations
      ├── sum [as=sum:8, outer=(2)]
      │    └── i:2
      ├── const-not-null-agg [as=f_agg:9, outer=(3)]
      │    └── f:3
      └── any-not-null-agg [as=s_agg:10, outer=(4)]
           └── s:4

# Don't eliminate the GroupBy if the grouping columns do not form a key in the
# input.
exprnorm expect-not=EliminateGroupBy
(GroupBy
    (Scan [ (Table "a") (Cols "i,f,s") ])
    [
        (AggregationsItem (ConstNotNullAgg (Var "f")) (NewColumn "f_agg" "float"))
        (AggregationsItem (AnyNotNullAgg (Var "s")) (NewColumn "s_agg" "string"))
    ]
    [ (GroupingCols "i") ]
)
----
group-by (hash)
 ├── columns: i:2!null f_agg:8 s_agg:9!null
 ├── grouping columns: i:2!null
 ├── key: (2)
 ├── fd: (2)-->(8,9)
 ├── scan a
 │    ├── columns: i:2!null f:3 s:4!null
 │    ├── key: (2,4)
 │    └── fd: (2,4)-->(3), (2,3)~~>(4)
 └── aggregations
      ├── const-not-null-agg [as=f_agg:8, outer=(3)]
      │    └── f:3
      └── any-not-null-agg [as=s_agg:9, outer=(4)]
           └── s:4

# Don't eliminate the GroupBy if the grouping columns form a lax key.
exprnorm expect-not=EliminateGroupBy
(GroupBy
    (Scan [ (Table "a") (Cols "i,f,s,j") ])
    [
        (AggregationsItem (ConstAgg (Var "s")) (LookupColumn "s"))
        (AggregationsItem (ConstNotNullAgg (Var "j")) (NewColumn "j_agg" "json"))
    ]
    [ (GroupingCols "i,f") ]
)
----
group-by (hash)
 ├── columns: i:2!null f:3 s:4!null j_agg:8
 ├── grouping columns: i:2!null f:3
 ├── key: (2,4)
 ├── fd: (2,4)-->(3,8), (2,3)-->(4,8)
 ├── scan a
 │    ├── columns: i:2!null f:3 s:4!null j:5
 │    ├── key: (2,4)
 │    └── fd: (2,4)-->(3,5), (2,3)~~>(4,5)
 └── aggregations
      ├── const-agg [as=s:4, outer=(4)]
      │    └── s:4
      └── const-not-null-agg [as=j_agg:8, outer=(5)]
           └── j:5

# --------------------------------------------------
# EliminateGroupByProject
# --------------------------------------------------
norm expect=EliminateGroupByProject disable=ConvertUnionToDistinctUnionAll
SELECT min(s) FROM (SELECT i, s FROM (SELECT * FROM a UNION SELECT * FROM a)) GROUP BY i
----
project
 ├── columns: min:20!null
 └── group-by (hash)
      ├── columns: i:16!null min:20!null
      ├── grouping columns: i:16!null
      ├── key: (16)
      ├── fd: (16)-->(20)
      ├── union
      │    ├── columns: k:15!null i:16!null f:17 s:18!null j:19
      │    ├── left columns: a.k:1 a.i:2 a.f:3 a.s:4 a.j:5
      │    ├── right columns: a.k:8 a.i:9 a.f:10 a.s:11 a.j:12
      │    ├── key: (15-19)
      │    ├── scan a
      │    │    ├── columns: a.k:1!null a.i:2!null a.f:3 a.s:4!null a.j:5
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2-5), (2,4)-->(1,3,5), (2,3)~~>(1,4,5)
      │    └── scan a
      │         ├── columns: a.k:8!null a.i:9!null a.f:10 a.s:11!null a.j:12
      │         ├── key: (8)
      │         └── fd: (8)-->(9-12), (9,11)-->(8,10,12), (9,10)~~>(8,11,12)
      └── aggregations
           └── min [as=min:20, outer=(18)]
                └── s:18

# ScalarGroupBy case.
norm expect=EliminateGroupByProject disable=ConvertUnionToDistinctUnionAll
SELECT min(s) FROM (SELECT i, s FROM (SELECT * FROM a UNION SELECT * FROM a))
----
scalar-group-by
 ├── columns: min:20
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(20)
 ├── union
 │    ├── columns: k:15!null i:16!null f:17 s:18!null j:19
 │    ├── left columns: a.k:1 a.i:2 a.f:3 a.s:4 a.j:5
 │    ├── right columns: a.k:8 a.i:9 a.f:10 a.s:11 a.j:12
 │    ├── key: (15-19)
 │    ├── scan a
 │    │    ├── columns: a.k:1!null a.i:2!null a.f:3 a.s:4!null a.j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5), (2,4)-->(1,3,5), (2,3)~~>(1,4,5)
 │    └── scan a
 │         ├── columns: a.k:8!null a.i:9!null a.f:10 a.s:11!null a.j:12
 │         ├── key: (8)
 │         └── fd: (8)-->(9-12), (9,11)-->(8,10,12), (9,10)~~>(8,11,12)
 └── aggregations
      └── min [as=min:20, outer=(18)]
           └── s:18

# DistinctOn case.
norm expect=EliminateGroupByProject disable=ConvertUnionToDistinctUnionAll
SELECT DISTINCT ON (i) s FROM (SELECT i, s, f FROM (SELECT * FROM a UNION SELECT * FROM a))
----
distinct-on
 ├── columns: s:18!null  [hidden: i:16!null]
 ├── grouping columns: i:16!null
 ├── key: (16)
 ├── fd: (16)-->(18)
 ├── union
 │    ├── columns: k:15!null i:16!null f:17 s:18!null j:19
 │    ├── left columns: a.k:1 a.i:2 a.f:3 a.s:4 a.j:5
 │    ├── right columns: a.k:8 a.i:9 a.f:10 a.s:11 a.j:12
 │    ├── key: (15-19)
 │    ├── scan a
 │    │    ├── columns: a.k:1!null a.i:2!null a.f:3 a.s:4!null a.j:5
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-5), (2,4)-->(1,3,5), (2,3)~~>(1,4,5)
 │    └── scan a
 │         ├── columns: a.k:8!null a.i:9!null a.f:10 a.s:11!null a.j:12
 │         ├── key: (8)
 │         └── fd: (8)-->(9-12), (9,11)-->(8,10,12), (9,10)~~>(8,11,12)
 └── aggregations
      └── first-agg [as=s:18, outer=(18)]
           └── s:18

# EnsureDistinctOn case.
# EliminateMax1Row is disabled to ensure that an EnsureDistinctOn operator is
# created.
norm expect=EliminateGroupByProject disable=EliminateMax1Row
SELECT (SELECT y FROM xy WHERE x+y=k) FROM a
----
project
 ├── columns: y:12
 ├── immutable
 ├── ensure-distinct-on
 │    ├── columns: k:1!null xy.y:9
 │    ├── grouping columns: k:1!null
 │    ├── error: "more than one row returned by a subquery used as an expression"
 │    ├── immutable
 │    ├── key: (1)
 │    ├── fd: (1)-->(9)
 │    ├── left-join (hash)
 │    │    ├── columns: k:1!null xy.y:9 column13:13
 │    │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-one)
 │    │    ├── immutable
 │    │    ├── scan a
 │    │    │    ├── columns: k:1!null
 │    │    │    └── key: (1)
 │    │    ├── project
 │    │    │    ├── columns: column13:13 xy.y:9
 │    │    │    ├── immutable
 │    │    │    ├── scan xy
 │    │    │    │    ├── columns: x:8!null xy.y:9
 │    │    │    │    ├── key: (8)
 │    │    │    │    └── fd: (8)-->(9)
 │    │    │    └── projections
 │    │    │         └── x:8 + xy.y:9 [as=column13:13, outer=(8,9), immutable]
 │    │    └── filters
 │    │         └── k:1 = column13:13 [outer=(1,13), constraints=(/1: (/NULL - ]; /13: (/NULL - ]), fd=(1)==(13), (13)==(1)]
 │    └── aggregations
 │         └── const-agg [as=xy.y:9, outer=(9)]
 │              └── xy.y:9
 └── projections
      └── xy.y:9 [as=y:12, outer=(9)]

# EnsureUpsertDistinctOn case.
norm expect=EliminateGroupByProject
INSERT INTO nullablecols (rowid, c1, c2, c3)
SELECT i, i, i, i FROM (SELECT * FROM a WHERE EXISTS(SELECT * FROM a) AND k>0)
ON CONFLICT (c1) DO UPDATE SET c3=1
----
upsert nullablecols
 ├── arbiter indexes: nullablecols_c1_key
 ├── columns: <none>
 ├── canary column: rowid:26
 ├── fetch columns: c1:23 c2:24 c3:25 rowid:26
 ├── insert-mapping:
 │    ├── i:8 => c1:1
 │    ├── i:8 => c2:2
 │    ├── i:8 => c3:3
 │    └── i:8 => rowid:4
 ├── update-mapping:
 │    └── upsert_c3:32 => c3:3
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_c3:32!null i:8!null c1:23 c2:24 c3:25 rowid:26
      ├── key: (8,26)
      ├── fd: (26)-->(23-25), (23)~~>(24-26), (24,25)~~>(23,26), (8,26)-->(32)
      ├── left-join (hash)
      │    ├── columns: i:8!null c1:23 c2:24 c3:25 rowid:26
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      │    ├── key: (8,26)
      │    ├── fd: (26)-->(23-25), (23)~~>(24-26), (24,25)~~>(23,26)
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: i:8!null
      │    │    ├── grouping columns: i:8!null
      │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    │    ├── key: (8)
      │    │    └── select
      │    │         ├── columns: k:7!null i:8!null
      │    │         ├── key: (7)
      │    │         ├── fd: (7)-->(8)
      │    │         ├── scan a
      │    │         │    ├── columns: k:7!null i:8!null
      │    │         │    ├── key: (7)
      │    │         │    └── fd: (7)-->(8)
      │    │         └── filters
      │    │              ├── coalesce [subquery]
      │    │              │    ├── subquery
      │    │              │    │    └── project
      │    │              │    │         ├── columns: column22:22!null
      │    │              │    │         ├── cardinality: [0 - 1]
      │    │              │    │         ├── key: ()
      │    │              │    │         ├── fd: ()-->(22)
      │    │              │    │         ├── limit
      │    │              │    │         │    ├── cardinality: [0 - 1]
      │    │              │    │         │    ├── key: ()
      │    │              │    │         │    ├── scan a
      │    │              │    │         │    │    └── limit hint: 1.00
      │    │              │    │         │    └── 1
      │    │              │    │         └── projections
      │    │              │    │              └── true [as=column22:22]
      │    │              │    └── false
      │    │              └── k:7 > 0 [outer=(7), constraints=(/7: [/1 - ]; tight)]
      │    ├── scan nullablecols
      │    │    ├── columns: c1:23 c2:24 c3:25 rowid:26!null
      │    │    ├── flags: avoid-full-scan
      │    │    ├── key: (26)
      │    │    └── fd: (26)-->(23-25), (23)~~>(24-26), (24,25)~~>(23,26)
      │    └── filters
      │         └── i:8 = c1:23 [outer=(8,23), constraints=(/8: (/NULL - ]; /23: (/NULL - ]), fd=(8)==(23), (23)==(8)]
      └── projections
           └── CASE WHEN rowid:26 IS NULL THEN i:8 ELSE 1 END [as=upsert_c3:32, outer=(8,26)]

# Don't eliminate project if it computes extra column(s).
norm expect-not=EliminateGroupByProject
SELECT min(s) FROM (SELECT i+1 AS i2, s FROM a) GROUP BY i2
----
project
 ├── columns: min:9!null
 ├── immutable
 └── group-by (hash)
      ├── columns: i2:8!null min:9!null
      ├── grouping columns: i2:8!null
      ├── immutable
      ├── key: (8)
      ├── fd: (8)-->(9)
      ├── project
      │    ├── columns: i2:8!null s:4!null
      │    ├── immutable
      │    ├── scan a
      │    │    ├── columns: i:2!null s:4!null
      │    │    └── key: (2,4)
      │    └── projections
      │         └── i:2 + 1 [as=i2:8, outer=(2), immutable]
      └── aggregations
           └── min [as=min:9, outer=(4)]
                └── s:4

# --------------------------------------------------
# ReduceGroupingCols
# --------------------------------------------------
norm expect=ReduceGroupingCols
SELECT k, min(i), f, s FROM a GROUP BY s, f, k
----
group-by (hash)
 ├── columns: k:1!null min:8!null f:3 s:4!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(3,4,8)
 ├── scan a
 │    ├── columns: k:1!null i:2!null f:3 s:4!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2-4), (2,4)-->(1,3), (2,3)~~>(1,4)
 └── aggregations
      ├── min [as=min:8, outer=(2)]
      │    └── i:2
      ├── const-agg [as=f:3, outer=(3)]
      │    └── f:3
      └── const-agg [as=s:4, outer=(4)]
           └── s:4

norm expect=ReduceGroupingCols
SELECT k, sum(DISTINCT i), f, s FROM a, xy GROUP BY s, f, k
----
group-by (hash)
 ├── columns: k:1!null sum:12!null f:3 s:4!null
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(3,4,12)
 ├── inner-join (cross)
 │    ├── columns: k:1!null i:2!null f:3 s:4!null
 │    ├── fd: (1)-->(2-4), (2,4)-->(1,3), (2,3)~~>(1,4)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2!null f:3 s:4!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2-4), (2,4)-->(1,3), (2,3)~~>(1,4)
 │    ├── scan xy
 │    └── filters (true)
 └── aggregations
      ├── agg-distinct [as=sum:12, outer=(2)]
      │    └── sum
      │         └── i:2
      ├── const-agg [as=f:3, outer=(3)]
      │    └── f:3
      └── const-agg [as=s:4, outer=(4)]
           └── s:4

# Eliminated columns are not part of projection.
norm expect=ReduceGroupingCols
SELECT min(f) FROM a GROUP BY i, s, k
----
project
 ├── columns: min:8
 └── group-by (hash)
      ├── columns: i:2!null s:4!null min:8
      ├── grouping columns: i:2!null s:4!null
      ├── key: (2,4)
      ├── fd: (2,4)-->(8)
      ├── scan a
      │    ├── columns: i:2!null f:3 s:4!null
      │    ├── key: (2,4)
      │    └── fd: (2,4)-->(3), (2,3)~~>(4)
      └── aggregations
           └── min [as=min:8, outer=(3)]
                └── f:3

# All grouping columns eliminated.
norm expect=ReduceGroupingCols
SELECT sum(f), i FROM a GROUP BY k, i, f HAVING k=1
----
group-by (streaming)
 ├── columns: sum:8 i:2!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(2,8)
 ├── select
 │    ├── columns: k:1!null i:2!null f:3
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1-3)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2!null f:3
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2,3), (2,3)~~>(1)
 │    └── filters
 │         └── k:1 = 1 [outer=(1), constraints=(/1: [/1 - /1]; tight), fd=()-->(1)]
 └── aggregations
      ├── sum [as=sum:8, outer=(3)]
      │    └── f:3
      └── const-agg [as=i:2, outer=(2)]
           └── i:2

norm expect=ReduceGroupingCols
SELECT DISTINCT ON (k, f, s) i, f, x FROM a JOIN xy ON i=y
----
distinct-on
 ├── columns: i:2!null f:3 x:8!null  [hidden: k:1!null]
 ├── grouping columns: k:1!null
 ├── key: (1)
 ├── fd: (1)-->(2,3,8), (2,3)~~>(1), (8)-->(2)
 ├── inner-join (hash)
 │    ├── columns: k:1!null i:2!null f:3 x:8!null y:9!null
 │    ├── key: (1,8)
 │    ├── fd: (1)-->(2,3), (2,3)~~>(1), (8)-->(9), (2)==(9), (9)==(2)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2!null f:3
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2,3), (2,3)~~>(1)
 │    ├── scan xy
 │    │    ├── columns: x:8!null y:9
 │    │    ├── key: (8)
 │    │    └── fd: (8)-->(9)
 │    └── filters
 │         └── i:2 = y:9 [outer=(2,9), constraints=(/2: (/NULL - ]; /9: (/NULL - ]), fd=(2)==(9), (9)==(2)]
 └── aggregations
      ├── first-agg [as=i:2, outer=(2)]
      │    └── i:2
      ├── first-agg [as=x:8, outer=(8)]
      │    └── x:8
      └── const-agg [as=f:3, outer=(3)]
           └── f:3

# --------------------------------------------------
# ReduceNotNullGroupingCols
# --------------------------------------------------

# UpsertDistinctOn should reduce non-nullable constant grouping column.
norm expect=ReduceNotNullGroupingCols
INSERT INTO xy (x)
SELECT y FROM xy WHERE y=0
ON CONFLICT (x) DO NOTHING
----
insert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── y:6 => x:1
 │    └── y_default:9 => y:2
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── limit
      ├── columns: y:6!null y_default:9
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(6,9)
      ├── anti-join (hash)
      │    ├── columns: y:6!null y_default:9
      │    ├── fd: ()-->(6,9)
      │    ├── limit hint: 1.00
      │    ├── project
      │    │    ├── columns: y_default:9 y:6!null
      │    │    ├── fd: ()-->(6,9)
      │    │    ├── select
      │    │    │    ├── columns: y:6!null
      │    │    │    ├── fd: ()-->(6)
      │    │    │    ├── scan xy
      │    │    │    │    └── columns: y:6
      │    │    │    └── filters
      │    │    │         └── y:6 = 0 [outer=(6), constraints=(/6: [/0 - /0]; tight), fd=()-->(6)]
      │    │    └── projections
      │    │         └── CAST(NULL AS INT8) [as=y_default:9]
      │    ├── scan xy
      │    │    ├── columns: x:10!null
      │    │    ├── flags: avoid-full-scan
      │    │    └── key: (10)
      │    └── filters
      │         └── y:6 = x:10 [outer=(6,10), constraints=(/6: (/NULL - ]; /10: (/NULL - ]), fd=(6)==(10), (10)==(6)]
      └── 1

# EnsureUpsertDistinctOn should reduce non-nullable constant grouping column.
norm expect=ReduceNotNullGroupingCols
INSERT INTO xy (x)
SELECT y FROM xy WHERE y=0
ON CONFLICT (x) DO UPDATE SET y=1
----
upsert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── canary column: x:10
 ├── fetch columns: x:10 y:11
 ├── insert-mapping:
 │    ├── y:6 => x:1
 │    └── y_default:9 => y:2
 ├── update-mapping:
 │    └── upsert_y:16 => y:2
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_y:16 y:6!null y_default:9 x:10 y:11
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(6,9-11,16)
      ├── left-join (hash)
      │    ├── columns: y:6!null y_default:9 x:10 y:11
      │    ├── cardinality: [0 - 1]
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      │    ├── key: ()
      │    ├── fd: ()-->(6,9-11)
      │    ├── max1-row
      │    │    ├── columns: y:6!null y_default:9
      │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    │    ├── cardinality: [0 - 1]
      │    │    ├── key: ()
      │    │    ├── fd: ()-->(6,9)
      │    │    └── project
      │    │         ├── columns: y_default:9 y:6!null
      │    │         ├── fd: ()-->(6,9)
      │    │         ├── select
      │    │         │    ├── columns: y:6!null
      │    │         │    ├── fd: ()-->(6)
      │    │         │    ├── scan xy
      │    │         │    │    └── columns: y:6
      │    │         │    └── filters
      │    │         │         └── y:6 = 0 [outer=(6), constraints=(/6: [/0 - /0]; tight), fd=()-->(6)]
      │    │         └── projections
      │    │              └── CAST(NULL AS INT8) [as=y_default:9]
      │    ├── scan xy
      │    │    ├── columns: x:10!null y:11
      │    │    ├── flags: avoid-full-scan
      │    │    ├── key: (10)
      │    │    └── fd: (10)-->(11)
      │    └── filters
      │         └── y:6 = x:10 [outer=(6,10), constraints=(/6: (/NULL - ]; /10: (/NULL - ]), fd=(6)==(10), (10)==(6)]
      └── projections
           └── CASE WHEN x:10 IS NULL THEN y_default:9 ELSE 1 END [as=upsert_y:16, outer=(9,10)]

# UpsertDistinctOn should not reduce nullable constant grouping column.
norm expect-not=ReduceNotNullGroupingCols
INSERT INTO xy (x)
SELECT y FROM xy WHERE y IS NULL
ON CONFLICT (x) DO NOTHING
----
insert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── y:6 => x:1
 │    └── y_default:9 => y:2
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── upsert-distinct-on
      ├── columns: y:6 y_default:9
      ├── grouping columns: y:6
      ├── lax-key: (6)
      ├── fd: ()-->(6,9)
      ├── anti-join (hash)
      │    ├── columns: y:6 y_default:9
      │    ├── fd: ()-->(6,9)
      │    ├── project
      │    │    ├── columns: y_default:9 y:6
      │    │    ├── fd: ()-->(6,9)
      │    │    ├── select
      │    │    │    ├── columns: y:6
      │    │    │    ├── fd: ()-->(6)
      │    │    │    ├── scan xy
      │    │    │    │    └── columns: y:6
      │    │    │    └── filters
      │    │    │         └── y:6 IS NULL [outer=(6), constraints=(/6: [/NULL - /NULL]; tight), fd=()-->(6)]
      │    │    └── projections
      │    │         └── CAST(NULL AS INT8) [as=y_default:9]
      │    ├── scan xy
      │    │    ├── columns: x:10!null
      │    │    ├── flags: avoid-full-scan
      │    │    └── key: (10)
      │    └── filters
      │         └── y:6 = x:10 [outer=(6,10), constraints=(/6: (/NULL - ]; /10: (/NULL - ]), fd=(6)==(10), (10)==(6)]
      └── aggregations
           └── first-agg [as=y_default:9, outer=(9)]
                └── y_default:9

# EnsureUpsertDistinctOn should not reduce nullable constant grouping column.
norm expect-not=ReduceNotNullGroupingCols
INSERT INTO xy (x)
SELECT y FROM xy WHERE y IS NULL
ON CONFLICT (x) DO UPDATE SET y=1
----
upsert xy
 ├── arbiter indexes: xy_pkey
 ├── columns: <none>
 ├── canary column: x:10
 ├── fetch columns: x:10 y:11
 ├── insert-mapping:
 │    ├── y:6 => x:1
 │    └── y_default:9 => y:2
 ├── update-mapping:
 │    └── upsert_y:16 => y:2
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_y:16 y:6 y_default:9 x:10 y:11
      ├── lax-key: (6,10)
      ├── fd: ()-->(6,9), (10)-->(11,16)
      ├── left-join (hash)
      │    ├── columns: y:6 y_default:9 x:10 y:11
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      │    ├── lax-key: (6,10)
      │    ├── fd: ()-->(6,9), (10)-->(11)
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: y:6 y_default:9
      │    │    ├── grouping columns: y:6
      │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    │    ├── lax-key: (6)
      │    │    ├── fd: ()-->(6,9)
      │    │    ├── project
      │    │    │    ├── columns: y_default:9 y:6
      │    │    │    ├── fd: ()-->(6,9)
      │    │    │    ├── select
      │    │    │    │    ├── columns: y:6
      │    │    │    │    ├── fd: ()-->(6)
      │    │    │    │    ├── scan xy
      │    │    │    │    │    └── columns: y:6
      │    │    │    │    └── filters
      │    │    │    │         └── y:6 IS NULL [outer=(6), constraints=(/6: [/NULL - /NULL]; tight), fd=()-->(6)]
      │    │    │    └── projections
      │    │    │         └── CAST(NULL AS INT8) [as=y_default:9]
      │    │    └── aggregations
      │    │         └── first-agg [as=y_default:9, outer=(9)]
      │    │              └── y_default:9
      │    ├── scan xy
      │    │    ├── columns: x:10!null y:11
      │    │    ├── flags: avoid-full-scan
      │    │    ├── key: (10)
      │    │    └── fd: (10)-->(11)
      │    └── filters
      │         └── y:6 = x:10 [outer=(6,10), constraints=(/6: (/NULL - ]; /10: (/NULL - ]), fd=(6)==(10), (10)==(6)]
      └── projections
           └── CASE WHEN x:10 IS NULL THEN y_default:9 ELSE 1 END [as=upsert_y:16, outer=(9,10)]

# Test removal of 2/3 grouping columns.
norm expect=ReduceNotNullGroupingCols
INSERT INTO abc (a, b, c)
SELECT 1, b, 2 FROM abc
ON CONFLICT (a, b, c) DO UPDATE SET a=1
----
upsert abc
 ├── arbiter indexes: abc_pkey
 ├── columns: <none>
 ├── canary column: a:13
 ├── fetch columns: a:13 b:14 c:15
 ├── insert-mapping:
 │    ├── "?column?":11 => a:1
 │    ├── b:7 => b:2
 │    └── "?column?":12 => c:3
 ├── update-mapping:
 │    └── "?column?":11 => a:1
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── left-join (hash)
      ├── columns: b:7!null "?column?":11!null "?column?":12!null a:13 b:14 c:15
      ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      ├── key: (7)
      ├── fd: ()-->(11,12), (7)-->(13-15)
      ├── ensure-upsert-distinct-on
      │    ├── columns: b:7!null "?column?":11!null "?column?":12!null
      │    ├── grouping columns: b:7!null
      │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    ├── key: (7)
      │    ├── fd: ()-->(11,12)
      │    ├── project
      │    │    ├── columns: "?column?":11!null "?column?":12!null b:7!null
      │    │    ├── fd: ()-->(11,12)
      │    │    ├── scan abc
      │    │    │    └── columns: b:7!null
      │    │    └── projections
      │    │         ├── 1 [as="?column?":11]
      │    │         └── 2 [as="?column?":12]
      │    └── aggregations
      │         ├── const-agg [as="?column?":11, outer=(11)]
      │         │    └── "?column?":11
      │         └── const-agg [as="?column?":12, outer=(12)]
      │              └── "?column?":12
      ├── scan abc
      │    ├── columns: a:13!null b:14!null c:15!null
      │    ├── flags: avoid-full-scan
      │    └── key: (13-15)
      └── filters
           ├── "?column?":11 = a:13 [outer=(11,13), constraints=(/11: (/NULL - ]; /13: (/NULL - ]), fd=(11)==(13), (13)==(11)]
           ├── b:7 = b:14 [outer=(7,14), constraints=(/7: (/NULL - ]; /14: (/NULL - ]), fd=(7)==(14), (14)==(7)]
           └── "?column?":12 = c:15 [outer=(12,15), constraints=(/12: (/NULL - ]; /15: (/NULL - ]), fd=(12)==(15), (15)==(12)]

# Test removal of not-null column, but not nullable column.
norm expect=ReduceNotNullGroupingCols
INSERT INTO abc
SELECT NULL, b, c FROM abc WHERE b=1
ON CONFLICT (a, b, c) DO UPDATE SET c=2
----
upsert abc
 ├── arbiter indexes: abc_pkey
 ├── columns: <none>
 ├── canary column: a:12
 ├── fetch columns: a:12 b:13 c:14
 ├── insert-mapping:
 │    ├── "?column?":11 => a:1
 │    ├── b:7 => b:2
 │    └── c:8 => c:3
 ├── update-mapping:
 │    └── upsert_c:20 => c:3
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_c:20!null b:7!null c:8!null "?column?":11 a:12 b:13 c:14
      ├── lax-key: (8,11-14)
      ├── fd: ()-->(7,11), (8,12)-->(20)
      ├── left-join (hash)
      │    ├── columns: b:7!null c:8!null "?column?":11 a:12 b:13 c:14
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      │    ├── lax-key: (8,11-14)
      │    ├── fd: ()-->(7,11)
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: b:7!null c:8!null "?column?":11
      │    │    ├── grouping columns: c:8!null "?column?":11
      │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    │    ├── lax-key: (8,11)
      │    │    ├── fd: ()-->(7,11)
      │    │    ├── project
      │    │    │    ├── columns: "?column?":11 b:7!null c:8!null
      │    │    │    ├── fd: ()-->(7,11)
      │    │    │    ├── select
      │    │    │    │    ├── columns: b:7!null c:8!null
      │    │    │    │    ├── fd: ()-->(7)
      │    │    │    │    ├── scan abc
      │    │    │    │    │    └── columns: b:7!null c:8!null
      │    │    │    │    └── filters
      │    │    │    │         └── b:7 = 1 [outer=(7), constraints=(/7: [/1 - /1]; tight), fd=()-->(7)]
      │    │    │    └── projections
      │    │    │         └── CAST(NULL AS INT8) [as="?column?":11]
      │    │    └── aggregations
      │    │         └── const-agg [as=b:7, outer=(7)]
      │    │              └── b:7
      │    ├── scan abc
      │    │    ├── columns: a:12!null b:13!null c:14!null
      │    │    ├── flags: avoid-full-scan
      │    │    └── key: (12-14)
      │    └── filters
      │         ├── "?column?":11 = a:12 [outer=(11,12), constraints=(/11: (/NULL - ]; /12: (/NULL - ]), fd=(11)==(12), (12)==(11)]
      │         ├── b:7 = b:13 [outer=(7,13), constraints=(/7: (/NULL - ]; /13: (/NULL - ]), fd=(7)==(13), (13)==(7)]
      │         └── c:8 = c:14 [outer=(8,14), constraints=(/8: (/NULL - ]; /14: (/NULL - ]), fd=(8)==(14), (14)==(8)]
      └── projections
           └── CASE WHEN a:12 IS NULL THEN c:8 ELSE 2 END [as=upsert_c:20, outer=(8,12)]

# --------------------------------------------------
# EliminateAggDistinctForKeys
# --------------------------------------------------

# ScalarGroupBy with key argument. Only the first aggregation can be
# simplified.
norm expect=EliminateAggDistinctForKeys
SELECT sum(DISTINCT k), sum(DISTINCT i) FROM a
----
scalar-group-by
 ├── columns: sum:8 sum:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8,9)
 ├── scan a
 │    ├── columns: k:1!null i:2!null
 │    ├── key: (1)
 │    └── fd: (1)-->(2)
 └── aggregations
      ├── sum [as=sum:8, outer=(1)]
      │    └── k:1
      └── agg-distinct [as=sum:9, outer=(2)]
           └── sum
                └── i:2

norm expect=EliminateAggDistinctForKeys
SELECT string_agg(DISTINCT s, ', ') FROM s
----
scalar-group-by
 ├── columns: string_agg:5
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(5)
 ├── project
 │    ├── columns: column4:4!null s:1!null
 │    ├── key: (1)
 │    ├── fd: ()-->(4)
 │    ├── scan s
 │    │    ├── columns: s:1!null
 │    │    └── key: (1)
 │    └── projections
 │         └── ', ' [as=column4:4]
 └── aggregations
      └── string-agg [as=string_agg:5, outer=(1,4)]
           ├── s:1
           └── column4:4

# GroupBy with key argument.
norm expect=EliminateAggDistinctForKeys
SELECT sum(DISTINCT k) FROM a GROUP BY i
----
project
 ├── columns: sum:8!null
 └── group-by (hash)
      ├── columns: i:2!null sum:8!null
      ├── grouping columns: i:2!null
      ├── key: (2)
      ├── fd: (2)-->(8)
      ├── scan a
      │    ├── columns: k:1!null i:2!null
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      └── aggregations
           └── sum [as=sum:8, outer=(1)]
                └── k:1

# GroupBy with no key. The AggDistinct is instead pushed into the GroupBy by
# PushAggDistinctIntoGroupBy.
norm expect-not=EliminateAggDistinctForKeys
SELECT sum(DISTINCT a) FROM abc GROUP BY b
----
project
 ├── columns: sum:6!null
 └── group-by (hash)
      ├── columns: b:2!null sum:6!null
      ├── grouping columns: b:2!null
      ├── key: (2)
      ├── fd: (2)-->(6)
      ├── distinct-on
      │    ├── columns: a:1!null b:2!null
      │    ├── grouping columns: a:1!null b:2!null
      │    ├── key: (1,2)
      │    └── scan abc
      │         └── columns: a:1!null b:2!null
      └── aggregations
           └── sum [as=sum:6, outer=(1)]
                └── a:1

# GroupBy with composite key formed by argument plus grouping columns.
norm expect=EliminateAggDistinctForKeys
SELECT sum(DISTINCT a) FROM abc GROUP BY b, c
----
project
 ├── columns: sum:6!null
 └── group-by (hash)
      ├── columns: b:2!null c:3!null sum:6!null
      ├── grouping columns: b:2!null c:3!null
      ├── key: (2,3)
      ├── fd: (2,3)-->(6)
      ├── scan abc
      │    ├── columns: a:1!null b:2!null c:3!null
      │    └── key: (1-3)
      └── aggregations
           └── sum [as=sum:6, outer=(1)]
                └── a:1

# GroupBy with multiple aggregations simplified.
norm expect=EliminateAggDistinctForKeys
SELECT sum(DISTINCT i), avg(DISTINCT f) FROM a GROUP BY k
----
project
 ├── columns: sum:8!null avg:9
 └── group-by (hash)
      ├── columns: k:1!null sum:8!null avg:9
      ├── grouping columns: k:1!null
      ├── key: (1)
      ├── fd: (1)-->(8,9)
      ├── scan a
      │    ├── columns: k:1!null i:2!null f:3
      │    ├── key: (1)
      │    └── fd: (1)-->(2,3), (2,3)~~>(1)
      └── aggregations
           ├── sum [as=sum:8, outer=(2)]
           │    └── i:2
           └── avg [as=avg:9, outer=(3)]
                └── f:3

# GroupBy where only some aggregations are simplified (the table has
# keys u,v and v,w).
norm expect=EliminateAggDistinctForKeys
SELECT sum(DISTINCT u), stddev(DISTINCT w), avg(DISTINCT z) FROM uvwz GROUP BY v
----
project
 ├── columns: sum:8!null stddev:9 avg:10!null
 └── group-by (hash)
      ├── columns: v:2!null sum:8!null stddev:9 avg:10!null
      ├── grouping columns: v:2!null
      ├── key: (2)
      ├── fd: (2)-->(8-10)
      ├── scan uvwz
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    ├── key: (2,3)
      │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      └── aggregations
           ├── sum [as=sum:8, outer=(1)]
           │    └── u:1
           ├── std-dev [as=stddev:9, outer=(3)]
           │    └── w:3
           └── agg-distinct [as=avg:10, outer=(4)]
                └── avg
                     └── z:4

# --------------------------------------------------
# EliminateAggFilteredDistinctForKeys
# --------------------------------------------------

# ScalarGroupBy with key argument. Only the first aggregation can be
# simplified.
norm expect=EliminateAggFilteredDistinctForKeys
SELECT sum(DISTINCT k) FILTER (WHERE k > 0), sum(DISTINCT i) FILTER (WHERE i > 0) FROM a
----
scalar-group-by
 ├── columns: sum:9 sum:11
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9,11)
 ├── project
 │    ├── columns: column8:8!null column10:10!null k:1!null i:2!null
 │    ├── key: (1)
 │    ├── fd: (1)-->(2,8), (2)-->(10)
 │    ├── scan a
 │    │    ├── columns: k:1!null i:2!null
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    └── projections
 │         ├── k:1 > 0 [as=column8:8, outer=(1)]
 │         └── i:2 > 0 [as=column10:10, outer=(2)]
 └── aggregations
      ├── agg-filter [as=sum:9, outer=(1,8)]
      │    ├── sum
      │    │    └── k:1
      │    └── column8:8
      └── agg-filter [as=sum:11, outer=(2,10)]
           ├── agg-distinct
           │    └── sum
           │         └── i:2
           └── column10:10

norm expect=EliminateAggFilteredDistinctForKeys
SELECT string_agg(DISTINCT s, ',') FILTER (WHERE s > 'a') FROM s
----
scalar-group-by
 ├── columns: string_agg:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── project
 │    ├── columns: column4:4!null s:1!null
 │    ├── key: (1)
 │    ├── fd: ()-->(4)
 │    ├── select
 │    │    ├── columns: s:1!null
 │    │    ├── key: (1)
 │    │    ├── scan s
 │    │    │    ├── columns: s:1!null
 │    │    │    └── key: (1)
 │    │    └── filters
 │    │         └── s:1 > 'a' [outer=(1), constraints=(/1: [/e'a\x00' - ]; tight)]
 │    └── projections
 │         └── ',' [as=column4:4]
 └── aggregations
      └── string-agg [as=string_agg:6, outer=(1,4)]
           ├── s:1
           └── column4:4

# GroupBy with key argument.
norm expect=EliminateAggFilteredDistinctForKeys
SELECT sum(DISTINCT k) FILTER (WHERE f > 0) FROM a GROUP BY i
----
project
 ├── columns: sum:9
 └── group-by (hash)
      ├── columns: i:2!null sum:9
      ├── grouping columns: i:2!null
      ├── key: (2)
      ├── fd: (2)-->(9)
      ├── project
      │    ├── columns: column8:8 k:1!null i:2!null
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,8)
      │    ├── scan a
      │    │    ├── columns: k:1!null i:2!null f:3
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2,3), (2,3)~~>(1)
      │    └── projections
      │         └── f:3 > 0.0 [as=column8:8, outer=(3)]
      └── aggregations
           └── agg-filter [as=sum:9, outer=(1,8)]
                ├── sum
                │    └── k:1
                └── column8:8

# GroupBy with no key.
norm expect-not=EliminateAggFilteredDistinctForKeys
SELECT sum(DISTINCT a) FILTER (WHERE c > 0) FROM abc GROUP BY b
----
project
 ├── columns: sum:7
 └── group-by (hash)
      ├── columns: b:2!null sum:7
      ├── grouping columns: b:2!null
      ├── key: (2)
      ├── fd: (2)-->(7)
      ├── project
      │    ├── columns: column6:6!null a:1!null b:2!null
      │    ├── scan abc
      │    │    ├── columns: a:1!null b:2!null c:3!null
      │    │    └── key: (1-3)
      │    └── projections
      │         └── c:3 > 0 [as=column6:6, outer=(3)]
      └── aggregations
           └── agg-filter [as=sum:7, outer=(1,6)]
                ├── agg-distinct
                │    └── sum
                │         └── a:1
                └── column6:6

# GroupBy with composite key formed by argument plus grouping columns.
norm expect=EliminateAggFilteredDistinctForKeys
SELECT sum(DISTINCT a) FILTER (WHERE c > 0) FROM abc GROUP BY b, c
----
project
 ├── columns: sum:7
 └── group-by (hash)
      ├── columns: b:2!null c:3!null sum:7
      ├── grouping columns: b:2!null c:3!null
      ├── key: (2,3)
      ├── fd: (2,3)-->(7)
      ├── project
      │    ├── columns: column6:6!null a:1!null b:2!null c:3!null
      │    ├── key: (1-3)
      │    ├── fd: (3)-->(6)
      │    ├── scan abc
      │    │    ├── columns: a:1!null b:2!null c:3!null
      │    │    └── key: (1-3)
      │    └── projections
      │         └── c:3 > 0 [as=column6:6, outer=(3)]
      └── aggregations
           └── agg-filter [as=sum:7, outer=(1,6)]
                ├── sum
                │    └── a:1
                └── column6:6

# GroupBy with multiple aggregations simplified.
norm expect=EliminateAggFilteredDistinctForKeys
SELECT sum(DISTINCT i) FILTER (WHERE f > 0), avg(DISTINCT f) FILTER (WHERE i > 0) FROM a GROUP BY k
----
project
 ├── columns: sum:9 avg:11
 └── group-by (hash)
      ├── columns: k:1!null sum:9 avg:11
      ├── grouping columns: k:1!null
      ├── key: (1)
      ├── fd: (1)-->(9,11)
      ├── project
      │    ├── columns: column8:8 column10:10!null k:1!null i:2!null f:3
      │    ├── key: (1)
      │    ├── fd: (1)-->(2,3), (2,3)~~>(1), (3)-->(8), (2)-->(10)
      │    ├── scan a
      │    │    ├── columns: k:1!null i:2!null f:3
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2,3), (2,3)~~>(1)
      │    └── projections
      │         ├── f:3 > 0.0 [as=column8:8, outer=(3)]
      │         └── i:2 > 0 [as=column10:10, outer=(2)]
      └── aggregations
           ├── agg-filter [as=sum:9, outer=(2,8)]
           │    ├── sum
           │    │    └── i:2
           │    └── column8:8
           └── agg-filter [as=avg:11, outer=(3,10)]
                ├── avg
                │    └── f:3
                └── column10:10

# GroupBy where only some aggregations are simplified (the table has
# keys u,v and v,w).
norm expect=EliminateAggFilteredDistinctForKeys
SELECT
    sum(DISTINCT u) FILTER (WHERE u > 0),
    stddev(DISTINCT w) FILTER (WHERE w > 0),
    avg(DISTINCT z) FILTER (WHERE z > 0)
FROM uvwz
GROUP BY v
----
project
 ├── columns: sum:9 stddev:11 avg:13
 └── group-by (hash)
      ├── columns: v:2!null sum:9 stddev:11 avg:13
      ├── grouping columns: v:2!null
      ├── key: (2)
      ├── fd: (2)-->(9,11,13)
      ├── project
      │    ├── columns: column8:8!null column10:10!null column12:12!null u:1!null v:2!null w:3!null z:4!null
      │    ├── key: (2,3)
      │    ├── fd: (1,2)-->(3,4), (2,3)-->(1,4), (1)-->(8), (3)-->(10), (4)-->(12)
      │    ├── scan uvwz
      │    │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    │    ├── key: (2,3)
      │    │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │    └── projections
      │         ├── u:1 > 0 [as=column8:8, outer=(1)]
      │         ├── w:3 > 0 [as=column10:10, outer=(3)]
      │         └── z:4 > 0 [as=column12:12, outer=(4)]
      └── aggregations
           ├── agg-filter [as=sum:9, outer=(1,8)]
           │    ├── sum
           │    │    └── u:1
           │    └── column8:8
           ├── agg-filter [as=stddev:11, outer=(3,10)]
           │    ├── std-dev
           │    │    └── w:3
           │    └── column10:10
           └── agg-filter [as=avg:13, outer=(4,12)]
                ├── agg-distinct
                │    └── avg
                │         └── z:4
                └── column12:12

# --------------------------------------------------
# EliminateDistinctNoColumns
# --------------------------------------------------

norm expect=EliminateDistinctNoColumns
SELECT DISTINCT ON (a) a, b FROM abc WHERE a = 1
----
limit
 ├── columns: a:1!null b:2!null
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(1,2)
 ├── select
 │    ├── columns: a:1!null b:2!null
 │    ├── fd: ()-->(1)
 │    ├── limit hint: 1.00
 │    ├── scan abc
 │    │    ├── columns: a:1!null b:2!null
 │    │    └── limit hint: 100.00
 │    └── filters
 │         └── a:1 = 1 [outer=(1), constraints=(/1: [/1 - /1]; tight), fd=()-->(1)]
 └── 1

norm expect=EliminateDistinctNoColumns
SELECT DISTINCT ON (b) b, c FROM abc WHERE b = 1 ORDER BY b, c
----
limit
 ├── columns: b:2!null c:3!null
 ├── internal-ordering: +3 opt(2)
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(2,3)
 ├── sort
 │    ├── columns: b:2!null c:3!null
 │    ├── fd: ()-->(2)
 │    ├── ordering: +3 opt(2) [actual: +3]
 │    ├── limit hint: 1.00
 │    └── select
 │         ├── columns: b:2!null c:3!null
 │         ├── fd: ()-->(2)
 │         ├── scan abc
 │         │    └── columns: b:2!null c:3!null
 │         └── filters
 │              └── b:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)]
 └── 1

norm expect=EliminateDistinctNoColumns
INSERT INTO a (k, i, s) SELECT 1, i, 'foo' FROM a WHERE i = 1
ON CONFLICT (s, i) DO NOTHING
----
insert a
 ├── arbiter indexes: si_idx
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── "?column?":15 => k:1
 │    ├── i:9 => i:2
 │    ├── f_default:17 => f:3
 │    ├── "?column?":16 => s:4
 │    └── j_default:18 => j:5
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── limit
      ├── columns: i:9!null "?column?":15!null "?column?":16!null f_default:17 j_default:18
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(9,15-18)
      ├── anti-join (hash)
      │    ├── columns: i:9!null "?column?":15!null "?column?":16!null f_default:17 j_default:18
      │    ├── fd: ()-->(9,15-18)
      │    ├── limit hint: 1.00
      │    ├── project
      │    │    ├── columns: f_default:17 j_default:18 "?column?":15!null "?column?":16!null i:9!null
      │    │    ├── fd: ()-->(9,15-18)
      │    │    ├── select
      │    │    │    ├── columns: i:9!null
      │    │    │    ├── fd: ()-->(9)
      │    │    │    ├── scan a
      │    │    │    │    └── columns: i:9!null
      │    │    │    └── filters
      │    │    │         └── i:9 = 1 [outer=(9), constraints=(/9: [/1 - /1]; tight), fd=()-->(9)]
      │    │    └── projections
      │    │         ├── CAST(NULL AS FLOAT8) [as=f_default:17]
      │    │         ├── CAST(NULL AS JSONB) [as=j_default:18]
      │    │         ├── 1 [as="?column?":15]
      │    │         └── 'foo' [as="?column?":16]
      │    ├── select
      │    │    ├── columns: i:20!null s:22!null
      │    │    ├── key: (20)
      │    │    ├── fd: ()-->(22)
      │    │    ├── scan a
      │    │    │    ├── columns: i:20!null s:22!null
      │    │    │    ├── flags: avoid-full-scan
      │    │    │    └── key: (20,22)
      │    │    └── filters
      │    │         └── s:22 = 'foo' [outer=(22), constraints=(/22: [/'foo' - /'foo']; tight), fd=()-->(22)]
      │    └── filters
      │         └── i:9 = i:20 [outer=(9,20), constraints=(/9: (/NULL - ]; /20: (/NULL - ]), fd=(9)==(20), (20)==(9)]
      └── 1

# --------------------------------------------------
# EliminateEnsureDistinctNoColumns
# --------------------------------------------------

# EnsureDistinctOn case.
norm expect=EliminateEnsureDistinctNoColumns
SELECT (SELECT x FROM xy WHERE y=i) FROM a WHERE k=5
----
project
 ├── columns: x:12
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(12)
 ├── max1-row
 │    ├── columns: k:1!null i:2!null xy.x:8 y:9
 │    ├── error: "more than one row returned by a subquery used as an expression"
 │    ├── cardinality: [0 - 1]
 │    ├── key: ()
 │    ├── fd: ()-->(1,2,8,9)
 │    └── left-join (hash)
 │         ├── columns: k:1!null i:2!null xy.x:8 y:9
 │         ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-one)
 │         ├── key: (8)
 │         ├── fd: ()-->(1,2,9)
 │         ├── select
 │         │    ├── columns: k:1!null i:2!null
 │         │    ├── cardinality: [0 - 1]
 │         │    ├── key: ()
 │         │    ├── fd: ()-->(1,2)
 │         │    ├── scan a
 │         │    │    ├── columns: k:1!null i:2!null
 │         │    │    ├── key: (1)
 │         │    │    └── fd: (1)-->(2)
 │         │    └── filters
 │         │         └── k:1 = 5 [outer=(1), constraints=(/1: [/5 - /5]; tight), fd=()-->(1)]
 │         ├── scan xy
 │         │    ├── columns: xy.x:8!null y:9
 │         │    ├── key: (8)
 │         │    └── fd: (8)-->(9)
 │         └── filters
 │              └── y:9 = i:2 [outer=(2,9), constraints=(/2: (/NULL - ]; /9: (/NULL - ]), fd=(2)==(9), (9)==(2)]
 └── projections
      └── xy.x:8 [as=x:12, outer=(8)]

# EnsureUpsertDistinctOn case.
norm expect=EliminateEnsureDistinctNoColumns
INSERT INTO a (k, i, s) SELECT 1, i, 'foo' FROM a WHERE i = 1
ON CONFLICT (s, i) DO UPDATE SET f=1.1
----
upsert a
 ├── arbiter indexes: si_idx
 ├── columns: <none>
 ├── canary column: s:22
 ├── fetch columns: k:19 i:20 f:21 s:22 j:23
 ├── insert-mapping:
 │    ├── "?column?":15 => k:1
 │    ├── i:9 => i:2
 │    ├── f_default:17 => f:3
 │    ├── "?column?":16 => s:4
 │    └── j_default:18 => j:5
 ├── update-mapping:
 │    └── upsert_f:29 => f:3
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_f:29 i:9!null "?column?":15!null "?column?":16!null f_default:17 j_default:18 k:19 i:20 f:21 s:22 j:23
      ├── cardinality: [0 - 1]
      ├── key: ()
      ├── fd: ()-->(9,15-23,29)
      ├── left-join (hash)
      │    ├── columns: i:9!null "?column?":15!null "?column?":16!null f_default:17 j_default:18 k:19 i:20 f:21 s:22 j:23
      │    ├── cardinality: [0 - 1]
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      │    ├── key: ()
      │    ├── fd: ()-->(9,15-23)
      │    ├── max1-row
      │    │    ├── columns: i:9!null "?column?":15!null "?column?":16!null f_default:17 j_default:18
      │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    │    ├── cardinality: [0 - 1]
      │    │    ├── key: ()
      │    │    ├── fd: ()-->(9,15-18)
      │    │    └── project
      │    │         ├── columns: f_default:17 j_default:18 "?column?":15!null "?column?":16!null i:9!null
      │    │         ├── fd: ()-->(9,15-18)
      │    │         ├── select
      │    │         │    ├── columns: i:9!null
      │    │         │    ├── fd: ()-->(9)
      │    │         │    ├── scan a
      │    │         │    │    └── columns: i:9!null
      │    │         │    └── filters
      │    │         │         └── i:9 = 1 [outer=(9), constraints=(/9: [/1 - /1]; tight), fd=()-->(9)]
      │    │         └── projections
      │    │              ├── CAST(NULL AS FLOAT8) [as=f_default:17]
      │    │              ├── CAST(NULL AS JSONB) [as=j_default:18]
      │    │              ├── 1 [as="?column?":15]
      │    │              └── 'foo' [as="?column?":16]
      │    ├── scan a
      │    │    ├── columns: k:19!null i:20!null f:21 s:22!null j:23
      │    │    ├── flags: avoid-full-scan
      │    │    ├── key: (19)
      │    │    └── fd: (19)-->(20-23), (20,22)-->(19,21,23), (20,21)~~>(19,22,23)
      │    └── filters
      │         ├── i:9 = i:20 [outer=(9,20), constraints=(/9: (/NULL - ]; /20: (/NULL - ]), fd=(9)==(20), (20)==(9)]
      │         └── "?column?":16 = s:22 [outer=(16,22), constraints=(/16: (/NULL - ]; /22: (/NULL - ]), fd=(16)==(22), (22)==(16)]
      └── projections
           └── CASE WHEN s:22 IS NULL THEN f_default:17 ELSE 1.1 END [as=upsert_f:29, outer=(17,22)]

# --------------------------------------------------
# EliminateDistinctOnValues
# --------------------------------------------------

# Eliminate DistinctOn when its immediate input is a Values operator.
norm expect=EliminateDistinctOnValues
SELECT DISTINCT ON (x) * FROM (VALUES (1), (2)) t(x)
----
values
 ├── columns: x:1!null
 ├── cardinality: [2 - 2]
 ├── (1,)
 └── (2,)

# Eliminate DistinctOn when Values operator is below Project, Select, and
# LeftJoin operators.
norm expect=EliminateDistinctOnValues
SELECT DISTINCT ON (x, y, z) *, x+1
FROM (VALUES (1, 2, 3), (4, 5, 6)) t(x, y, z)
LEFT JOIN (SELECT a, b, c FROM abc)
ON a=x AND b=y AND c=z
WHERE x > 100 OR b > 100
----
project
 ├── columns: x:1!null y:2!null z:3!null a:4 b:5 c:6 "?column?":9!null
 ├── cardinality: [0 - 2]
 ├── immutable
 ├── fd: (1)-->(9)
 ├── select
 │    ├── columns: column1:1!null column2:2!null column3:3!null a:4 b:5 c:6
 │    ├── cardinality: [0 - 2]
 │    ├── left-join (hash)
 │    │    ├── columns: column1:1!null column2:2!null column3:3!null a:4 b:5 c:6
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
 │    │    ├── values
 │    │    │    ├── columns: column1:1!null column2:2!null column3:3!null
 │    │    │    ├── cardinality: [2 - 2]
 │    │    │    ├── (1, 2, 3)
 │    │    │    └── (4, 5, 6)
 │    │    ├── scan abc
 │    │    │    ├── columns: a:4!null b:5!null c:6!null
 │    │    │    └── key: (4-6)
 │    │    └── filters
 │    │         ├── a:4 = column1:1 [outer=(1,4), constraints=(/1: (/NULL - ]; /4: (/NULL - ]), fd=(1)==(4), (4)==(1)]
 │    │         ├── b:5 = column2:2 [outer=(2,5), constraints=(/2: (/NULL - ]; /5: (/NULL - ]), fd=(2)==(5), (5)==(2)]
 │    │         └── c:6 = column3:3 [outer=(3,6), constraints=(/3: (/NULL - ]; /6: (/NULL - ]), fd=(3)==(6), (6)==(3)]
 │    └── filters
 │         └── (column1:1 > 100) OR (b:5 > 100) [outer=(1,5)]
 └── projections
      └── column1:1 + 1 [as="?column?":9, outer=(1), immutable]

# Right input of left join does not have a key, so left side may have dups.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) *
FROM (VALUES (1), (2)) t(x)
LEFT JOIN (SELECT a FROM abc)
ON a=x
----
distinct-on
 ├── columns: x:1!null a:2
 ├── grouping columns: column1:1!null
 ├── cardinality: [1 - ]
 ├── key: (1)
 ├── fd: (1)-->(2)
 ├── left-join (hash)
 │    ├── columns: column1:1!null a:2
 │    ├── cardinality: [2 - ]
 │    ├── values
 │    │    ├── columns: column1:1!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1,)
 │    │    └── (2,)
 │    ├── scan abc
 │    │    └── columns: a:2!null
 │    └── filters
 │         └── a:2 = column1:1 [outer=(1,2), constraints=(/1: (/NULL - ]; /2: (/NULL - ]), fd=(1)==(2), (2)==(1)]
 └── aggregations
      └── first-agg [as=a:2, outer=(2)]
           └── a:2

# Left join does not join on all columns of the right input's key, so dups are
# possible.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x, y) *
FROM (VALUES (1, 2), (3, 4)) t(x, y)
LEFT JOIN (SELECT * FROM abc)
ON x=a AND y=c
----
distinct-on
 ├── columns: x:1!null y:2!null a:3 b:4 c:5
 ├── grouping columns: column1:1!null column2:2!null
 ├── cardinality: [1 - ]
 ├── key: (1,2)
 ├── fd: (1,2)-->(3-5)
 ├── left-join (hash)
 │    ├── columns: column1:1!null column2:2!null a:3 b:4 c:5
 │    ├── cardinality: [2 - ]
 │    ├── values
 │    │    ├── columns: column1:1!null column2:2!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1, 2)
 │    │    └── (3, 4)
 │    ├── scan abc
 │    │    ├── columns: a:3!null b:4!null c:5!null
 │    │    └── key: (3-5)
 │    └── filters
 │         ├── column1:1 = a:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)]
 │         └── column2:2 = c:5 [outer=(2,5), constraints=(/2: (/NULL - ]; /5: (/NULL - ]), fd=(2)==(5), (5)==(2)]
 └── aggregations
      ├── first-agg [as=a:3, outer=(3)]
      │    └── a:3
      ├── first-agg [as=b:4, outer=(4)]
      │    └── b:4
      └── first-agg [as=c:5, outer=(5)]
           └── c:5

# Grouping columns are not passthrough Project columns.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (y) *
FROM (SELECT x, x+1 AS y FROM (VALUES (1), (2)) t(x))
----
distinct-on
 ├── columns: x:1!null y:2!null
 ├── grouping columns: y:2!null
 ├── cardinality: [1 - 2]
 ├── immutable
 ├── key: (2)
 ├── fd: (1)-->(2), (2)-->(1)
 ├── project
 │    ├── columns: y:2!null column1:1!null
 │    ├── cardinality: [2 - 2]
 │    ├── immutable
 │    ├── fd: (1)-->(2)
 │    ├── values
 │    │    ├── columns: column1:1!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1,)
 │    │    └── (2,)
 │    └── projections
 │         └── column1:1 + 1 [as=y:2, outer=(1), immutable]
 └── aggregations
      └── first-agg [as=column1:1, outer=(1)]
           └── column1:1

# Grouping columns are on the right side of a LeftJoin.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) *
FROM (SELECT k FROM a)
LEFT JOIN (VALUES (1), (2)) t(x)
ON k=x
----
distinct-on
 ├── columns: k:1!null x:8
 ├── grouping columns: column1:8
 ├── key: (8)
 ├── fd: (8)-->(1)
 ├── left-join (hash)
 │    ├── columns: k:1!null column1:8
 │    ├── multiplicity: left-rows(one-or-more), right-rows(zero-or-one)
 │    ├── scan a
 │    │    ├── columns: k:1!null
 │    │    └── key: (1)
 │    ├── values
 │    │    ├── columns: column1:8!null
 │    │    ├── cardinality: [2 - 2]
 │    │    ├── (1,)
 │    │    └── (2,)
 │    └── filters
 │         └── k:1 = column1:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]
 └── aggregations
      └── first-agg [as=k:1, outer=(1)]
           └── k:1

# DistinctOn with multiple grouping columns should be eliminated when there are
# not duplicate rows.
norm expect=EliminateDistinctOnValues
SELECT DISTINCT ON (b, c) * FROM (VALUES (1, 1, 1, 1), (1, 2, 2, 1)) t(a, b, c, d)
----
values
 ├── columns: a:1!null b:2!null c:3!null d:4!null
 ├── cardinality: [2 - 2]
 ├── (1, 1, 1, 1)
 └── (1, 2, 2, 1)

# Composite string type should be considered as not distinct.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) * FROM (VALUES ('ä' COLLATE en), (e'a\u0308' COLLATE en)) t(x)
----
distinct-on
 ├── columns: x:1!null
 ├── grouping columns: column1:1!null
 ├── cardinality: [1 - 2]
 ├── key: (1)
 └── values
      ├── columns: column1:1!null
      ├── cardinality: [2 - 2]
      ├── (e'\u00E4' COLLATE en,)
      └── (e'a\u0308' COLLATE en,)

# Composite decimal type should be considered as not distinct.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) * FROM (VALUES (1.0::decimal), (1.00::decimal)) t(x)
----
distinct-on
 ├── columns: x:1!null
 ├── grouping columns: column1:1!null
 ├── cardinality: [1 - 2]
 ├── key: (1)
 └── values
      ├── columns: column1:1!null
      ├── cardinality: [2 - 2]
      ├── (1.0,)
      └── (1.00,)

# Non-constant value should be considered as not distinct.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) * FROM (VALUES (1), (unique_rowid())) t(x)
----
distinct-on
 ├── columns: x:1
 ├── grouping columns: column1:1
 ├── cardinality: [1 - 2]
 ├── volatile
 ├── key: (1)
 └── values
      ├── columns: column1:1
      ├── cardinality: [2 - 2]
      ├── volatile
      ├── (1,)
      └── (unique_rowid(),)

# Tuple values are not handled.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) * FROM (VALUES ((1, 2, 3)), ((1, 2, 3))) t(x)
----
distinct-on
 ├── columns: x:1
 ├── grouping columns: column1:1
 ├── cardinality: [1 - 2]
 ├── key: (1)
 └── values
      ├── columns: column1:1
      ├── cardinality: [2 - 2]
      ├── ((1, 2, 3),)
      └── ((1, 2, 3),)

# DistinctOn should not be eliminated when there are duplicate rows.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (y, z) * FROM (VALUES (1, 1, 1), (2, 1, 1)) t(x, y, z)
----
distinct-on
 ├── columns: x:1!null y:2!null z:3!null
 ├── grouping columns: column2:2!null column3:3!null
 ├── cardinality: [1 - 2]
 ├── key: (2,3)
 ├── fd: (2,3)-->(1)
 ├── values
 │    ├── columns: column1:1!null column2:2!null column3:3!null
 │    ├── cardinality: [2 - 2]
 │    ├── (1, 1, 1)
 │    └── (2, 1, 1)
 └── aggregations
      └── first-agg [as=column1:1, outer=(1)]
           └── column1:1

# DistinctOn treats NULL values as not distinct, so it can't be eliminated when
# there are duplicate NULL values.
norm expect-not=EliminateDistinctOnValues
SELECT DISTINCT ON (x) * FROM (VALUES (NULL), (NULL)) t(x)
----
distinct-on
 ├── columns: x:1
 ├── grouping columns: column1:1
 ├── cardinality: [1 - 2]
 ├── key: (1)
 └── values
      ├── columns: column1:1
      ├── cardinality: [2 - 2]
      ├── (NULL,)
      └── (NULL,)

# UpsertDistinctOn treats NULL values as distinct, so it can be eliminated.
norm expect=EliminateDistinctOnValues
INSERT INTO a (k, s, i) VALUES (1, NULL, NULL), (1, NULL, NULL)
ON CONFLICT (s, i) DO NOTHING
----
insert a
 ├── arbiter indexes: si_idx
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── column1:8 => k:1
 │    ├── column3:10 => i:2
 │    ├── f_default:11 => f:3
 │    ├── column2:9 => s:4
 │    └── j_default:12 => j:5
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── anti-join (hash)
      ├── columns: column1:8!null column2:9 column3:10 f_default:11 j_default:12
      ├── cardinality: [0 - 2]
      ├── fd: ()-->(11,12)
      ├── project
      │    ├── columns: f_default:11 j_default:12 column1:8!null column2:9 column3:10
      │    ├── cardinality: [2 - 2]
      │    ├── fd: ()-->(11,12)
      │    ├── values
      │    │    ├── columns: column1:8!null column2:9 column3:10
      │    │    ├── cardinality: [2 - 2]
      │    │    ├── (1, NULL, NULL)
      │    │    └── (1, NULL, NULL)
      │    └── projections
      │         ├── CAST(NULL AS FLOAT8) [as=f_default:11]
      │         └── CAST(NULL AS JSONB) [as=j_default:12]
      ├── scan a
      │    ├── columns: i:14!null s:16!null
      │    ├── flags: avoid-full-scan
      │    └── key: (14,16)
      └── filters
           ├── column3:10 = i:14 [outer=(10,14), constraints=(/10: (/NULL - ]; /14: (/NULL - ]), fd=(10)==(14), (14)==(10)]
           └── column2:9 = s:16 [outer=(9,16), constraints=(/9: (/NULL - ]; /16: (/NULL - ]), fd=(9)==(16), (16)==(9)]

# EnsureUpsertDistinctOn treats NULL values as distinct, so it can be eliminated.
norm expect=EliminateDistinctOnValues
INSERT INTO a (k, s, i) VALUES (1, NULL, NULL), (1, NULL, NULL)
ON CONFLICT (s, i) DO UPDATE SET f=1.0
----
upsert a
 ├── arbiter indexes: si_idx
 ├── columns: <none>
 ├── canary column: s:16
 ├── fetch columns: k:13 i:14 f:15 s:16 j:17
 ├── insert-mapping:
 │    ├── column1:8 => k:1
 │    ├── column3:10 => i:2
 │    ├── f_default:11 => f:3
 │    ├── column2:9 => s:4
 │    └── j_default:12 => j:5
 ├── update-mapping:
 │    └── upsert_f:23 => f:3
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_f:23 column1:8!null column2:9 column3:10 f_default:11 j_default:12 k:13 i:14 f:15 s:16 j:17
      ├── cardinality: [2 - 2]
      ├── fd: ()-->(11,12), (13)-->(14-17), (14,16)-->(13,15,17), (14,15)~~>(13,16,17)
      ├── left-join (hash)
      │    ├── columns: column1:8!null column2:9 column3:10 f_default:11 j_default:12 k:13 i:14 f:15 s:16 j:17
      │    ├── cardinality: [2 - 2]
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more)
      │    ├── fd: ()-->(11,12), (13)-->(14-17), (14,16)-->(13,15,17), (14,15)~~>(13,16,17)
      │    ├── project
      │    │    ├── columns: f_default:11 j_default:12 column1:8!null column2:9 column3:10
      │    │    ├── cardinality: [2 - 2]
      │    │    ├── fd: ()-->(11,12)
      │    │    ├── values
      │    │    │    ├── columns: column1:8!null column2:9 column3:10
      │    │    │    ├── cardinality: [2 - 2]
      │    │    │    ├── (1, NULL, NULL)
      │    │    │    └── (1, NULL, NULL)
      │    │    └── projections
      │    │         ├── CAST(NULL AS FLOAT8) [as=f_default:11]
      │    │         └── CAST(NULL AS JSONB) [as=j_default:12]
      │    ├── scan a
      │    │    ├── columns: k:13!null i:14!null f:15 s:16!null j:17
      │    │    ├── flags: avoid-full-scan
      │    │    ├── key: (13)
      │    │    └── fd: (13)-->(14-17), (14,16)-->(13,15,17), (14,15)~~>(13,16,17)
      │    └── filters
      │         ├── column3:10 = i:14 [outer=(10,14), constraints=(/10: (/NULL - ]; /14: (/NULL - ]), fd=(10)==(14), (14)==(10)]
      │         └── column2:9 = s:16 [outer=(9,16), constraints=(/9: (/NULL - ]; /16: (/NULL - ]), fd=(9)==(16), (16)==(9)]
      └── projections
           └── CASE WHEN s:16 IS NULL THEN f_default:11 ELSE 1.0 END [as=upsert_f:23, outer=(11,16)]

# EnsureUpsertDistinctOn is not removed when there are duplicates.
norm expect-not=EliminateDistinctOnValues
INSERT INTO a (k, s, i) VALUES (1, 'foo', 1), (2, 'bar', 2), (3, 'foo', 1)
ON CONFLICT (s, i) DO UPDATE SET f=1.0
----
upsert a
 ├── arbiter indexes: si_idx
 ├── columns: <none>
 ├── canary column: s:16
 ├── fetch columns: k:13 i:14 f:15 s:16 j:17
 ├── insert-mapping:
 │    ├── column1:8 => k:1
 │    ├── column3:10 => i:2
 │    ├── f_default:11 => f:3
 │    ├── column2:9 => s:4
 │    └── j_default:12 => j:5
 ├── update-mapping:
 │    └── upsert_f:23 => f:3
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── project
      ├── columns: upsert_f:23 column1:8!null column2:9!null column3:10!null f_default:11 j_default:12 k:13 i:14 f:15 s:16 j:17
      ├── cardinality: [1 - 3]
      ├── key: (9,10)
      ├── fd: ()-->(11,12), (9,10)-->(8,13-17,23), (13)-->(14-17), (14,16)-->(13,15,17), (14,15)~~>(13,16,17)
      ├── left-join (hash)
      │    ├── columns: column1:8!null column2:9!null column3:10!null f_default:11 j_default:12 k:13 i:14 f:15 s:16 j:17
      │    ├── cardinality: [1 - 3]
      │    ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one)
      │    ├── key: (9,10)
      │    ├── fd: ()-->(11,12), (9,10)-->(8,13-17), (13)-->(14-17), (14,16)-->(13,15,17), (14,15)~~>(13,16,17)
      │    ├── ensure-upsert-distinct-on
      │    │    ├── columns: column1:8!null column2:9!null column3:10!null f_default:11 j_default:12
      │    │    ├── grouping columns: column2:9!null column3:10!null
      │    │    ├── error: "UPSERT or INSERT...ON CONFLICT command cannot affect row a second time"
      │    │    ├── cardinality: [1 - 3]
      │    │    ├── key: (9,10)
      │    │    ├── fd: ()-->(11,12), (9,10)-->(8,11,12)
      │    │    ├── project
      │    │    │    ├── columns: f_default:11 j_default:12 column1:8!null column2:9!null column3:10!null
      │    │    │    ├── cardinality: [3 - 3]
      │    │    │    ├── fd: ()-->(11,12)
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:8!null column2:9!null column3:10!null
      │    │    │    │    ├── cardinality: [3 - 3]
      │    │    │    │    ├── (1, 'foo', 1)
      │    │    │    │    ├── (2, 'bar', 2)
      │    │    │    │    └── (3, 'foo', 1)
      │    │    │    └── projections
      │    │    │         ├── CAST(NULL AS FLOAT8) [as=f_default:11]
      │    │    │         └── CAST(NULL AS JSONB) [as=j_default:12]
      │    │    └── aggregations
      │    │         ├── first-agg [as=column1:8, outer=(8)]
      │    │         │    └── column1:8
      │    │         ├── first-agg [as=f_default:11, outer=(11)]
      │    │         │    └── f_default:11
      │    │         └── first-agg [as=j_default:12, outer=(12)]
      │    │              └── j_default:12
      │    ├── scan a
      │    │    ├── columns: k:13!null i:14!null f:15 s:16!null j:17
      │    │    ├── flags: avoid-full-scan
      │    │    ├── key: (13)
      │    │    └── fd: (13)-->(14-17), (14,16)-->(13,15,17), (14,15)~~>(13,16,17)
      │    └── filters
      │         ├── column3:10 = i:14 [outer=(10,14), constraints=(/10: (/NULL - ]; /14: (/NULL - ]), fd=(10)==(14), (14)==(10)]
      │         └── column2:9 = s:16 [outer=(9,16), constraints=(/9: (/NULL - ]; /16: (/NULL - ]), fd=(9)==(16), (16)==(9)]
      └── projections
           └── CASE WHEN s:16 IS NULL THEN f_default:11 ELSE 1.0 END [as=upsert_f:23, outer=(11,16)]

# DO NOTHING case where all distinct ops can be removed.
norm expect=EliminateDistinctOnValues
INSERT INTO a (k, s, i, f) VALUES (1, 'foo', 1, 1.0), (2, 'bar', 2, 2.0), (3, 'foo', 2, 1.0)
ON CONFLICT DO NOTHING
----
insert a
 ├── arbiter indexes: a_pkey si_idx fi_idx
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── column1:8 => k:1
 │    ├── column3:10 => i:2
 │    ├── column4:11 => f:3
 │    ├── column2:9 => s:4
 │    └── j_default:12 => j:5
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── anti-join (hash)
      ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null j_default:12
      ├── cardinality: [0 - 3]
      ├── fd: ()-->(12)
      ├── anti-join (hash)
      │    ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null j_default:12
      │    ├── cardinality: [0 - 3]
      │    ├── fd: ()-->(12)
      │    ├── anti-join (hash)
      │    │    ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null j_default:12
      │    │    ├── cardinality: [0 - 3]
      │    │    ├── fd: ()-->(12)
      │    │    ├── project
      │    │    │    ├── columns: j_default:12 column1:8!null column2:9!null column3:10!null column4:11!null
      │    │    │    ├── cardinality: [3 - 3]
      │    │    │    ├── fd: ()-->(12)
      │    │    │    ├── values
      │    │    │    │    ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null
      │    │    │    │    ├── cardinality: [3 - 3]
      │    │    │    │    ├── (1, 'foo', 1, 1.0)
      │    │    │    │    ├── (2, 'bar', 2, 2.0)
      │    │    │    │    └── (3, 'foo', 2, 1.0)
      │    │    │    └── projections
      │    │    │         └── CAST(NULL AS JSONB) [as=j_default:12]
      │    │    ├── scan a
      │    │    │    ├── columns: k:13!null
      │    │    │    ├── flags: avoid-full-scan
      │    │    │    └── key: (13)
      │    │    └── filters
      │    │         └── column1:8 = k:13 [outer=(8,13), constraints=(/8: (/NULL - ]; /13: (/NULL - ]), fd=(8)==(13), (13)==(8)]
      │    ├── scan a
      │    │    ├── columns: i:21!null s:23!null
      │    │    ├── flags: avoid-full-scan
      │    │    └── key: (21,23)
      │    └── filters
      │         ├── column3:10 = i:21 [outer=(10,21), constraints=(/10: (/NULL - ]; /21: (/NULL - ]), fd=(10)==(21), (21)==(10)]
      │         └── column2:9 = s:23 [outer=(9,23), constraints=(/9: (/NULL - ]; /23: (/NULL - ]), fd=(9)==(23), (23)==(9)]
      ├── scan a
      │    ├── columns: i:28!null f:29
      │    ├── flags: avoid-full-scan
      │    └── lax-key: (28,29)
      └── filters
           ├── column3:10 = i:28 [outer=(10,28), constraints=(/10: (/NULL - ]; /28: (/NULL - ]), fd=(10)==(28), (28)==(10)]
           └── column4:11 = f:29 [outer=(11,29), constraints=(/11: (/NULL - ]; /29: (/NULL - ]), fd=(11)==(29), (29)==(11)]

# DO NOTHING case where one distinct op can be removed (k), but two others
# can't: (s, i) and (f, i).
norm expect=EliminateDistinctOnValues
INSERT INTO a (k, s, f) VALUES (1, 'foo', 1.0), (2, 'bar', 2.0), (3, 'foo', 1.0)
ON CONFLICT DO NOTHING
----
insert a
 ├── arbiter indexes: a_pkey si_idx fi_idx
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── column1:8 => k:1
 │    ├── i_default:11 => i:2
 │    ├── column3:10 => f:3
 │    ├── column2:9 => s:4
 │    └── j_default:12 => j:5
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── upsert-distinct-on
      ├── columns: column1:8!null column2:9!null column3:10!null i_default:11 j_default:12
      ├── grouping columns: column3:10!null i_default:11
      ├── cardinality: [0 - 3]
      ├── lax-key: (9,11)
      ├── fd: ()-->(11,12), (9,11)~~>(8,10), (10,11)~~>(8,9,12)
      ├── upsert-distinct-on
      │    ├── columns: column1:8!null column2:9!null column3:10!null i_default:11 j_default:12
      │    ├── grouping columns: column2:9!null i_default:11
      │    ├── cardinality: [0 - 3]
      │    ├── lax-key: (9,11)
      │    ├── fd: ()-->(11,12), (9,11)~~>(8,10,12)
      │    ├── anti-join (hash)
      │    │    ├── columns: column1:8!null column2:9!null column3:10!null i_default:11 j_default:12
      │    │    ├── cardinality: [0 - 3]
      │    │    ├── fd: ()-->(11,12)
      │    │    ├── anti-join (hash)
      │    │    │    ├── columns: column1:8!null column2:9!null column3:10!null i_default:11 j_default:12
      │    │    │    ├── cardinality: [0 - 3]
      │    │    │    ├── fd: ()-->(11,12)
      │    │    │    ├── anti-join (hash)
      │    │    │    │    ├── columns: column1:8!null column2:9!null column3:10!null i_default:11 j_default:12
      │    │    │    │    ├── cardinality: [0 - 3]
      │    │    │    │    ├── fd: ()-->(11,12)
      │    │    │    │    ├── project
      │    │    │    │    │    ├── columns: i_default:11 j_default:12 column1:8!null column2:9!null column3:10!null
      │    │    │    │    │    ├── cardinality: [3 - 3]
      │    │    │    │    │    ├── fd: ()-->(11,12)
      │    │    │    │    │    ├── values
      │    │    │    │    │    │    ├── columns: column1:8!null column2:9!null column3:10!null
      │    │    │    │    │    │    ├── cardinality: [3 - 3]
      │    │    │    │    │    │    ├── (1, 'foo', 1.0)
      │    │    │    │    │    │    ├── (2, 'bar', 2.0)
      │    │    │    │    │    │    └── (3, 'foo', 1.0)
      │    │    │    │    │    └── projections
      │    │    │    │    │         ├── CAST(NULL AS INT8) [as=i_default:11]
      │    │    │    │    │         └── CAST(NULL AS JSONB) [as=j_default:12]
      │    │    │    │    ├── scan a
      │    │    │    │    │    ├── columns: k:13!null
      │    │    │    │    │    ├── flags: avoid-full-scan
      │    │    │    │    │    └── key: (13)
      │    │    │    │    └── filters
      │    │    │    │         └── column1:8 = k:13 [outer=(8,13), constraints=(/8: (/NULL - ]; /13: (/NULL - ]), fd=(8)==(13), (13)==(8)]
      │    │    │    ├── scan a
      │    │    │    │    ├── columns: i:21!null s:23!null
      │    │    │    │    ├── flags: avoid-full-scan
      │    │    │    │    └── key: (21,23)
      │    │    │    └── filters
      │    │    │         ├── i_default:11 = i:21 [outer=(11,21), constraints=(/11: (/NULL - ]; /21: (/NULL - ]), fd=(11)==(21), (21)==(11)]
      │    │    │         └── column2:9 = s:23 [outer=(9,23), constraints=(/9: (/NULL - ]; /23: (/NULL - ]), fd=(9)==(23), (23)==(9)]
      │    │    ├── scan a
      │    │    │    ├── columns: i:28!null f:29
      │    │    │    ├── flags: avoid-full-scan
      │    │    │    └── lax-key: (28,29)
      │    │    └── filters
      │    │         ├── i_default:11 = i:28 [outer=(11,28), constraints=(/11: (/NULL - ]; /28: (/NULL - ]), fd=(11)==(28), (28)==(11)]
      │    │         └── column3:10 = f:29 [outer=(10,29), constraints=(/10: (/NULL - ]; /29: (/NULL - ]), fd=(10)==(29), (29)==(10)]
      │    └── aggregations
      │         ├── first-agg [as=column1:8, outer=(8)]
      │         │    └── column1:8
      │         ├── first-agg [as=column3:10, outer=(10)]
      │         │    └── column3:10
      │         └── first-agg [as=j_default:12, outer=(12)]
      │              └── j_default:12
      └── aggregations
           ├── first-agg [as=column1:8, outer=(8)]
           │    └── column1:8
           ├── first-agg [as=column2:9, outer=(9)]
           │    └── column2:9
           └── first-agg [as=j_default:12, outer=(12)]
                └── j_default:12

# DO NOTHING case where innermost distinct op cannot be removed (because it
# groups on a non-constant column). Ensure that outer distinct ops can still be
# removed.
norm
INSERT INTO a (k, s, i, f) VALUES (unique_rowid(), 'foo', 1, 1.0), (unique_rowid(), 'bar', 2, 2.0)
ON CONFLICT DO NOTHING
----
insert a
 ├── arbiter indexes: a_pkey si_idx fi_idx
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── column1:8 => k:1
 │    ├── column3:10 => i:2
 │    ├── column4:11 => f:3
 │    ├── column2:9 => s:4
 │    └── j_default:12 => j:5
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── upsert-distinct-on
      ├── columns: column1:8 column2:9!null column3:10!null column4:11!null j_default:12
      ├── grouping columns: column1:8
      ├── cardinality: [0 - 2]
      ├── volatile
      ├── lax-key: (8)
      ├── fd: ()-->(12), (8)~~>(9-12)
      ├── anti-join (hash)
      │    ├── columns: column1:8 column2:9!null column3:10!null column4:11!null j_default:12
      │    ├── cardinality: [0 - 2]
      │    ├── volatile
      │    ├── fd: ()-->(12)
      │    ├── anti-join (hash)
      │    │    ├── columns: column1:8 column2:9!null column3:10!null column4:11!null j_default:12
      │    │    ├── cardinality: [0 - 2]
      │    │    ├── volatile
      │    │    ├── fd: ()-->(12)
      │    │    ├── anti-join (hash)
      │    │    │    ├── columns: column1:8 column2:9!null column3:10!null column4:11!null j_default:12
      │    │    │    ├── cardinality: [0 - 2]
      │    │    │    ├── volatile
      │    │    │    ├── fd: ()-->(12)
      │    │    │    ├── project
      │    │    │    │    ├── columns: j_default:12 column1:8 column2:9!null column3:10!null column4:11!null
      │    │    │    │    ├── cardinality: [2 - 2]
      │    │    │    │    ├── volatile
      │    │    │    │    ├── fd: ()-->(12)
      │    │    │    │    ├── values
      │    │    │    │    │    ├── columns: column1:8 column2:9!null column3:10!null column4:11!null
      │    │    │    │    │    ├── cardinality: [2 - 2]
      │    │    │    │    │    ├── volatile
      │    │    │    │    │    ├── (unique_rowid(), 'foo', 1, 1.0)
      │    │    │    │    │    └── (unique_rowid(), 'bar', 2, 2.0)
      │    │    │    │    └── projections
      │    │    │    │         └── CAST(NULL AS JSONB) [as=j_default:12]
      │    │    │    ├── scan a
      │    │    │    │    ├── columns: k:13!null
      │    │    │    │    ├── flags: avoid-full-scan
      │    │    │    │    └── key: (13)
      │    │    │    └── filters
      │    │    │         └── column1:8 = k:13 [outer=(8,13), constraints=(/8: (/NULL - ]; /13: (/NULL - ]), fd=(8)==(13), (13)==(8)]
      │    │    ├── scan a
      │    │    │    ├── columns: i:21!null s:23!null
      │    │    │    ├── flags: avoid-full-scan
      │    │    │    └── key: (21,23)
      │    │    └── filters
      │    │         ├── column3:10 = i:21 [outer=(10,21), constraints=(/10: (/NULL - ]; /21: (/NULL - ]), fd=(10)==(21), (21)==(10)]
      │    │         └── column2:9 = s:23 [outer=(9,23), constraints=(/9: (/NULL - ]; /23: (/NULL - ]), fd=(9)==(23), (23)==(9)]
      │    ├── scan a
      │    │    ├── columns: i:28!null f:29
      │    │    ├── flags: avoid-full-scan
      │    │    └── lax-key: (28,29)
      │    └── filters
      │         ├── column3:10 = i:28 [outer=(10,28), constraints=(/10: (/NULL - ]; /28: (/NULL - ]), fd=(10)==(28), (28)==(10)]
      │         └── column4:11 = f:29 [outer=(11,29), constraints=(/11: (/NULL - ]; /29: (/NULL - ]), fd=(11)==(29), (29)==(11)]
      └── aggregations
           ├── first-agg [as=column2:9, outer=(9)]
           │    └── column2:9
           ├── first-agg [as=column3:10, outer=(10)]
           │    └── column3:10
           ├── first-agg [as=column4:11, outer=(11)]
           │    └── column4:11
           └── first-agg [as=j_default:12, outer=(12)]
                └── j_default:12

# DO NOTHING case with explicit conflict columns (only add upsert-distinct-on
# for one index).
norm expect-not=EliminateDistinctOnValues
INSERT INTO a (k, s, i) SELECT i, 'foo', i FROM a
ON CONFLICT (s, i) DO NOTHING
----
insert a
 ├── arbiter indexes: si_idx
 ├── columns: <none>
 ├── insert-mapping:
 │    ├── i:9 => k:1
 │    ├── i:9 => i:2
 │    ├── f_default:16 => f:3
 │    ├── "?column?":15 => s:4
 │    └── j_default:17 => j:5
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 └── upsert-distinct-on
      ├── columns: i:9!null "?column?":15!null f_default:16 j_default:17
      ├── grouping columns: i:9!null
      ├── key: (9)
      ├── fd: ()-->(15-17)
      ├── anti-join (hash)
      │    ├── columns: i:9!null "?column?":15!null f_default:16 j_default:17
      │    ├── fd: ()-->(15-17)
      │    ├── project
      │    │    ├── columns: f_default:16 j_default:17 "?column?":15!null i:9!null
      │    │    ├── fd: ()-->(15-17)
      │    │    ├── scan a
      │    │    │    └── columns: i:9!null
      │    │    └── projections
      │    │         ├── CAST(NULL AS FLOAT8) [as=f_default:16]
      │    │         ├── CAST(NULL AS JSONB) [as=j_default:17]
      │    │         └── 'foo' [as="?column?":15]
      │    ├── select
      │    │    ├── columns: i:19!null s:21!null
      │    │    ├── key: (19)
      │    │    ├── fd: ()-->(21)
      │    │    ├── scan a
      │    │    │    ├── columns: i:19!null s:21!null
      │    │    │    ├── flags: avoid-full-scan
      │    │    │    └── key: (19,21)
      │    │    └── filters
      │    │         └── s:21 = 'foo' [outer=(21), constraints=(/21: [/'foo' - /'foo']; tight), fd=()-->(21)]
      │    └── filters
      │         └── i:9 = i:19 [outer=(9,19), constraints=(/9: (/NULL - ]; /19: (/NULL - ]), fd=(9)==(19), (19)==(9)]
      └── aggregations
           ├── first-agg [as=f_default:16, outer=(16)]
           │    └── f_default:16
           ├── first-agg [as=j_default:17, outer=(17)]
           │    └── j_default:17
           └── const-agg [as="?column?":15, outer=(15)]
                └── "?column?":15

# --------------------------------------------------
# PushAggDistinctIntoGroupBy
# --------------------------------------------------

# SUM case.
norm expect=PushAggDistinctIntoGroupBy
SELECT sum(DISTINCT y) FROM xyzbs
----
scalar-group-by
 ├── columns: sum:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── distinct-on
 │    ├── columns: y:2
 │    ├── grouping columns: y:2
 │    ├── key: (2)
 │    └── scan xyzbs
 │         └── columns: y:2
 └── aggregations
      └── sum [as=sum:8, outer=(2)]
           └── y:2

# COUNT case. Expecting an index scan because opt command is used.
opt expect=PushAggDistinctIntoGroupBy
SELECT count(DISTINCT y) FROM xyzbs
----
scalar-group-by
 ├── columns: count:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── distinct-on
 │    ├── columns: y:2
 │    ├── grouping columns: y:2
 │    ├── internal-ordering: +2
 │    ├── key: (2)
 │    └── scan xyzbs@xyzbs_y_idx
 │         ├── columns: y:2
 │         └── ordering: +2
 └── aggregations
      └── count [as=count:8, outer=(2)]
           └── y:2

# AVG case.
norm expect=PushAggDistinctIntoGroupBy
SELECT avg(DISTINCT y) FROM xyzbs
----
scalar-group-by
 ├── columns: avg:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── distinct-on
 │    ├── columns: y:2
 │    ├── grouping columns: y:2
 │    ├── key: (2)
 │    └── scan xyzbs
 │         └── columns: y:2
 └── aggregations
      └── avg [as=avg:8, outer=(2)]
           └── y:2

# JSON_AGG case.
norm expect=PushAggDistinctIntoGroupBy
SELECT json_agg(DISTINCT y) FROM xyzbs
----
scalar-group-by
 ├── columns: json_agg:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── distinct-on
 │    ├── columns: y:2
 │    ├── grouping columns: y:2
 │    ├── key: (2)
 │    └── scan xyzbs
 │         └── columns: y:2
 └── aggregations
      └── json-agg [as=json_agg:8, outer=(2)]
           └── y:2

# CORR case.
# Multiple input arguments for aggregate function.
norm expect=PushAggDistinctIntoGroupBy
SELECT corr(DISTINCT y, z) FROM xyzbs
----
scalar-group-by
 ├── columns: corr:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── distinct-on
 │    ├── columns: y:2 z:3!null
 │    ├── grouping columns: y:2 z:3!null
 │    ├── key: (2,3)
 │    └── scan xyzbs
 │         └── columns: y:2 z:3!null
 └── aggregations
      └── corr [as=corr:8, outer=(2,3)]
           ├── y:2
           └── z:3

# STRING_AGG case.
# Multiple input arguments for aggregate function.
norm expect=PushAggDistinctIntoGroupBy
SELECT string_agg(DISTINCT s, '-') FROM xyzbs
----
scalar-group-by
 ├── columns: string_agg:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── distinct-on
 │    ├── columns: s:5 column8:8!null
 │    ├── grouping columns: s:5
 │    ├── key: (5)
 │    ├── fd: ()-->(8)
 │    ├── project
 │    │    ├── columns: column8:8!null s:5
 │    │    ├── fd: ()-->(8)
 │    │    ├── scan xyzbs
 │    │    │    └── columns: s:5
 │    │    └── projections
 │    │         └── '-' [as=column8:8]
 │    └── aggregations
 │         └── const-agg [as=column8:8, outer=(8)]
 │              └── column8:8
 └── aggregations
      └── string-agg [as=string_agg:9, outer=(5,8)]
           ├── s:5
           └── column8:8

# STRING_AGG case with an ORDER BY.
# Multiple input arguments for aggregate function.
norm expect=PushAggDistinctIntoGroupBy
SELECT string_agg(DISTINCT s, '-') FROM (SELECT s FROM xyzbs ORDER BY s)
----
scalar-group-by
 ├── columns: string_agg:9
 ├── internal-ordering: +5 opt(8)
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── sort
 │    ├── columns: s:5 column8:8!null
 │    ├── key: (5)
 │    ├── fd: ()-->(8)
 │    ├── ordering: +5 opt(8) [actual: +5]
 │    └── distinct-on
 │         ├── columns: s:5 column8:8!null
 │         ├── grouping columns: s:5
 │         ├── key: (5)
 │         ├── fd: ()-->(8)
 │         ├── project
 │         │    ├── columns: column8:8!null s:5
 │         │    ├── fd: ()-->(8)
 │         │    ├── scan xyzbs
 │         │    │    └── columns: s:5
 │         │    └── projections
 │         │         └── '-' [as=column8:8]
 │         └── aggregations
 │              ├── first-agg [as=s:5, outer=(5)]
 │              │    └── s:5
 │              └── const-agg [as=column8:8, outer=(8)]
 │                   └── column8:8
 └── aggregations
      └── string-agg [as=string_agg:9, outer=(5,8)]
           ├── s:5
           └── column8:8

# Case with a GroupBy operator.
norm expect=PushAggDistinctIntoGroupBy
SELECT b, count(DISTINCT y) FROM xyzbs GROUP BY b
----
group-by (hash)
 ├── columns: b:4!null count:8!null
 ├── grouping columns: b:4!null
 ├── cardinality: [0 - 2]
 ├── key: (4)
 ├── fd: (4)-->(8)
 ├── distinct-on
 │    ├── columns: y:2 b:4!null
 │    ├── grouping columns: y:2 b:4!null
 │    ├── key: (2,4)
 │    └── scan xyzbs
 │         └── columns: y:2 b:4!null
 └── aggregations
      └── count [as=count:8, outer=(2)]
           └── y:2

# Case with a GroupBy operator grouping on multiple columns.
norm expect=PushAggDistinctIntoGroupBy
SELECT b, s, count(DISTINCT y) FROM xyzbs GROUP BY b, s
----
group-by (hash)
 ├── columns: b:4!null s:5 count:8!null
 ├── grouping columns: b:4!null s:5
 ├── key: (4,5)
 ├── fd: (4,5)-->(8)
 ├── distinct-on
 │    ├── columns: y:2 b:4!null s:5
 │    ├── grouping columns: y:2 b:4!null s:5
 │    ├── key: (2,4,5)
 │    └── scan xyzbs
 │         └── columns: y:2 b:4!null s:5
 └── aggregations
      └── count [as=count:8, outer=(2)]
           └── y:2

# Case with a GroupBy operator and an aggregate with multiple input columns.
norm expect=PushAggDistinctIntoGroupBy
SELECT s, corr(DISTINCT y, z) FROM xyzbs GROUP BY s
----
group-by (hash)
 ├── columns: s:5 corr:8
 ├── grouping columns: s:5
 ├── key: (5)
 ├── fd: (5)-->(8)
 ├── distinct-on
 │    ├── columns: y:2 z:3!null s:5
 │    ├── grouping columns: y:2 z:3!null s:5
 │    ├── key: (2,3,5)
 │    └── scan xyzbs
 │         └── columns: y:2 z:3!null s:5
 └── aggregations
      └── corr [as=corr:8, outer=(2,3)]
           ├── y:2
           └── z:3

# The ordering columns of the GroupBy operator should become FirstAggs in the
# DistinctOn. This ensures that the ordering columns are available for the
# GroupBy operator.
norm expect=PushAggDistinctIntoGroupBy
SELECT array_agg(DISTINCT s) FROM (SELECT * FROM a ORDER BY i) GROUP BY f
----
project
 ├── columns: array_agg:8!null
 └── group-by (hash)
      ├── columns: f:3 array_agg:8!null
      ├── grouping columns: f:3
      ├── internal-ordering: +2 opt(3)
      ├── key: (3)
      ├── fd: (3)-->(8)
      ├── sort
      │    ├── columns: i:2!null f:3 s:4!null
      │    ├── key: (2,4)
      │    ├── fd: (2,4)-->(3), (2,3)~~>(4), (3,4)-->(2)
      │    ├── ordering: +2 opt(3) [actual: +2]
      │    └── distinct-on
      │         ├── columns: i:2!null f:3 s:4!null
      │         ├── grouping columns: f:3 s:4!null
      │         ├── key: (2,4)
      │         ├── fd: (2,4)-->(3), (2,3)~~>(4), (3,4)-->(2)
      │         ├── scan a
      │         │    ├── columns: i:2!null f:3 s:4!null
      │         │    ├── key: (2,4)
      │         │    └── fd: (2,4)-->(3), (2,3)~~>(4)
      │         └── aggregations
      │              └── first-agg [as=i:2, outer=(2)]
      │                   └── i:2
      └── aggregations
           └── array-agg [as=array_agg:8, outer=(4)]
                └── s:4

# No-op case where the same aggregate function is called on different
# columns.
norm expect-not=PushAggDistinctIntoGroupBy
SELECT count(DISTINCT y), count(DISTINCT z) FROM xyzbs
----
scalar-group-by
 ├── columns: count:8!null count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8,9)
 ├── scan xyzbs
 │    └── columns: y:2 z:3!null
 └── aggregations
      ├── agg-distinct [as=count:8, outer=(2)]
      │    └── count
      │         └── y:2
      └── agg-distinct [as=count:9, outer=(3)]
           └── count
                └── z:3

# No-op case where different aggregate functions are called on the same
# column.
norm expect-not=PushAggDistinctIntoGroupBy
SELECT count(DISTINCT y), sum(DISTINCT y) FROM xyzbs
----
scalar-group-by
 ├── columns: count:8!null sum:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8,9)
 ├── scan xyzbs
 │    └── columns: y:2
 └── aggregations
      ├── agg-distinct [as=count:8, outer=(2)]
      │    └── count
      │         └── y:2
      └── agg-distinct [as=sum:9, outer=(2)]
           └── sum
                └── y:2

# No-op cases where EliminateAggDistinct removes the AggDistinct before
# PushAggDistinctIntoGroupBy is applied. Applies to MAX, MIN, BOOL_AND,
# and BOOL_OR.
norm expect-not=PushAggDistinctIntoGroupBy
SELECT max(DISTINCT y) FROM xyzbs
----
scalar-group-by
 ├── columns: max:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 │    └── columns: y:2
 └── aggregations
      └── max [as=max:8, outer=(2)]
           └── y:2

norm expect-not=PushAggDistinctIntoGroupBy
SELECT bool_and(DISTINCT b) FROM xyzbs
----
scalar-group-by
 ├── columns: bool_and:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 │    └── columns: b:4!null
 └── aggregations
      └── bool-and [as=bool_and:8, outer=(4)]
           └── b:4

# --------------------------------------------------
# PushAggFilterIntoScalarGroupBy
# --------------------------------------------------

# SUM case.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT sum(y) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: sum:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── select
 │    ├── columns: y:2!null
 │    ├── scan xyzbs
 │    │    └── columns: y:2
 │    └── filters
 │         └── y:2 < 50 [outer=(2), constraints=(/2: (/NULL - /49]; tight)]
 └── aggregations
      └── sum [as=sum:9, outer=(2)]
           └── y:2

# COUNT case. Expecting an index scan because opt command is used.
opt expect=PushAggFilterIntoScalarGroupBy
SELECT count(y) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── scan xyzbs@xyzbs_y_idx
 │    ├── columns: y:2!null
 │    └── constraint: /2/1: (/NULL - /49]
 └── aggregations
      └── count-rows [as=count:9]

# AVG case.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT avg(y) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: avg:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── select
 │    ├── columns: y:2!null
 │    ├── scan xyzbs
 │    │    └── columns: y:2
 │    └── filters
 │         └── y:2 < 50 [outer=(2), constraints=(/2: (/NULL - /49]; tight)]
 └── aggregations
      └── avg [as=avg:9, outer=(2)]
           └── y:2

# MAX case.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT max(y) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: max:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── select
 │    ├── columns: y:2!null
 │    ├── scan xyzbs
 │    │    └── columns: y:2
 │    └── filters
 │         └── y:2 < 50 [outer=(2), constraints=(/2: (/NULL - /49]; tight)]
 └── aggregations
      └── max [as=max:9, outer=(2)]
           └── y:2

# BOOL_AND case.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT bool_and(b) FILTER (WHERE b) FROM xyzbs
----
scalar-group-by
 ├── columns: bool_and:8
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── select
 │    ├── columns: b:4!null
 │    ├── fd: ()-->(4)
 │    ├── scan xyzbs
 │    │    └── columns: b:4!null
 │    └── filters
 │         └── b:4 [outer=(4), constraints=(/4: [/true - /true]; tight), fd=()-->(4)]
 └── aggregations
      └── bool-and [as=bool_and:8, outer=(4)]
           └── b:4

# JSON_AGG case.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT json_agg(y) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: json_agg:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── select
 │    ├── columns: y:2!null
 │    ├── scan xyzbs
 │    │    └── columns: y:2
 │    └── filters
 │         └── y:2 < 50 [outer=(2), constraints=(/2: (/NULL - /49]; tight)]
 └── aggregations
      └── json-agg [as=json_agg:9, outer=(2)]
           └── y:2

# CORR case.
# Multiple input arguments for aggregate function.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT corr(y, z) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: corr:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── select
 │    ├── columns: y:2!null z:3!null
 │    ├── scan xyzbs
 │    │    └── columns: y:2 z:3!null
 │    └── filters
 │         └── y:2 < 50 [outer=(2), constraints=(/2: (/NULL - /49]; tight)]
 └── aggregations
      └── corr [as=corr:9, outer=(2,3)]
           ├── y:2
           └── z:3

# STRING_AGG case.
# Multiple input arguments for aggregate function.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT string_agg(s, '-') FILTER (WHERE s < 'abc') FROM xyzbs
----
scalar-group-by
 ├── columns: string_agg:10
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(10)
 ├── project
 │    ├── columns: column8:8!null s:5!null
 │    ├── fd: ()-->(8)
 │    ├── select
 │    │    ├── columns: s:5!null
 │    │    ├── scan xyzbs
 │    │    │    └── columns: s:5
 │    │    └── filters
 │    │         └── s:5 < 'abc' [outer=(5), constraints=(/5: (/NULL - /'abc'); tight)]
 │    └── projections
 │         └── '-' [as=column8:8]
 └── aggregations
      └── string-agg [as=string_agg:10, outer=(5,8)]
           ├── s:5
           └── column8:8

# STRING_AGG case with an ORDER BY.
# Expecting an index scan because opt command is used.
# Multiple input arguments for aggregate function.
opt expect=PushAggFilterIntoScalarGroupBy
SELECT string_agg(s, '-') FILTER (WHERE s < 'abc') FROM (SELECT s FROM xyzbs ORDER BY s)
----
scalar-group-by
 ├── columns: string_agg:10
 ├── internal-ordering: +5 opt(8)
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(10)
 ├── project
 │    ├── columns: column8:8!null s:5!null
 │    ├── fd: ()-->(8)
 │    ├── ordering: +5 opt(8) [actual: +5]
 │    ├── scan xyzbs@xyzbs_s_idx
 │    │    ├── columns: s:5!null
 │    │    ├── constraint: /5/1: (/NULL - /'abc')
 │    │    └── ordering: +5
 │    └── projections
 │         └── '-' [as=column8:8]
 └── aggregations
      └── string-agg [as=string_agg:10, outer=(5,8)]
           ├── s:5
           └── column8:8

# Case with multiple conditions.
norm expect=PushAggFilterIntoScalarGroupBy
SELECT count(y) FILTER (WHERE y < 50 AND z > 5) FROM xyzbs
----
scalar-group-by
 ├── columns: count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── select
 │    ├── columns: y:2!null z:3!null
 │    ├── scan xyzbs
 │    │    └── columns: y:2 z:3!null
 │    └── filters
 │         ├── y:2 < 50 [outer=(2), constraints=(/2: (/NULL - /49]; tight)]
 │         └── z:3 > 5 [outer=(3), constraints=(/3: [/6 - ]; tight)]
 └── aggregations
      └── count-rows [as=count:9]

# No-op case where the same aggregate function is called on different
# columns.
norm expect-not=PushAggFilterIntoScalarGroupBy
SELECT count(y) FILTER (WHERE y < 50), count(z) FILTER (WHERE z > 50) FROM xyzbs
----
scalar-group-by
 ├── columns: count:9!null count:11!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9,11)
 ├── project
 │    ├── columns: column8:8 column10:10!null y:2 z:3!null
 │    ├── fd: (2)-->(8), (3)-->(10)
 │    ├── scan xyzbs
 │    │    └── columns: y:2 z:3!null
 │    └── projections
 │         ├── y:2 < 50 [as=column8:8, outer=(2)]
 │         └── z:3 > 50 [as=column10:10, outer=(3)]
 └── aggregations
      ├── agg-filter [as=count:9, outer=(2,8)]
      │    ├── count
      │    │    └── y:2
      │    └── column8:8
      └── agg-filter [as=count:11, outer=(3,10)]
           ├── count
           │    └── z:3
           └── column10:10

# No-op case where different aggregate functions are called on the same
# column.
norm expect-not=PushAggFilterIntoScalarGroupBy
SELECT count(y) FILTER (WHERE y < 50), sum(y) FILTER (WHERE y < 50) FROM xyzbs
----
scalar-group-by
 ├── columns: count:9!null sum:10
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9,10)
 ├── project
 │    ├── columns: column8:8 y:2
 │    ├── fd: (2)-->(8)
 │    ├── scan xyzbs
 │    │    └── columns: y:2
 │    └── projections
 │         └── y:2 < 50 [as=column8:8, outer=(2)]
 └── aggregations
      ├── agg-filter [as=count:9, outer=(2,8)]
      │    ├── count
      │    │    └── y:2
      │    └── column8:8
      └── agg-filter [as=sum:10, outer=(2,8)]
           ├── sum
           │    └── y:2
           └── column8:8

# --------------------------------------------------
# ConvertCountToCountRows
# --------------------------------------------------

# ScalarGroupBy cases.
norm expect=ConvertCountToCountRows
SELECT count(z) FROM xyzbs
----
scalar-group-by
 ├── columns: count:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 └── aggregations
      └── count-rows [as=count:8]

norm expect=ConvertCountToCountRows
SELECT count(1) FROM xyzbs
----
scalar-group-by
 ├── columns: count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── scan xyzbs
 └── aggregations
      └── count-rows [as=count:9]

norm expect=ConvertCountToCountRows
SELECT count(1 + z) FROM xyzbs
----
scalar-group-by
 ├── columns: count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── scan xyzbs
 └── aggregations
      └── count-rows [as=count:9]

# GroupBy cases.
norm expect=ConvertCountToCountRows
SELECT count(z) FROM xyzbs GROUP BY s
----
project
 ├── columns: count:8!null
 └── group-by (hash)
      ├── columns: s:5 count:8!null
      ├── grouping columns: s:5
      ├── key: (5)
      ├── fd: (5)-->(8)
      ├── scan xyzbs
      │    └── columns: s:5
      └── aggregations
           └── count-rows [as=count:8]

norm expect=ConvertCountToCountRows
SELECT count(1) FROM xyzbs GROUP BY s
----
project
 ├── columns: count:9!null
 └── group-by (hash)
      ├── columns: s:5 count:9!null
      ├── grouping columns: s:5
      ├── key: (5)
      ├── fd: (5)-->(9)
      ├── scan xyzbs
      │    └── columns: s:5
      └── aggregations
           └── count-rows [as=count:9]

norm expect=ConvertCountToCountRows
SELECT count(1+z) FROM xyzbs GROUP BY s
----
project
 ├── columns: count:9!null
 └── group-by (hash)
      ├── columns: s:5 count:9!null
      ├── grouping columns: s:5
      ├── key: (5)
      ├── fd: (5)-->(9)
      ├── scan xyzbs
      │    └── columns: s:5
      └── aggregations
           └── count-rows [as=count:9]

# Case with multiple aggregate functions.
# Expecting to activate on z and b but not y, because y can be null.
norm expect=ConvertCountToCountRows
SELECT count(y), corr(y, z), count(z), sum(y), count(b) FROM xyzbs
----
scalar-group-by
 ├── columns: count:8!null corr:9 count:10!null sum:11 count:12!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8-12)
 ├── scan xyzbs
 │    └── columns: y:2 z:3!null
 └── aggregations
      ├── count [as=count:8, outer=(2)]
      │    └── y:2
      ├── corr [as=corr:9, outer=(2,3)]
      │    ├── y:2
      │    └── z:3
      ├── count-rows [as=count:10]
      ├── sum [as=sum:11, outer=(2)]
      │    └── y:2
      └── count-rows [as=count:12]

# No-op case because y can contain nulls.
norm expect-not=ConvertCountToCountRows
SELECT count(y) FROM xyzbs
----
scalar-group-by
 ├── columns: count:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 │    └── columns: y:2
 └── aggregations
      └── count [as=count:8, outer=(2)]
           └── y:2

# No-op case because the DISTINCT requires the count input column, so the count
# can't be eliminated.
norm expect-not=ConvertCountToCountRows
SELECT count(DISTINCT y) FROM xyzbs GROUP BY z
----
project
 ├── columns: count:8!null
 └── group-by (hash)
      ├── columns: z:3!null count:8!null
      ├── grouping columns: z:3!null
      ├── key: (3)
      ├── fd: (3)-->(8)
      ├── distinct-on
      │    ├── columns: y:2 z:3!null
      │    ├── grouping columns: y:2 z:3!null
      │    ├── key: (2,3)
      │    └── scan xyzbs
      │         └── columns: y:2 z:3!null
      └── aggregations
           └── count [as=count:8, outer=(2)]
                └── y:2

# --------------------------------------------------
# ConvertRegressionCountToCount
# --------------------------------------------------

# ScalarGroupBy cases.
norm expect=ConvertRegressionCountToCount
SELECT regr_count(z, x) FROM xyzbs
----
scalar-group-by
 ├── columns: regr_count:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 └── aggregations
      └── count-rows [as=regr_count:8]

norm expect=ConvertRegressionCountToCount
SELECT regr_count(1, 1) FROM xyzbs
----
scalar-group-by
 ├── columns: regr_count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── scan xyzbs
 └── aggregations
      └── count-rows [as=regr_count:9]

norm expect=ConvertRegressionCountToCount
SELECT regr_count(1 + z, x) FROM xyzbs
----
scalar-group-by
 ├── columns: regr_count:9!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9)
 ├── scan xyzbs
 └── aggregations
      └── count-rows [as=regr_count:9]

# GroupBy cases.
norm expect=ConvertRegressionCountToCount
SELECT regr_count(z, x) FROM xyzbs GROUP BY s
----
project
 ├── columns: regr_count:8!null
 └── group-by (hash)
      ├── columns: s:5 regr_count:8!null
      ├── grouping columns: s:5
      ├── key: (5)
      ├── fd: (5)-->(8)
      ├── scan xyzbs
      │    └── columns: s:5
      └── aggregations
           └── count-rows [as=regr_count:8]

norm expect=ConvertRegressionCountToCount
SELECT regr_count(1, 1) FROM xyzbs GROUP BY s
----
project
 ├── columns: regr_count:9!null
 └── group-by (hash)
      ├── columns: s:5 regr_count:9!null
      ├── grouping columns: s:5
      ├── key: (5)
      ├── fd: (5)-->(9)
      ├── scan xyzbs
      │    └── columns: s:5
      └── aggregations
           └── count-rows [as=regr_count:9]

norm expect=ConvertRegressionCountToCount
SELECT regr_count(1+z, x) FROM xyzbs GROUP BY s
----
project
 ├── columns: regr_count:9!null
 └── group-by (hash)
      ├── columns: s:5 regr_count:9!null
      ├── grouping columns: s:5
      ├── key: (5)
      ├── fd: (5)-->(9)
      ├── scan xyzbs
      │    └── columns: s:5
      └── aggregations
           └── count-rows [as=regr_count:9]

# Case with multiple aggregate functions.
# Expecting to normalize to count-rows as both z and x are not nullable.
norm expect=ConvertRegressionCountToCount
SELECT corr(y, z), regr_count(z, x), sum(y) FROM xyzbs
----
scalar-group-by
 ├── columns: corr:8 regr_count:9!null sum:10
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8-10)
 ├── scan xyzbs
 │    └── columns: y:2 z:3!null
 └── aggregations
      ├── corr [as=corr:8, outer=(2,3)]
      │    ├── y:2
      │    └── z:3
      ├── count-rows [as=regr_count:9]
      └── sum [as=sum:10, outer=(2)]
           └── y:2

# Case when y is nullable, and z is not, expecting to have count(y).
norm expect=ConvertRegressionCountToCount
SELECT regr_count(y, z) FROM xyzbs
----
scalar-group-by
 ├── columns: regr_count:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 │    └── columns: y:2
 └── aggregations
      └── count [as=regr_count:8, outer=(2)]
           └── y:2

# Case when z is not nullable, and y is nullable, expecting to have count(y).
norm expect=ConvertRegressionCountToCount
SELECT regr_count(z, y) FROM xyzbs
----
scalar-group-by
 ├── columns: regr_count:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan xyzbs
 │    └── columns: y:2
 └── aggregations
      └── count [as=regr_count:8, outer=(2)]
           └── y:2

# No-op case because both c1 and c2 are nullable.
norm expect-not=ConvertRegressionCountToCount
SELECT regr_count(c1, c2) FROM nullablecols
----
scalar-group-by
 ├── columns: regr_count:7!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(7)
 ├── scan nullablecols
 │    ├── columns: c1:1 c2:2
 │    ├── lax-key: (1,2)
 │    └── fd: (1)~~>(2)
 └── aggregations
      └── regression-count [as=regr_count:7, outer=(1,2)]
           ├── c1:1
           └── c2:2

# --------------------------------------------------
# FoldGroupingOperators
# --------------------------------------------------

# Case with sum aggregate.
norm expect=FoldGroupingOperators
SELECT sum(s) FROM (SELECT sum(x) FROM xy GROUP BY y) AS f(s)
----
scalar-group-by
 ├── columns: sum:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── scan xy
 │    ├── columns: x:1!null
 │    └── key: (1)
 └── aggregations
      └── sum [as=sum:6, outer=(1)]
           └── x:1

# Case with count-rows aggregate.
norm expect=FoldGroupingOperators
SELECT sum_int(c) FROM (SELECT count(x) FROM xy GROUP BY y) AS f(c)
----
scalar-group-by
 ├── columns: sum_int:6!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── scan xy
 └── aggregations
      └── count-rows [as=sum_int:6]

# Case with a count aggregate.
norm expect=FoldGroupingOperators
SELECT sum_int(cnt) FROM (SELECT count(c2) FROM nullablecols GROUP BY c1) AS f(cnt)
----
scalar-group-by
 ├── columns: sum_int:8!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(8)
 ├── scan nullablecols
 │    └── columns: c2:2
 └── aggregations
      └── count [as=sum_int:8, outer=(2)]
           └── c2:2

# Case with max aggregate.
norm expect=FoldGroupingOperators
SELECT max(m) FROM (SELECT max(x) FROM xy GROUP BY y) AS f(m)
----
scalar-group-by
 ├── columns: max:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── scan xy
 │    ├── columns: x:1!null
 │    └── key: (1)
 └── aggregations
      └── max [as=max:6, outer=(1)]
           └── x:1

# Case with bit_and aggregate.
norm expect=FoldGroupingOperators
SELECT bit_and(b) FROM (SELECT bit_and(x) FROM xy GROUP BY y) AS f(b)
----
scalar-group-by
 ├── columns: bit_and:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── scan xy
 │    ├── columns: x:1!null
 │    └── key: (1)
 └── aggregations
      └── bit-and-agg [as=bit_and:6, outer=(1)]
           └── x:1

# Case with multiple aggregates.
norm expect=FoldGroupingOperators
SELECT max(m), sum(s), sum_int(c)
FROM (SELECT sum(b), count(c), max(b) FROM abc GROUP BY a)
AS f(s, c, m)
----
scalar-group-by
 ├── columns: max:9 sum:10 sum_int:11!null
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(9-11)
 ├── scan abc
 │    └── columns: b:2!null
 └── aggregations
      ├── max [as=max:9, outer=(2)]
      │    └── b:2
      ├── sum [as=sum:10, outer=(2)]
      │    └── b:2
      └── count-rows [as=sum_int:11]

# Case with aggregation on a grouping column.
norm expect=FoldGroupingOperators
SELECT max(x)
FROM (SELECT DISTINCT ON (a) a FROM abc) AS f(x)
----
scalar-group-by
 ├── columns: max:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── scan abc
 │    └── columns: a:1!null
 └── aggregations
      └── max [as=max:6, outer=(1)]
           └── a:1

# GroupBy on GroupBy case where the inner grouping columns determine the outer
# grouping columns, but they do not intersect.
norm expect=FoldGroupingOperators
SELECT sum(s) FROM (SELECT y, sum(x) AS s FROM xy GROUP BY x) GROUP BY y
----
project
 ├── columns: sum:6!null
 └── group-by (hash)
      ├── columns: y:2 sum:6!null
      ├── grouping columns: y:2
      ├── key: (2)
      ├── fd: (2)-->(6)
      ├── scan xy
      │    ├── columns: x:1!null y:2
      │    ├── key: (1)
      │    └── fd: (1)-->(2)
      └── aggregations
           └── sum [as=sum:6, outer=(1)]
                └── x:1

# GroupBy on GroupBy case with multiple-column grouping.
norm expect=FoldGroupingOperators
SELECT sum(s) FROM (SELECT a, sum(c) AS s FROM abc GROUP BY a, b) GROUP BY a
----
project
 ├── columns: sum:7!null
 └── group-by (hash)
      ├── columns: a:1!null sum:7!null
      ├── grouping columns: a:1!null
      ├── key: (1)
      ├── fd: (1)-->(7)
      ├── scan abc
      │    └── columns: a:1!null c:3!null
      └── aggregations
           └── sum [as=sum:7, outer=(3)]
                └── c:3

# DistinctOn on a DistinctOn case.
norm expect=FoldGroupingOperators
SELECT DISTINCT ON (x) x, y, z
FROM (SELECT DISTINCT ON (a, b) a, b, c FROM abc) AS f(x, y, z)
----
distinct-on
 ├── columns: x:1!null y:2!null z:3!null
 ├── grouping columns: a:1!null
 ├── key: (1)
 ├── fd: (1)-->(2,3)
 ├── scan abc
 │    ├── columns: a:1!null b:2!null c:3!null
 │    └── key: (1-3)
 └── aggregations
      ├── first-agg [as=b:2, outer=(2)]
      │    └── b:2
      └── first-agg [as=c:3, outer=(3)]
           └── c:3

# DistinctOn on a DistinctOn case with no aggregates.
norm expect=FoldGroupingOperators
SELECT DISTINCT ON (x) x
FROM (SELECT DISTINCT ON (a, b) a FROM abc) AS f(x)
----
distinct-on
 ├── columns: x:1!null
 ├── grouping columns: a:1!null
 ├── key: (1)
 └── scan abc
      └── columns: a:1!null

# No-op case with an AvgOp. Note: this query actually could be folded if the
# groups were known to be of the same size.
norm expect-not=FoldGroupingOperators
SELECT sum(a) FROM (SELECT avg(x) FROM xy GROUP BY y) AS f(a)
----
scalar-group-by
 ├── columns: sum:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── group-by (hash)
 │    ├── columns: y:2 avg:5!null
 │    ├── grouping columns: y:2
 │    ├── key: (2)
 │    ├── fd: (2)-->(5)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    └── aggregations
 │         └── avg [as=avg:5, outer=(1)]
 │              └── x:1
 └── aggregations
      └── sum [as=sum:6, outer=(5)]
           └── avg:5

# No-op case with several valid aggregate pairs and one invalid pair.
norm expect-not=FoldGroupingOperators
SELECT sum(c), sum(s), max(s) FROM (SELECT sum(x), count(x) FROM xy GROUP BY y) AS f(s, c)
----
scalar-group-by
 ├── columns: sum:7 sum:8 max:9
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(7-9)
 ├── group-by (hash)
 │    ├── columns: y:2 sum:5!null count:6!null
 │    ├── grouping columns: y:2
 │    ├── key: (2)
 │    ├── fd: (2)-->(5,6)
 │    ├── scan xy
 │    │    ├── columns: x:1!null y:2
 │    │    ├── key: (1)
 │    │    └── fd: (1)-->(2)
 │    └── aggregations
 │         ├── sum [as=sum:5, outer=(1)]
 │         │    └── x:1
 │         └── count-rows [as=count:6]
 └── aggregations
      ├── sum [as=sum:7, outer=(6)]
      │    └── count:6
      ├── sum [as=sum:8, outer=(5)]
      │    └── sum:5
      └── max [as=max:9, outer=(5)]
           └── sum:5

# No-op case because the outer grouping columns are not functionally determined
# by the inner grouping columns in the functional dependencies of the input of
# the inner grouping operator.
norm expect-not=FoldGroupingOperators
SELECT max(m) FROM (SELECT max(x) AS m, sum(x) AS s FROM xy GROUP BY y) GROUP BY s
----
project
 ├── columns: max:7!null
 └── group-by (hash)
      ├── columns: sum:6!null max:7!null
      ├── grouping columns: sum:6!null
      ├── key: (6)
      ├── fd: (6)-->(7)
      ├── group-by (hash)
      │    ├── columns: y:2 max:5!null sum:6!null
      │    ├── grouping columns: y:2
      │    ├── key: (2)
      │    ├── fd: (2)-->(5,6)
      │    ├── scan xy
      │    │    ├── columns: x:1!null y:2
      │    │    ├── key: (1)
      │    │    └── fd: (1)-->(2)
      │    └── aggregations
      │         ├── max [as=max:5, outer=(1)]
      │         │    └── x:1
      │         └── sum [as=sum:6, outer=(1)]
      │              └── x:1
      └── aggregations
           └── max [as=max:7, outer=(5)]
                └── max:5

# No-op case because one of the grouping operators has an internal ordering. The
# array_agg ensures that the GroupBy has an internal ordering.
norm expect-not=FoldGroupingOperators
SELECT sum(s) FROM (SELECT sum(z) AS s, array_agg(z) FROM (SELECT * FROM uvwz ORDER BY w DESC) GROUP BY u)
----
scalar-group-by
 ├── columns: sum:10
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(10)
 ├── group-by (hash)
 │    ├── columns: u:1!null sum:8!null
 │    ├── grouping columns: u:1!null
 │    ├── internal-ordering: -3 opt(1)
 │    ├── key: (1)
 │    ├── fd: (1)-->(8)
 │    ├── sort
 │    │    ├── columns: u:1!null w:3!null z:4!null
 │    │    ├── ordering: -3 opt(1) [actual: -3]
 │    │    └── scan uvwz
 │    │         └── columns: u:1!null w:3!null z:4!null
 │    └── aggregations
 │         └── sum [as=sum:8, outer=(4)]
 │              └── z:4
 └── aggregations
      └── sum [as=sum:10, outer=(8)]
           └── sum:8

# No-op case because sum does not ignore duplicates.
norm expect-not=FoldGroupingOperators
SELECT sum(x)
FROM (SELECT DISTINCT ON (a) a FROM abc) AS f(x)
----
scalar-group-by
 ├── columns: sum:6
 ├── cardinality: [1 - 1]
 ├── key: ()
 ├── fd: ()-->(6)
 ├── distinct-on
 │    ├── columns: a:1!null
 │    ├── grouping columns: a:1!null
 │    ├── key: (1)
 │    └── scan abc
 │         └── columns: a:1!null
 └── aggregations
      └── sum [as=sum:6, outer=(1)]
           └── a:1

# --------------------------------------------------
# FoldGroupByAndWindow
# --------------------------------------------------

# Case with one partition column and an aggregate that references an input col.
# NOTE: the "foo" and "bar" grouping columns are simplified to ConstAgg.
norm expect=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, foo, bar;
----
project
 ├── columns: sum:10!null foo:8!null bar:9!null
 └── group-by (hash)
      ├── columns: u:1!null count:8!null array_agg:9!null sum:10!null
      ├── grouping columns: u:1!null
      ├── internal-ordering: +4 opt(1)
      ├── key: (1)
      ├── fd: (1)-->(8-10)
      ├── sort
      │    ├── columns: u:1!null v:2!null z:4!null
      │    ├── key: (1,2)
      │    ├── fd: (1,2)-->(4)
      │    ├── ordering: +4 opt(1) [actual: +4]
      │    └── scan uvwz
      │         ├── columns: u:1!null v:2!null z:4!null
      │         ├── key: (1,2)
      │         └── fd: (1,2)-->(4)
      └── aggregations
           ├── sum [as=sum:10, outer=(2)]
           │    └── v:2
           ├── count-rows [as=count:8]
           └── array-agg [as=array_agg:9, outer=(4)]
                └── z:4

# Case with a ConstAgg referencing an input column.
norm expect=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo
  FROM (SELECT *, 100 AS bar FROM uvwz)
  WINDOW w AS (
    PARTITION BY u ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, foo, bar;
----
project
 ├── columns: sum:10!null foo:9!null bar:8!null
 ├── fd: ()-->(8)
 └── group-by (hash)
      ├── columns: u:1!null bar:8!null count:9!null sum:10!null
      ├── grouping columns: u:1!null
      ├── internal-ordering: +4 opt(1,8)
      ├── key: (1)
      ├── fd: ()-->(8), (1)-->(8-10)
      ├── project
      │    ├── columns: bar:8!null u:1!null v:2!null z:4!null
      │    ├── key: (1,2)
      │    ├── fd: ()-->(8), (1,2)-->(4)
      │    ├── ordering: +4 opt(1,8) [actual: +4]
      │    ├── sort
      │    │    ├── columns: u:1!null v:2!null z:4!null
      │    │    ├── key: (1,2)
      │    │    ├── fd: (1,2)-->(4)
      │    │    ├── ordering: +4 opt(1) [actual: +4]
      │    │    └── scan uvwz
      │    │         ├── columns: u:1!null v:2!null z:4!null
      │    │         ├── key: (1,2)
      │    │         └── fd: (1,2)-->(4)
      │    └── projections
      │         └── 100 [as=bar:8]
      └── aggregations
           ├── sum [as=sum:10, outer=(2)]
           │    └── v:2
           ├── const-agg [as=bar:8, outer=(8)]
           │    └── bar:8
           └── count-rows [as=count:9]

# Case with no partition columns.
norm expect=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY foo, bar;
----
group-by (streaming)
 ├── columns: sum:10!null foo:8!null bar:9!null
 ├── internal-ordering: +4
 ├── cardinality: [0 - 1]
 ├── key: ()
 ├── fd: ()-->(8-10)
 ├── sort
 │    ├── columns: v:2!null z:4!null
 │    ├── ordering: +4
 │    └── scan uvwz
 │         └── columns: v:2!null z:4!null
 └── aggregations
      ├── sum [as=sum:10, outer=(2)]
      │    └── v:2
      ├── count-rows [as=count:8]
      └── array-agg [as=array_agg:9, outer=(4)]
           └── z:4

# Case with multiple partition columns.
norm expect=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(v) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u, w ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, w, foo, bar;
----
project
 ├── columns: sum:10!null foo:8!null bar:9!null
 └── group-by (hash)
      ├── columns: u:1!null w:3!null count:8!null array_agg:9!null sum:10!null
      ├── grouping columns: u:1!null w:3!null
      ├── internal-ordering: +4 opt(1,3)
      ├── key: (1,3)
      ├── fd: (1,3)-->(8-10)
      ├── sort
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    ├── key: (2,3)
      │    ├── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │    ├── ordering: +4 opt(1,3) [actual: +4]
      │    └── scan uvwz
      │         ├── columns: u:1!null v:2!null w:3!null z:4!null
      │         ├── key: (2,3)
      │         └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      └── aggregations
           ├── sum [as=sum:10, outer=(2)]
           │    └── v:2
           ├── count-rows [as=count:8]
           └── array-agg [as=array_agg:9, outer=(4)]
                └── z:4

# Case with grouping/partitioning on key columns.
norm expect=FoldGroupByAndWindow
SELECT sum(w), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u, v ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, v, foo, bar;
----
project
 ├── columns: sum:10!null foo:8!null bar:9!null
 └── group-by (hash)
      ├── columns: u:1!null v:2!null count:8!null array_agg:9!null sum:10!null
      ├── grouping columns: u:1!null v:2!null
      ├── key: (1,2)
      ├── fd: (1,2)-->(8-10)
      ├── scan uvwz
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    ├── key: (2,3)
      │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      └── aggregations
           ├── sum [as=sum:10, outer=(3)]
           │    └── w:3
           ├── count-rows [as=count:8]
           └── array-agg [as=array_agg:9, outer=(4)]
                └── z:4

# No-op because of an ordered GroupBy.
norm expect-not=FoldGroupByAndWindow
SELECT array_agg(v), foo, bar
FROM (
  SELECT * FROM (
    SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
    FROM uvwz
    WINDOW w AS (
      PARTITION BY u ORDER BY z
      ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    )
  )
  ORDER BY v DESC
)
GROUP BY u, foo, bar;
----
project
 ├── columns: array_agg:10!null foo:8 bar:9
 └── group-by (hash)
      ├── columns: u:1!null count:8 array_agg:9 array_agg:10!null
      ├── grouping columns: u:1!null
      ├── internal-ordering: -2 opt(1,8,9)
      ├── key: (1)
      ├── fd: (1)-->(8-10)
      ├── sort
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null count:8 array_agg:9
      │    ├── key: (2,3)
      │    ├── fd: (1,2)-->(3,4), (2,3)-->(1,4), (1)-->(8,9)
      │    ├── ordering: -2 opt(1,8,9) [actual: -2]
      │    └── window partition=(1) ordering=+4 opt(1)
      │         ├── columns: u:1!null v:2!null w:3!null z:4!null count:8 array_agg:9
      │         ├── key: (2,3)
      │         ├── fd: (1,2)-->(3,4), (2,3)-->(1,4), (1)-->(8,9)
      │         ├── scan uvwz
      │         │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │         │    ├── key: (2,3)
      │         │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │         └── windows
      │              ├── count [as=count:8, frame="rows from unbounded to unbounded", outer=(3)]
      │              │    └── w:3
      │              └── array-agg [as=array_agg:9, frame="rows from unbounded to unbounded", outer=(4)]
      │                   └── z:4
      └── aggregations
           ├── array-agg [as=array_agg:10, outer=(2)]
           │    └── v:2
           ├── const-agg [as=count:8, outer=(8)]
           │    └── count:8
           └── const-agg [as=array_agg:9, outer=(9)]
                └── array_agg:9

# No-op because the grouping columns reference a window function.
norm expect-not=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar, row_number() OVER w AS baz
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, foo, bar, baz;
----
project
 ├── columns: sum:11!null foo:8 bar:9
 └── group-by (hash)
      ├── columns: u:1!null count:8 array_agg:9 row_number:10 sum:11!null
      ├── grouping columns: u:1!null row_number:10
      ├── key: (1,10)
      ├── fd: (1)-->(8,9), (1,10)-->(8,9,11)
      ├── window partition=(1) ordering=+4 opt(1)
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null count:8 array_agg:9 row_number:10
      │    ├── key: (2,3)
      │    ├── fd: (1,2)-->(3,4), (2,3)-->(1,4,10), (1)-->(8,9)
      │    ├── scan uvwz
      │    │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    │    ├── key: (2,3)
      │    │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │    └── windows
      │         ├── count [as=count:8, frame="rows from unbounded to unbounded", outer=(3)]
      │         │    └── w:3
      │         ├── array-agg [as=array_agg:9, frame="rows from unbounded to unbounded", outer=(4)]
      │         │    └── z:4
      │         └── row-number [as=row_number:10, frame="rows from unbounded to unbounded"]
      └── aggregations
           ├── sum [as=sum:11, outer=(2)]
           │    └── v:2
           ├── const-agg [as=count:8, outer=(8)]
           │    └── count:8
           └── const-agg [as=array_agg:9, outer=(9)]
                └── array_agg:9

# No-op because the grouping columns are not the same as the partition columns.
norm expect-not=FoldGroupByAndWindow
SELECT sum(u), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY v, foo, bar;
----
project
 ├── columns: sum:10!null foo:8 bar:9
 └── group-by (hash)
      ├── columns: v:2!null count:8 array_agg:9 sum:10!null
      ├── grouping columns: v:2!null count:8 array_agg:9
      ├── key: (2,8,9)
      ├── fd: (2,8,9)-->(10)
      ├── window partition=(1) ordering=+4 opt(1)
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null count:8 array_agg:9
      │    ├── key: (2,3)
      │    ├── fd: (1,2)-->(3,4), (2,3)-->(1,4), (1)-->(8,9)
      │    ├── scan uvwz
      │    │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    │    ├── key: (2,3)
      │    │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │    └── windows
      │         ├── count [as=count:8, frame="rows from unbounded to unbounded", outer=(3)]
      │         │    └── w:3
      │         └── array-agg [as=array_agg:9, frame="rows from unbounded to unbounded", outer=(4)]
      │              └── z:4
      └── aggregations
           └── sum [as=sum:10, outer=(1)]
                └── u:1

# No-op because the grouping columns are not the same as the partition columns.
norm expect-not=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, foo, bar;
----
project
 ├── columns: sum:10!null foo:8 bar:9
 ├── fd: ()-->(8,9)
 └── group-by (hash)
      ├── columns: u:1!null count:8 array_agg:9 sum:10!null
      ├── grouping columns: u:1!null
      ├── key: (1)
      ├── fd: ()-->(8,9), (1)-->(8-10)
      ├── window partition=() ordering=+4
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null count:8 array_agg:9
      │    ├── key: (2,3)
      │    ├── fd: ()-->(8,9), (1,2)-->(3,4), (2,3)-->(1,4)
      │    ├── scan uvwz
      │    │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    │    ├── key: (2,3)
      │    │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │    └── windows
      │         ├── count [as=count:8, frame="rows from unbounded to unbounded", outer=(3)]
      │         │    └── w:3
      │         └── array-agg [as=array_agg:9, frame="rows from unbounded to unbounded", outer=(4)]
      │              └── z:4
      └── aggregations
           ├── sum [as=sum:10, outer=(2)]
           │    └── v:2
           ├── const-agg [as=count:8, outer=(8)]
           │    └── count:8
           └── const-agg [as=array_agg:9, outer=(9)]
                └── array_agg:9

# No-op because the window frame is not unbounded.
norm expect-not=FoldGroupByAndWindow
SELECT sum(v), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u ORDER BY z
  )
)
GROUP BY u, foo, bar;
----
project
 ├── columns: sum:10!null foo:8 bar:9
 └── group-by (hash)
      ├── columns: u:1!null count:8 array_agg:9 sum:10!null
      ├── grouping columns: u:1!null count:8 array_agg:9
      ├── key: (1,8,9)
      ├── fd: (1,8,9)-->(10)
      ├── window partition=(1) ordering=+4 opt(1)
      │    ├── columns: u:1!null v:2!null w:3!null z:4!null count:8 array_agg:9
      │    ├── key: (2,3)
      │    ├── fd: (1,2)-->(3,4), (2,3)-->(1,4,8,9)
      │    ├── scan uvwz
      │    │    ├── columns: u:1!null v:2!null w:3!null z:4!null
      │    │    ├── key: (2,3)
      │    │    └── fd: (1,2)-->(3,4), (2,3)-->(1,4)
      │    └── windows
      │         ├── count [as=count:8, outer=(3)]
      │         │    └── w:3
      │         └── array-agg [as=array_agg:9, outer=(4)]
      │              └── z:4
      └── aggregations
           └── sum [as=sum:10, outer=(2)]
                └── v:2

# No-op because the (non ConstAgg-like) Sum GroupBy aggregate references a
# Window aggregate.
norm expect-not=FoldGroupByAndWindow
SELECT sum(foo), foo, bar
FROM (
  SELECT *, count(w) OVER w AS foo, array_agg(z) OVER w AS bar
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, foo, bar;
----
project
 ├── columns: sum:10 foo:8 bar:9
 └── group-by (hash)
      ├── columns: u:1!null count:8 array_agg:9 sum:10
      ├── grouping columns: u:1!null
      ├── key: (1)
      ├── fd: (1)-->(8-10)
      ├── window partition=(1) ordering=+4 opt(1)
      │    ├── columns: u:1!null w:3!null z:4!null count:8 array_agg:9
      │    ├── fd: (1)-->(8,9)
      │    ├── scan uvwz
      │    │    └── columns: u:1!null w:3!null z:4!null
      │    └── windows
      │         ├── count [as=count:8, frame="rows from unbounded to unbounded", outer=(3)]
      │         │    └── w:3
      │         └── array-agg [as=array_agg:9, frame="rows from unbounded to unbounded", outer=(4)]
      │              └── z:4
      └── aggregations
           ├── sum [as=sum:10, outer=(8)]
           │    └── count:8
           ├── const-agg [as=count:8, outer=(8)]
           │    └── count:8
           └── const-agg [as=array_agg:9, outer=(9)]
                └── array_agg:9

# No-op case with row_number, which can produce a different result for each row
# in a window frame.
norm expect-not=FoldGroupByAndWindow
SELECT sum(v), foo
FROM (
  SELECT *, row_number() OVER w AS foo
  FROM uvwz
  WINDOW w AS (
    PARTITION BY u ORDER BY z
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
)
GROUP BY u, foo;
----
project
 ├── columns: sum:9!null foo:8
 └── group-by (hash)
      ├── columns: u:1!null row_number:8 sum:9!null
      ├── grouping columns: u:1!null row_number:8
      ├── key: (1,8)
      ├── fd: (1,8)-->(9)
      ├── window partition=(1) ordering=+4 opt(1)
      │    ├── columns: u:1!null v:2!null z:4!null row_number:8
      │    ├── key: (1,2)
      │    ├── fd: (1,2)-->(4,8)
      │    ├── scan uvwz
      │    │    ├── columns: u:1!null v:2!null z:4!null
      │    │    ├── key: (1,2)
      │    │    └── fd: (1,2)-->(4)
      │    └── windows
      │         └── row-number [as=row_number:8, frame="rows from unbounded to unbounded"]
      └── aggregations
           └── sum [as=sum:9, outer=(2)]
                └── v:2
