Skip to content

DuckDB: a loss that reads a view with a multiply-read CTE fails (Not implemented Error: CTE) #195

Closed

Description

@alxmrs

Summary

On DuckDB, a loss that reads a view fails if the view has a CTE that it reads more than once:

duckdb.NotImplementedException: Not implemented Error: CTE

The error comes from DuckDB's Substrait producer (get_substrait) in DuckDBBackend._plan. When a CTE is read more than once, DuckDB materializes it, and the producer has no Substrait for that operator. Written directly in the query, the same CTE plans fine, because ddx splits a query's shared CTEs itself. Inside a view, it reaches the producer as written.

This blocks all of xarray-sql's ML demos on DuckDB. Each model is a view, and a model reads its activations more than once: softmax weights feed both the normalizer and the output, and a layer norm reads its input three times.

Repro (ddxdb 0.4.1, DuckDB 1.5.6)

import duckdb
from ddxdb import duckdb as ad

con = duckdb.connect()
con.execute("CREATE TABLE w AS SELECT i, val::DOUBLE AS val FROM (VALUES (0, 1.0), (1, 2.0)) v(i, val)")
con.execute("""CREATE VIEW m AS
    WITH s AS (SELECT i, val * 2 AS y FROM w)
    SELECT a.i, a.y * b.y AS y FROM s a JOIN s b ON a.i = b.i""")
ad.sql(con, "WITH loss AS (SELECT SUM(y) AS l FROM m) SELECT i, grad(loss, val) AS g FROM w")
# NotImplementedException: Not implemented Error: CTE
Variant Result
View whose CTE is read once works
View whose CTE is read twice fails as above
The same view with WITH s AS NOT MATERIALIZED (...) works: [(0, 8.0), (1, 16.0)]
get_substrait('SELECT SUM(y) FROM m') on its own, no ddx fails the same way
get_substrait on the CTE written directly in the query works

Possible fixes

  1. Expand views before planning. On the planner connection, replace each view a loss reads with its SQL as a subquery or CTE, so ddx's own CTE splitting sees the view's shared CTEs.
  2. Mirror views with their CTEs marked NOT MATERIALIZED on the planner connection. It's only a schema copy, so how it executes doesn't matter, and the producer then gets inlined CTEs. Rewriting the view text needs a parse, so this is probably simpler to do on the parsed AST.
  3. At minimum, refuse the case with a typed ddx error that names the view and the CTE, rather than DuckDB's bare "Not implemented Error: CTE".

A workaround on the caller's side isn't great. NOT MATERIALIZED is DuckDB- and Postgres-only syntax (DataFusion's parser refuses it), so a model written once for both engines can't use it. It would also make DuckDB's own forward queries recompute the shared CTE.

Found while running xarray-sql's ML demos on ddxdb 0.4.1.

Activity

  1. alxmrs commented on Oct 25, 2026

    @alxmrs
    MemberAuthor

    Two cases still raise Not implemented Error: CTE at 3c95aa7 (built locally with maturin develop --release, duckdb 1.5.6). A view whose CTE is read twice now works when every grad table is on the path (case A below). The failures are a grad with respect to a table the view doesn't read (B), and a CTE with a window read twice in a view (D). D written without the view (all CTEs in the query) works and gives the same gradient as the window read once (C).

    import duckdb
    from ddxdb import duckdb as ddx
    
    def run(label, view, q):
        con = duckdb.connect()
        con.execute("CREATE TABLE x AS SELECT i::INT AS i, (i*0.5)::DOUBLE AS v FROM range(3) t(i)")
        con.execute("CREATE TABLE z AS SELECT i::INT AS i, 1.0::DOUBLE AS v FROM range(3) t(i)")
        con.execute(view)
        try: print(label, ddx.sql(con, q).fetchall())
        except Exception as e: print(label, type(e).__name__, str(e)[:120])
    
    TWICE = "CREATE VIEW y AS WITH w AS (SELECT i, v*v AS s FROM x) SELECT a.i, a.s + b.s AS o FROM w a JOIN w b ON a.i = b.i"
    run("A", TWICE, "WITH loss AS (SELECT SUM(o) AS l FROM y) SELECT x.i, grad(loss, x.v) AS g FROM x ORDER BY x.i")
    run("B", TWICE, "WITH loss AS (SELECT SUM(o) AS l FROM y) SELECT z.i, grad(loss, z.v) AS h FROM z ORDER BY z.i")
    WIN1 = "CREATE VIEW y AS WITH w AS (SELECT i, v - MAX(v) OVER () AS s FROM x) SELECT i, s AS o FROM w"
    run("C", WIN1, "WITH loss AS (SELECT SUM(o*o) AS l FROM y) SELECT x.i, grad(loss, x.v) AS g FROM x ORDER BY x.i")
    WIN2 = "CREATE VIEW y AS WITH w AS (SELECT i, v - MAX(v) OVER () AS s FROM x) SELECT a.i, a.s * b.s AS o FROM w a JOIN w b ON a.i = b.i"
    run("D", WIN2, "WITH loss AS (SELECT SUM(o) AS l FROM y) SELECT x.i, grad(loss, x.v) AS g FROM x ORDER BY x.i")
    A [(0, 0.0), (1, 2.0), (2, 4.0)]
    B NotImplementedException Not implemented Error: CTE
    C [(0, -2.0), (1, -1.0), (2, 3.0)]
    D NotImplementedException Not implemented Error: CTE
    

    B is expected to give zeros. With the same view, B also fails as a second grad(loss, z.v) column alongside grad(loss, x.v); that's how it shows up in xarray-sql's attention demo, where the loss takes gradients with respect to q, k and v. D is the softmax in the window spelling of attention (exp(s - MAX(s) OVER (PARTITION BY i)) read by both the normalizer and the output), so the window spelling and the GPT demo still fail on DuckDB. The GROUP BY spelling of attention now differentiates on DuckDB.

  2. added a commit that references this issue on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions