Repository navigation
Conversation
Verification-only commit for #12041: exercise WITH RECURSIVE through con.sql(), a filter on top of con.sql(), Table.sql() with a recursive query, and Table.sql() over a recursive parent, on every backend. The tests deliberately carry no xfail markers so CI reports the raw state. Also add compile-only BigQuery unit tests (the dialect from #11949) and temporarily run pytest with -v so per-test outcomes appear in CI logs even if a job times out. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Instead of saving and restoring the flag around the rebuild, hand it to
sqlglot's with_(recursive=...) directly. In add_query_to_expr, also keep
the flag from the compiled ibis expression, not just the user's query,
so `con.sql(recursive_query).alias("t").sql("SELECT ... FROM t")` works.
Build ibis's own CTE aliases as TableAlias rather than bare Identifier.
With RECURSIVE set, sqlglot's Presto/Trino generator reads
`cte.args["alias"].columns` on every CTE, which raised AttributeError
on Trino and Athena.
SingleStoreDB overrides add_query_to_expr with a copy of the same logic,
so apply the same changes there.
Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Verification report:
|
| Commit | Contents | Backends | BigQuery | Athena | Databricks |
|---|---|---|---|---|---|
1 7d9467fc4 |
tests only, on main |
run | run | run | run |
2 08492755b |
#12041's base.py change, cherry-picked unchanged |
run | run | run | run |
3 35489f658 |
proposed fix | run | run | run | run |
test_dot_sql_recursive_cte has no xfail markers, so these are raw outcomes. It checks only that the query runs and returns the right rows. It doesn't assert on the SQL text, so MSSQL (no RECURSIVE keyword) and SQLite (keyword optional) aren't failed for no reason. The cases:
con_sql:con.sql(recursive), which is the path in bug: Ibis removes mandatory "RECURSIVE" keyword from BigQuery recursive CTE definition #11949con_sql_filtered:con.sql(recursive).filter(...), where the recursiveWITHstays nested in a subquerytable_sql_recursive_query:con.sql("SELECT 1 AS x").alias("t").sql(recursive query reading from t)table_sql_recursive_parent:con.sql(recursive).alias("t").sql("SELECT n FROM t")
BigQuery also has compile-only unit tests asserting WITH RECURSIVE is in the output. Those run in fork CI without credentials.
Results
Each cell shows main → #12041 → proposed. ✅ pass, ❌ fail, 🔒 infrastructure error, — didn't run (BigQuery hit its 60-minute job timeout first). Where a backend has several jobs (OS/Python versions), they all agreed.
| Backend | con_sql |
con_sql_filtered |
table_sql_recursive_query |
table_sql_recursive_parent |
|---|---|---|---|---|
| athena | ❌✅✅ | ✅✅✅ | ❌❌✅ | ❌❌✅ |
| bigquery | ❌✅✅ | —❌❌ | ❌✅✅ | ❌❌❌ |
| bigquery (compile-only unit) | ❌✅✅ | ——— | ❌✅✅ | ❌❌✅ |
| clickhouse | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| databricks | 🔒🔒🔒 | 🔒🔒🔒 | 🔒🔒🔒 | 🔒🔒🔒 |
| datafusion | ❌✅✅ | ✅✅✅ | ❌✅✅ | ❌❌✅ |
| druid | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| duckdb | ❌✅✅ | ✅✅✅ | ❌✅✅ | ❌❌✅ |
| exasol | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| flink | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| impala | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| materialize | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| mssql | ✅✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ |
| mysql | ❌✅✅ | ✅✅✅ | ❌✅✅ | ❌❌✅ |
| oracle | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| polars | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| postgres | ❌✅✅ | ✅✅✅ | ❌✅✅ | ❌❌✅ |
| pyspark | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| risingwave | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| singlestoredb | ❌❌❌ | ❌❌❌ | ❌❌❌ | ❌❌❌ |
| sqlite | ✅✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ |
| trino | ❌✅✅ | ✅✅✅ | ❌❌✅ | ❌❌✅ |
Snowflake isn't included because its workflow is disabled. Databricks errored on every test in every run with Invalid access token (same on main).
Findings
- fix(sql): preserve RECURSIVE keyword when rebuilding WITH clause #12041 misses the "recursive parent" case.
add_query_to_expronly reads the flag from the user's query and drops it from the compiled ibis expression.table_sql_recursive_parentfails with fix(sql): preserve RECURSIVE keyword when rebuilding WITH clause #12041 on DuckDB, Postgres, MySQL, Trino, DataFusion and Athena, and passes with commit 3 (also in the BigQuery unit test). - New: preserving
RECURSIVEbroke Trino and Athena (AttributeError: 'Identifier' object has no attribute 'columns'with fix(sql): preserve RECURSIVE keyword when rebuilding WITH clause #12041). WithRECURSIVEset, sqlglot's Presto/Trino generator runsadd_recursive_cte_column_names, which reads.columnson every CTE's alias. ibis built its own CTE aliases as bareIdentifiers. Commit 3 wraps them insge.TableAlias(this=...), the pattern several other ibis compilers already use. - The save/restore isn't needed. sqlglot's
with_()takesrecursive=, and has at least since 23.6, which covers our>=23.4floor. Commit 3 passes it straight through. - SingleStoreDB has its own copy of
add_query_to_expr. Commit 3 applies the same change there. With the fix, the SingleStoreDB error changes from "counter doesn't exist" to SingleStore's own "unsupported recursive CTE query shape" (see below). - One side effect of the
TableAliaschange:test_cte_refs_in_topo_order[mssql]needs a snapshot update. sqlglot's T-SQL generator now aliases CTE projections explicitly ([t0].[key]→[t0].[key] AS [key]). The SQL means the same thing, and I checked locally that the other 20 dialect snapshots are unchanged. Nothing else regressed: apart from this one, the set of other failing tests is the same as onmain. One Athenare_extractfailure showed up only in commit 2's run and looks flaky.
Remaining failures, not caused by this fix
- BigQuery
con_sql_filtered/table_sql_recursive_parent: in both, ibis puts the recursiveWITHinside a subquery or CTE body, and the compiled SQL still containsWITH RECURSIVE. The two top-level cases pass, so this looks like BigQuery only accepting recursiveWITHat the top level. That's inferred: the job timed out before pytest printed error details. Fixing it would mean moving nested recursive CTEs to the top level, which is out of scope here. - ClickHouse: this one is the test's fault. Results stop one step early (
[1, 2]) because ClickHouse letsWHEREseeSELECTaliases. InSELECT "n" + 1 AS "n" FROM "counter" WHERE "n" < 3, theWHEREchecks the incremented value. Dropping the redundantAS "n"should fix it (not yet run in CI). - Trino caveat: if the ibis side compiles to
SELECT *(e.g. a barecon.table(...)), sqlglot's transform emits"t"("*"), which is invalid. That needs fixing in sqlglot. The tested shapes compile to"t"(x)and work. - Recursive CTEs unsupported or failing at the engine level: Polars ("recursive CTEs are not supported"), Materialize (only
WITH MUTUALLY RECURSIVE), SingleStoreDB ("unsupported recursive CTE query shape"), PySpark 3.5/4.0 (planning errors), Oracle (ORA-02000: sqlglot emitsRECURSIVE, which Oracle doesn't accept), and Druid, Exasol, Flink, Impala, RisingWave (generic engine errors; I didn't confirm each one is a missing feature).
Suggested follow-up for #12041
- Use commit 3's approach: pass
recursive=throughwith_(), also keep the flag from the ibis expression, wrap aliases inTableAlias, and mirror it in SingleStoreDB. Update the MSSQL snapshot. - Adopt these tests, minus the
-vchange topyproject.toml. DropAS "n"from the recursive step for ClickHouse. Addnotyet/notimplmarkers for the engine-level failures above, and for BigQuery on the two nested cases.
🤖 Generated with Claude Code
Restrict pytest to the tests relevant to #12041 so the BigQuery job finishes within its 60-minute limit and prints its failure summary, and so the other backends turn around faster. Jobs that select none of these tests (doctests, core) will exit with "no tests collected". Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
…ests ClickHouse lets WHERE refer to SELECT aliases, so in `SELECT "n" + 1 AS "n" FROM "counter" WHERE "n" < 3` the WHERE tests the incremented value and recursion stops one step early ([1, 2] instead of [1, 2, 3]). The recursive term's column names come from the anchor term anyway, so the alias was redundant. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Markers follow the errors observed in CI on #12153. Polars, Exasol, RisingWave, Druid, Flink and Impala don't support recursive CTEs; Materialize only supports WITH MUTUALLY RECURSIVE; sqlglot's Oracle dialect emits a RECURSIVE keyword Oracle rejects; SingleStoreDB rejects the query shape; Spark < 4.1 fails to parse (3.5) or plan (4.0) them. BigQuery fails only when the recursive WITH ends up nested inside a subquery or CTE. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
With CTE aliases built as TableAlias, sqlglot's T-SQL generator names CTE projections explicitly. The SQL is equivalent. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Temporary, verification-only: run raw recursive CTE placements against BigQuery so the failure messages explain the two xfailed nested cases. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up: subset run, markers, and the BigQuery limitationNew commits on top of the first report:
Results (Backends, BigQuery, Athena)
Compared with Why the markers say what they do
BigQuery: understood, and not caused by this changeThe probe shows exactly what BigQuery accepts:
Unrelated: Python 3.10 jobs now fail at collectionEvery Python 3.10 job in this run failed while collecting 🤖 Generated with Claude Code |
sqlglot's `With` node has had a `recursive` property since before our 23.4 floor, so use it instead of reading the raw arg. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
The compile-only tests exercised base compiler behavior but lived in the BigQuery unit tests with their own copy of the recursive query. Move them next to the execution test, reuse its query, and run them for every SQL backend's dialect the way `test_sql.py` does, so they need no connection. Compilers are loaded by module path because `ibis.backends.sql.compilers` doesn't export `singlestoredb`. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Verification PR — do not merge. Measures #12041's fix for #11949 (
WITH RECURSIVEdropped when ibis rebuilds theWITHclause) across every backend in CI, including cloud backends viaci-run-cloud.Commits, each run separately through CI:
main.test_dot_sql_recursive_cteruns four shapes on every backend with no xfail markers, so CI shows raw outcomes. It checks execution only, so backends that don't use the keyword (MSSQL) or treat it as optional (SQLite) aren't failed for no reason. It also adds compile-only BigQuery unit tests and temporarily adds-vto pytest's options for per-test logs.recursive=throughwith_(), and also keep the flag from the compiled ibis expression inadd_query_to_expr.A full report will be posted as a comment once all three runs finish.
🤖 Generated with Claude Code