Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sources

A query reads from one source, named after FROM, and joins may read more (Joins).

sourcesyntax
SQL tableFROM erp.dbo.SC5010
SQL table (per-row name)FROM erp.dbo.IDENTIFIER($name) (dynamic names) — still a table read
raw queryFROM erp.QUERY($$SELECT ...$$) (no dialect translation)
file — CSV, Parquet, Arrow IPC or ExcelFROM 'path.csv' / FROM 'path.parquet' / FROM 'path.arrow' (also .feather, .ipc, .arrows) / FROM 'path.xlsx' (also .xlsm) — the extension picks the reader; local or HTTPS URL (Arrow IPC: local only). Any other extension is a plan-time error unless WITH (format = 'csv' | 'parquet' | 'arrow' | 'xlsx') names one
compressed fileFROM 'path.csv.gz' / .csv.zst — the inner name picks the reader
file inside a zipFROM 'archive.zip :: inner.csv', or just FROM 'archive.zip' when it holds one file
folderFROM 'sales/' — a trailing /, local or remote, reads every Parquet file under it (subfolders too, as Spark’s year=2026/ layout) as one table, or every .csv/.tsv/.txt when it holds CSVs; see below
object storageFROM 'az://account/container/path.parquet' or FROM 's3://bucket/key.parquet'; a trailing / reads the prefix as a folder
SFTPFROM 'sftp://bank/retorno/2026-10.csv' — bank a CREATE CONNECTION bank TYPE sftp (below), or sftp://user@host[:port]/path; any file format, a trailing / a folder; LOAD INTO 'sftp://…' writes
FTPFROM 'ftp://ftp.example.org/dados/arquivo.csv' — anonymous, ftp://user:pass@host/path, or ftp://name/path through a CREATE CONNECTION name TYPE ftp; downloaded once per run, then read like a local file in any format; a trailing / a folder; read only (FTP)
Windows share (SMB)FROM 'smb://fs/share/2026-10.xlsx' — fs a CREATE CONNECTION fs TYPE smb (below), or smb://[domain;]user@host/share/path; any file format, a trailing / a folder; LOAD INTO 'smb://…' writes
REST (connection)FROM crm.GET('/v1/customers', status = 'open') — path on the conn’s base URL; each name = value is a URL-encoded query param, and path and values are expressions ('/v1/customers/' || $id). crm.POST('/search', body = $$...$$) sends a body. crm.'/v1/customers' is the older spelling of a bare GET
REST resourceFROM crm.customers — an endpoint named with CREATE RESOURCE (HTTP APIs)
REST (raw URL)FROM HTTP('https://host/api/x') — the URL exactly as written, the way QUERY() is raw SQL
request bodyFROM BODY (col TYPE [NOT NULL], ...) (HTTP mode)
durable bufferFROM BUFFER 'name' (HTTP mode)
discovered unionFROM EACH TABLE OF (...) (UNION ALL BY NAME)
generated integersFROM RANGE(10) / FROM RANGE(2, 5) — lo..hi-1 as a range column; bounds are int literals or params
no sourceSELECT 1 AS x, now() AS t; — a SELECT with no FROM yields one row of computed values
CTEFROM <name>
table functionFROM paid_orders($since) p — a CREATE FUNCTION ... RETURNS TABLE (user functions), also as a JOIN’s right side

Every source takes an alias, with or without AS: FROM 'x.xlsx' AS xl, FROM sr.db.t t.

Folders

A folder read lists the folder — subfolders included, names starting _ or . skipped (_SUCCESS, _temporary/, .crc) — and reads the files sorted by path. Parquet files and no CSVs read as Parquet; CSVs as CSV, other files ignored; a folder of both is refused unless WITH (format = 'parquet') or 'csv' picks one. Every Parquet file must have the first one’s columns, in its order and of its types, among those the query reads: a file with one missing, moved or retyped fails the read naming it, rather than putting values under the wrong name. Each file still skips the columns and row groups the query does not need. CSV files must repeat the first one’s header.

SQL sources

A raw QUERY($$…$$) is sent as it is and may hold several statements — a DELETE and an INSERT before its SELECT, a statement after it. The batch runs to its end: every statement executes, and an error in any of them fails the read with the server’s message. It may return one result set; a second is an error (the query returned more than one result set) rather than dropped or, if it has the same columns, appended as if it were more rows. A read that stops early — a LIMIT, a row cap — cancels whatever of the batch had not run yet. MySQL and StarRocks refuse a multi-statement QUERY outright.

A SQL Server session is opened as SSMS and the ODBC, JDBC and .NET drivers open one: ANSI_WARNINGS, ANSI_NULLS, ANSI_PADDING, ANSI_NULL_DFLT_ON, QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL and ARITHABORT on. So a value too long for its column is refused (String or binary data would be truncated) rather than cut, a division by zero is an error rather than NULL, a varchar keeps its trailing spaces, a column created without NULL / NOT NULL allows nulls, and a table with a filtered index or an indexed view can be written.

Source clauses

