From fa0b6a67db5193d967c1a2a989b0a7bb3ad334a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ferenc=20Gyarmati?= Date: Thu, 18 Jun 2026 18:09:07 +0200 Subject: [PATCH] fix: rewrite double-quoted DuckDB sources in WASM --- marimo/_runtime/_wasm/_duckdb/sources.py | 35 ++++++++++++++++++------ tests/_runtime/test_duckdb_wasm.py | 34 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/marimo/_runtime/_wasm/_duckdb/sources.py b/marimo/_runtime/_wasm/_duckdb/sources.py index fa89cfba5ae..ba4551ec66f 100644 --- a/marimo/_runtime/_wasm/_duckdb/sources.py +++ b/marimo/_runtime/_wasm/_duckdb/sources.py @@ -130,17 +130,17 @@ def _read_function_source( if not args: return None source_expr = args[0] - if isinstance(source_expr, exp.Literal) and source_expr.is_string: - return str(source_expr.this) + source = _string_literal_value(source_expr) + if source is not None: + return source if isinstance(source_expr, exp.Array) and source_expr.expressions: urls: list[str] = [] for item_expr in source_expr.expressions: - if not ( - isinstance(item_expr, exp.Literal) and item_expr.is_string - ): + item = _string_literal_value(item_expr) + if item is None: return None - urls.append(str(item_expr.this)) + urls.append(item) return tuple(urls) return None @@ -191,10 +191,29 @@ def _literal_value(value_expr: exp.Expression) -> Any: """Convert sqlglot literals while preserving falsy values via _MISSING.""" import sqlglot.expressions as exp + value = _string_literal_value(value_expr) + if value is not None: + return value if isinstance(value_expr, exp.Boolean): return value_expr.this if isinstance(value_expr, exp.Literal): - if value_expr.is_string: - return value_expr.this return value_expr.to_py() return _MISSING + + +def _string_literal_value(value_expr: exp.Expression) -> str | None: + """Return SQL string literals, including DuckDB double-quoted strings.""" + import sqlglot.expressions as exp + + if isinstance(value_expr, exp.Literal) and value_expr.is_string: + return str(value_expr.this) + if isinstance(value_expr, exp.Identifier): + return str(value_expr.this) if value_expr.args.get("quoted") else None + if isinstance(value_expr, exp.Column) and all( + value_expr.args.get(part) is None + for part in ("table", "db", "catalog") + ): + identifier = value_expr.this + if isinstance(identifier, exp.Identifier): + return _string_literal_value(identifier) + return None diff --git a/tests/_runtime/test_duckdb_wasm.py b/tests/_runtime/test_duckdb_wasm.py index cd87c5d4262..c7967ea6aa5 100644 --- a/tests/_runtime/test_duckdb_wasm.py +++ b/tests/_runtime/test_duckdb_wasm.py @@ -969,6 +969,40 @@ def test_wrapped_sql_rewrites_remote_literal_with_explicit_connection() -> ( "https://datasets.marimo.app/cars.csv" ) + @staticmethod + def test_wrapped_sql_rewrites_double_quoted_reader_url() -> None: + import duckdb + + from marimo._sql.utils import wrapped_sql + + connection = duckdb.connect(":memory:") + try: + with ( + mock_pyodide(), + patch( + "marimo._runtime._wasm._fetch.fetch_url_bytes", + return_value=b"make,mpg\nford,25\ntoyota,18\n", + ) as fetch_url_bytes, + ): + relation = wrapped_sql( + """ + SELECT make + FROM read_csv( + "https://datasets.marimo.app/cars.csv" + ) + WHERE mpg > 20 + """, + connection, + ) + rows = relation.fetchall() + finally: + connection.close() + + assert rows == [("ford",)] + fetch_url_bytes.assert_called_once_with( + "https://datasets.marimo.app/cars.csv" + ) + @staticmethod def test_execute_duckdb_sql_rewrites_remote_literal_with_explicit_connection() -> ( None