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_titleand 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.
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_dirset, 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 ruleoutput_diruses. The name is for humans: the time, thenstrategy_title(falling back totitle) andbenchmark_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_dirthat 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.