Synsemadocsv0.6.xENES

Standard library / primitives

Files & I/O

All file access is gated by the file capability (file.read / file.write for least-privilege). Writes are atomic (temp file + rename).

A scope is a glob by segment (engine v0.6.45+): file.read("./data/") is the files directly in ./data; file.read("./data/*") is ./data itself and everything below it, at any depth. The full rules — .., links, the whole disk — are in Capabilities.

io.syn
-- Doc example: files & I/O, and the file formats (CSV, Parquet, JSON Lines). Needs the file capability (deny-by-default).
intent: "doc example: files and I/O"
require file("_doctest_io.txt")
require file("_doctest_io.parquet")
require file("_doctest_io.jsonl")
require file.write("_doctest_io_gone.txt")
require file.read(".")           -- cwd() needs exactly this: the same grant list_dir(".") needs
require file("./_doctest_tree/**")   -- `**`: the folder, its files AND every subfolder (v0.6.45+)
require file.read("./_doctest_flat/*")   -- `*`: only the files directly inside, one level

write_file("_doctest_io.txt", "hi from the io demo")
print("read back → " + read_file("_doctest_io.txt"))

test "delete_file needs file.write on the path and is honest about what happened (engine v0.6.20+)"
    write_file("_doctest_io_gone.txt", "bye")
    assert_eq(delete_file("_doctest_io_gone.txt"), true)
    assert_error(() => delete_file("_doctest_io_gone.txt"))     -- already gone → error, not false

test "cwd() is the real working directory, normalized to forward slashes (engine v0.6.20+)"
    assert_eq(type_of(cwd()), "text")
    assert(length(cwd()) > 0)
    assert_eq(contains(cwd(), "\\"), false)

test "write then read round-trips (atomic write)"
    write_file("_doctest_io.txt", "hello\nworld")
    assert_eq(read_file("_doctest_io.txt"), "hello\nworld")
    assert(file_exists("_doctest_io.txt"))
    assert_eq(keys(file_info("_doctest_io.txt")), ["exists", "is_dir", "size", "modified", "is_link"])
    assert_eq(file_info("_doctest_io.txt").size, 11)

test "path scopes are globs by segment: `*` stays in one folder, `**` crosses them (v0.6.45+)"
    when file_exists("_doctest_tree")
        delete_dir("_doctest_tree", {"recursive": true})              -- a fresh tree for this test
    write_file("_doctest_tree/top.txt", "top")
    write_file("_doctest_tree/sub/deep.txt", "deep")
    assert_eq(read_file("_doctest_tree/sub/deep.txt"), "deep")       -- `**` reaches every level
    assert_eq(length(list_dir("_doctest_tree")), 2)                  -- ... and the folder itself
    assert_error(() => read_file("_doctest_tree/../_doctest_x.txt"))  -- no wildcard matches `..`
    assert_error(() => read_file("_doctest_flat/sub/x.txt"))         -- `*` is one level: denied
    assert_error(() => list_dir("_doctest_flat"))                    -- `./x/*` names the files, not the folder

test "grep on a folder reads the tree, so it needs `<folder>/**`; edit_file reads before it writes"
    write_file("_doctest_tree/sub/deep.txt", "needle here")
    let g be grep("_doctest_tree", "needle")
    assert_eq(apply(g.matches, (m) => m.file), ["_doctest_tree/sub/deep.txt"])
    assert_eq(edit_file("_doctest_tree/sub/deep.txt", "needle", "pin"), {"replaced": 1})   -- file.read + file.write
    assert_eq(read_file("_doctest_tree/sub/deep.txt"), "pin here")
    delete_dir("_doctest_tree", {"recursive": true})

task why_not(path)
    try
        read_file(path)
    recover e
        give e
    give "read"

