Skip to content

Commit af360b1

Browse files
committed
docs: clarify def details
1 parent 607190a commit af360b1

4 files changed

Lines changed: 85 additions & 17 deletions

File tree

marimo/_runtime/_wasm/_duckdb/__init__.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# Copyright 2026 Marimo. All rights reserved.
2-
"""WASM-only DuckDB fallbacks for remote file scans.
2+
"""Install WASM-only DuckDB fallbacks for remote file scans.
33
44
DuckDB-WASM cannot use ``httpfs``. marimo fetches supported URLs itself,
55
materializes supported files as pandas DataFrames, then hands those frames
6-
back to DuckDB.
6+
back to DuckDB through replacement scans.
77
88
We have two concrete use cases to patch:
99
1010
* **Direct read methods** — ``duckdb.read_csv/read_parquet/read_json`` are
11-
wrapped with :class:`WasmPatchSet`; supported remote URLs are fetched by
11+
wrapped with :class:`WasmPatchSet`. Supported remote URLs are fetched by
1212
marimo and returned as DuckDB relations.
1313
* **SQL remote scans** — raw DuckDB APIs and marimo's ``mo.sql`` path call the
14-
same rewrite helper, which replaces supported URLs with generated
15-
replacement-scan tables backed by fetched pandas DataFrames.
14+
same sqlglot rewrite helper. It replaces supported URL scans with generated
15+
table names and evaluates the original DuckDB call with fetched DataFrames
16+
in scope so DuckDB replacement scans can resolve them.
1617
"""
1718

1819
from __future__ import annotations
@@ -212,15 +213,19 @@ def patch_duckdb_query_for_wasm(
212213
*,
213214
reserved_names: Sequence[str] = (),
214215
) -> WasmDuckDBQueryPatch | None:
215-
"""Rewrite remote file sources to generated DataFrame names.
216+
"""Replace supported remote file reads with generated table names.
216217
217-
Example: ``SELECT * FROM read_csv('https://example.com/cars.csv')``
218-
becomes ``SELECT * FROM __marimo_wasm_duckdb_remote_0``, and
219-
``tables["__marimo_wasm_duckdb_remote_0"]`` holds the fetched DataFrame.
218+
For example, ``SELECT * FROM read_csv('https://example.com/cars.csv')``
219+
becomes ``SELECT * FROM __marimo_wasm_duckdb_remote_0`` when suffix ``0``
220+
is free. The returned ``tables`` mapping binds that name to the fetched
221+
DataFrame. If the query or ``reserved_names`` already use that identifier,
222+
the rewriter uses the next free suffix.
220223
221-
In Pyodide this raises if sqlglot is unavailable. Returns ``None`` when
222-
not running in Pyodide, when the query has no supported remote file
223-
source, or when the query cannot be parsed.
224+
In Pyodide this raises if sqlglot is unavailable. Returns ``None`` when:
225+
226+
- marimo is not running in Pyodide;
227+
- the query has no supported remote file source;
228+
- the query cannot be parsed.
224229
"""
225230
if not is_pyodide():
226231
return None
@@ -419,6 +424,8 @@ def _make_sql_api_wrapper(
419424
query_arg_index: int,
420425
query_kwarg_names: tuple[str, ...],
421426
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
427+
"""Wrap DuckDB SQL APIs while preserving the caller's local namespace."""
428+
422429
def _make_wrapper(original: Callable[..., Any]) -> Callable[..., Any]:
423430
@functools.wraps(original)
424431
def _wrapper(*args: Any, **kwargs: Any) -> Any:
@@ -451,6 +458,7 @@ def _query_argument(
451458
query_arg_index: int,
452459
query_kwarg_names: tuple[str, ...],
453460
) -> Any:
461+
"""Find the query text without assuming the caller used positional args."""
454462
if len(args) > query_arg_index:
455463
return args[query_arg_index]
456464
for name in query_kwarg_names:
@@ -467,6 +475,7 @@ def _replace_query_argument(
467475
query_arg_index: int,
468476
query_kwarg_names: tuple[str, ...],
469477
) -> tuple[tuple[Any, ...], dict[str, Any]]:
478+
"""Replace the query at the same call site shape used by the caller."""
470479
kwargs_dict = dict(kwargs)
471480
if len(args) > query_arg_index:
472481
patched_args = list(args)
@@ -490,6 +499,7 @@ def _eval_duckdb_original_call(
490499
binding_names: _EvalBindingNames,
491500
extra_locals: Mapping[str, Any] | None = None,
492501
) -> Any:
502+
"""Evaluate the original call where DuckDB can see replacement DataFrames."""
493503
locals_for_eval = dict(eval_locals)
494504
if extra_locals is not None:
495505
locals_for_eval.update(extra_locals)
@@ -551,6 +561,7 @@ def _duckdb_catalog_names(
551561
original: Callable[..., Any],
552562
args: tuple[Any, ...],
553563
) -> tuple[str, ...]:
564+
"""Reserve existing DuckDB table names before generating replacements."""
554565
try:
555566
relation = _show_duckdb_tables(original, args)
556567
rows = relation.fetchall()
@@ -563,6 +574,7 @@ def _show_duckdb_tables(
563574
original: Callable[..., Any],
564575
args: tuple[Any, ...],
565576
) -> Any:
577+
"""Run ``SHOW TABLES`` through the same DuckDB entry point being patched."""
566578
import duckdb
567579

568580
original_call = inspect.unwrap(original)
@@ -614,6 +626,7 @@ def _direct_reader_source(
614626
*,
615627
call_spec: _DirectReaderCallSpec,
616628
) -> tuple[RemoteFileSource, Any] | None:
629+
"""Return a remote source only for direct reader calls we can emulate."""
617630
options = dict(kwargs)
618631
try:
619632
source, rest_args = _pop_source_argument(
@@ -646,6 +659,7 @@ def _pop_source_argument(
646659
*,
647660
call_spec: _DirectReaderCallSpec,
648661
) -> tuple[Any, tuple[Any, ...]]:
662+
"""Remove the source argument so remaining kwargs are pure reader options."""
649663
source_positional_index = call_spec.source_positional_index
650664
if len(args) > source_positional_index:
651665
return (
@@ -688,6 +702,7 @@ def _replace_remote_sources(
688702
statements: Sequence[exp.Expression],
689703
table_names: _RemoteTableNames,
690704
) -> list[exp.Expression]:
705+
"""Replace supported remote table nodes while preserving aliases."""
691706
from sqlglot import exp
692707

693708
def replace_table(node: exp.Expression) -> exp.Expression:
@@ -715,6 +730,7 @@ def replace_table(node: exp.Expression) -> exp.Expression:
715730
def _reserved_sql_names(
716731
statements: Sequence[exp.Expression],
717732
) -> tuple[str, ...]:
733+
"""Collect SQL identifiers that generated table names must not shadow."""
718734
from sqlglot import exp
719735

720736
names: set[str] = set()
@@ -731,6 +747,7 @@ def _reserved_sql_names(
731747
def _format_duckdb_query(
732748
statements: Sequence[exp.Expression], *, original_query: str
733749
) -> str:
750+
"""Serialize sqlglot statements without dropping a trailing semicolon."""
734751
patched_query = "; ".join(
735752
statement.sql(dialect="duckdb") for statement in statements
736753
)

marimo/_runtime/_wasm/_duckdb/dataframe.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Copyright 2026 Marimo. All rights reserved.
2-
"""Decode fetched DuckDB file bytes into pandas DataFrames."""
2+
"""Decode fetched DuckDB file bytes into pandas DataFrames.
3+
4+
The WASM patch fetches remote bytes in Python, but DuckDB's Python readers
5+
still expect local paths for CSV, JSON, and parquet parsing. This module
6+
materializes fetched bytes through short-lived temp files where DuckDB parsing
7+
is needed and synthesizes DataFrames for ``read_text`` and ``read_blob``.
8+
"""
39

410
from __future__ import annotations
511

@@ -29,6 +35,7 @@
2935
def read_csv_dataframe(
3036
data: bytes, options: Mapping[str, Any], *, url: str
3137
) -> pd.DataFrame:
38+
"""Read CSV/TSV bytes with a suffix that preserves compression hints."""
3239
import duckdb
3340

3441
return _read_temp_dataframe(
@@ -71,6 +78,7 @@ def read_json_dataframe(
7178
def read_json_objects_dataframe(
7279
data: bytes, options: Mapping[str, Any], *, url: str
7380
) -> pd.DataFrame:
81+
"""Read JSON-object bytes through DuckDB's SQL-only table function."""
7482
return _read_temp_dataframe(
7583
data,
7684
suffix=_temp_suffix(
@@ -99,6 +107,7 @@ def _read_json_objects_path(
99107

100108

101109
def read_text_dataframe(data: bytes, url: str) -> pd.DataFrame:
110+
"""Match DuckDB's ``read_text`` shape for an already-fetched object."""
102111
import pandas as pd
103112

104113
return pd.DataFrame(
@@ -112,6 +121,7 @@ def read_text_dataframe(data: bytes, url: str) -> pd.DataFrame:
112121

113122

114123
def read_blob_dataframe(data: bytes, url: str) -> pd.DataFrame:
124+
"""Match DuckDB's ``read_blob`` shape for an already-fetched object."""
115125
import pandas as pd
116126

117127
return pd.DataFrame(
@@ -127,6 +137,7 @@ def read_blob_dataframe(data: bytes, url: str) -> pd.DataFrame:
127137
def append_filename_column(
128138
df: pd.DataFrame, url: str, column_name: str
129139
) -> pd.DataFrame:
140+
"""Apply DuckDB's filename option after bytes have been decoded."""
130141
if column_name in df.columns:
131142
raise ValueError(
132143
f'Option filename adds column "{column_name}", but a column with this '
@@ -158,6 +169,7 @@ def _read_temp_dataframe(
158169

159170

160171
def _temp_suffix(url: str, *, suffixes: tuple[str, ...], default: str) -> str:
172+
"""Preserve file extensions so DuckDB can infer format details."""
161173
path = urlparse(url).path.lower()
162174
for suffix in suffixes:
163175
if path.endswith(suffix):

marimo/_runtime/_wasm/_duckdb/io.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Copyright 2026 Marimo. All rights reserved.
2-
"""Map DuckDB remote files to readers."""
2+
"""Resolve DuckDB remote file reads into fetch and DataFrame reader steps.
3+
4+
DuckDB's native network scanner is unavailable in Pyodide. This module is the
5+
compatibility layer that recognizes supported URL shapes, validates the reader
6+
options we can reproduce, fetches bytes through WASM fetch shim, and
7+
dispatches to a local DataFrame reader. Unsupported readers or option
8+
combinations return ``None`` so callers can use the unpatched DuckDB path
9+
and surface the underlying error.
10+
"""
311

412
from __future__ import annotations
513

@@ -40,6 +48,7 @@ class RemoteFile:
4048
fetcher_name: str
4149

4250
def fetch(self) -> _FetchedBytes:
51+
"""Fetch through a named strategy so sources stay hashable."""
4352
return _fetcher_by_name(self.fetcher_name).fetch(self.url)
4453

4554

@@ -265,9 +274,11 @@ class RemoteFileSource:
265274
options: tuple[tuple[str, Any], ...] = ()
266275

267276
def read_options(self) -> dict[str, Any]:
277+
"""Expose sorted, hashable options as regular reader kwargs."""
268278
return dict(self.options)
269279

270280
def read_dataframe(self) -> pd.DataFrame:
281+
"""Read one or more remote files using DuckDB-compatible concat rules."""
271282
frames = [self._read_file_dataframe(file) for file in self.files]
272283
if len(frames) == 1:
273284
return frames[0]
@@ -287,6 +298,7 @@ def read_dataframe(self) -> pd.DataFrame:
287298
return pd.concat(frames, ignore_index=True)
288299

289300
def _read_file_dataframe(self, file: RemoteFile) -> pd.DataFrame:
301+
"""Fetch bytes, decode them, then apply DuckDB's filename option."""
290302
fetched = file.fetch()
291303
options = self.read_options()
292304
reader = _reader_by_name(self.reader_name)
@@ -300,6 +312,7 @@ def _read_file_dataframe(self, file: RemoteFile) -> pd.DataFrame:
300312

301313

302314
def remote_file_from_url(url: str) -> RemoteFile | None:
315+
"""Return a fetchable remote file only for URL schemes marimo supports."""
303316
fetcher = _fetcher_for_url(url)
304317
if fetcher is None:
305318
return None
@@ -311,6 +324,7 @@ def remote_file_source_from_reader_args(
311324
source: Any,
312325
raw_options: Mapping[str, Any],
313326
) -> RemoteFileSource | None:
327+
"""Map a DuckDB reader call to a reproducible remote DataFrame source."""
314328
reader = reader_for_function(function_name)
315329
if reader is None:
316330
return None
@@ -328,6 +342,7 @@ def remote_file_source_from_reader_args(
328342
def _remote_files_from_source_arg(
329343
source: Any,
330344
) -> tuple[RemoteFile, ...] | None:
345+
"""Accept DuckDB source shapes that are static URL strings or URL lists."""
331346
if isinstance(source, str):
332347
file = remote_file_from_url(source)
333348
return (file,) if file is not None else None
@@ -372,6 +387,7 @@ def _reader_by_name(
372387

373388

374389
def reader_for_url(url: str) -> _DataFrameReader | None:
390+
"""Infer a reader from direct URL table syntax such as ``FROM 'x.csv'``."""
375391
path = urlparse(url).path.lower()
376392
return next(
377393
(
@@ -386,6 +402,7 @@ def reader_for_url(url: str) -> _DataFrameReader | None:
386402
def reader_for_function(
387403
function_name: str,
388404
) -> _DataFrameReader | None:
405+
"""Resolve DuckDB table-function names to marimo's fallback readers."""
389406
return next(
390407
(
391408
reader
@@ -397,6 +414,7 @@ def reader_for_function(
397414

398415

399416
def _csv_reader_options(options: Mapping[str, Any]) -> dict[str, Any]:
417+
"""Drop options implemented outside DuckDB's CSV reader call."""
400418
return {
401419
key: value
402420
for key, value in options.items()
@@ -405,6 +423,7 @@ def _csv_reader_options(options: Mapping[str, Any]) -> dict[str, Any]:
405423

406424

407425
def _json_reader_options(options: Mapping[str, Any]) -> dict[str, Any]:
426+
"""Translate DuckDB JSON option spelling to DuckDB Python API spelling."""
408427
return {
409428
key: _normalize_json_reader_option(key, value)
410429
for key, value in options.items()
@@ -413,12 +432,14 @@ def _json_reader_options(options: Mapping[str, Any]) -> dict[str, Any]:
413432

414433

415434
def _normalize_delimiter(value: Any) -> Any:
435+
"""Convert SQL's escaped tab literal to the byte delimiter DuckDB expects."""
416436
if value == r"\t":
417437
return "\t"
418438
return value
419439

420440

421441
def _normalize_json_reader_option(key: str, value: Any) -> Any:
442+
"""Normalize JSON values whose SQL names differ from Python reader values."""
422443
if key == "compression":
423444
return _normalize_json_compression(value)
424445
if key == "format":
@@ -427,6 +448,7 @@ def _normalize_json_reader_option(key: str, value: Any) -> Any:
427448

428449

429450
def _normalize_json_compression(value: Any) -> str:
451+
"""Map SQL compression aliases to DuckDB Python JSON reader values."""
430452
compression = str(value).lower()
431453
if compression == "auto":
432454
return "auto_detect"
@@ -436,6 +458,7 @@ def _normalize_json_compression(value: Any) -> str:
436458

437459

438460
def _normalize_json_format(value: Any) -> str:
461+
"""Map DuckDB SQL JSON format aliases to Python reader values."""
439462
fmt = str(value).lower()
440463
if fmt == "ndjson":
441464
return "newline_delimited"
@@ -445,6 +468,7 @@ def _normalize_json_format(value: Any) -> str:
445468

446469

447470
def _apply_json_option(options: dict[str, Any], key: str, value: Any) -> bool:
471+
"""Keep JSON options only when the fallback can safely pass them through."""
448472
if key == "format":
449473
options["format"] = _normalize_json_format(value)
450474
return True
@@ -457,12 +481,14 @@ def _apply_json_option(options: dict[str, Any], key: str, value: Any) -> bool:
457481

458482

459483
def _is_safe_reader_option_name(key: str) -> bool:
484+
"""Reject option names that cannot be passed as Python reader kwargs."""
460485
return key.isidentifier()
461486

462487

463488
def _apply_common_table_option(
464489
options: dict[str, Any], key: str, value: Any
465490
) -> bool:
491+
"""Handle options marimo applies after per-file reads are decoded."""
466492
if key == "filename" and isinstance(value, bool | str):
467493
options["filename"] = value
468494
return True
@@ -475,6 +501,7 @@ def _apply_common_table_option(
475501
def _apply_compression_option(
476502
options: dict[str, Any], key: str, value: Any
477503
) -> bool:
504+
"""Accept only compression modes supported by the byte-fetch fallback."""
478505
if key == "compression" and _is_supported_compression(value):
479506
options["compression"] = str(value).lower()
480507
return True
@@ -484,10 +511,12 @@ def _apply_compression_option(
484511
def _apply_shared_source_option(
485512
options: dict[str, Any], key: str, value: Any
486513
) -> bool:
514+
"""Apply source options shared by CSV, parquet, and JSON fallbacks."""
487515
return _apply_compression_option(
488516
options, key, value
489517
) or _apply_common_table_option(options, key, value)
490518

491519

492520
def _is_supported_compression(value: Any) -> bool:
521+
"""Limit compression to modes the fallback knows how to preserve."""
493522
return str(value).lower() in {"auto", "none", "gzip"}

0 commit comments

Comments
 (0)