Repository navigation
Releases: gizmodata/hardcharger
Release list
python-v2.0.1
Documentation
- Add a total PyPI downloads badge to the package and repository READMEs.
- Use absolute logo image URLs in the repository README.
- This Python package still downloads Hardcharger server 2.0.0 by default.
v2.0.0
The first general release of Hardcharger: a SQL server on DuckDB 2.0.0 that speaks Warp SQL
1.0.1 over plain HTTP, with access control (users, roles, grants, catalog access, scoped
tokens, sign-in through an identity provider or a client certificate), a web UI embedded in
the server (query editor, results, schema browser, uploads, sessions, API page), the
hardcharger command-line client, the Python package, Docker images, builds for Linux, macOS
and Windows, and the app for iPhone and iPad. This section lists what changed since
2.0.0-rc.1; the rc.1 section below has everything since 0.3.0.
Added
-
Docker: multi-architecture
:2.0.0-adbc/:latest-adbcvariants on GHCR and Docker
Hub, with GizmoSQL's ten dbc-managed drivers and the native GizmoSQL, Oracle, Db2 and
Quack drivers, plus the Hardcharger ADBC driver. System-wide manifests, stable library
paths, version/digest inventories and notices are included. The separateADBC images
workflow checks every driver loads and queries Hardcharger by manifest on both native
architectures before publishing the multi-architecture tags. -
Server: a startup banner in a text log: the "Hardcharger" wordmark (plain ASCII), the
server's, Warp SQL's and DuckDB's versions, the web UI's address (or the Warp SQL endpoint's
without--web-ui) and the documentation's, within 80 columns. Never in a JSON log.
--no-banner,--banner=false,HARDCHARGER_NO_BANNER=true,HARDCHARGER_BANNER=falseor
banner: falseleaves it out; every boolean option now takes--no-NAME. The Warp SQL
version is the specification's (info.versionof its OpenAPI document, read at configure
time). Tests:test_startup_log.py(the art byte for byte against
tests/fixtures/banner-art.txt, each way to turn it off, JSON),startup_banner_test.cc. -
Server:
log_emoji(--log-emoji,HARDCHARGER_LOG_EMOJI):auto(the default) marks
text log lines' levels with emoji only when the log goes to a terminal and the locale is
UTF-8 (on Windows, a UTF-8 console code page),always, ornever; JSON lines never have
them. ✅ marks an outcome (ready, stopped),⚠️ a warning, ❌ an error, 💥 the error that stops
the server; info and debug lines get none. Tests at a pty (auto with a UTF-8 locale, with
C,never, JSON) and in a file. -
Client:
--no-tlsconnects without TLS (plain HTTP), as--tls=falsedoes;--tls false
(with a space) works too, as dotrue/false,1/0,yes/noandon/offafter
--tlsand--tls=. Only those words are taken after a space; anything else stays the
positional URI, and the error then names it (--uri cannot be combined with --tls (the argument 'maybe' is read as the URI)).--helpstarts with examples, the TLS-off one among
them. In the shell,.connect HOST PORT USERNAME [PASSWORD]takes--no-tls,--tls,
--tls=BOOLor--tls BOOL(a URI says it itself and refuses them), and.helpand the
"not connected" hint say so. Tests:test_client_cli.py(each spelling against a plain
server, and each spelling of on as its twin). -
Upload data in the web UI: a CSV, TSV, Excel (.xlsx, .xls, .xlsb), Parquet, JSON lines /
JSON array or Arrow IPC file into a new or existing table, from the query toolbar's Upload
data button, the schema browser (its toolbar, and Upload data… on a catalog's or
schema's menu), or a file dropped anywhere on the page. The dialog follows GizmoData Cloud's
SQL editor (the same fields, labels, modes, table-name derivation and result message): File,
Catalog, Schema (or a new one), Table, If the table… (create, which fails if it exists;
append, with the file's columns matched to the table's by name and NULL for the ones it
lacks; replace, the table's columns and rows), Header row, and the CSV delimiter or the Excel
worksheet (a picker of the workbook's sheets). Preview & column types, folded away until
opened, shows the first 100 rows as they will load, with each column's inferred type to
change (VARCHAR, BIGINT, DOUBLE, DECIMAL(p,s), BOOLEAN, DATE, TIMESTAMP, TIMESTAMPTZ, TIME),
rename or skip; a value that wouldn't fit shows in red. An append warns where the file's
types differ from the table's, and stops before it starts when the file has a column the
table hasn't. The file is read and converted to Arrow in the browser, in a Web Worker (a
large or malformed file can't freeze the page), and sent over Warp SQL's bulk ingest in
pieces of about 8 MB, one request each, in one transaction of the upload's own session (as
the tab's Run as role): the server never stores the raw file, a progress bar counts
rows and bytes, Cancel upload rolls everything back, and a failed upload leaves the
table as it was. CSV, JSON lines, Parquet (row group by row group, with parquet-wasm) and
Arrow streams are read as they upload, at any size; Excel workbooks and JSON arrays are read
whole, up to 100 MB. CSV: the delimiter (,;tab|, or one typed, up to 4 characters),
quotes, line breaks inside quotes and the header row are detected; UTF-8, UTF-16 with a byte
order mark, and otherwise Windows-1252 with a note. A whole number with a leading zero
(00123) stays text. Excel: dates and times from the cells' number formats and the
workbook's own date system (1900 or 1904), a formula's saved value (none is calculated),
numbers, booleans, text (a "number stored as text" stays text), error cells as NULL, empty
rows left out and merged ranges' values in their first cell, each said in a note. A value
that doesn't convert fails the upload with its line or cell and column (Cell C12, column "amount": "n/a" is not a whole number); the server's permission errors (CREATE, INSERT,
row policies) are shown as it words them. The result says the rows, mode, format and time,
lists the table's columns, refreshes the schema browser and autocomplete, and Query this
table opens a tab withSELECT * … LIMIT 100. Excel is read with SheetJS Community
Edition 0.20.3 (Apache-2.0), vendored from SheetJS's own CDN at a pinned SHA-256
(ui/vendor/sheetjs.json, checked bynpm run vendor:verify), not npm's abandonedxlsx
0.18.5 (CVE-2023-30533, CVE-2024-22363): it reads .xlsx, .xls and .xlsb without running
macros or formulas and needs no change to the UI's Content-Security-Policy (noeval, no
workers of its own, nothing fetched). It and the converters load only when a file is read,
in the upload worker (SheetJS about 490 KB, the worker 230 KB, the dialog 33 KB; the main
bundle grows by about 3 KB). -
default_cataloganddefault_schema: where every new session starts, unless the client's
owncatalog/schemasession options say otherwise. With neither set, an init SQL that
ends inUSE x.ysets it, as in GizmoSQL 1.x (on rc.1 sessions ignored it and started in
the database's catalog), and the server logs that it did. Both are checked after the init
SQL: a catalog that isn't attached, a missing schema or one of the server's own catalogs
stops the server (exit 1). Under access control a session moves there only if its user's own
USEcould; otherwise it starts where it would have, and a tenant's default catalog wins.
hardcharger_server_info()shows the administrator the effective defaults. -
rbac_store_timeout_seconds(default 10): every access-control store round trip is
bounded. With PostgreSQL paused, aCREATE TABLEwaited 152 s for the store; it now fails
withaccess control store unavailable: timed out after 10 s, and what it was doing is
rolled back once the store answers. PostgreSQL connection strings get libpq's
connect_timeout, TCP keepalives,tcp_user_timeoutand astatement_timeout, each
unless they set it. -
Store outages are visible: a warning when the store stops answering (then every 30 s,
with the snapshot's age), a line when it answers again, and the metrics
hardcharger_rbac_store_upandhardcharger_rbac_snapshot_age_seconds.
rbac_max_snapshot_age_seconds(default 0, no limit) refuses every principal but the
administrator once the store has not answered for longer (fail closed). -
The access-control store belongs to one cluster: it records the first
cluster_idthat
opens it and refuses a server of another cluster (the access control store belongs to cluster ...);rbac_store_shared: trueshares users, roles and grants between clusters
on purpose. The store's schema version is checked too: a store a later version upgraded
is refused. -
The engine, host and process gauges GizmoData Cloud's Engine Metrics page reads from
GizmoSQL 1.x, underhardcharger_*names with 1.x's meanings and units, in/metricsand
hardcharger_metrics():hardcharger_duckdb_threads,hardcharger_duckdb_temp_storage_bytes
(fromduckdb_memory()),hardcharger_duckdb_spill_filesandhardcharger_duckdb_spill_bytes
(live files induckdb_temporary_files()),hardcharger_database_file_bytesand
hardcharger_wal_bytes(the main database file DuckDB opened and its.wal; attached catalogs
don't count),hardcharger_health_check_duration_seconds,
hardcharger_tls_certificate_expiry_seconds(the serving certificate's notAfter as Unix time,
read again whenever a client CRL reload rebuilds the TLS context),
hardcharger_metrics_collection_duration_seconds,hardcharger_host_memory_bytes,
hardcharger_host_cpus,hardcharger_cgroup_memory_limit_bytesand
hardcharger_cgroup_cpu_quota_cores(cgroup v2, else v1, read on every collection so an
in-place pod resize shows), and the standardprocess_start_time_seconds,process_open_fds
andprocess_threads(Linux, macOS and Windows; Windows has noprocess_open_fds). As in 1.x,
a measurement that can't be taken is left out rather than reported as 0: no file sizes for an
in-memory or remote database, no certificate expiry without TLS, no cgroup gauges wi...
python-v2.0.0
Changed
-
The package's version now tracks the server's: 2.0.0 installs Hardcharger 2.0.0 (the Python
package was 0.1.0 while the server was pre-release). Both the runtime__version__and
the default downloaded server are 2.0.0; the[adbc]extra requires the 2.0.0 driver.
PyPI metadata identifies the package as stable, and the download documentation uses
anonymous access with an optional token to raise the API rate limit. -
The server version to download (
SERVER_VERSION,HARDCHARGER_VERSION,ensure_server(version=...))
may be a release candidate in the server's form (2.0.0-rc.1, tagv2.0.0-rc.1) or in
PEP 440's (2.0.0rc1, how this package spells its own candidates); both fetchv2.0.0-rc.1. -
A
python-v*GitHub Release is never made the repository's latest release, which stays the
server's (whatreleases/latestand the installer resolve), and a PEP 440 pre-release
(python-v2.0.0rc1) is marked a pre-release. -
The README's banner is the Hardcharger™ bull logo (the horizontal mascot lockup).
Documentation
- README, License: the package itself has no third-party code; the server it downloads carries
THIRD_PARTY_NOTICES.mdandTHIRD_PARTY_NOTICES-web-ui.txt, which the install keeps beside
bin/(a unit test checks they arrive), and prints them with--licenses.
Fixed
- CI (
.github/workflows/workflow.yml) failed on every push to main: theclientjob installs
the[adbc]extra from PyPI, whereadbc-driver-hardchargerisn't published yet. It runs on
python-v*tags only now, where it still gates the publish.
v2.0.0-rc.1
Added
-
Third-party notices in everything that ships.
THIRD_PARTY_NOTICES.mdlists the 49
third-party components the binaries link statically, each with its version, licence, the
binaries and platforms it is in, and its full licence and NOTICE texts: DuckDB, the 31
libraries and data sets it vendors (Mbed TLS, RE2, Thrift, the Unicode data, jemalloc on
Linux, the TPC-H generator, ...), the httpfs, DuckLake and PostgreSQL extensions, Arrow and
what it bundles (zstd, LZ4, uriparser, xsimd), Boost, OpenSSL, jwt-cpp, yaml-cpp, zlib,
replxx, CRoaring, libpq and libcurl. It is generated
(server/scripts/third-party-notices.py) fromserver/third-party-notices/: each
component's reviewed version and its texts, fetched from the source at that version. CI
(--check, in every build job) fails when a recipe pins another version than the one the
notices were made for, when the build declares a dependency (FetchContent, a DuckDB
extension, an extension library, a vcpkg package) no component covers, or when the file is
stale. The web UI's build writesthird-party-notices.txt(served at
/ui/third-party-notices.txt, linked from About as Open-source licences) from the
packages actually in the bundle, lazy chunks and the editor's worker included (15 today:
React, Monaco, Swagger UI, Apache Arrow, parquet-wasm, ...), with every licence and notice
file each ships (Monaco'sThirdPartyNotices.txt, Swagger UI's NOTICE and its bundle's
swagger-ui-es-bundle.js.LICENSE.txt), and fails when a bundled package declares no
licence, has no licence file, or has a licence outside the allow-list (MIT, ISC, BSD-2/3,
Apache-2.0, 0BSD, BlueOak, CC0, Unlicense, Python-2.0, MPL-2.0; Inter's OFL-1.1 by name).
The tarballs, the Windows zips (whoseTHIRD_PARTY_NOTICES.mdends with the versions and
copyright files of the libraries the Windows build links from vcpkg), the MSIs (installed
under Program Files\Hardcharger) and the container image (/opt/hardcharger/and
/usr/share/doc/hardcharger/) carryTHIRD_PARTY_NOTICES.mdand
THIRD_PARTY_NOTICES-web-ui.txt, andhardcharger-server --licensesand
hardcharger --licensesprint them from the binary itself (the server's followed by its
embedded UI's). Tests:test_server_cli.py,test_client_cli.py, the unit test of the UI
file reader,third-party-notices.test.ts(the allow-list, module-to-package mapping, the
file's contents and its failures),about.spec.ts(Chromium and WebKit), the tarball smoke
test and the image smoke test. -
Web UI, API page (
/ui/api, next to Query and Sessions): the server's Warp SQL API in
Swagger UI (swagger-ui-dist 5.33.3, in the build, loaded only when the page is opened:
+1.7 MB, +435 KiB gzipped in the binary), from the OpenAPI 3.1 document the server now serves
atGET /warp/v1/openapi.json(no sign-in; discovery lists capabilityopenapiand
links.openapi). Every request goes to the server's own origin (no server switcher, no
Authorize dialog); Try it out carries the signed-in user's token, added to each request
to this origin as it leaves and never to another origin, stored, or shown in the Curl box.
Arrow answers are offered as a download (result.arrows); the dark theme applies; a server
without the document gets a note. No change to the Content-Security-Policy. e2e:
api.spec.tsin Chromium and WebKit; unit:apiDocs.test.ts. -
Web UI, the rest of the editor's behavior: How the editor works (the ? in the tab bar):
sessions, with this server's idle timeout (the session document'sidle_timeout_s),
transactions, statements, results, and what stays in this browser; the Saved queries panel
says they're kept in this browser. On a screen 760 px wide or less the side panel sits above
the editor at full width (it was hidden) and the page scrolls, with a note, hidden for good
by its ×, that a larger screen is easier. After a save, the cursor goes back to the editor
(it was left on the page, so the next shortcut went nowhere). e2e:idle.spec.tsagainst a
second server whose sessions end after 2 s idle (a new session takes over and says so; with
a transaction open the run is refused and nothing of it executes; the schema browser carries
on),layout.spec.ts, and every editor shortcut (Run, Run all, Save, close) acting on the
shown tab rather than the tab opened last (tabs.spec.ts). -
Web UI: the README's screenshots show the editor's tabs, a script's result tabs and an open
transaction; tests for a script's 64 MiB cap (unit) and for a browser that blocks IndexedDB
(saved-blocked.spec.ts: the panel says it can't keep saved queries, Save says so, queries
still run). -
Web UI, saved queries, Run as and downloads: saved queries in folders, kept in this
browser (IndexedDB, per user; the page works without it), with Save (Ctrl/⌘+S; the same
query after the first time), Save as…, the unsaved dot, new folder, rename, move by drag,
delete (asking, with counts), import .sql files, a query's .sql or a folder's .zip, opening
a query (or going to its tab), and the whole library exported to a file and imported in
another browser. Run as: under access control a user's tab acts with one of their roles
(SET ROLE) or all of them, the schema browser and autocomplete listing what that role sees;
locked in a transaction; no picker for an administrator or without access control.
Downloads: CSV, TSV, JSON and Excel (.xlsx, written here) of the rows as shown (the grid's
filter and sort), Parquet and Arrow IPC (.arrows) of the whole result with its exact types.
e2esaved.spec.ts,roles.spec.ts,export.spec.ts(Excel, Arrow, as shown); unit tests of
the zip and xlsx writers (a workbook also read back with openpyxl). -
Web UI, the schema browser and autocomplete: Tables and Views groups, column type icons,
double-click or drag a fully qualified, quoted-where-needed name into the editor, a menu
(insert or copy a name, preview data in a new tab on that catalog and schema, insert column
names or a SELECT, a view's definition, use this catalog and schema; not during a
transaction), search of every table and view the user may see (two or more characters, up
to 100), re-listing after a run that changes the catalog, Retry on a failed listing, and a
hideable, resizable side panel (kept in this browser). Autocomplete of catalogs, schemas,
tables, views, columns (through aliases), functions and keywords from the server's
metadata, as access control filters it; nothing inside strings or comments. e2e
explorer.spec.ts,autocomplete.spec.ts(an RBAC user is offered only what they may see),
unit tests of the completion parser and quoting. -
Web UI, the results grid: sort by a column (asc, desc, off; NULLs last, typed: exact
decimals and 64-bit integers, byte-order text), a filter across every column, column
resizing (drag, double-click to fit, arrow keys), the spreadsheet selection with its whole
keyboard (arrows, Home/End, Page Up/Down, Ctrl/⌘ jumps, Shift ranges, drag with edge
scrolling, row numbers, Ctrl/⌘+A, Esc), a copy that pastes into Excel and Google Sheets as
cells (text plus an HTML table: full values, NULL empty, line breaks inside a cell,
00123kept as text; one cell as its bare value; through the copy command, so Safari
copies), a cell inspector (double-click or Enter; nested values and JSON text
pretty-printed with exact numbers; Copy value), Copy of the rows shown, and Expand /
Restore. e2egrid.spec.ts(Chromium and WebKit; with the copy command taken out, its
WebKit copy test fails); unit tests for the selection moves, the clipboard forms, sorting
and the JSON printer. -
Web UI, the SQL editor's tabs, runs and sessions:
up to 20 tabs (Alt+N, Alt+W asking first about an open transaction or unsaved text, rename,
drag to reorder, running/failed/transaction markers), each its own Warp SQL session; Run
runs the statement at the cursor (margin bar) or exactly the selection, and Run all
(⇧+Ctrl/⌘+Enter) every statement, stopping at the first error, with a result tab per result
set and Messages (at most 50 result sets and 64 MiB kept in the browser); Cancel notes how
far a script got and an aborted transaction; In transaction / Transaction failed badges with
Commit and Rollback, from the session document'stransaction; Catalog and Schema pickers
(session options, locked in a transaction, followingUSE); row limits 100 to 1,000,000
(default 1,000) with "truncated at … rows"; the session chip (the server's session id, copied
on click); New session; a session the server lost is replaced with a note, or, with a
transaction open, nothing runs and the message says it was rolled back; leaving the page
with a transaction open asks first; a resizable editor (drag, keys, double-click, kept).
The editor's tab connections no longer replace a lost session silently. e2e (Chromium and
WebKit):tabs.spec.ts,run.spec.ts,session.spec.ts; unit tests for the tab store,
the script runner and the session's loss handling. -
Introspection without instrumentation or metrics (README, Introspection; warp-sql 64eb65c):
hardcharger_active_connections(), a row per open session from the server's own session
tables: session_id (the Warp SQLs_...id), instance_id, cluster_id, username, role,
tenant, auth_method; the latest request's client_address and client_info (User-Agent);
session_tags (MAP(VARCHAR, VARCHAR), the Warp SQLsession_tagoption's keys, then
SET hardcharger.session_tag's); tls, tls_version, certificate_identity,
certificate_thumbprint, token_certificate_bound; state (idle,queued,running,
streaming,idle_in_transaction,idle_in_transaction_aborted) and transaction; the
statement_id and statement (redacted by the statement log's own code), statement_started_at,
queued_since, session_started_...
v0.3.0
A built-in web UI, client certificates as identity (mutual TLS), a DuckDB-file
access-control store for single servers, faster remote transfer (split results and
partitioned uploads by default), Windows builds, and the hardcharger command-line client,
TLS by default. Security fixes from adversarial reviews: metadata follows privileges,
CREATE VIEW needs SELECT on what a view reads, catalog_access holes, and SQL can no
longer reach the server's own files (see Fixed).
Added
-
A refused token says why (Warp SQL §4.2): after
ALTER USER ... WITH PASSWORD,ALTER USER ... DISABLE,DROP USERorDROP SCOPED TOKEN, the session's own token gets401 invalid_tokenwith areason(credentials_changed,principal_dropped,
token_revoked), and an expired tokentoken_expired. Before, only a token from a new
sign-in, used on the old session, learned the reason (fromsession_ended). Only a
token this server signed and that nobody altered gets a reason; one signed with another
key or with a changed signature gets none, whatever user it names. A session opened with
a scoped token now ends when the scope expires (session_ended,token_expired)
instead of refusing each statement.test_token_refusal_reasons.py(8 tests), and the
conformance suite's expired-token case. -
hardcharger: when the server refuses the sign-in's token it says why, from the reason,
e.g. "The server no longer accepts this sign-in: the user's password or credentials
changed (credentials_changed). Sign in again (.connect)."; a session the server ended
reads "the server ended this session: an administrator ended it (killed)". -
hardcharger: HUGEINT and UHUGEINT print as exact integers with
arrow_lossless_conversionon (DuckDB's lossless form, Arrow's opaque 16-byte type), in
every output mode (unquoted numbers in JSON, right-aligned in boxes, type
hugeint/uhugeint); they printed as raw bytes. -
Allowed CORS origins (
cors_origins) getAccess-Control-Allow-Credentials: true, preflights
included (Warp SQL §9.5), so a page on another origin can sign in with the user's client
certificate (credentials: "include"). Other origins still get no CORS headers. The README's
mTLS section says such pages needmtls_require_client_cert: false(a preflight carries no
certificate). From warp-sqlec318ff; CI runs the conformance suite's credentials tests with
WARP_SQL_TEST_SERVER_CORS_FLAG=--cors-origins. -
Client certificates as identity (Warp SQL §4.4): with
mtls_ca_cert, the
client_certificategrant signs a client in with its verified certificate and no
password.mtls_identity_source(san_email, the default;san_uri;subject_cn) says
where the identity comes from; a certificate with none or several values is refused.
mtls_admin_identitiessign in as the administrator; under access control any other
identity must be an enabled store user (CREATE USER "joe@example.com", no password
needed), and grants decide what it may do; without access control nothing else signs in.
The tokens carry RFC 8705'scnf(x5t#S256) and work only on connections presenting the
same certificate.mtls_crl_filerefuses revoked certificates at the handshake, is reloaded
when the file changes (looked at every 5 s), and reaches open connections at their next
request.mtls_require_client_cert(default true) andmtls_require_identity_match
(default false: a password or token sign-in over a certificate must be for its identity).
Certificate sign-ins, allowed and refused, are audit records (kind: authentication,
method: client_certificate, the identity, subject, issuer, serial and thumbprint).
A session opened with a bound token is used only with tokens bound to the same certificate,
and a scoped token (CREATE SCOPED TOKEN) minted over a bound token carries itscnftoo.
hardcharger_sessions()showsclient_certificateas the auth method. Tests:
test_mtls_identity.py(42), and the conformance suite'stest_mtls.py(41). -
From the security review: certificate sign-in is opt-in (
mtls_certificate_sign_in,
default false;mtls_admin_identitiesneeds it), somtls_ca_certalone keeps mTLS a
transport check. CRLs cover every certificate of the chain, and a CA without one is refused
unlessmtls_crl_allow_missing_ca_crls; open connections are re-checked when a CRL passes
its nextUpdate. Identities are printable ASCII. A session takes only tokens bound as its
opener's was, so a scoped token always carries the binding of the request that minted it,
and a bound session can't mintFOR USERanother user. Certificate refusals are security
warnings and audit records (user, token id, thumbprint, peer), and a token is logged as
validated only after its certificate checks. -
Second review: SPIFFE IDs (
spiffe://…, any URI-form name) can be access-control users;
they are always system scope, a tenant admin can't create or drop one, and a role can't be
named like one. Certificate refusals are rate-limited per source by the SDK (summaries carry
suppressed), and nothing else logs them per request; the handshake refusing a client
certificate is an audit record (the TLS handshake refused a client certificate). CRL
files: the newest CRL per CA, a CA's CRL must be signed by it, no delta CRLs (the SDK). -
Third review: handshake refusals are rate-limited by the client's address (IPv6 /64), not
by the unverified certificate, and every client-supplied field of a refusal record is cut
at 512 bytes; a refused password sign-in logs at most 256 bytes of the user name. A failed
CRL reload is retried. A scoped token bound to a certificate can't be exchanged (as user
token's password) over another one. URI identities (SPIFFE IDs) must match a user name
exactly, case included; e-mail and CN identities still ignore ASCII case. CI runs
warpsql-sdk-tests(the limiter's unit tests), under TSan too. -
The
hardchargerclient:--auth-type mtls(orauthType=mtlsin a URI) signs in with
the--mtls-certcertificate, and signs in again with it when the server ends the session. -
The built-in web UI:
web_ui(--web-ui,HARDCHARGER_WEB_UI, off by default) serves a
browser UI at/uion the server's own listener (same port, origin and TLS certificate as
Warp SQL): a SQL editor with tabs and cancel, a results grid that copies in every browser,
a schema browser that follows access control, CSV/TSV/JSON/Parquet exports, the session
list, and sign-in with a password or, under mutual TLS, a client certificate. Its source is
inui/(folded in from the hardcharger-ui repository with its history), styled in
GizmoData's brand with Inter embedded (SIL Open Font License 1.1), with Hardcharger™ and
GizmoData™ marked on the wordmark, the badge and a trademark notice in its footers. CI builds it once per
run and every server build in that run embeds it (-DHARDCHARGER_WEB_UI): CMake checks each
file against the tarball's manifest, embeds it gzipped and logs the content SHA-256. The
server answers gzip to clients that accept it and inflates (zlib 1.3.2) for those that
don't, with an ETag per representation and304forIf-None-Match. Answers follow the
UI's serving contract:index.htmlfor the app's routes under/ui/, hashed assets
immutable, everything else revalidated, 404 (never cached) for missing files, and a strict
Content-Security-Policy and security headers on every answer; nothing at all when off.
A build without the UI refusesweb_ui. The Warp SQL SDK gainedplain_handler, through
which the server answers GET and HEAD outside/warp, with the request's headers. Under
mutual TLS the page needs a client certificate like any client, and offers certificate
sign-in whenmtls_certificate_sign_inis on. Tests:test_web_ui.pyand
web_ui_test.ccon the server; the UI's lint, typecheck, unit tests and Playwright e2e in
Chromium and WebKit against the server built in the same run. Releases attach the UI's
own tarball (hardcharger-ui-<version>.tar.gz) and its content SHA-256. The Windows
builds embed it too. -
hardcharger, the command-line SQL client: GizmoSQL 1.x'sgizmosql_clienton Warp
SQL, with the same options (in Hardcharger's names:HARDCHARGER_HOST,_PORT,_USER,
_PASSWORD,_TLS,_TLS_ROOTS,_OAUTH_PORT,_SESSION_TAG,_QUERY_TAG;
hardcharger://URIs;~/.hardchargerrc,~/.hardcharger_history), dot commands, output
modes, heredoc/pipe/-c/-fscripting and exit codes, the pager, history, completion,
syntax highlighting, query and session tags, TLS and mutual TLS, and the server's
browser sign-in (--auth-type external). Ctrl-C cancels the running statement through
Warp SQL's cancel and the shell goes on. It ships in the tarballs'bin/next to
hardcharger-server, and the image has both onPATH(the entrypoint stays the server).
docs/migrating-from-gizmosql.md maps the names and lists what behaves differently. -
GizmoSQL 1.x's client tests, ported:
test_interactive_client.cpp(115 cases, as C++
against the client's classes),test_client_cli.cpp(18),test_client_config.cpp(5),
the 4 client cases oftest_instance_session_query_tags.cpp, andtest_client_shell.sh
(64 assertions), all against a real hardcharger-server; plus the shell at a terminal
(pexpect), TLS, mutual TLS, browser sign-in and session-recovery cases. -
replxx (release-0.0.4), the client's line editor, built by
scripts/build-deps.shand
cached with DuckDB and Arrow. -
Results split into partitions that a client reads at once over separate connections
(Warp SQL §6.10, capabilityresult_partitions):max_result_partitions(default 32,
1 never splits) caps the split. With Warp SQL's drivers'result_partitionsoption,
one large result uses several network flows and client cores. -
`hardcharger-server --print-options...
v0.2.0
Role-based access control and GizmoSQL 1.x's server features (instrumentation, catalog
logging, catalog access rules, statement queue, metrics, KILL SESSION, the OAuth server,
JWKS tokens and more), checked against 404 of the 423 server-side cases of GizmoSQL's
test battery, plus two security reviews of the result (see Fixed).
Added
- Role-based access control, lifted from GizmoSQL 1.x's RBAC:
CREATE USER/ROLE,
GRANT/REVOKE(roles, table, view, column, schema and catalog privileges,WITH GRANT OPTION,WITH ADMIN OPTION, cascading revokes),
SHOW GRANTS, ownership (ALTER ... OWNER TO), views that read with their owner's
rights, system privileges (READ FILESYSTEM,MANAGE CATALOGS, ...), row and masking
policies, tenants,SET ROLE, identity-provider group mappings, scoped tokens and an
Open Policy Agent service. Every statement is checked inside DuckDB against its own
plan, when it is bound and again before it is optimized; anything the check cannot
classify is refused. Turned on byrbac_store_uri(a PostgreSQL database every replica
shares, reached through DuckDB'spostgresextension);rbac_opa_url,
rbac_opa_timeout_msandtoken_groups_claimtoo. - Store users sign in with their password; their access tokens are checked against the
store on every request, so dropping or disabling a user, changing their password or
revoking a scoped token ends their sessions and tokens at once, on every replica, and a
revoke stops a statement that is running. - Statements that reach outside the database (files and URLs,
ATTACH, extensions,
secrets, instance-wide settings,query(), table functions not on the allow-list) are
refused for store users before DuckDB binds them, unless they hold the system privilege
that opens them. Settings that write files (log_query_path,profiling_output) need
WRITE FILESYSTEM, and settings that hold credentials (http_proxy_password, ...) need
MANAGE CONFIG to read. - A tenant's user is told about another tenant's objects exactly what it is told about
missing ones, in errors, metadata,DESCRIBE,CREATE VIEWand session options. - Access-control audit records form a tamper-evident hash chain (HMAC-SHA256 with
audit_chain_key, SHA-256 without);hardcharger --verify-audit-log FILEchecks a JSON log. - DuckDB is built with a small binder patch (
server/scripts/patch-duckdb.cmake) that
lets access control see which part of a plan came from which view. - Tests for access control (
server/tests/test_rbac_*.py, 262), including two replicas on
one store and a gate that access control costs no concurrency. They need PostgreSQL. - SQL functions for the session's context:
HARDCHARGER_USER(),_ROLE(),_VERSION(),
_CURRENT_SESSION()(a UUID),_CURRENT_INSTANCE(),_CURRENT_CLUSTER(),
_INSTRUMENTATION_ENABLED(),_INSTRUMENTATION_CATALOG(),_INSTRUMENTATION_SCHEMA();
table functionshardcharger_settings(),hardcharger_sessions()and
hardcharger_metrics(). - Server settings in SQL:
SET [SESSION|GLOBAL] hardcharger.<name>andRESET, with
1.x's scopes:query_timeout,query_log_level,graceful_shutdown,
shutdown_grace_period_seconds, the statement queue's limits,bypass_queue, the
session and query tags; startup settings say where to change them.SET GLOBALneeds
an admin. KILL SESSION '<uuid>': ends another session at once,
frees itsmax_sessionsplace, and its next request getssession_ended.max_sessions(admins are never capped;too_many_sessions),session_idle_timeout.- The admin command guard (servers without access control): users without the admin role can't ATTACH/DETACH, INSTALL or
LOAD, SET GLOBAL (or instance-wide settings), CHECKPOINT, COPY to or from local files,
read local files (read_csv('/x'),FROM 'x.csv'), EXPORT/IMPORT DATABASE, CONNECT,
manage secrets, or usequack_*(); only literal remote URLs pass where a path decides. - The sensitive path guard (
block_sensitive_paths,sensitive_paths): no statement may
name credential or system files (~/.ssh,/etc/shadow, the server's TLS key, ...), however spelled;secret_directorycan't change after startup. And
allow_unredacted_secretscan't be turned on (block_unredacted_secrets); SQL with a
NUL byte is refused. _hardcharger_system, a read-only catalog withhardcharger_index_infoand
hardcharger_view_definition.- The statement queue:
max_concurrent_statements,max_queued_statements,
max_queue_wait,admin_bypass_queue_default. - Metrics:
enable_metrics,GET /metricsonmetrics_port(hardcharger_*metrics),
hardcharger_metrics(), the unclean-exit marker. - The health check runs
health_check_queryin the background every
health_check_interval_seconds; a probe answers from the last outcome, which goes stale
afterhealth_check_staleness_seconds. - Graceful shutdown:
graceful_shutdown(off: the first signal stops at once) and
shutdown_grace_period_seconds(0: wait forever), both adjustable live with
SET GLOBAL; a second signal stops at once. token_authorized_emails: which users a sign-in token may name.auth_log_level,query_log_level,session_log_level,log_file; a bearer token's
first use logs at INFO, its repeats at DEBUG; JSON lines carryinstance_idand
cluster_id.cluster_id,max_header_bytes(what 1.x's--max-metadata-sizecapped),
result_batch_rows(default 2048, as 1.x streamed).- Instrumentation:
enable_instrumentationrecords instances, sessions, statements and
executions (with their queue wait, fetch timeline and status) in_hardcharger_instr,
with 1.x's tables, views and columns (the version column ishardcharger_version), in a DuckDB
file next to the database (instrumentation_db_path) or in a DuckLake or PostgreSQL
catalog the init SQL attaches (instrumentation_catalog,instrumentation_schema). A
killed server's open rows are closed at the next start. Queued statements show asqueuedwhile they wait. instance_tag,SET hardcharger.session_tagandSET hardcharger.query_tag, recorded with
the instance, the session and each statement;capture_query_profile(off,
standard,detailed; alsoSET [GLOBAL]) stores DuckDB's JSON query profile with
each execution.- Catalog logging:
enable_catalog_loggingalso writes every log line to
_hardcharger_logs.logs(1.x's columns), in its own file
(log_catalog_db_path) or a DuckLake or PostgreSQL catalog (log_catalog,
log_schema), in batches from a background writer. - The instrumentation and log catalogs are the server's: admins may read them, nobody may
write, detach orUSEthem, and non-admins don't see them in metadata. - Identity provider sign-in: ID tokens verified with the
provider's JWKS (token_jwks_uri, or the onetoken_allowed_issuer's OpenID
configuration names), RS256/384/512 and ES256/384/512, throughtoken_exchangeor as
the password of usertoken. - Browser sign-in:
oauth_client_idruns GizmoSQL 1.x's OAuth server onoauth_port
(default 31339) with its routes (/oauth/initiate,
/oauth/start,/oauth/callback,/oauth/token/<uuid>), as a confidential client
(oauth_client_secret,oauth_scopes,oauth_base_url,oauth_redirect_uri,
oauth_instance_idfor a shared callback proxy,oauth_disable_tlsbehind a TLS proxy). --verify-tls URL: fetches a URL with the system's CA bundle (SSL_CERT_FILE, else the
first well-known bundle), as outbound HTTPS does, and exits 0 or 1.- The GizmoSQL 1.x test battery, ported (
server/tests, scoreboard in
server/tests/BATTERY.md), with C++ unit tests (server/unit, googletest 1.18.0), on
the one build that ships. - Per-catalog permissions: a sign-in token's
catalog_accessrules (read,write,noneper catalog name or glob, first match
wins, no match is no access) are enforced on every statement's plan by access control,
without a store: reads and writes per catalog (temp included), and what the user sees
in SHOW DATABASES,information_schema,duckdb_*(), the_hardcharger_systemviews and
the metadata requests; refusals read as in GizmoSQL 1.x ("Access denied: You do not have
write access to catalog ..."). Under RBAC the rules grant nothing. - The instrumentation and log catalogs are now also left out of SQL's own catalog
functions (duckdb_databases(),information_schema, SHOW ...) for non-administrators. - Instrumentation writes to a shared DuckLake catalog that lose a commit race to another
server are tried again (a few times, backing off) instead of being dropped. PIVOTwithout anINlist runs (it expands into several statements), also for
read-only principals.- Access control and the battery's features work together: store users never see or
write the instrumentation and log catalogs (admins read them, nobody writes them), may
call the server's SQL functions (hardcharger_settings(), ...), and are refused files,
URLs, ATTACH and the like by access control's system privileges rather than by the
admin command guard, which stays for servers without access control. Access-control
statements are counted, logged and recorded like other statements
(test_rbac_battery.py).
Changed
- The server's executable is
hardcharger-server(the Docker image's entrypoint too); the
plainhardchargername is kept for a command-line client. - No license: every feature that was Enterprise in GizmoSQL 1.x is available to everyone,
each turned on by its own option. The license options (license_key,
license_key_file) and their environment variables are gone: a license flag is an
unknown option like any other. There is one build, and it has everything: no
test-license build, no Enterprise/Core build switch, no edition (EDITION(), the
editionsetting, server-info and metrics label, and the instances table's...
v0.1.2
0.1.1 was tagged but never released: its release gate failed on a race in deleting a
running write (fixed here). 0.1.2 carries everything listed for it.
Fixed
- A use-after-free: prepared statements kept parameter types that DuckDB had lent
them, so closing a prepared statement or ending its session could crash the server or
corrupt its heap (found by the JDBC driver's tests). Bulk ingest freed borrowed column
types the same way. Both are fixed, and CI now runs the server tests and the
conformance suite on an AddressSanitizer build that gates every release. - Deleting a statement before the engine had said whether it writes could cancel a
write; such a delete is now settled once it does (a write finishes, a read stops). - A long poll on a finished statement answered only after its whole wait; it answers at
once. - A streamed query sent its Arrow schema only with its first batch, so an aggregate's
response stayed silent until the query ended and proxies' idle timeouts could cut it;
the schema now goes first. - A client that hung up while a query's stream waited for its next batch went unnoticed
until the query ended; the query is cancelled within a second. A statement that
changes data (INSERT … RETURNING) is never undone because its reader left or deleted
it: it runs to the end. - Finished statements stayed in their session forever; they are forgotten 10 minutes
after they finish (once any retained result has expired), or beyond 1,000 a session. - Prepared statements describe their result (
result_schema,result_kind) without
running; executing a query for zero parameter rows returns no rows instead of a count. - Table schemas from metadata report nullability, precision and scale; the session
document follows SQLUSE; a missing or existing table is SQLSTATE 42P01 / 42P07.
Added
POST /warp/v1/sessions/{id}/cancelstops whatever the session is running.- Primary and imported keys of every table in one metadata request.
v0.1.0
Added
- The Hardcharger server (
server/): DuckDB 2.0 behind the Warp SQL protocol, built on
the Warp SQL server SDK (third_party/warp-sql). It passes the Warp SQL conformance
suite. - Configuration from flags, environment variables (
HARDCHARGER_*, plus GizmoSQL 1.x's
names, so GizmoData Cloud's settings work unchanged) and a YAML file
(--config/HARDCHARGER_CONFIG), in that order of precedence; unknown keys are refused. - Access tokens as JWTs signed with
secret_key(HS256): valid across restarts and, with
allow_cross_instance_tokens, across replicas. GizmoSQL 1.x-issued server tokens are
accepted. - Sign-in with RS256 tokens from a trusted issuer (
token_exchange, or as the password of
usertokenas in 1.x), with issuer and audience checks and theadmin,userand
readonlyroles;catalog_accessneeds thecatalog_permissionslicense feature. - GizmoData Enterprise license keys, verified exactly as GizmoSQL 1.x does; a bad key
stops the server. - Init SQL (commands, then file; any failure stops the server), read-only databases,
memory limit, storage version, unsigned extensions, a plaintext health port, a default
statement timeout, statement logging, text or JSON logs, TLS and mTLS, graceful drain. - Tests (
server/tests, 27) for all of the above. - Release artifacts:
hardcharger-linux-amd64,-linux-arm64and-macos-arm64
tarballs (server pluslibduckdb), and the imageghcr.io/gizmodata/hardcharger. - A TPC-H SF10 benchmark (
bench/tpch.py, results indocs/benchmarks.md): 1.44× faster
than GizmoSQL 1.41 over the 22 queries, and 1.6× faster at moving 10M rows.
Known gaps
- The
readonlyrole allows only SELECT and EXPLAIN until the authorization module
brings DuckDB's own read-only analysis. - Not yet ported from GizmoSQL 1.x: RBAC, instrumentation, metrics, the statement queue,
KILL SESSION, OAuth sign-in, the admin and sensitive-path guards.
v0.0.1
Added
- First release, holding the
hardchargername on PyPI. No functionality yet.