test "read_file says why it cannot read (v0.6.45+): File not found is only for what does not exist"
    write_file("_doctest_tree/bin.dat", bytes([255, 254, 0]))
    write_file("_doctest_tree/sub/deep.txt", "deep")
    assert(starts_with(why_not("_doctest_tree/nope.txt"), "File not found"))
    assert(contains(why_not("_doctest_tree/sub"), "it is a directory (use list_dir)"))
    assert(contains(why_not("_doctest_tree/bin.dat"), "not valid UTF-8 text (use read_file_bytes)"))
    assert_eq(read_file_bytes("_doctest_tree/bin.dat"), bytes([255, 254, 0]))
    delete_dir("_doctest_tree", {"recursive": true})

test "gzip / gunzip: bytes in, bytes out; deterministic; a bomb is stopped (v0.6.45+)"
    let z be gzip("hello hello hello")                                -- text or bytes in, bytes out
    assert_eq(decode(gunzip(z), "utf8"), "hello hello hello")
    assert_eq(gzip("same"), gzip("same"))                             -- no name or time in the header
    assert_eq(decode(gunzip(z + gzip(" world")), "utf8"), "hello hello hello world")   -- concatenated members
    assert_error(() => gunzip(gzip("aaaaaaaaaaaaaaaaaaaa"), {"max_bytes": 10}))

test "csv_parse: an empty field is nothing, a quoted \"\" is the empty text, untyped text stays lossless"
    assert_eq(csv_parse("a,b\n007,\n"), [{"a": "007", "b": nothing}])
    assert_eq(csv_parse("a,b\n\"\",\n"), [{"a": "", "b": nothing}])
    assert_eq(csv_parse("x\n1\n\n2\n"), [{"x": "1"}, {"x": "2"}])            -- a blank line is skipped
    assert_eq(csv_parse("a;b\n1;2\n", {"delimiter": ";"}), [{"a": "1", "b": "2"}])
    assert_eq(csv_parse("1,2\n3,4\n", {"headers": false}), [["1", "2"], ["3", "4"]])
    assert_eq(csv_parse("a,b\n007,2\n", {"numbers": true}), [{"a": 7, "b": 2}])    -- the old guess: ids lose zeros

test "csv_parse types: int, float, decimal (exponent too), text, bool, date, datetime"
    let rows be csv_parse("a,b,c,d,e,f,g\n007,1.5,1.5e-3,x,yes,2026-01-02,2026-01-03T10:00:00\n", {"types": {"a": "int", "b": "float", "c": "decimal", "d": "text", "e": "bool", "f": "date", "g": "datetime"}})
    assert_eq(rows[0].a, 7)
    assert_eq(rows[0].b, 1.5)
    assert_eq(rows[0].c, 0.0015d)
    assert_eq(rows[0].d, "x")
    assert_eq(rows[0].e, true)
    assert_eq(rows[0].f, date(2026, 1, 2))
    assert_eq(rows[0].g, datetime("2026-01-03T10:00:00Z"))                  -- no zone → UTC
    assert_eq(csv_parse("a,b\n,1\n", {"types": {"a": "int"}}), [{"a": nothing, "b": "1"}])   -- empty is nothing, typed or not
    let msg be ""
    try
        csv_parse("a\nx\n", {"types": {"a": "int"}})
    recover e
        set msg to e
    assert(contains(msg, "line 2") and contains(msg, "column \"a\""))
    assert_error(() => csv_parse("a\n1e400\n", {"types": {"a": "float"}}))      -- never infinity
    assert(is_nan(csv_parse("a\nnan\n", {"types": {"a": "float"}})[0].a))
    assert_error(() => csv_parse("a\n1\n", {"types": {"zz": "int"}}))          -- not in the header
    assert_error(() => csv_parse("a,b\n1\n"))                                   -- uneven fields
    assert_error(() => csv_parse("a\n1\n", {"delimiter": "\""}))

test "missing marks: {missing: [...]} reads them as nothing; csv_encode writes them back"
    assert_eq(csv_parse("a,b\nNA,1\n\"NA\",2\n", {"missing": ["NA"]}), [{"a": nothing, "b": "1"}, {"a": "NA", "b": "2"}])
    let rows be [{"a": nothing, "b": "NA"}, {"a": "x", "b": nothing}]
    assert_eq(csv_encode(rows, {"missing": "NA", "eol": "\n"}), "a,b\nNA,\"NA\"\nx,NA\n")
    assert_eq(csv_parse(csv_encode(rows, {"missing": "NA"}), {"missing": ["NA"]}), rows)
    assert_error(() => csv_encode(rows, {"missing": ""}))

