Skip to content

Releases

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 12 Nov 17:07
Immutable release. Only release title and notes can be modified.
v0.2.0
691c391

[0.2.0] - 2026-11-12

Upgrade

  • When allowed_write_paths is non-empty, spill_dir and shuffle_dir must 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 return Error::Parse. Long IN lists still bind. (#223)
  • Session::register_parquet refuses a prune_row_group hook (Error::Plan). Pass filters instead. Registration reads footers only; a file that changes after registration fails the next scan with Error::Io. (#224)

Added

  • Tagged releases publish mizuchi-tenkai.toml: the product template with the image pinned by digest and a profile v2 block that provides mizuchi.query at 1.0.0. (#232)
  • Release tags are verified as signed and annotated. The GitHub Release includes a CycloneDX SBOM and a family release manifest. -rc.N tags 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 AVG spills 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

v0.2.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 12 Nov 16:25
Immutable release. Only release title and notes can be modified.
v0.2.0-rc.1
209d25e

[0.2.0-rc.1] - 2026-11-12

Upgrade

  • When allowed_write_paths is non-empty, spill_dir and shuffle_dir must 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 return Error::Parse. Long IN lists still bind. (#223)
  • Session::register_parquet refuses a prune_row_group hook (Error::Plan). Pass filters instead. Registration reads footers only; a file that changes after registration fails the next scan with Error::Io. (#224)

Added

  • Tagged releases publish mizuchi-tenkai.toml: the product template with the image pinned by digest and a profile v2 block that provides mizuchi.query at 1.0.0. (#232)
  • Release tags are verified as signed and annotated. The GitHub Release includes a CycloneDX SBOM and a family release manifest. -rc.N tags 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 AVG spills 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

Choose a tag to compare

@github-actions github-actions released this 02 Nov 20:24
v0.1.0
b24dc23

[0.1.0] - 2026-10-30

Fixed

  • tpch_q1 and 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_q1 is about 28 ms from 148 and tpcds_store_sales about 37 ms from 43. docs/bench/results.md is re-timed at SF1 and SF10. tpch_q3 at SF10 remains below the 3x target (#168).
  • tpch_q3 uses the extra workers again (#165). Its unique-key join fold with one float SUM ran 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 vectorised take instead 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_q3 is 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_q3 is back to about 120 ms from 172, tpcds_store_sales to about 51 ms from 74, and tpch_q6 to 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_q3 and tpcds_store_sales got 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.sh compares 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 Scalar keys 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) reuses hash_join_streamed when 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 made tpcds_store_sales hundreds of times slower with workers > 1. scripts/bench-gb.sh and docs/bench/baseline.json time the suite with workers > 1; scripts/check-bench-regression.sh rejects 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_sql always uses the morsel scheduler (ADR 0015), including the default of one worker. Pipelines split at join build, aggregate, and sort. try_parallel is 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 ANALYZE still times the pull operator tree.
  • The library is a layered workspace (ADR 0013): mizuchi-core, mizuchi-plan, mizuchi-io, mizuchi-postgres, mizuchi-exec, and the mizuchi facade. Embedder crate-root re-exports are unchanged. parse_sql no longer re-exports sqlparser::ast::Statement. Query API JSON Schema lives in crates/mizuchi-exec/schemas/query-api-v1.json. Flight stays behind --features flight in mizuchi-flight.

Added

  • Tagged v* releases publish a GitHub Release with crate packages, wheels, checksums, and build provenance, and push ghcr.io/sannrox/mizuchi:<version> (never latest). deploy/tenkai.toml is the Tenkai product entry. crates.io and PyPI stay unpublished while sekai-keys is a git pin (#146).

  • A standby coordinator takes over through a fenced lease (#161, ADR 0016). LeaseStore has 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 with Error::Fenced and does not lead again under a new token. RemoteCluster::with_leadership fences a cluster and leader_status reports leadership, lease age and token; mizuchi coordinator --lease-dir waits as a standby. The compose and Kubernetes layouts run a standby coordinator and mount the shared lease directory on every coordinator and worker. serve_worker and spawn_worker take 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.governed refuses execute without a PolicyContext. The context names tenant, principal, and actor, lists allowed tables, injects row filters, and strips masked columns from the planning catalog so SELECT *, joins, and aggregates cannot name them. Tables bound with Session::bind_table_tenant reject 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-supplied oct keys, with no network fetch. PolicyContext can 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 profile on live SF1/SF10 is unchanged here (no bench-data/ in this change).

  • A per-query memory budget covers join, aggregate, and sort. Session::with_system_memory_budget caps 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. QueryResult reports peak_bytes and spilled_bytes. EXPLAIN ANALYZE prints query peak_bytes= and spilled_bytes=. Session::new stays 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 ANALYZE prints runtime_filter=[...] and the scan's rows= 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 until SessionConfig.query_api is set. /healthz lists the contract id; /v1/queries lists, filters, searches, and paginates completed queries; /v1/queries/{id} returns plan, read/write report, and per-operator metrics; /v1/queries/running and POST .../cancel cover 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 require allow_network; non-loopback binds do. Optional bearer token is supplied by the embedder. QueryApiConfig.ui_dir serves an unpacked mizuchi-console zip at /ui/ (ADR 0014); default is off.

  • Flight coordinator and worker process...

Read more