Repository navigation
Releases
v0.2.0
[0.2.0] - 2026-11-12
Upgrade
- When
allowed_write_pathsis non-empty,spill_dirandshuffle_dirmust sit under those grants. Add the spill directory to the write list (the governed-run example does). (#223) - SQL expressions nested deeper than 128 levels (
mizuchi::sql::MAX_EXPR_DEPTH) now returnError::Parse. LongINlists still bind. (#223) Session::register_parquetrefuses aprune_row_grouphook (Error::Plan). Passfiltersinstead. Registration reads footers only; a file that changes after registration fails the next scan withError::Io. (#224)
Added
- Tagged releases publish
mizuchi-tenkai.toml: the product template with the image pinned by digest and a profile v2 block that providesmizuchi.queryat1.0.0. (#232) - Release tags are verified as signed and annotated. The GitHub Release includes a CycloneDX SBOM and a family release manifest.
-rc.Ntags publish as pre-releases and are never latest. (#208, #214) - Constrained single-worker bench profile (
--constrained, 256 MiB cap) with published SF1/SF10 ratios. (#225)
Changed
- Grouped aggregates charge the group table, not the whole input, so few-group queries stay under a memory cap. (#228)
- Small integer, boolean, or short-string group domains may use the perfect-hash aggregate under a memory limit when the index fits 1/16 of the cap. (#231)
- Parquet scans with several conjuncts evaluate them as one predicate. (#230)
Fixed
- Grouped
AVGspills under a memory cap instead of failing closed.COUNT(DISTINCT)still fails closed. (#221) - Registered Parquet tables stream within the session cap instead of decoding at registration. (#224)
- Deep expression chains, spill/shuffle paths outside write grants, and
../ symlink path escapes fail closed. (#223)
v0.2.0-rc.1
[0.2.0-rc.1] - 2026-11-12
Upgrade
- When
allowed_write_pathsis non-empty,spill_dirandshuffle_dirmust sit under those grants. Add the spill directory to the write list (the governed-run example does). (#223) - SQL expressions nested deeper than 128 levels (
mizuchi::sql::MAX_EXPR_DEPTH) now returnError::Parse. LongINlists still bind. (#223) Session::register_parquetrefuses aprune_row_grouphook (Error::Plan). Passfiltersinstead. Registration reads footers only; a file that changes after registration fails the next scan withError::Io. (#224)
Added
- Tagged releases publish
mizuchi-tenkai.toml: the product template with the image pinned by digest and a profile v2 block that providesmizuchi.queryat1.0.0. (#232) - Release tags are verified as signed and annotated. The GitHub Release includes a CycloneDX SBOM and a family release manifest.
-rc.Ntags publish as pre-releases and are never latest. (#208, #214) - Constrained single-worker bench profile (
--constrained, 256 MiB cap) with published SF1/SF10 ratios. (#225)
Changed
- Grouped aggregates charge the group table, not the whole input, so few-group queries stay under a memory cap. (#228)
- Small integer, boolean, or short-string group domains may use the perfect-hash aggregate under a memory limit when the index fits 1/16 of the cap. (#231)
- Parquet scans with several conjuncts evaluate them as one predicate. (#230)
Fixed
- Grouped
AVGspills under a memory cap instead of failing closed.COUNT(DISTINCT)still fails closed. (#221) - Registered Parquet tables stream within the session cap instead of decoding at registration. (#224)
- Deep expression chains, spill/shuffle paths outside write grants, and
../ symlink path escapes fail closed. (#223)
v0.1.0
[0.1.0] - 2026-10-30
Fixed
tpch_q1and the other few-group aggregates no longer update each aggregate row by row (#160). Perfect-hash keys are encoded one column at a time. COUNT and float SUM/AVG without nulls fold a whole batch in one pass into per-group locals, and the domain check reads string offsets instead of every cell. On the hash path, a single string key is dictionary-encoded per batch, so groups resolve once per distinct value. Join output builds its take index and unmatched-row mask once per batch rather than once per column. At SF1 with 10 workers,tpch_q1is about 28 ms from 148 andtpcds_store_salesabout 37 ms from 43.docs/bench/results.mdis re-timed at SF1 and SF10.tpch_q3at SF10 remains below the 3x target (#168).tpch_q3uses the extra workers again (#165). Its unique-key join fold with one floatSUMran on one thread; workers now fold contiguous runs of probe morsels into their own per-build-row sums and merge them in worker order, and the result is built with vectorisedtakeinstead of per-row scalars. The fused join-then-aggregate path also collects a scan-pipeline probe side on all workers, since it collects that side anyway. At SF1,tpch_q3is about 2.4× faster at 4 workers than at one.- Runtime join-key filters and scan pruning no longer slow down single-worker queries (#164). The per-morsel key-range check read every key row through a generic per-row accessor, twice for a range and once per value of an IN-list; it now takes one vectorised min/max per morsel and key. A runtime range filter only skips morsels it rules out and no longer filters rows, so the probe side is not copied. At SF1 one worker,
tpch_q3is back to about 120 ms from 172,tpcds_store_salesto about 51 ms from 74, andtpch_q6to about 33 ms from 86. - Join queries no longer slow down as workers increase (#159). A streamed hash-join probe spawned one thread per worker for every 4,096-row morsel and copied each thread's rows, so
tpch_q3andtpcds_store_salesgot slower at 2, 4 and 8 workers. The probe now gathers about 16,384 rows per worker and spreads whole morsels across workers without copying, and build sides under 131,072 rows stay one table with their dense key index. A morsel much larger than one worker's share is sliced zero-copy so it still spreads across workers.scripts/check-worker-scaling.shcompares a 1-worker and an N-worker run and fails any query that is more than 25% slower with more workers; the bench workflow runs it at SF1 with 4 workers. - Grouped aggregation uses a perfect hash when integer, boolean, or single-byte string key ranges fit a dense index, so those groups do not allocate
Scalarkeys or probe a hash table. Wider domains radix-partition by hash bits, flush small thread-local tables, and merge partitions in parallel. - Unbounded inner joins scatter build morsels into hash partitions on worker threads, finalise each partition without an insert lock, and probe left morsels as they arrive so the probe side is not collected first.
- Memory-limited and spilled inner joins hash-partition both sides and join each partition once, so a hash table is not rebuilt for every left×right chunk pair.
- Unbounded inner join plus aggregate (
run_then_agg) reuseshash_join_streamedwhen unique-key fold does not apply: one build table, then parallel probe morsels. After morsels stayed unconcatenated, the batch-pair fallback rebuilt a hash table per left×right pair and madetpcds_store_saleshundreds of times slower with workers > 1.scripts/bench-gb.shanddocs/bench/baseline.jsontime the suite with workers > 1;scripts/check-bench-regression.shrejects a 1-worker run. - Query API bearer comparison hashes both tokens and compares the digests in constant time, so response timing does not leak a prefix of the configured token.
Changed
execute_sqlalways uses the morsel scheduler (ADR 0015), including the default of one worker. Pipelines split at join build, aggregate, and sort.try_parallelis gone. Fused inner-join-plus-aggregate stays. The SQL logic tests and the tiny decision-support suite run at 1 and 2 workers with identical results.EXPLAIN ANALYZEstill times the pull operator tree.- The library is a layered workspace (ADR 0013):
mizuchi-core,mizuchi-plan,mizuchi-io,mizuchi-postgres,mizuchi-exec, and themizuchifacade. Embedder crate-root re-exports are unchanged.parse_sqlno longer re-exportssqlparser::ast::Statement. Query API JSON Schema lives incrates/mizuchi-exec/schemas/query-api-v1.json. Flight stays behind--features flightinmizuchi-flight.
Added
-
Tagged
v*releases publish a GitHub Release with crate packages, wheels, checksums, and build provenance, and pushghcr.io/sannrox/mizuchi:<version>(neverlatest).deploy/tenkai.tomlis the Tenkai product entry. crates.io and PyPI stay unpublished whilesekai-keysis a git pin (#146). -
A standby coordinator takes over through a fenced lease (#161, ADR 0016).
LeaseStorehas an in-memory backend and a shared-directory backend (DirLeaseStore). Every takeover raises the fencing token, every Flight assignment carries it (x-mizuchi-fencing-token), and workers refuse a lower token and re-check it before publishing a result, registration or shuffle write; workers that mount the lease directory (--lease-dir) also check the directory's current token, so a restart cannot admit a superseded leader's work. A coordinator that lost the lease stops its query withError::Fencedand does not lead again under a new token.RemoteCluster::with_leadershipfences a cluster andleader_statusreports leadership, lease age and token;mizuchi coordinator --lease-dirwaits as a standby. The compose and Kubernetes layouts run a standby coordinator and mount the shared lease directory on every coordinator and worker.serve_workerandspawn_workertake the lease directory as a new argument. -
Table files and query spills encrypt with caller-supplied keys (
EncryptionSession+sekai-keys). Parquet modular encryption wraps per-file DEKs in footer metadata; Delta and Iceberg record key ids only. A revoked unwrap grant fails the read. Spill files use an ephemeral per-query DEK (MZS1) dropped with the pool. Encrypted writes refuse Hive partition paths. Session object-store Delta and Iceberg methods honor the same keys. Schema evolution keeps existing lake encryption identifiers. Unkeyed writes to an encrypted lake table fail closed, as do keyed appends onto an unencrypted table. Appends must use the table's tenant and KEK. Encrypted sessions refuse disk shuffle, streaming queries, and JSON/Avro/ORC writes. Keyed Parquet reads refuse plaintext files. Spill readers refuse unauthenticated frame lengths above 64 MiB before allocating. Large spill batches split into frames under that cap, encoded one frame at a time. Flight shuffle IPC is unchanged. -
Query policy context and HS256 JWT/JWKS verification, off by default.
SessionConfig.governedrefuses execute without aPolicyContext. The context names tenant, principal, and actor, lists allowed tables, injects row filters, and strips masked columns from the planning catalog soSELECT *, joins, and aggregates cannot name them. Tables bound withSession::bind_table_tenantreject a different tenant. Query reports record principal and actor. The query API accepts a Bearer JWT next to the static token (QueryApiConfig.jwt); JWKS is embedder-suppliedoctkeys, with no network fetch.PolicyContextcan attach opaque table and column marking labels; the planner unions them through projections, expressions, joins, aggregates, window functions, filters, CTEs and subqueries. Results and write reports list output columns with those labels. In governed mode a write whose destination labels do not cover the output is refused. Mizuchi does not interpret labels. -
Hash tables pack Utf8 grouping and join keys of at most 16 bytes into a fixed-width cell, including null and empty keys, and unpack only when emitting. Dictionary-encoded Utf8 stays dictionary through filter; a comparison against a Utf8 literal does not decode the stored column.
gb_bench profileon live SF1/SF10 is unchanged here (nobench-data/in this change). -
A per-query memory budget covers join, aggregate, and sort.
Session::with_system_memory_budgetcaps at one quarter of machine memory and spills under the process temp directory. Hash join already spills per partition; aggregate merge reloads one spill file at a time; sort spills runs.QueryResultreportspeak_bytesandspilled_bytes.EXPLAIN ANALYZEprintsquery peak_bytes=andspilled_bytes=.Session::newstays unbounded so embedders keep policy. -
After an inner hash-join build, integer equality keys become a min/max or small IN-list filter on the probe-side scan.
EXPLAIN ANALYZEprintsruntime_filter=[...]and the scan'srows=count is the rows that survive the filter. In-memory scans skip morsels that cannot match. Parquet footer prune treats IN-list as OR. The filter is optional:Session::with_runtime_join_filters(false)is still correct. -
Query history and a versioned HTTP metrics API (
mizuchi.query-api/v1, ADR 0012). Off untilSessionConfig.query_apiis set./healthzlists the contract id;/v1/querieslists, filters, searches, and paginates completed queries;/v1/queries/{id}returns plan, read/write report, and per-operator metrics;/v1/queries/runningandPOST .../cancelcover in-flight work. Schema:crates/mizuchi-exec/schemas/query-api-v1.json. History persists to a caller-provided JSONL file and survives restart. Loopback binds do not requireallow_network; non-loopback binds do. Optional bearer token is supplied by the embedder.QueryApiConfig.ui_dirserves an unpacked mizuchi-console zip at/ui/(ADR 0014); default is off. -
Flight coordinator and worker process...