test "csv_encode: nothing and empty text apart, dates as ISO, headers/eol, escape_formulas"
    assert_eq(csv_encode([{"a": "", "b": nothing}], {"eol": "\n"}), "a,b\n\"\",\n")
    let tricky be [{"a": "", "b": nothing, "c": "x,y"}]
    assert_eq(csv_parse(csv_encode(tricky)), tricky)
    assert_eq(csv_encode([{"d": date(2026, 1, 31), "u": duration(hours = 1)}], {"eol": "\n"}), "d,u\n2026-01-31,PT1H\n")
    assert_eq(csv_encode([{"a": "1", "b": "2"}], {"headers": ["b"], "eol": "\n"}), "b\n2\n")
    assert_eq(csv_encode([["x", "y"]], {"delimiter": ";", "eol": "\n"}), "x;y\n")
    assert_eq(csv_encode([{"f": "=SUM(A1)", "n": -5}], {"escape_formulas": true, "eol": "\n"}), "f,n\n'=SUM(A1),-5\n")
    assert_error(() => csv_encode([{"a": [1, 2]}]))                          -- nested: json_encode that field
    assert_eq(csv_encode([{"b": bytes("Hi")}], {"eol": "\n"}), "b\nSGk=\n")
    assert_eq(csv_encode([{"a": nothing}], {"eol": "\n"}), "a\n\"\"\n")         -- one column: nothing is written "" (warns once)
    assert_eq(csv_parse(csv_encode([{"a": nothing}]), {"types": {"a": "int"}}), [{"a": nothing}])
    assert_error(() => csv_parse("a,a\n1,2\n"))                               -- duplicate headers
    assert_error(() => csv_parse("a\n\"open\n"))                              -- unclosed quote

test "parquet: typed columns round-trip through a file"
    let rows be [
        {"id": 1, "x": 1.5, "p": 1.50d, "s": "a", "ok": true, "b": bytes("hi"), "d": date(2026, 1, 3), "t": datetime("2026-01-03T10:00:00", "Europe/Madrid"), "u": duration(minutes = 90), "n": nothing},
        {"id": 2, "x": 2.0, "p": 10.25d, "s": "b", "ok": false, "b": bytes("yo"), "d": date(2026, 1, 4), "t": datetime("2026-01-04T10:00:00", "Europe/Madrid"), "u": duration(seconds = 1), "n": nothing}]
    write_file("_doctest_io.parquet", parquet_write(rows, {"compression": "zstd"}))
    let back be parquet_read(read_file_bytes("_doctest_io.parquet"))
    assert_eq(back, rows)
    assert_eq(text(back[0].t), "2026-01-03T10:00:00+01:00[Europe/Madrid]")   -- the zone comes back
    assert_eq(type_of(back[0].u), "duration")
    each c in ["snappy", "gzip", "lz4", "none"]
        assert_eq(parquet_read(parquet_write(rows, {"compression": c})), rows)
    let f be decode(parquet_write(rows), "utf8_lossy")
    assert(contains(f, "ARROW:schema"))                                      -- pandas / pyarrow / polars read the types
    assert_eq(text(parquet_read(parquet_write([{"t": datetime("2026-01-03T14:30:00+05:30")}]))[0].t), "2026-01-03T14:30:00+05:30")
    assert_eq(parquet_read(parquet_write([{"t": datetime("2026-01-03T10:00:00.123456789Z")}]))[0].t, datetime("2026-01-03T10:00:00.123456789Z"))
    assert_eq(parquet_read(parquet_write([{"a": 1}, {"a": 2.5}])), [{"a": 1.0}, {"a": 2.5}])      -- int + float → DOUBLE
    assert_eq(parquet_read(parquet_write([{"a": 1}, {"b": 2}])), [{"a": 1, "b": nothing}, {"a": nothing, "b": 2}])

