Harbor release tags use vX.Y.Z. Entries are ordered by release date,
newest first. Separately tagged DuckDB engine mirrors are build artifacts, not
Harbor releases, and are not included here.
FROM 'v', which the engine resolves to the view),
json_execute_serialized_sql(…), and EXPORT DATABASE or COPY FROM
DATABASE. Each killed the server within seconds beside an ATTACH/DETACH
loop, on 2.0.1 as on 2.0.0, and runs 60 s clean. A client’s ATTACH or
DETACH of a harbor_probe_* name, which harbor keeps for its own use, is
refused.SELECT 42, passes as when the
views are known; one that started before they were read holds no ATTACH
once a listing says it reads nothing (47 s to 0.01 s measured); and the
server reads them again each second on its own, beside the statements
running, so the wait ends about a second after the file reads again, not
at the next ATTACH, DETACH or /catalog. Under ATTACH/DETACH churn
SELECT 42’s median went from 22.6 ms to 0.3 ms.DELETE /sql/queries/<id> told by its statement was answered by one
thread, so a client that did not read its answer held every other
waiting cancel’s answer for its socket’s ten-second write timeout. Four
threads answer them, and a stop waits for the answers its statements
told. Another client’s cancel answers in 0.00 s, where it took 9 s.t.{l,m}[ 1 ] is t.l[ 1 ], t.m[ 1 ], where the group split at
the space; r.{a[1:2], b} expands, where the slice’s colon made it look
like a struct literal; and a comment in a group comes out after each
alternative of the item before it, so r.{a /* x */, b} is
r.a /* x */, r.b and an item commented out of a long list stays out.
Expansion writes each alternative as it makes it: the worst case peaks
at 74 MB, where it took 237 MB.[r.{a,b}], which made a list of each alternative), an empty
group before another (r.{}.{a,b}, which stopped the later groups), and
an item that a comma or space inside its parentheses would cut (a call
or a lambda such as r.{coalesce(a, b), c}) are each refused with a 400
that names the fix.cargo clippy passes on the harbor workspace, and the pull-request
gate runs it with warnings denied. The engine layer’s streams, chunks,
readers and VARIANT caster borrow what they point into, so the compiler
keeps each from outliving it. Speed is unchanged.DELETE /sql/queries/<id> aimed at an INSERT … RETURNING while
its rows streamed, or at a write as it committed, was answered
{"cancelled":true} while the rows were kept. A cancel answers true
only when the engine’s interrupt stopped the statement and nothing of it
was kept; a write that had already run is answered false, with its
rows, and a write that fails with an error of its own after a cancel
gets that error and a false. Waiting for the engine’s word holds no
worker, not the lane that answers /ready, renewals and releases, and
not another client’s cancel. A
SELECT that a cancel reached before its end ends with the cancel (499,
the error record in NDJSON) instead of an ordinary end; inside a session
that aborts the transaction, so a Ctrl-C landing as a SELECT finishes
leaves the next COMMIT answered 400 and rolled back. A cancel is fired
again every half second while its statement runs, a stop included, since
the engine now and then loses one. In a race of 2,000 cancels against
autocommit writes, 2.0.0 answered wrongly 611 times and 2.0.1 never.$$ after a word, a Unicode space or an
unterminated quote could make the inside of a string look like code:
{x}E'\'''{a,b}' rewrote the string '{a,b}'. A vertical tab, a
Unicode space or a comment did not end a term, so r.{a,b}-- c copied
the comment into each alternative. The statement is read once, as the
engine reads it, each alternative is cut from the source so every byte
keeps its reading, and a result the engine would read differently, a
string that would gain or lose backslash escapes among them, is refused
with a 400 that says what to write instead. Of 200,000 generated
statements 2.0.0 expanded 49,701 wrongly; 2.0.1 expands 1,000,000 with
none.x.{a,b}::DECIMAL(10,2) cast the first column to the
bare DECIMAL; both are DECIMAL(10,2). A type the engine reads on past
a space or comment, ::DECIMAL (10,2), ::INT [], ::TIMESTAMP WITH
TIME ZONE or ::DOUBLE PRECISION, cast all but the last alternative to
its first word; such a statement is refused with a 400 that names the
fix. A braces suite checks expansion against the engine’s own parser.lossless only when its members
are. The schema line called every container lossless, so a
GEOMETRY[] said true while its child said false.query() or query_table(),
a prepared statement, or a view made by IMPORT DATABASE or COPY FROM
DATABASE went unseen, and a DuckDB file attached AS sqlite counted as
SQLite. Each of these killed 2.0.0 within a second beside an
ATTACH/DETACH loop and ran 60 s clean on 2.0.1. So did a view attached,
or present at the start, while an attached SQLite file was locked, when
the server cannot read which views read the catalog: until it can, an
ATTACH or DETACH waits for every statement running, and for up to a
second reads them again beside them, so the statements that come after
it need not wait for it; they are read again at each ATTACH, DETACH and
/catalog too, without holding the others for the lock’s five-second
wait. query and query_table
count only when called, so a column named query costs nothing. Twenty
thousand views start in 0.4 s./catalog. One gone
for good (replaced by a file that is not SQLite, or a read-only file
deleted) is detached and listed under attached with why; the workspace
attaches it again from its record once it reads, and one a client
attached, or attached again over it, is the client’s to attach again.
One only busy stays attached, and /catalog answers as last read, the
file’s entry saying it is busy, and a stale field at the document’s
end saying every database in it may be stale; before any good read it
answers the 400 naming the file, as 2.0.0 did.sqlite_scanner or spatial not installed and the extension repository
not answering, a start waited on the engine’s install before serving. The
server serves at once, installs what is missing in the background, and
attaches recorded SQLite files after; until then /catalog lists them
with the reason.autostart waits for a slow start, and starts at standard priority.
A macOS login item ran at launchd’s Background priority, which throttles
CPU and disk, and on a busy Mac a large database took longer than the
15 s autostart waits; it reported failure and unloaded a server that
was about to serve. The item runs at Standard priority, and the wait goes
on, up to 120 s, while the server’s process is alive. harbor <db>
autostart restart rewrites an existing item and restarts it under the
new priority.{"p":1,"a":2} comes
back {"a":2,"p":1}, the values unchanged. A bare table name is not a
statement: orders alone is a Parser Error, where the alpha engine read
it as FROM orders; DuckDB keeps that shorthand in its own CLI.--sealed, --unsigned, --block-size), the file
attached to it as the default database, and connections made from the
instance; values, types and casts go through each connection’s factory,
so they see its settings as its statements do. The bindings are
regenerated from the 2.0.0 spec, still pure FFI resolved at runtime, with
nothing linked and no shim. The loader refuses an engine without the
2.0.0 API, the alpha builds among them, and make fetch-duckdb, CI and
the release builds take the engine and its CLI from DuckDB’s GitHub
release, each archive checked against a recorded SHA-256. The NDJSON
protocol is unchanged: a corpus of 34 requests over every type, VARIANT
in every shape, params and mid-stream errors answers byte for byte as
0.46.0 does, but for the engine’s version string and two setting
descriptions.DELETE /sql/queries/<id> landing in that window was
answered {"cancelled":true} while the statement ran on. The server asks
again once the statement is submitted and before any of it runs, and a
cancel found there stops it with nothing run.SELECT 1 AS `it's`, '{a,b}' AS y keeps
its string, where the expansion would have rewritten it. The REPL’s
splitter, validator and highlighter, and DuckTable’s Query view, read the
same lexer. A digit separator joins a number only between two digits, as
the 2.0.0 tokenizer has it.{"n":1,"l":2} is read back as {"l":2,"n":1}); a SQL NULL VARIANT is
written to a backup as the null marker, no longer as the JSON text null
(duckdb#25873); a quoted name alone is no statement ("t" is refused,
FROM "t" reads the table); a literal right after a number no longer
parses as its alias; and a compute-bound query runs
about 14% slower in the engine itself (a 100-million-row
count(DISTINCT), 1.21 s on the alpha CLI and 1.38 s on 2.0.0’s, and
the same through harbor).harbor <file> opens anything DuckDB reads. A CSV, TSV, Parquet file
or folder of them (Hive partitions included), JSON, Excel workbook,
GeoJSON, shapefile or GeoPackage opens in the workspace, a DuckDB database
at ~/.local/share/harbor/workspace.duckdb, as a view named for the file
that reads it where it lies, so harbor sales.csv answers SELECT * FROM
sales at once and a large Parquet file opens without being copied. A
SQLite file is attached there, read-write. A file is told by its first
bytes where it has a signature and by its name where it does not, and one
whose contents contradict its name, or that cannot be told, is refused
with what was looked at. Paths are glob-escaped before a reader sees them,
so [, * and ? in a file name name that file alone. DuckTable opens
the same workspace, so a file opened in either is there in the other.spatial when a view needs it, since
that extension does not load on demand, and never waits on a download to
start serving: a missing extension installs in the background./catalog lists views and attached databases. Each view comes with its
columns and its SQL, and no row count, since counting a view over a file
reads the file; a view whose file is missing is listed with the engine’s
error instead of failing the read. Each attached database comes with its
path, type, tables and views. The fields are new; nothing existing
changed. Views are bound off the worker pool, each within two seconds and
again only when its file changes, so a view over a slow or blocking file
cannot hold the server, and a SQLite file another process has locked
fails the read in about two seconds, naming the file./catalog survives a table dropped while it reads. It listed the
tables and then counted each, so a table another client dropped in between
failed the whole read: 68 of 200 reads under concurrent DDL. A relation
that disappears is left out: 0 of 1,000.ATTACH or
DETACH of a SQLite database that ran beside a read of the catalog could
crash DuckDB or mark the database invalid until a restart, through
/catalog or through statements such as SHOW ALL TABLES and
information_schema: 11 of 11 runs of a 20-second churn. Harbor runs an
ATTACH or DETACH alone against every catalog read; 0 of 11.The server
params was affected. A whole number past 64
bits is refused with a 400 that says to send it as a string and cast it,
where it was bound as an approximate double, and a body that names
params twice is a 400, where the second one won silently./sql was a 503 while the engine burned
CPU. The server watches the connection while a statement computes and
cancels it when the client hangs up; inside a session the transaction is
left aborted, as any statement cut short leaves it. A client that shuts
down only its sending side reads the same way and has its statement
cancelled. A COMMIT still runs to its answer.COMMIT were answered
503 and the transaction died at its idle limit. They are relayed on a
thread of their own, capped by the session count.POST
/sql/sessions/<id>/renew accepts any session: it resets the idle clock and
leaves the five-minute ceiling alone. The REPL and DuckTable keep a
transaction alive this way, and fall back to SELECT 1 against an older
server.true only when it stopped something. One that landed
after its statement finished was answered true while the whole result
arrived.Content-Length must be
plain digits, Transfer-Encoding exactly chunked, a chunk size plain hex,
and a folded header line or a CR, LF or NUL inside a header value is a
400.SELECT 1; --
note is accepted, the refusal reads “exactly one SQL statement is allowed
per request”, and a protected SET inside a transaction is refused by the
engine, which aborts the transaction.wire::scan. It agrees with the engine on non-ASCII
dollar-quote tags, $ inside a word, E strings, numbers with an exponent
or _, and the Unicode spaces the engine strips; brace expansion can no
longer rewrite text the engine reads as a string, and EXPLAIN (…, FORMAT
')') COMMIT is read as the COMMIT it is.null; a
TYPE value goes out as its text.NULL param binds untyped, as EXECUTE binds it, so coalesce(?,
'x') works.GEOMETRY('OGC:CRS84') keeps
its CRS and 2.0’s keywords are quoted.-000043-03-15, the form JavaScript
and DuckDB both read.lib, and never loads a
libduckdb from the working directory.503 instead of leaving
them unanswered, and a summoned server leaves only when no client has come
since it last looked, so a quick open of a server that is leaving
succeeds. /info reports the bound port, so --port 0 lists the real one.Starting, stopping and the configuration
sealed,
statement-timeout or init. start and a summon refuse, in one line
naming the file, the line and the fix; a server already running is not
affected. A [defaults] section, which nothing read, is refused with the
same kind of message, and a relative HARBOR_HOME is an error.restart brings a server back as it was. It dropped --sealed,
--port and an ephemeral server’s lifetime. A server keeps its start
options in a private file beside its socket, not in /info, and restart
reads them. It checks the configuration before it stops anything and
leaves the server running when it would not start again. A server started
by 0.44 or earlier published no options, so it is restarted only with its
options typed, and harbor update --restart names it instead.HARBOR_HOME.restore accepted and which turned the
string 'NULL' into a null; an interrupted restore left half a database.
Both work under a temporary name and rename at the end, a backup is private
to its owner, and restore accepts only what harbor wrote.harbor update. Each --restart runs in its own process group and
logs to runtime/log/update.log. On Windows the update moves the running
harbor.exe aside first, which could not be replaced before. Uninstalling
refuses while a login item runs this copy, and names the commands that
take it down, where it left the item retrying a binary that was gone.runtime/log, starting over past 1 MiB.The REPL
"", as DuckDB writes CSV; .nullvalue is for the display
modes..read files, and a failed one fails the script.--mode creates no file and
starts no server; an empty or repeated -c is refused; --flag=value
works.| in cells; DDL draws no empty
frame; the terminal’s late answer to the colour query no longer appears at
the prompt, and a terminal that answers nothing costs 120 ms; the history
directory is private; a request that fails says whether the statement was
sent; completion stops asking a server that cannot autocomplete.Shared code and the build
harbor-http crate,
used by the REPL and DuckTable. Every wait on a server is bounded.DUCKDB_LIB_BUILD
naming a build, make fetch-duckdb takes the library and the duckdb CLI
from this repository’s engine-<build> release, checked against its
checksums, and nothing from DuckDB’s channel, whose outages had failed the
nightly suite. Pull requests run the regressions suite, and the unit
tests run beside the quick gate.COMMIT is not answered 499 after it landed. A cancel
that reached a COMMIT as it finished was reported by the engine on the
fetch that followed, after the transaction was durable: the client was
told cancelled for work that was kept, and one that retries on a cancel
would do it twice. Raced on purpose, 40 of 600 commits answered that way
(#138). A COMMIT now runs to its answer: a cancel or a deadline either
lands before it starts, a 499 with nothing kept and the transaction
aborted, or does not land at all, and the answer is the engine’s own.COMMIT on an aborted transaction says it was rolled back. Almost
any error inside a transaction aborts it, and so does a cancelled
statement. The engine answers the COMMIT that follows with success and
rolls back, so a client that carried on after an error was told its work
was kept when none of it was. A session’s COMMIT, in any spelling the
engine runs as one, is now preceded by the question: an aborted
transaction is rolled back and answered 400, sql_error, with what
happened, and a healthy one commits as before.COMMIT after it answered success
either way. A statement that did not get to send its whole result leaves
the transaction aborted whenever that happens, as a cancel leaves it, and
with the item above the COMMIT is told. One whose result had all been
sent before the client left ran to its end, and its transaction stands.BEGIN, the work and the ROLLBACK
each landed on a different pooled connection: BEGIN answered success,
every statement after it committed alone, and the ROLLBACK failed with
nothing to undo. A migration whose second statement failed left its first
one committed. In the REPL, a -c string, a piped script and .read, a
BEGIN now opens a session, every statement after it runs there, and
the COMMIT or ROLLBACK that ends the transaction releases it; one the
parser refuses, or that never reached the server, ends nothing and the
session is kept. A script that fails or ends with its transaction open has
it rolled back and says so, and so does leaving the REPL or .opening
another database. The session is an ordinary one, so
the server’s limits on it (thirty seconds idle, five minutes in all) are
the transaction’s; the REPL prints them when one opens, and a statement
sent after the server took the session back is told the transaction is
over.BEGIN without a session is a 400. The server answered success to a
transaction it could not hold, which is the silence the item above came
from, and any client could walk into it. BEGIN and START TRANSACTION
outside a session are refused the way USE is, with what to do instead.
COMMIT and ROLLBACK with nothing open were always the engine’s to
refuse. The refusal, the client and the inTransaction flag in
/sessions read a statement by one rule, the engine’s as measured: every
space the engine skips is skipped (a byte order mark, a no-break space, a
vertical tab, which also closes that gap in the SET fence), an analyzed
EXPLAIN in any of its spellings acts as the statement it runs, so
EXPLAIN ANALYZE COMMIT ends a transaction, and a quoted "begin" is a
table’s name.detach removes the login item the database was filed under. For a
database whose config name is not its file’s stem, the name was asked for
again after the entry was gone, so the login item of the stem’s name was
removed (some other database’s, if one had it) and the real one kept
starting at every login with no command left that could reach it. Two
verb pairs the grammar accepts are carried out: autostart off restart
restarts, and detach restart comes back refcounted, as detach start
does.sealed, the statement
ceiling or the memory limit, and say nothing. It is reported as invalid,
with the reason; a statement-timeout that does not parse is reported
too, where it was dropped." was
escaped, so a % in a path was read as a specifier and a $ in the
database’s path as a variable: a login item for /data/50%off.duckdb
served a new, empty database under the real one’s name. %, \ and "
now arrive as the bytes the filesystem holds, and so does $ where
systemd substitutes one, which is in a command’s arguments and not in the
program’s path. A path with a control character in it is refused for a
login item on either platform, where a newline could have added a
directive. The item’s log directory is private from the moment it exists.INSTALL
autocomplete on the server when LOAD failed: a download and a lasting
write on the server’s host, asked for by a keypress. It loads the
extension if the server has it and completes from the catalog if not.CHECK or DEFAULT
that named a type was read as the column’s own, so a table with s
VARCHAR CHECK (s::VARIANT IS NOT NULL) was refused in both formats and
one whose default cast through TIME WITH TIME ZONE was moved to text
for nothing. And a restore finds a table’s document decode by what it
does, so a directory whose decode also names a generated VARIANT
restores, where running that statement as written failed on the column
the table computes..read.
A statement the server stopped on its own account, at a deadline or a
session’s limit, says so and fails, where it printed Interrupted. as if
a key had been pressed.
start with options on a server that is already up says a restart
applies them, where it reported success and ignored them. The list
removes a socket only when two tries are refused, since a server with a
full listen queue refuses one. attach and detach write a config that
is a symbolic link where it is kept, where they replaced the link with a
file.harbor says when a second copy of it is on the machine. install.sh
and Homebrew each keep their own copy and upgrade only their own, so a
machine with both runs whichever PATH reaches first while the other falls
behind, and the version depends on how a thing was launched: a shell, a
login item, DuckTable. The list and harbor update now name the other
copy, its version and how it was installed, give the command that removes
it, and say which copy a bare harbor runs. Homebrew’s launcher and the
binary behind it count as one copy, as do two paths to the same file. With
one copy there is nothing said, and the note goes to stderr, so a piped
list stays a list.harbor update leaves a Homebrew copy to Homebrew. Run from a copy in
a Homebrew cellar, it would have written the installer’s files over
Homebrew’s and left Homebrew’s record of the version wrong. It refuses and
names brew upgrade duckdb-harbor; --check and the report of servers on
old code work as before.brew install shreeve/tap/duckdb-harbor,
beside install.sh. scripts/update-formula.sh points the formula at a
release, and is the last step of one.EXPLAIN ANALYZE a timing is bold red at a quarter
of the query, yellow at a tenth, and gray under a hundredth. DuckDB’s shell
gets those roles from the engine, which draws a plan from typed pieces;
a harbor client is handed the finished drawing, so it reads the roles back
from the text by the engine’s own rules: a title follows ╭─, a leaf that
names a Table or a Function is a scan whatever it is called, ~7 rows
is an estimate and 7 rows a count. The text under the colors is
unchanged, and output to a pipe or under NO_COLOR carries none.EXPLAIN prints the plan. The boxed modes showed a plan as a cell:
its newlines escaped to \n, the rest cut off at the column edge, one
line of ╭─ Projection ───╮\n│ Projections: a │\n… where a drawing should
be (#80). A result with the two columns the engine names explain_key and
explain_value, which nothing but EXPLAIN produces, prints each plan as
the engine drew it, the way DuckDB’s own shell does, with no row count
after it. A single plan prints bare; several, as under
explain_output = 'all' or beside an analyzed plan, each carry a one-line
label. Markdown fences the text; csv, json and line keep the raw value, as
a script reading a plan wants.harbor says why a running server is not listed. The list
finds servers by their unix sockets, and Windows has none, so a server
started with --port was running while harbor drew an empty table and
said “Nothing running”, then suggested harbor <db.duckdb>, which on
Windows cannot join it either (#80). The Windows listing now says that
running servers are not shown there and gives the form that reaches one,
harbor http://127.0.0.1:<p>, after the empty frame and after a table of
attached databases alike. The README’s claim that the client half works the
same everywhere is narrowed to what is true: past the connection, it does.vendor/reedline is gone: 0.52.0 is the same source,
every file identical, and it holds all four fixes the REPL relies on
(nushell/reedline#1175, #1203, #1209 and #1226). Nothing about the REPL
changes.harbor update installs the newest release over the binary that ran
it. It is the install one-liner, run from inside harbor and pointed at
the directories this binary lives in, so a copy outside ~/.local updates
in place; harbor update 0.42.0 picks a release, older or newer, and
--check only says whether a newer one exists. An update to the version
already installed downloads nothing. A running server keeps the file it
opened until it is restarted, so the update ends by naming every server
still on the old code and the harbor <db> restart each needs; --restart
runs those, and nothing restarts otherwise, since a restart drops that
server’s clients mid-request.main, unpatched. Every fix the REPL
carried as a local patch is merged upstream, the last of them on
2026-09-23 (nushell/reedline#1226: Up and Down report whether they
moved anything). vendor/reedline is a verbatim git archive of upstream
at that point, since crates.io still serves 0.51.0, and it goes when 0.52
ships. The completion menu is built .with_word_chars("_."), upstream’s
spelling of the rule that a menu closes at the end of the word it was
opened for. One behavior follows upstream’s plainer rule: Down on a
recalled line that has been edited opens the completion panel rather than
walking history back to the live line, because reedline’s history walk
ends at the first edit. A recalled line left as it came still walks. The
binary loses reedline’s crossbeam, fd-lock, itertools, thiserror
and unicase dependencies along the way.POST a text/plain body to /sql with no CORS preflight and
run any statement, and through DNS rebinding it could read the answer under
a hostname it controls. Measured on 0.41.3: a foreign-origin form post
created a table, and a request with Host: attacker.example read it back.
While DuckTable holds an SSH tunnel to a remote Harbor, that remote is on
the Mac’s loopback too. A TCP request carrying an Origin header, or a
Host that is a hostname rather than localhost or an IP address, is now
refused with 403 forbidden before anything runs. Harbor’s own clients,
Rip and curl send no Origin, and pass whenever they dial localhost or an
IP address, an SSH tunnel’s local end included. A hostname that only
/etc/hosts points at loopback is refused. A browser client belongs behind an edge proxy
that drops Origin and sends the upstream’s own Host. The unix socket is
out of a browser’s reach and is not checked.--format parquet backup keeps a VARIANT beside a TIMETZ. Parquet
normalises a TIMETZ to UTC, so such a table is written as text instead,
but the move skipped everything text does for a VARIANT: the column went
out as its display text, {'a': 1, 'b': [2, 3]}, with no decode in
after.sql, and every document restored as a string. A table moved to text
is now written exactly as one that is text by default: the VARIANT as
JSON, decoded after the load, with what JSON cannot carry named and a file
holding a blank record quoted throughout."infinity", not a date. DuckDB stores
one as a sentinel, the type’s largest value or its negation, and harbor
formatted the sentinel as a date: 'infinity'::TIMESTAMP crossed the wire as
"294247-01-10T04:00:54.775807", which DuckDB itself cannot parse,
TIMESTAMP_NS as a plausible "2262-04-11T23:47:16.854775807", and
-infinity as a DATE as "-5877641-06-24", all marked lossless. Every DATE
and TIMESTAMP flavour now sends "infinity" or "-infinity", the words
DuckDB prints and parses back.x{a,b}{a,b}… held a server thread for seconds and thirty groups
asked for a billion alternatives. A term of one-item groups, which
multiplies nothing, recursed once per group and could overflow the stack.
Expansion now works from an explicit stack, and one that would write or
re-read more than 16 MiB of text is refused with a 400 before anything
runs. Reaching the budget takes under a tenth of a second.GENERATED column is computed by the restored table, and no backup
file holds one. CREATE TABLE o (id INTEGER PRIMARY KEY, doc VARIANT, qty
INTEGER, doubled INTEGER GENERATED ALWAYS AS (qty * 2), note VARCHAR)
backed up without a word and did not restore: table "o" has 4 columns but
5 values were supplied. EXPORT DATABASE leaves a generated column out of
the file it writes, since no COPY takes one back, and harbor writes some
files again in its own words, where a SELECT * or a COPY <table> TO
puts the column back in. Measured on 0.41.2, that was four routes and not
only the VARIANT one: the JSON export of a table with a VARIANT column
(header id doc qty doubled note); the file quoted throughout for a blank
record, with no VARIANT in sight (CREATE TABLE b (s VARCHAR, len BIGINT
GENERATED ALWAYS AS (length(s))) holding an empty string wrote "s" "len"
and failed at the CSV sniff); the swap to parquet for a UNION (column
count mismatch: expected 2 columns but found 3); and the swap to text for
a TIMETZ under --format parquet. All four now write the table as
SELECT * EXCLUDE (<generated>) …, the names read from schema.sql, so
every file matches what EXPORT writes for such a table: id doc qty
note. Restore stages a VARIANT table with the same EXCLUDE, so the
staging table holds what the file holds and the INSERT lines up with the
table’s stored columns in declared order. A whole-database --format
parquet never had the defect, since those files are EXPORT’s own, and a
generated column beside no VARIANT, no blank record and no swap restored
all along.e VARCHAR GENERATED ALWAYS AS (CAST(d AS VARIANT)::VARCHAR)
was refused outright, cannot round-trip in either backup format: parquet
has no writer for a VARIANT nested inside another type, because the word
in the expression was read as a type to be held. A generated VARIANT
column was cast to JSON and named in after.sql’s UPDATE, which no
generated column accepts. Neither is written, so neither is looked at.INSERT, with a VARIANT beside them or without. A
directory written by 0.41.1 and one by 0.41.2, each holding o, a table
whose generated columns read the document, the blank-record table, an
ordinary generated column and a VARIANT under a CHECK, restored with
every row equal; so did 0.41.2’s TIMETZ text file and its directory of a
quoted schema, a quoted table and a column with a tab in its name. The one
that does not is the parquet file a UNION table with a generated column
was swapped to: a parquet header is not harbor’s to read, the engine’s own
message names both column lists, and the backup wants taking again.--strict: o; a table whose
generated columns read the document ("the doc"['a']::VARCHAR,
variant_typeof("the doc"), a generated VARIANT) under quoted names and
sitting before, between and after the stored ones; an ordinary generated
DECIMAL; the UNION, blank-record and TIMETZ tables; and a VARIANT
under a CHECK with no generated column. Every row IS NOT DISTINCT FROM
its source, the generated values and variant_typeof included, the SQL
NULL rows too, duckdb_columns().generation_expression the same on both
sides and no staging table left. The restored table computes from the
loaded document: {"a":5} gives 5 and OBJECT(a). A stock IMPORT
DATABASE of the directory, then after.sql, gives the same table, where
it failed at the sniff. The fidelity note is unchanged: a DATE inside the
VARIANT of such a table is still named, and --strict still refuses it.
Under --format parquet the same database restores with every value and
every generated value equal, the TIMETZ table swapped to text included;
the one difference is parquet’s own, the number 42 relabelled from UINT64
to INT64 inside the VARIANT, as it is without a generated column.
The engine (alpha42289) has only VIRTUAL generated columns, Can not
create a STORED generated column!, and refuses doc.a inside one
(Qualified (tbl.name) column references are not allowed), so a generated
column reaches into a document with doc['a']. The roundtrip suite holds
all of it, under text and again under --format parquet.recursion limit exceeded at line 1 column 178.
Every layer now keeps the one number. An object or array parameter is
measured by its own levels ({} is one, [[1]] is two) where parameters
are read, before anything is bound: 100 levels bind, store and read back,
and 101 is a 400 bad_request saying a document param nests at most 100
levels, with nothing stored. The parser’s own refusal past 125 levels says
the same words. A string parameter is never inspected: 150 levels of
brackets through ?::JSON bind and store as before. The unit tests and the
asserts suite hold the boundary for arrays and objects, a deep branch that
is not the first, and the string.SELECT error('cancelled') on the
connection, dropping the error: measured on the engine, inside a
transaction that leaves Current transaction is aborted (please ROLLBACK)
for every statement until the ROLLBACK, and a COMMIT sent instead rolls
back; in autocommit it leaves nothing, and the next statement runs and
commits; an interrupt still pending from the cancel does not reach it,
since the engine clears that as a statement starts, and it fails with its
own error; it costs about 80 µs, on the cancel path only. The connection
needs no more resetting than it did, since a lease is replaced at release
and a worker holds no transaction between requests. The cancel suite binds
a 7 MB document inside a transaction, cancels during the cast, and finds
the next statement refused, the earlier write gone after ROLLBACK, a
careless COMMIT committing nothing, and a session in autocommit
untouched; a unit test drives both early paths on a connection.harbor restore loads a VARIANT column as documents, so a CHECK on
one restores. A table such as CREATE TABLE g (id INT, v VARIANT CHECK (v
IS NULL OR variant_typeof(v) LIKE 'OBJECT%')) backed up without a word and
could not be restored: load.sql landed each cell as a VARIANT string,
after.sql decoded it afterwards, and the constraint was tested in
between. The backup is unchanged, byte for byte: text files, COPY lines in
load.sql, the decode in after.sql, so a stock duckdb imports the
directory as before. What changed is the restore. IMPORT DATABASE is
schema.sql and then each COPY of load.sql with its file joined to the
directory, and nothing more, so harbor runs those statements itself and
loads a table whose decode is in after.sql another way: the table’s own
COPY, options untouched, fills a staging table that differs only in
holding the VARIANT columns as VARCHAR, one INSERT … SELECT * REPLACE
(v::JSON::VARIANT AS v) moves the rows across, and the staging table is
dropped. Every other column is parsed exactly as it was, and the table
never holds a VARIANT string. Measured with a table-level and a column-level
CHECK, a NOT NULL VARIANT, a quoted schema and table name, a child table
with a foreign key, and DECIMAL, TIMESTAMPTZ, BLOB, LIST, STRUCT, MAP, ENUM,
HUGEINT, UUID, INTERVAL and DOUBLE columns beside the VARIANT: every row
IS NOT DISTINCT FROM its source with variant_typeof equal, the SQL NULL
row included. That row travels as the JSON text null (duckdb#25873), and
a file edited to hold the null marker there instead restores to the same
NULL. It is also faster: a 300,000-row table of small documents restored in
1.0 s where the COPY and the UPDATE took 9.2 s, to a file of the same size.
A table written as parquet, and a directory with no after.sql, load by
their COPY as before. A stock duckdb still cannot restore a table whose
CHECK refuses strings from a text backup; the README says so, and the
roundtrip suite holds both halves. Running the files itself, restore sends
each statement of schema.sql as a request of its own, so each one meets
harbor’s one-statement rule and its 8 MiB request cap, where
IMPORT DATABASE took the file whole.VARIANT and a
GENERATED column backs up and does not restore, in this release as in
0.41.1. The VARIANT export is a SELECT *, which writes the generated
column into the data file, and no COPY takes it back: 0.41.1 fails at the
CSV sniff, 0.41.2 with table "o" has 4 columns but 5 values were supplied.
A table with a generated column and no VARIANT exports without it and
restores on both.UPDATE t SET doc = ? WHERE id = 1 with an object parameter went
from a median of 2.73 ms to 0.44 ms, against 0.37 ms for the same statement
with a string; a 100-row INSERT of object parameters went from 232 ms to
1.5 ms, against 1.4 ms with strings. A type the engine refuses to make, as
inside an aborted transaction, is not kept: the text is bound, the
statement reports the abort, and the next request asks again.DELETE /sql/queries/<id> could answer
{"cancelled":true} and the statement then ran to its deadline: with 300
object parameters that stretch was 0.9 s, and a cancel 0.4 s in left a
7-second statement running its full 7 seconds. Binding is now its own step
(Conn::bind), and the slot is asked once the values are built and before
anything runs. The same request answers 499 at 0.4 s; with one 7 MB
document, whose cast alone takes a third of a second, a cancel at any
moment of it answers 499 when the cast returns, and inside a transaction
leaves the transaction open with its writes, since nothing ran. The cancel
suite holds the 7 MB case, outside a transaction and inside one.CHECK on variant_typeof.
CHECK (v IS NULL OR variant_typeof(v) LIKE 'OBJECT%') refuses arrays,
scalars and JSON strings along with the unparsed string it was offered
against, and it breaks the default backup: the tsv backup succeeds and the
restore fails with CHECK constraint failed, because load.sql lands each
VARIANT cell as a string and after.sql decodes it afterwards.
--format parquet backs up and restores such a table.VARIANT nested in a LIST, STRUCT or MAP is not backed up by
either format, and the README now says so where it said parquet carried
it: the backup is refused whole, naming the table, and nothing is written.{ "params": [ … ] } uses two; the README, this file and
the suite said 128. Level 125 binds and stores, 126 is a 400, and the suite
measures both. A string through ?::JSON has no such limit. Rip’s ORM and
DuckTable’s editor refuse a document past 100 levels, so nothing a
first-party client writes reaches it.-0 stores
as the DOUBLE -0.0 and 1e400 is a 400 at the body parse, where the
?::JSON text path stores 0 and Infinity; for a VARIANT inside a list,
struct or map it is the nested schema entry that reads "lossless": false,
not the column’s; and the REPL’s json modes splice a document of any depth,
because the well-formedness check does not recurse."params": [{"a":1}] went to the engine as its JSON text, a VARCHAR, and
text cast to VARIANT is a VARIANT string: SET doc = ? stored a value
that read back as JSON while every path into it was NULL, and
WHERE doc = ? never found a document. harbor now asks the engine what
each parameter expects — one bind pass, made only when a request carries
an object or an array — and binds such a parameter as the document where
the answer is VARIANT. Aimed anywhere else it is its JSON text as before:
an untyped slot, a VARCHAR column, a $1 used against two types, a
VARIANT[]. A string parameter is a string wherever it goes, whatever it
spells; a client holding JSON text still writes ?::JSON, and statements
that already cast are untouched, since the parameter then expects JSON.
The bind pass drops an interrupt that lands during it, so the slot is asked
again before the statement runs; a cast the engine refuses, as inside an
aborted transaction, leaves the text and the statement reports the error.
Reading a string as JSON was considered and measured out: JSON text nested
tens of thousands deep, cast to VARIANT, recurses inside the engine until
the process dies, and no string parameter should be able to reach that.
An object or array cannot: the request parser reads 127 levels, the body’s
own { "params": [ … ] } is two of them, and a document parameter nested
past 125 is a 400.DUCKDB_LIB_BUILD=alpha42289
tells scripts/fetch-duckdb.sh, CI and Release.yml to take libduckdb
build v2.0.0-alpha42289 from the engine-alpha42289 release of this
repository, which holds that one library for each of the five platforms;
latest, the default, is the channel’s current build. It takes the place
of DUCKDB_ENGINE_RELEASE, which named a harbor release to lift the
library out of: what has to stay fixed is a DuckDB build, and the name now
says which one. The script checks that the library it fetched says it is
that build.duckdb_v2_connect, which is also the start of duckdb_v2_connection_create
in the reworked API — so the channel’s current engine passed, and harbor
then refused it at dlopen. The check now runs on the downloaded library,
asks for duckdb_v2_create_environment, the symbol harbor’s loader gates
on, and a library that fails it never lands. Release.yml asks for the
same symbol.{a,b}, expanded
in the server before the engine sees it: raw_request.{requisitionNumber,
visitDate, patient.{lastName, firstName}} is four path expressions,
joined with , . Items are separated by commas, or by whitespace alone;
a group nests; it can sit anywhere in a term (orders_{2025,2026},
r.{a,b}::VARCHAR) and two groups in one term multiply. A struct literal
— a group with a lone : at its top level — is left as it came, as are
strings, quoted identifiers, dollar quotes, comments, an empty {} and
any statement with a brace that never closes. Every client gets it, and
DuckDB is untouched.VARIANT or JSON cell as JSON. The wire
carries such a cell as JSON text, and --mode json / --mode jsonlines
used to quote that text as a string, so doc.patient.age came out as
"43" and a document as "{\"a\":1}" — JSON inside JSON, parsed twice
on the other end. The text is now spliced in as the JSON it is: 43, an
object, an array, true, null. This is what duckdb -json does with a
JSON column. A SQL NULL and a JSON null are both null; the wire and
csv still tell them apart. The text is checked first, so a row is always
well-formed: NaN and Infinity, which the engine’s cast writes bare and
JSON cannot say, stay the strings "NaN" and "Infinity". The check does
not recurse, so a document is spliced whole at any depth. A pretty-printed
JSON column
keeps its newlines in the engine; jsonlines is one record per line, so
between tokens they become spaces. JSON nested inside a struct, list or
map column is a string, as the wire holds it. The display modes are
unchanged: a VARIANT string shows bare, a JSON column shows its text
with the quotes, as DuckDB’s own table does. csv and the wire are
unchanged.VARIANT string shows without its quotes in a table. The wire
carries a VARIANT cell as JSON text, and the display modes (box,
markdown, line, list) used to print that text as it came, so a string
read by path — raw_request.requisitionNumber — showed as
"L2605106156". A cell that is a JSON string now shows its content, the
way a VARCHAR always has; a number, a boolean, a null, an object or an
array is unchanged. Only the display modes do this: csv, json and
jsonlines stay raw, so a program on the other end still tells 42 from
“42”, and the wire is untouched. Cast a path to JSON to see the quotes
in a table.DUCKDB_ENGINE_RELEASE
names a release (v0.39.0) whose archive supplies libduckdb to
scripts/fetch-duckdb.sh, CI and Release.yml, in place of the channel’s
current build; the CLI and headers still come from the channel. The
channel is a moving pointer with no way to ask for an older build, and on
2026-09-17 it moved to an engine whose v2 C API had been reworked
(duckdb/duckdb#25751) and which this harbor cannot load. The repository
variable of the same name holds the pin; clearing it returns to the
channel.VARIANT column arrives over HTTP as JSON text. It used to arrive
as the engine’s display text — {'method': POST} — which no client could
parse, and which printed the number 42 and the string “42” the same way.
The encoder now casts each variant to JSON on the connection that produced
it, so a document that entered as JSON leaves as the same JSON, and the
schema line says "encoding": "json" instead of "varchar-cast". JSON has
no date or timestamp of its own, so those variant members arrive as strings
and the column stays "lossless": false. GEOMETRY is unchanged. The
README’s known limitations now carry the measured list of where a
VARIANT is not quite JSON, and the write rule that goes with it.VARIANT from JSON, and
on the default 2 MiB thread stack a document some 7,700 levels deep
overflowed the executor and aborted the whole process. The threads that
run SQL now have a 16 MiB stack, reserved rather than committed, which
puts the edge past 60,000 levels.engine-<pin>
prerelease. The channel now ships the v2 API nightly, so that scaffolding
is gone: make fetch-duckdb, CI and the release workflow all pull DuckDB’s
own v2.0-cyanoptera tarballs through one script, which refuses a library
without the v2 API should the channel ever regress. Release archives
bundle the build they were made with, as before.VARIANT column backs up as JSON text, and comes back exactly when
it entered as JSON. Text could not carry one at all: the number 42 and
the string “42” both print as 42, and the reader hands every cell back
as a string, so any table with a variant column was quietly written as
parquet. It is now written as JSON — 42 for the number, "42" for the
string, still greppable — and decoded on restore, which returns every
value that entered as JSON with its inner types and nesting intact. The
decode is an UPDATE, and DuckDB’s IMPORT DATABASE accepts nothing but
COPY, so it lives in a new after.sql beside load.sql: restore runs
both, and a stock duckdb importing the directory by hand gets the JSON
text and can run the second file itself. What JSON has no word for — a
DATE, a DECIMAL, a BLOB put inside a variant from SQL — returns as
JSON’s nearest type, and the backup says so once per column, naming the
types; --strict refuses instead, and --format parquet keeps them. A
VARIANT nested inside a STRUCT, LIST or MAP still goes to parquet.The blank-record check, which decides whether a file is written with
every value quoted, now looks at the file that will be restored rather
than the one EXPORT wrote first.
show tab, Tab, les;,
Enter appended the highlighted table to a finished statement rather
than running it. Every character in a batch is now read, so the word ends
wherever its boundary falls.cannot reach harbor: No such file
or directory on the next statement. The mooring now asks /ready once and
holds the answered connection, renewing every 240 seconds inside the
server’s 300-second idle clock, so the berth stays for as long as the
client does. This reaches every client that summons a server: the repl,
piped and -c scripts, backup, and restore.\\?\ prefix is hidden only when
rendering paths in the banner and fleet display, preserving long-path
support while keeping displayed paths readable.backup
may export a table again after scanning its first export for blank records
or deciding that its types need another format. These passes previously
used separate requests that could see different committed data. They now
share one transaction. A session opened with {"purpose":"backup"} has no
statement-idle timeout; it lives in a 60-second window that
POST /sql/sessions/<id>/renew extends, and the client renews it from a heartbeat every twenty seconds,
through SQL and file work alike. Renewals travel the control path, so a busy
worker cannot starve them. Losing the lease aborts the backup, even
mid-response, rather than resuming on a snapshot the first pass never saw,
and an expired or released session cannot be revived. Ordinary sessions
cannot be renewed; /sessions reports renewable. An older server without
the route is refused with an explicit ask to upgrade.400 and nothing executes.ROLLBACK undoes a transaction and nothing else — a SET VARIABLE,
a temp table, and a PREPARE survived it onto the next request.
Any statement that can leave such state, including one wrapped in
EXPLAIN ANALYZE, now costs the worker a fresh engine connection, and a
released session gets the same treatment. The parsed-statement cache goes
with the connection.start, and DuckDB’s own allowed_configs and
lock_configuration now enforce it — a wrapped SET threads or a SET
lock_configuration=false is refused by the engine rather than by a keyword
check. Other settings registered at startup remain changeable unless
initialization imposed stricter locks. Load
extensions whose settings must be tunable during --init; settings that
arrive later are outside the allowed list.413 on both endpoints, chunked or not, where before the
excess was quietly cut off and parsed. The parsed-statement cache keeps at
most 64 texts and 1 MiB of SQL, and does not retain a statement over 64
KiB, so a one-off bulk INSERT runs but cannot pin every connection’s cache.config.toml writes take a lock. Concurrent membership updates each
land through a lock spanning read, modification, and atomic replacement.
Temporary files use exclusive creation. On Unix, the lock file is opened
without following symlinks, owned config directories are secured to 0700,
and foreign-owned directories, permission failures, and unsafe config files
are refused. This does not reject symlinks throughout the entire config path.C:\... rather than the \\?\C:\... that
canonicalize returns.regressions suite, run on an isolated one-worker server so connection
reuse is deterministic; make test now runs the workspace with all
features in release mode.harbor <db> backup [dir] and harbor <new.duckdb> restore <dir>.
A .duckdb file is only as portable as the engine that wrote it, so copying
one is a snapshot, not a backup. backup writes the durable thing —
schema.sql, load.sql, and one tab-separated file per table — greppable,
diffable, and readable by anything.NULL is a real null, a
quoted "NULL" is the string, an empty field is an empty string. Quotes are
always allowed and rarely required — the reader takes a bare field and a
written "" the same way, so a backup stays editable by hand.FORCE_QUOTE takes a column list rather than a predicate, so it is spent per
FILE: a table whose export contains a blank record is written again with
every value quoted, and no other file pays for it. The test is the failure
itself rather than a proxy — a blank record between rows, walked
quote-aware, so a blank line inside a multi-line value is left alone.COPY names its file and
nothing more, so it can be moved, renamed, copied to another machine or
committed to a repo and still restore — an absolute path would have nailed
it to the machine that wrote it.restore builds a new database and refuses one that exists, since a
restore that can overwrite can be run at the wrong moment and destroy what
it was meant to protect. It is also where --block-size applies, block size
being fixed at creation. Both verbs act on contents rather than lifetime, so
they stand alone and combine with no other verb. Retention, rotation,
scheduling, compression and remote targets stay out: cron, a filesystem and
rsync already do those.backup takes --format tsv|parquet and --strict. Neither format holds
every type and the holes are not the same shape: text loses a UNION’s tag
(the restore then refuses) and retypes a VARIANT’s contents (it does not),
while parquet refuses a negative INTERVAL outright and normalises a
TIMETZ to UTC — 12:00:00+02:30 returns as 09:30:00+00, the same
instant, a different value, and nothing said. Each hole is the other
format’s solid ground, so a table the chosen format cannot carry is written
in the other one and named out loud, with load.sql recording the format
per table. --strict refuses instead of swapping. The rule under all three
is the same: never write something that will not come back.roundtrip suite: back up and restore every type in the shared corpus,
a schema of constraints, indexes, views and sequences, the strings that
attack the format, and a seeded fuzz of random tables — then attach both
databases and ask DuckDB whether anything differs, rather than reading the
export back and finding the writer agrees with the writer.--block-size now also works on the summon — harbor <db> --block-size 64k
-c "..." shapes the database that call creates, instead of the size being
reachable only through an explicit start. A size that reached nothing,
because a server was already up or the file already existed, says so.USE outside a session is now refused instead of silently discarded.
It sets the current database on the connection it runs on; that connection
is pooled, and a request carries exactly one statement, so nothing could
ever follow it there. It reported success and was thrown away, every time.
The refusal names the session that does persist (POST /sql/sessions) and
the qualified names — database.schema.table — that need no session at all.
Inside a session USE is unchanged.--block-size and the matching block-size config key: the block size
for a database the call CREATES, from 16k to 256k. A block is both the unit
of allocation and the window a column’s compression works in, and the
default suits few large tables rather than many small ones — a dozen
near-empty tables cost 9.5 MiB at 256k. It travels as an open-time option
because a later SET cannot reach it.default_block_size under [settings] is now dropped and reported rather
than emitted as a SET that succeeds and changes nothing, and a server
asked for a block size the file does not have says so — block size is fixed
when a database is created, so a config naming another one was a wish that
read like a setting.COPY FROM DATABASE
and IMPORT DATABASE — which could not run at all. The result schema was
read before anything had stepped, and for these the result-producing member
of the group is not prepared until something does. A failing group statement
now reports its own complaint rather than the readiness error that was only
how it surfaced.unit suite runs --workspace --all-features. default-members and two
off-by-default features had been hiding 101 tests — all of crates/justhttp
and all of the config reader — which compiled but never ran.harbor restart) now names the attached
databases in its redirect, with the command spelled out for the first one,
instead of showing only the file form.autostart a service: the login item is loaded the moment it is
installed, so the server starts now under launchd or systemd, at every
login, and again after a crash (KeepAlive on failure only; systemd
Restart=on-failure). A clean stop stays stopped until the next login.restart, which bounces a database under its login item with a fresh
read of config.toml, and autostart off, which drops the login item and
leaves a running server alone (autostart off stop takes both down).start or stop from removing the login item; only
autostart off and detach do.autostart and on a login item’s restart,
pointing at the [connection.<name>] entry a login item actually reads.runtime/log/.start at a terminal bring the server up in the background and
return — under the database’s login item when it has one, otherwise as a
detached process that runs until stop. The prompt-as-server helm is
gone: a prompt is what bare harbor <db> opens. Headless start still
serves in place until SIGTERM, and --foreground asks for that shape at a
terminal. stop returns once the server has actually gone quiet, so
stop followed by start — by hand or inside restart — always meets a
free database.attach, detach — so an entry
such as [connection.warehouse] pointing at inventory.duckdb never grows
an inventory twin and never boots without its settings.HARBOR_HOME and the XDG home variables into the login item when
the installing shell had them set, so the manager’s server and the CLI
agree on where the socket is.start at a terminal a success when the server is already up, the
way systemctl start treats an active unit; headless it stays a refusal,
so a manager or a spawn never reads a clean exit as a server it did not
get. A running server announces the config key that lists it in /info,
so its bare name is the same word whether it is running or stopped.start.stopped rows in
bare harbor, with the file path and whether a login item will bring them
back. Their names and footnote numbers now resolve everywhere a running
one’s do, so harbor medlabs start, harbor 2 autostart and a bare
harbor medlabs all work with nothing running.harbor.VERSION column with the version reported by each running database
server, making processes that still need a restart immediately visible./catalog through generated and
generationExpression fields.httpfs initialization using Harbor’s boot-SQL support./catalog row counts exact by counting every table in one
ordered UNION ALL query./catalog?style=lite for fast inventory requests without counts,
columns, constraints, indexes, DDL, or sequences./info with the
loopback-only TCP contract./tmp so Unix socket paths fit platform limits./info and packages ICU and JSON with
the DuckDB engine.[connection.*.settings] values for
DuckDB and extension configuration.start, stop, attach, detach, and
autostart lifecycle grammar./info so restarts preserve
how it was started.TIMETZ UTC offsets in ISO output.24:00:00 end-of-day value without wrapping it to
midnight.VARIANT and GEOMETRY,
and structurally encoded tuples.libduckdb only when database work begins and moves Harbor into the
shared monorepo under harbor/.show, start, forget, and doctor
commands with shared discovery and plain-language output./catalog with database sizes, table row statistics, and engine DDL,
plus a lightweight inventory form./catalog index metadata, nested union honesty, map typing, and
FLOAT rendering.~/.config/harbor/config.toml and runtime fleet
state to its protected runtime/ directory.version, -V, and --version commands to Harbor and Pilot.justhttp HTTP/1.1 crate.sudo, keeping
user-space files owned by the invoking user.wire and pilot companion crates./catalog schema document.GET /catalog as one structured call for tables, columns, constraints,
indexes, sequences, views, macros, and attached catalogs./ready, which verifies the
database through the normal query path.NULL decoding on
DuckDB v2./sql server, readiness checks, lifecycle commands,
graceful checkpointing, and keep-alive connections.