Skip to main content

Output and browser

Where one generated report can end up:

Writing the reports to files​

output_dir takes a directory and writes one file per report, with the name generated by the function: <time>-<strategy>-<benchmark>-<random>.html (the benchmark part is absent when no benchmark is configured). That arrangement buys three things:

  • no path to build in SQL: a name has to carry "which instrument, against which benchmark, at what time", which only the function knows — and with one instrument against several benchmarks, a path built from the instrument alone would necessarily overwrite itself;
  • no overwriting, ever: the random suffix plus an existence check before writing (retrying with another suffix on a collision) means two calls each write their own files and nothing existing is ever replaced;
  • names you can read: the last two parts are the display names the report itself uses (strategy_title and the benchmark's), so a directory full of reports still says which is which.
WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
)
SELECT (r).symbol, (r).benchmark, (r).file_path
FROM (
SELECT unnest(qs_html_reports_by_prices(
symbol, date, price,
{'benchmark': ['SPX'],
'benchmark_title': ['S&P 500'],
'title': symbol,
'strategy_title': symbol,
'output_dir': './'}::qs_html_report_options)) AS r
FROM prices
)
ORDER BY (r).symbol;

The file_path in each returned row is the path that very call wrote to (NULL when nothing was written), so "which files were written" can be read off the result instead of guessed.

The directory has to exist already (it is not created for you), and it has to be a local one: the write is a plain std::fs one, so output_dir is an ordinary file system path. A path such as s3://bucket/reports — DuckDB mounted that, not this extension — ends in a write error.

A browser writes no files

The query itself does not change, but a wasm build writes nothing: it runs, the reports come back in the html column, and every file_path is NULL (the file operation is skipped there — see WebAssembly for why). On a native DuckDB the same block creates the files in ./ and file_path carries their real paths.

Opening the report in a browser​

open_in_browser hands the reports to the system default browser once they have been generated, so a terminal session does not have to end with "…and now go find that file and double-click it". A browser needs a local file that actually exists, which decides the rest:

  • with output_dir set, those files are written there and then opened;
  • without it, each report is written to a temporary file first — <temp dir>/<time>-<strategy>-<benchmark>-<random>.html, the same naming rule output_dir uses. The name is for humans: the time, then strategy_title (falling back to title) and benchmark_title (taken by index, falling back to the benchmark symbol), with characters a file name cannot hold replaced by _. Nothing existing is ever overwritten, and two reports from the same second cannot collide;
  • with output_dir that is not a local path (s3://…, memory://…), it is an error rather than a silent no-op, since no browser can open it. That is checked before anything is rendered.
-- Not runnable here: it needs a browser process to launch, which a wasm build does not have.
SELECT unnest(qs_html_reports_by_prices(
symbol, date, price,
{'benchmark': ['SPX'], 'title': symbol, 'open_in_browser': true}::qs_html_report_options)) AS report
FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv');

The browser is started in a non-blocking way: the reports are already on disk, so the query neither waits for the browser nor looks at what the browser does with the files. The only failure reported is the launcher itself not starting.

One call opens one tab per report (one instrument against two benchmarks is two tabs; and, without output_dir, each report writes its own temporary file, so at least nothing overwrites anything).

WebAssembly​

A wasm build writes no files: output_dir is accepted and then ignored there — no error, no file, and file_path is NULL in every row, while the report itself still comes back in the html column. The extension skips the whole file operation on that target instead of attempting one, because DuckDB-Wasm's file system is not a faithful one: every path that does not exist still comes back as a phantom one-byte entry, DuckDB's own glob / read_text / file_size report it as present, and a raw write offset is off by a byte there too. "Is this name free?" therefore has no answer that can be trusted, and the never-overwrite guarantee could not be kept. COPY … TO is no substitute either: it exports query results in a format (CSV / JSON / parquet), and none of those can carry an arbitrary HTML document through byte for byte.

So on wasm the report stays in the html column and the host page decides what to do with it — the quick start renders one in an iframe, a host page can hand the string to a blob URL and window.open, or it can simply keep it.

open_in_browser is the other deliberate exception: a wasm build has no browser process to launch, so the option is ignored there — no browser, and no temporary file either.