test "parquet: errors instead of silent loss"
    assert_error(() => parquet_write([{"a": 1}, {"a": "x"}]))                -- one type per column
    assert_error(() => parquet_write([{"a": [1, 2]}]))                       -- nested: json_encode it first
    assert_error(() => parquet_write([{"a": 1.5}, {"a": 9007199254740993}])) -- a DOUBLE would round it
    assert_error(() => parquet_write([{"a": 2 ** 70}]))                      -- beyond 64 bits
    let msg be ""
    try
        parquet_write([{"t": datetime("2026-01-03T10:00:00.123456789Z")}, {"t": datetime("2300-01-01T00:00:00Z")}])
    recover e
        set msg to e
    assert(contains(msg, "column \"t\""))                                    -- nanoseconds + a far date: one unit cannot hold both
    assert_error(() => parquet_write([{"a": 1}], {"compression": "brotli"}))
    assert_error(() => parquet_read(bytes("not parquet")))
    let small be parquet_write([{"a": 1, "b": 2}, {"a": 3, "b": 4}])
    assert_error(() => parquet_read(small, {"max_cells": 3}))                -- 4 cells > 3
    assert_eq(length(parquet_read(small, {"max_cells": 4, "max_bytes": 1000000})), 2)
    assert_error(() => parquet_read(small, {"bogus": 1}))

test "parquet: read a part of a file, or only its shape (v0.6.45+)"
    let p be parquet_write([{"a": 1, "b": "x"}, {"a": 2, "b": "y"}, {"a": 3, "b": "z"}])
    let info be parquet_info(p)                                      -- no data is read
    assert_eq(info.rows, 3)
    assert_eq(info.columns, [{"name": "a", "type": "int64"}, {"name": "b", "type": "string"}])
    assert_eq(length(info.row_groups), 1)
    assert_eq(parquet_read(p, {"columns": ["a"], "limit": 2}), [{"a": 1}, {"a": 2}])
    assert_eq(length(parquet_read(p, {"row_groups": [0]})), 3)
    assert_error(() => parquet_read(p, {"columns": ["zz"]}))          -- "the file has no column \"zz\" (columns: a, b)"

test "parquet_scan reads the part it is asked for straight from the disk (v0.6.46+)"
    write_file("_doctest_io.parquet", parquet_write([{"a": 1, "b": "x"}, {"a": 2, "b": "y"}, {"a": 3, "b": "z"}]))
    assert_eq(parquet_scan("_doctest_io.parquet", {"columns": ["a"], "limit": 2}), [{"a": 1}, {"a": 2}])
    assert_eq(keys(parquet_scan("_doctest_io.parquet", {"columns": ["b", "a"], "limit": 1})[0]), ["a", "b"])   -- the file's column order
    assert_eq(parquet_scan("_doctest_io.parquet"), parquet_read(read_file_bytes("_doctest_io.parquet")))
    assert_eq(parquet_scan("_doctest_io.parquet", {"row_groups": []}), [])
    let last be lineage()[-1]                                        -- an input with the path, hashed as canonical_json
    assert_eq(last.source, "parquet_scan")
    assert_eq(last.what, "_doctest_io.parquet")
    assert_eq(last.encoding, "jcs")
    assert_eq(last.sha256, decode(sha256(canonical_json([])), "hex"))
    assert_error(() => parquet_scan(read_file_bytes("_doctest_io.parquet")))   -- bytes: that is parquet_read
    assert_error(() => parquet_scan("_doctest_io.parquet", {"row_groups": [1]}))
    assert_error(() => parquet_scan("_doctest_io.parquet", {"limit": 0}))
    assert_error(() => parquet_scan("_doctest_io.parquet", {"bogus": 1}))
    assert_error(() => parquet_scan("_doctest_not_granted.parquet"))      -- needs file.read on the path

test "JSON Lines files"
    write_file("_doctest_io.jsonl", jsonl_encode([{"ev": "a"}, {"ev": "b"}]))
    assert_eq(collect(jsonl_decode(read_file("_doctest_io.jsonl")), "ev"), ["a", "b"])

Reading§

