Repository navigation
|
Hi there 👋🏼 I am a Narwhals maintainer; Narwhals is a compatibility layer that lets library authors write dataframe-agnostic code, DuckDB included. We wrap a What I am trying to achieveRun SQL against a user-supplied relation without leaving anything behind in their catalog. A few operations we expose have no relational-API equivalent yet ( What I'd like is for the virtual table that Why I would like to achieve itWe are a library: the connection belongs to the user and typically outlives any individual call, so anything we register on it is effectively permanent from their point of view. Three things compound:
Individually none of this is fatal, and the memory cost is small, yet we aim to avoid adding hidden costs for users as much as possible Questions
|
Replies: 3 comments
|
Your observations line up with the current API: there is no public relation-scoped lifetime or release operation for the virtual name introduced by The name is resolved through the owning connection's context, and the derived relation remains lazy. Consequently, unregistering/dropping that name before the derived relation is bound or executed breaks it, while reusing the name can change what an already-created lazy relation binds to. Because a relation does not expose its connection, a library receiving only the relation cannot call the connection's public The only bounded pattern available today is to change the library contract so the caller supplies the connection as well, then:
That does not preserve a lazy returned relation. If laziness is required and the library owns neither the connection nor the registration lifetime, there is no supported cleanup pattern at present; staying inside relational operators is the only catalog-free route. I would file the late-rebinding reproduction separately as a bug or documentation issue, and treat relation-scoped replacement scans / missing relational ASOF, UNPIVOT, and UNION ALL BY NAME operations as feature work. In other words, this is a real API gap, not a cleanup method you overlooked. |
|
This changed in DuckDB 2.0.0. The 2.0 release notes explicitly list I reproduced the rebinding and catalog growth with the official 1.5.6 wheel, then ran the same checks with 2.0.0 on CPython 3.14.7. On 2.0.0 this works without obtaining the relation's connection or doing cleanup: import duckdb
con = duckdb.connect()
a = con.sql("SELECT 1 AS x").query("input_data", "SELECT x + 10 AS x FROM input_data")
assert a.fetchall() == [(11,)]
b = con.sql("SELECT 2 AS x").query("input_data", "SELECT x + 10 AS x FROM input_data")
assert a.fetchall() == [(11,)] # 1.5.6 instead returns [(12,)] here
assert b.fetchall() == [(12,)]
assert con.sql("SELECT count(*) FROM duckdb_views() WHERE NOT internal").fetchall() == [(0,)]The 2.0 implementation builds the SELECT over a common table expression. Its temporary catalog registration is scoped to the call, rather than the returned relation's lifetime. I also checked 5,000 retained query relations, cleanup after a binding error, and four workers using separate cursors: none left views behind on 2.0.0. A table-backed SELECT remained lazy when rows were inserted before fetching. This is not a guarantee that every SQL statement stays lazy: writes and some catalog-dependent queries execute at the call. For avoiding SQL altogether, 2.0 also provides:
I ran small examples of all three. For Narwhals, this needs a version-specific path: it does not solve the 1.x lifetime problem, and 2.0 requires Python 3.11+ and has other breaking changes covered in those release notes. |
|
Thanks for the detailed report, and thanks @nazeeh111 for checking it on 2.0.0. I ran the same checks on 2.0.0 and on main. query() now binds the name as a common table expression and leaves nothing in the catalog: reusing a name no longer changes an existing relation, dropping a view of that name no longer breaks it, and 200 unique names left no views behind. unpivot, union_by_name and how='asof' joins are on the relation in 2.0.0 too. One caveat: a statement that writes runs at the call instead of staying lazy. I will mark this answered. |
This changed in DuckDB 2.0.0. The 2.0 release notes explicitly list
rel.query(name, sql)leaving no temporary view behind, and add the three relational operations you mentioned.I reproduced the rebinding and catalog growth with the official 1.5.6 wheel, then ran the same checks with 2.0.0 on CPython 3.14.7. On 2.0.0 this works without obtaining the relation's connection or doing cleanup: