Repository: ibis-project/ibis

Issue:
bug: AttributeError: 'NoneType' object has no attribute 'pop'
### What happened?

# Context

Not sure if this is an `sqlglot` bug or truely an `ibis` bug - but either ways would need a fix in ibis code or dependency on a certain version of `sqlglot`. The error `AttributeError: 'NoneType' object has no attribute 'pop'` is coming from deep inside sqlglot's `add_within_group_for_percentiles` function.

# Error

```
Traceback (most recent call last):
  File "/Users/xyz/abc/abc_ds/trial/tests_trial/sanity/ibis/test_ibis_sanity.py", line 129, in test_sanity_snowflake
    .to_pandas()
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/ibis/expr/types/relations.py", line 3332, in to_pandas
    return self.execute(**kwargs)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/ibis/expr/types/core.py", line 359, in execute
    return self._find_backend(use_default=True).execute(
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/ibis/backends/sql/__init__.py", line 242, in execute
    sql = self.compile(table, params=params, limit=limit, **kwargs)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/ibis/backends/sql/__init__.py", line 127, in compile
    sql = query.sql(dialect=self.dialect, pretty=pretty, copy=False)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/expressions.py", line 561, in sql
    return Dialect.get_or_raise(dialect).generate(self, **opts)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/dialects/dialect.py", line 498, in generate
    return self.generator(**opts).generate(expression, copy=copy)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 583, in generate
    sql = self.sql(expression).strip()
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 734, in sql
    sql = transform(self, expression)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/transforms.py", line 601, in _to_sql
    return _sql_handler(expression)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 2199, in select_sql
    expressions = self.expressions(expression)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 3229, in expressions
    sql = self.sql(e, comment=False)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 739, in sql
    sql = getattr(self, exp_handler_name)(expression)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 2725, in alias_sql
    return f"{self.sql(expression, 'this')}{alias}"
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 728, in sql
    return self.sql(value)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/generator.py", line 734, in sql
    sql = transform(self, expression)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/transforms.py", line 595, in _to_sql
    expression = transforms[0](expression)
  File "/Users/xyz/opt/anaconda3/envs/trial_env/lib/python3.10/site-packages/sqlglot/transforms.py", line 329, in add_within_group_for_percentiles
    column = expression.this.pop()
AttributeError: 'NoneType' object has no attribute 'pop'
```

# Snippet that cuased the error

```
# Using the following import to avoid confusion with ignore variables (`_`)
from ibis import _ as c
```

```
# INFORMATION_SCHEMA
tables_table: Table = snowflake_backend.table("TABLES")

filtered_table: Table = tables_table.filter(
	tables_table.TABLE_SCHEMA == "TESTING"
).select(
	"TABLE_CATALOG",
	"TABLE_SCHEMA",
	"TABLE_NAME",
	"TABLE_OWNER",
	"ROW_COUNT",
	"BYTES",
)

row_count_stats: dict[str, int | float] = (
	filtered_table.aggregate(
		min_row_count=c.ROW_COUNT.min(),
		max_row_count=c.ROW_COUNT.max(),
		sum_row_count=c.ROW_COUNT.sum(),
		mean_row_count=c.ROW_COUNT.mean(),
		median_row_count=c.ROW_COUNT.median(),
		quantile_0_25_row_count=c.ROW_COUNT.quantile(0.25),
		quantile_0_75_row_count=c.ROW_COUNT.quantile(0.75),
	)
	.to_pandas()
	.to_dict("records")[0]
)
```

# Environment Information

## Python

```
Anaconda Python 3.10 on Mac
```

## Installed requirements

```
sqlglot==23.0.5
ibis-framework[pandas,snowflake,duckdb]==9.0.0.dev511
```


### What version of ibis are you using?

latest dev veriosn - `9.0.0.dev511`

### What backend(s) are you using, if any?

Snowflake

### Relevant log output

```sh
Stacktrace in "What happened?"
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Hey @yazad3!  Thanks for reporting this!
I can reproduce the issue and I've got a proximate cause identified, just working on tracking down a simpler reproducer and then we should be able to push up a fix.

Resolving patch:
diff --git a/ibis/backends/snowflake/compiler.py b/ibis/backends/snowflake/compiler.py
--- a/ibis/backends/snowflake/compiler.py
+++ b/ibis/backends/snowflake/compiler.py
@@ -451,7 +451,20 @@ def visit_Quantile(self, op, *, arg, quantile, where):
         # the constant into an expression
         if where is not None:
             arg = self.if_(where, arg, NULL)
-        return self.f.percentile_cont(arg, quantile)
+
+        # The Snowflake SQLGlot dialect rewrites calls to `percentile_cont` to
+        # include     WITHIN GROUP (ORDER BY ...)
+        # as per https://docs.snowflake.com/en/sql-reference/functions/percentile_cont
+        # using the rule `add_within_group_for_percentiles`
+        #
+        # If we have copy=False set in our call to `compile`, if there is more
+        # than one quantile, the rewrite rule fails on the second pass because
+        # of some mutation in the first pass. To avoid this error, we create the
+        # expression with the within group included already and skip the (now
+        # unneeded) rewrite rule.
+        order_by = sge.Order(expressions=[sge.Ordered(this=arg)])
+        quantile = self.f.percentile_cont(quantile)
+        return sge.WithinGroup(this=quantile, expression=order_by)
 
     def visit_CountStar(self, op, *, arg, where):
         if where is None:


Test patch:
diff --git a/ibis/backends/snowflake/tests/snapshots/test_compiler/test_more_than_one_quantile/two_quantiles.sql b/ibis/backends/snowflake/tests/snapshots/test_compiler/test_more_than_one_quantile/two_quantiles.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/snowflake/tests/snapshots/test_compiler/test_more_than_one_quantile/two_quantiles.sql
@@ -0,0 +1,6 @@
+SELECT
+  PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY
+    "t0"."ROW_COUNT") AS "quantile_0_25",
+  PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY
+    "t0"."ROW_COUNT") AS "quantile_0_75"
+FROM "t" AS "t0"
\ No newline at end of file
diff --git a/ibis/backends/snowflake/tests/test_compiler.py b/ibis/backends/snowflake/tests/test_compiler.py
new file mode 100644
--- /dev/null
+++ b/ibis/backends/snowflake/tests/test_compiler.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+import ibis
+from ibis import _
+
+
+def test_more_than_one_quantile(snapshot):
+    tables = ibis.table(name="t", schema={"ROW_COUNT": "int"})
+
+    expr = tables.aggregate(
+        quantile_0_25=_.ROW_COUNT.quantile(0.25),
+        quantile_0_75=_.ROW_COUNT.quantile(0.75),
+    )
+
+    sql = ibis.to_sql(expr, dialect="snowflake")
+    snapshot.assert_match(sql, "two_quantiles.sql")