read_file(path)                 -- whole file as text
read_file(path, 1, 100)         -- a line range: lines 1–100 (1-based, EOLs preserved)
read_file(path, 500)            -- from line 500 to EOF
read_file_bytes(path)           -- byte-exact, for binary

read_file says why it cannot read a file (engine v0.6.45+). File not found: <path> is only for a path that does not exist; anything else names its cause:

The error saysWhat it means
… it is a directory (use list_dir)the path is a folder
… it is not valid UTF-8 text (use read_file_bytes)a binary file, or text in another encoding
… in use by another process … retry laterWindows: another process holds the file open without sharing it
a broken link, a permissionthat, by name

read_file_bytes and edit_file give the same answers.

Writing & editing§

write_file(path, text)          -- atomic overwrite
append_file(path, text)         -- append
edit_file(path, old, new)       -- exact-string replace (old must be unique; replace_all? for all)

edit_file needs file.read as well as file.write on the path (engine v0.6.45+): it reads the file to find the text, and its answers (not found, ambiguous, how many replaced) tell what is inside. require file("./notes/**") gives both.

file_exists(path)               -- bool
file_info(path)                 -- {exists, is_dir, size, modified, is_link}
list_dir(path)                  -- list of {name, is_dir, size, is_link}
grep(target, pattern, opts?)    -- {matches: [{file, line, col, text}], truncated} — streams per line

Deleting (engine v0.6.20+)§

delete_file(path)                        -- true; a missing file (or a directory) is an error, never a silent false
delete_dir(path)                         -- empty directories only — "is not empty (pass {\"recursive\": true} …)"
delete_dir(path, {"recursive": true})    -- the whole tree

Deleting is a write: delete_file needs file.write on that path; delete_dir needs it on the directory and, recursively, on every path inside it — the same grant that would let you delete them one by one. With file.write("./tmp") alone, a recursive delete of ./tmp is refused at the first child (file_write("tmp/k")) and nothing is removed; file.write("./tmp/**") covers the folder and everything below it. Every path is checked before anything is deleted. A path inside a synsema build bundle is read-only, so deleting it is refused.

Where am I — cwd(), ~ (engine v0.6.20+)§

cwd() returns the real working directory, normalized to forward slashes (C:/Users/me/proj), and needs file.read(".") — the same grant list_dir(".") needs; file.read("./") and file.read("") cover it too. file.read("./") and file.read("") do not (engine v0.6.45+): they name the files of the folder, not the folder. The absolute path is information about the host (user name, disk layout), so under --sandbox it is not free. In a synsema build binary it is the directory the binary was started from, never the bundle.

~ and ~/… expand to the home directory (HOME, else USERPROFILE) in paths and in scopes: require file.read("~/.config/app/*"), then read_file("~/.config/app/x.toml"). ~user/ is not supported (clear error).

The program's assets vs. the user's files — bundle: / disk:§

In a binary made with synsema build, a path that is in the bundle is part of the program: read_file, read_file_bytes, file_exists, file_info and grep read it from the bundle, with no file capability and an audit line that says so (bundled asset (part of the program)); a file with the same relative name in the working directory never shadows it. A path that is not in the bundle is the user's and goes to the disk with file.read, as always. Two prefixes make it explicit: read_file("bundle:data.txt") forces the bundle (an error if it is not there), read_file("disk:data.txt") forces the disk (needs file.read). list_dir(path) lists the disk only; list_dir("bundle:") and list_dir("bundle:sub/") list the bundle.

Archives — zip and tar (engine v0.6.20+)§

let z be zip_create([{"path": "a.txt", "bytes": bytes("hi")},
                     {"path": "dir/hello.txt", "from": "./data/hello.txt"}])   -- bytes
zip_extract(z, "./out")                          -- ["a.txt", "dir/hello.txt"] — what was written
let t be tar_create(entries, {"gzip": true})     -- .tar.gz bytes
tar_extract(t, "./out2")                         -- gzip detected by its magic bytes

gzip — gzip / gunzip (engine v0.6.45+)§

For loose .json.gz / .csv.gz files (a .tar.gz is tar_extract):