These follow the source, in any order. PUSHDOWN and the rules for what a SQL source is asked to do are on Pushdown.

  • PAGINATE BY page|offset|cursor (param = 'page', size = 100, total = 'count', field = 'next', start = 2, max = 50) — REST pagination. Friendly keys map to the engine hints (param→page_param/cursor_param, size→page_size, total→total_field, field→cursor_field, start→start_page, max→max_pages); unknown keys pass through.
  • RETRY n [ON (429, 503)] — retries + retryable statuses.
  • WITH (delimiter = ';', encoding = 'latin1') — the CSV dialect. The delimiter is one character, or the word tab; the encodings are utf8 (default), latin1 / iso-8859-1, and cp1252 / windows-1252. Non-UTF-8 input is decoded to UTF-8 as it is read, so everything downstream — comparisons, length(), a parquet sink — sees proper text. A local file read with a dialect still fans out over byte-range chunks under -j, each lane decoding its chunk in that dialect. Multi-byte encodings are not supported: they would break that chunking. A file whose bytes are not what you claimed does not fail, it just yields mojibake, so prefer the publisher’s stated encoding over guessing. The delimiter is also accepted on a LOAD INTO file target; encoding is not — a CSV sink always writes UTF-8, and being told otherwise is an error rather than ignored.
  • WITH (format = 'csv' | 'parquet' | 'arrow' | 'xlsx') — read or write a path as this format whatever its extension says (xlsx reads only). Needed for a file named .dat or .txt, and for a URL that serves CSV from an extensionless path. Without it, an extension basalt does not know is refused at plan time rather than parsed as CSV, which would read a binary file’s bytes as rows.
  • WITH (k = v, flag, ...) — every other option of a read, listed in full below.

WITH options

A read’s WITH (...) takes key = value pairs and bare flags. PAGINATE BY and RETRY (above) are shorthands that set some of the REST ones.

Files

optionmeaning
formatcsv, parquet, arrow or xlsx, whatever the extension says
delimitera CSV’s separator: one character, or tab (also on a LOAD INTO file)
encodinga CSV’s text encoding: utf8 (default), latin1 / iso-8859-1, cp1252 / windows-1252
sheetan Excel worksheet by name (default the first)
rangean Excel block such as 'B3:F200', or 'B3:F' to the last row
headerfalse reads an Excel sheet’s first row as data, the columns named A, B, …
buffera flag: drain the source fully before opening the sink

REST sources — HTTP(...), conn.GET(...), conn.POST(...) and resources

optiondefaultmeaning
itemsthe responsedotted path to the array of rows when the response nests it, as 'data.rows'
methodgetpost sends body
bodynonethe request body; the page parameter is added to it when paginating a POST
body_typeformjson sends the body as application/json, anything else as a form
headernoneone extra header, 'Name: value'; a User-Agent given this way replaces basalt’s
bearer, bearer_envnonea bearer token, or the environment variable holding one
auth, auth_envnonean Authorization value sent verbatim, or the variable holding it
user_env, pass_envnonevariables holding a basic-auth user and password
paginatenonepage, offset or cursor (PAGINATE BY)
page_parampagethe query parameter that carries the page number or offset (param in PAGINATE)
start_page1the first page number (start)
start_offset0the first offset, in offset mode
size_paramnonethe query parameter that carries the page size, as OData’s $top
page_size100rows per page, sent in size_param and the step of offset mode (size)
cursor_paramcursorthe query parameter that carries a cursor (param in cursor mode)
cursor_fieldnextdotted path to the next cursor or URL in each response (field)
total_fieldnonedotted path to a page count, for APIs that never return an empty page (total)
max_pages10000a cap on pages fetched (max)
stop_shortoffa flag: stop after a page shorter than page_size
prefetch1pages kept in flight at once, in page and offset modes
retries2retries of a transient failure (RETRY n)
retry_statusesnoneextra HTTP codes treated as transient, as '404,408' (RETRY n ON (...))
retry_base_ms500the first retry’s back-off, doubled each time, ±30% jitter
timeout_ms300000the limit on one page’s fetch
progress_ms30000how often a long fetch logs its progress

Joins — on the JOIN clause: max_build = '16GB' sets the right side’s memory ceiling, past which the join spills to disk (Joins); join_order = 'written' (default 'auto') keeps an inner join’s right side in memory and its place in a chain, whatever the size estimates say.

WHERE on a REST source runs in basalt after the fetch; on a SQL table it is pushdown. Same word, different plan — EXPLAIN shows which.

Reading a REST API

A complete REST read, for orientation:

CREATE CONNECTION crates TYPE http OPTIONS (base_url = 'https://crates.io/api/v1')
  RETRY 2 ON (429, 503);

SELECT id AS crate, downloads, json_get(links, 'owners') AS owners
FROM crates.GET('/crates', sort = 'downloads', per_page = 100)
  PAGINATE BY page (param = 'page', size = 100, total = 'meta.total', max = 5)
  WITH (items = 'crates')
WHERE downloads > 0;

The rows are the array at items (or the response itself); a single object — a detail endpoint’s answer — is one row. Columns are typed from the first object: numbers, booleans and strings as themselves, nested objects and arrays as JSON text, which json_get and JSON_EACH (JSON functions, and UNNEST in Queries) take apart.