require file.read("./in/*")
require file.write("./out/*")

let raw be gunzip(read_file_bytes("./in/events.json.gz"))      -- bytes
let rows be json_decode(decode(raw, "utf8"))
write_file("./out/rows.json.gz", gzip(json_encode(rows)))     -- text or bytes in, bytes out
let big be gunzip(read_file_bytes("./in/dump.csv.gz"), {"max_bytes": 2147483648})   -- raise the cap on purpose

File formats — CSV, Parquet, JSON Lines§

The format builtins are pure: text or bytes in, values out. The disk is read_file / read_file_bytes / write_file, with their own file capability. They all produce or take a table — a list of maps, the same shape sql() returns — which is what Data analysis works on.

require file.read("./data/*")
require file.write("./out/*")
let sales be csv_parse(read_file("./data/sales.csv"), {"types": {"day": "date", "amount": "decimal"}})
let events be jsonl_decode(read_file("./data/events.jsonl"))
let orders be parquet_read(read_file_bytes("./data/orders.parquet"))
write_file("./out/sales.parquet", parquet_write(sales, {"compression": "zstd"}))

CSV — csv_parse / csv_encode§

csv_parse(text, opts?) → a list of maps (first row = headers). Full RFC 4180: quoted fields, embedded delimiters, quotes and newlines, "" escapes, CRLF/LF, a UTF-8 BOM.

OptionMeaning
"types"{"col": type} per column — the recommended form. "int" (exact: "007" → 7), "float" (reads nan/inf; 1e400 is an error, never infinity), "decimal" (exact, any number of digits, an exponent allowed: 1.5e-3 → 0.0015), "text", "bool" (true/false/1/0/yes/no, any case), "date" (YYYY-MM-DD), "datetime" (ISO 8601; no zone → UTC). Unnamed columns stay text; naming a column the header does not have is an error
"missing"a list of texts that mean missing, like ["NA", "NULL", "-"] (polars null_values, pandas na_values): those texts unquoted are nothing; a quoted "NA" stays text
"headers"false → a list of lists (every row is data)
"delimiter"one character, e.g. ";" or "\t" (not the quote or a line end)
"numbers"true → the old guess: every numeric-looking field becomes a number (ids and zip codes lose their zeros). Prefer types

csv_encode(rows, opts?) → text. Rows are a list of maps (headers = the first map's keys, in order) or a list of lists. Integers without decimals; nothing → an empty field and the empty text → "", so csv_parse(csv_encode(rows)) == rows; dates, datetimes and durations → their ISO text; bytes → base64; a secret → an error (engine v0.6.46+: csv_encode: row 1, column "k": secret(K) is never written as text — leave it out of the rows; before, it came out as [redacted]); a nested list or map → an error suggesting json_encode for that field.

OptionMeaning
"headers"a list of column names → the order and subset of columns
"delimiter"as in csv_parse
"eol""\r\n" (default, Excel-friendly) or "\n"
"missing"a mark written for each nothing, unquoted, such as "NA" (pandas na_rep); a text equal to the mark is quoted. Read it back with {"missing": ["NA"]} for an exact round trip. An empty mark is an error
"escape_formulas"true → a text cell that starts with =, +, -, @, tab or CR gets a leading ', so Excel and Sheets show it instead of running it (OWASP "CSV injection"). Numbers are untouched. Off by default, because it changes the data — turn it on for any CSV a person opens in a spreadsheet with text that came from users

One corner: in a one-column CSV a row whose only field is nothing would be a blank line, which reading skips, so it is written "" (as Python's writer does) and comes back as the empty text — or nothing if you read that column with a type; csv_encode warns once on stderr. The "missing" option makes that round trip exact too.

Parquet — parquet_read / parquet_write§

Columnar files as written by polars, pandas, Spark and DuckDB. Bytes in, bytes out. Native builds only (not in the wasm build).

parquet_read(bytes, opts?) → typed rows: Int64 → exact integer, Double/Float → float, Decimal → decimal, String → text, Binary → bytes, Bool → bool, Date → date, Timestamp → datetime (UTC, or the column's zone — the one Synsema stored, or the one polars/pyarrow wrote, fixed offsets like +05:30 included; milli-, micro- and nanoseconds), Duration → duration, Time (time of day) → a duration since midnight, lists and groups → nested lists and maps, null → nothing. Compression zstd, snappy, gzip and lz4. A file that is not Parquet, or that has two columns with the same name, is an error.

#### parquet_scan — a part of a file, straight from the disk (engine v0.6.46+)

parquet_read takes the file's bytes, so the whole file is read into memory first. parquet_scan(path, opts?) reads the file on disk by parts: the footer with the metadata, then only the pages of the columns and row groups it is asked for. A file of gigabytes with one column asked reads that column.

require file.read("./data/**")
let few be parquet_scan("./data/events.parquet", {"columns": ["id", "kind"], "limit": 1000})
let first_group be parquet_scan("./data/events.parquet", {"row_groups": [0]})
let everything be parquet_scan("./data/events.parquet")
ErrorFix
parquet_scan: expected a path, got bytes — for bytes already in memory, parquet_read(bytes)pass the path, or use parquet_read
parquet_scan: the file has no column "zz" (columns: id, name, v)a name from that list
parquet_scan: row group 1 does not exist (the file has 1: 0..1)an index below the count in parquet_info(…).row_groups
parquet_scan: unknown option "bogus" (valid: columns, row_groups, limit, max_cells, max_bytes)one of those
parquet_scan: option "limit" must be a positive integer, got 0a whole number from 1
parquet_scan: opts must be a map, got texta map
parquet_scan: the file has 30 cells (rows × columns); the limit is 5 — raise it on purpose with {"max_cells": n}read less, or raise it for a file you trust
parquet_scan: the file expands to more than 10 bytes uncompressed (max_bytes) — raise it on purpose with {"max_bytes": n}the same
parquet_scan: a page declares 2047 MiB uncompressed (the limit is 256 MiB per page; writers use about 1 MiB) — the file is malformed or hostile, refusing to allocate itthe file is broken or hostile
parquet_scan: not a Parquet file: …it is not Parquet
parquet_scan: the file has two columns named "a" — a row is a map and would lose one; rename it where the file is writtenrewrite the file with distinct names
File not found: missing.parquetthe path
`Capability not granted: file_read("other.parquet") — … add require file.read("other.parquet") …`that require

parquet_write(rows, opts?) → bytes. One schema per column, inferred, every column nullable: int → INT64, float → DOUBLE (a column mixing ints and floats → DOUBLE), decimal → DECIMAL(38, the column's largest scale), text → STRING, bool → BOOLEAN, bytes → BINARY, date → DATE, datetime → TIMESTAMP in µs (ns when a value has nanoseconds) with its zone, duration → INT64 in µs (or ns) with its unit. A key missing from a row is nothing there. opts.compression = "snappy" (default), "zstd", "gzip", "lz4" or "none".

JSON Lines — jsonl_encode / jsonl_decode§

One JSON value per line: jsonl_decode(read_file("events.jsonl")), write_file("out.jsonl", jsonl_encode(rows)). Blank lines are skipped and a bad line is an error naming its number. The full contract (big integers, NaN, fallback form) is in JSON, XML & TOML.

Path scopes are faithful — a .. escape outside the declared scope is denied, and a link (symlink or junction) below the grant that leads out of it is refused (engine v0.6.45+). See Capabilities.

No window between the check and the open (engine v0.6.46+, Linux and Windows). v0.6.45 checked a path on disk and then opened it by name, so a folder swapped for a link in between was followed outside the grant. Now the file is opened without following any link (openat2 with RESOLVE_NO_SYMLINKS on Linux, NtCreateFile with OBJ_DONT_REPARSE on Windows): if it opens, it is the file the check judged. With a link on the way, it is opened following it but without access to its data, the same check runs again on where it really is, and only then the same file is reopened with the access asked for. A link that stays inside the grant still works. To replace a file (write_file, edit_file, zip_extract/tar_extract) the folder is opened the same way, and the temporary file, the rename and the cleanup all go relative to it, so the name is never resolved again.