Repository navigation
v0.22.0
[0.22.0] - 2026-10-05
Changed
- SQL spill is on by default when
ravel-serverruns with--cache-dir
(ADR-0954 amendment, issue #2416). With no spill environment set, an
eligible query that outgrows its memory pool now spills under
<cache-dir>/sql-spill/<instance-id>, with a ceiling of half of what is
left of the free bytes measured at startup on that volume once the read
cache's disk-tier bound is subtracted, capped at four times the process
memory budget and raised to 1 GiB when that cap is below 1 GiB. When that
half is below 1 GiB, spill stays off, and both startup lines give the
sourcecache-dir-insufficient-space. The
ceiling, derived or configured, now bounds
all of the process's queries together: each qualifying query reserves its
spill limit from it, and one that cannot reserve 64 MiB (or the whole of a
smaller ceiling) runs with spill off.RAVEL_SQL_SPILL_DIRwith
RAVEL_SQL_SPILL_MAX_BYTESstill wins, andRAVEL_SQL_SPILL_MAX_BYTESset
alone replaces the derived ceiling. The new--sql-spill offflag disables
spill whatever the environment or--cache-dirsays. Startup logs two more
performance default resolvedlines,sql_spill_dirand
sql_spill_max_bytes, each with its source. The startup of a process that
serves SQL first sweeps orphaned spill directories under
<cache-dir>/sql-spill, whenever
--cache-diris set,--sql-spillis notoffand that directory exists:
it removes each directory whose owner lock it can take and leaves and logs
any whose owner still holds it. The sweep runs before the free space is
measured, so it also runs when spill then resolves to the
RAVEL_SQL_SPILL_DIRpair's directory or off for lack of space. A process
whose spill resolves under--cache-dirthen takes an exclusive lock on
its own spill directory before serving. With spill resolving under
--cache-dir, startup now refuses to start when it cannot create
<cache-dir>/sql-spill, measure its free space, or lock its own
directory, and a half-set spill environment names the missing variable.
The lock only proves ownership on one host, so--cache-dirmust be a
local volume unless--sql-spill offis set: the sweep runs wherever
spill resolves. - An
ORDER BYover a spill-eligible aggregation can now spill (ADR-0954
amendment, issue #2416). Previously anySortkept a query from spilling.
Whenever a statement's plan has aSortdirectly over an aggregation of
onlyCOUNT,SUMandAVGwith no floatGROUP BYcolumn (or over a
projection directly over it), its sort key gets the aggregation's
GROUP BYcolumns appended as trailing tiebreak terms, ascending with
nulls last, whatever the spill setting and whatever the types of the
aggregated columns. The order is then total, and a
spill-eligible statement returns the same rows in the same order with
spill forced, with spill off, and in memory, including one whoseAVG
reads a 32-bit integer column. A group column the projection renames is
matched under its new name, one it drops is carried through to the sort
and removed again, and a select-list alias that only shares a group
column's name is not taken for it. ASortover any other aggregate, over
a float group column, over grouping sets, over aHAVINGfilter, or over
nested projections is left as written and still keeps the query from
spilling. - A logs statement that stripes a segment across SQL partitions now decodes
the segment's directories once and deals whole row groups (ADR-2414 decision
A1, issue #2417). The plan phase decodes STREAM_DIR, FIELD_DIR, SKIP_IDX and
PAGE_DIR once per relevant segment and every partition's open reuses them, so
the per-phasedecompressed_bytesof such a statement moves from the scan
phase to the plan phase: the plan phase carries the four sections' decoded
length and the scan phase only the projected block pages. A partition reads
only the pages of the row groups it owns, and the chunk-run reads of
partitions sharing one object no longer overlap, so the block bytes they move
for it sum to at most its BLOCKS section. A partition sharing an object never
reads it whole: its coverage crossover weighs its runs against its own span
and collapses them to that span, split only where another partition's pages
lie inside it. Request counts change with it: on
the 8-segment selective-scan fixture the q37 and q20 shapes issue 48 GETs
where they issued 56, and a segment whose carried plan result was dropped to
bound memory is reopened with 4 chunk-run GETs where it was reopened with 1
block-data GET. The carried directories are reserved against the fetch memory
budget, for segments that have a surviving block, from the plan phase until
every partition that owns one of the segment's row groups has finished it.
The plan phase reserves all of them before the scan starts, so the peak is
the decoded directories of every segment with a surviving block, and a
statement can now fail with the fetch memory error where it did not before.
An open whose own pruning disagrees with the plan's survivor list fails with a
survivor-mismatch error instead of reading a subset of what the plan counted.
A statement that late-materializes rows through the whole-segment fast path
fails with the same error, rather than returning another block's rows, when
that path's open has pruned a block the catalog's bounds placed inside the
window ahead of a block it stamps. - The whole-segment fast path pipelines a partition's ranged opens (issue
#2418). While a partition drains a segment it opened ranged, it opens its
next owned segments ahead of their turn, up to max(2, store GET concurrency
/ SQL partition count) segments at once, and emits them in owned order, so a
partition's ranged reads overlap instead of costing its round trips one
after another. Rows and accounting are unchanged while the fetch memory
budget refuses none of those opens. When it refuses a partition's current
open, the statement's pipeline turns off, every partition's unconsumed
prefetches are dropped (their segments open again at their turns, in owned
order), and the refused open is retried once, at once, without waiting for
any other partition. Within one statement a current open is reported
refused only if the budget refused it twice, the second time after every
unconsumed prefetch of every partition had been dropped; across statements
the budget stays fail-fast, and that refusal is typed. The scan counts the
retries inprefetch_memory_reopensand the dropped prefetches in
prefetch_revocations; rows are unchanged, and the requests the dropped
opens already made are counted on top. - The default fetch policy reads narrow projections of large log objects
ranged on the intra-region profile (issue #2418). The store cost profile
gains two optional measured timings,request_latency_microsand
per_connection_throughput_bytes_per_s(set together or not at all), and
cost-basedtakes the larger of its price term and their product. The
reference profile records 70 ms and 90 MB/s, so its request cost is 6,300,000
bytes instead of saturating, and a projection is read ranged only when it
skips more than the projection break-even, the larger of the routing threshold
and three request costs (18,900,000 bytes by default, issue #2555: a ranged
read of a narrow projection was measured at about four GETs per object, so it
pays when the bytes it skips exceed three request costs); every object at or
below that is still read whole, a 3 MB flush object included, and with no tail
probe on the planned route (a statement the whole-segment fast path refuses)
and on the log-series route's stream-directory read. An explicit
--logs-block-range-thresholdis no longer overridden on that profile. The
logs fetch policy resolvedstartup line gainsrate_term,
projection_break_even_bytes(the break-even in force, the routing threshold
when the policy derives none) andbreak_even_source(profileor
routing-threshold); on the reference profile it reads
projection_break_even_bytes=18900000 break_even_source="profile". An
explicit--logs-request-cost-bytes, the other fetch policies, the coverage
crossover and the coalescing gap are unchanged. Above the break-even the
planned route still probes an object before its coverage crossover reads it
whole for a wide projection, since it does not weigh the projected fraction;
that is left to a follow-up. ravel-server's process memory budget now starts from available memory,
not raw total (ADR-1170, amended 2026-10-03 by issue #2367, issue #2483).
With no cgroup memory limit and a readableMemAvailablein
/proc/meminfo,memory_budget_bytesresolves tomin(MemTotal - RESERVE, max(FLOOR, MemAvailable + own RSS - RESERVE)), logged with
source="derived-available".FLOORis 1 GiB and floors only the
MemAvailable + own RSS - RESERVEterm; when it binds, aWARNnames the
MemAvailablereading and--memory-budget-bytesas the remedy. The outer
minkeeps the budget at or belowMemTotal - RESERVE, so a host whose
MemTotalis at or below the reserve still derives 0 and is refused at
startup, as before. A cgroup memory limit keeps the pre-amendment rule
(source="derived-cgroup",MemAvailablenot consulted), including when
/proc/meminfocannot be read, since the limit is then the memory figure.
An unreadableMemAvailableon a host whoseMemTotalparses keeps the
MemTotal-minus-reserve rule (source="derived"). With neither a readable
MemTotalnor a cgroup limit (every non-Linux build, for example) no budget
is derived:source="fallback", budget unlimited, as before. The new
--memory-budget-bytesflag overrides every derivation except gateway mode,
which claims no budget, and is still subject to the existing startup refusal.
A derived (not explicit-flag)--sql-max-query-bytesor
--sql-tenant-max-bytesis now capped at 90% of the shared SQL/fetch
remainder, logged per pool asremainder_capped; an explicit flag on either
is never capped. The cap binds on every stock deployment whose store is on
loopback (its larger fetch-cache share leaves a remainder whose 90% is below
50% ofMemTotal), and on an S3 deployment whose effective memory is below
about 9.7 GiB, cgroup pods included: those deployments resolve smaller SQL
pools than before. A host with co-resident processes now also derives a
smaller budget, and smaller caches with it.- An SQL result the JSON encoding cannot hold is a client error naming the
column (issue #2514). A timestamp or duration past the i64 nanosecond
range, a date outside years 0000 to 9999, aDate64that is not a whole
day, a time of day outside one day, or a column type with no JSON encoding
now fails
/api/v1/sqlwith 422executionand the messagecolumn "<name>" of type <type> cannot be encoded as JSON: <reason>; request the Arrow IPC format to read it exactly. The message never carries the value. These used to answer
422executionwith the fixedinternal query engine errormessage, which
named neither the column nor the remedy; the reference documented them as
500internal, which the server never returned. The same query sent with
Accept: application/vnd.apache.arrow.streamreads every such column
exactly. - A wide-window resolve now lists its shards concurrently on the prefix path
(ADR-2509 decision 1, issue #2538). Up toresolve_get_concurrencyshards
list at once instead of one after another, so the prefix scan's serial LIST
depth falls from the sum of the shards' page counts toward the largest one.
Every page reserves a slot in the resolve-widemax_catalog_list_requests
cap before it is issued, so the cap stays exact: a window over it is still
refused withWindowTooWide(estimatethe cap plus one,limitthe cap)
on every run, though which shard reaches the cap, and how many LISTs were
issued before the refusal, can now differ between runs. The resolved
snapshot is unchanged. - A query against a tenant that has never folded no longer GETs the catalog
HEAD on every statement (ADR-2509 decision 3, issue #2538). The resolve
caches a HEADNotFoundforhead_cache_ttl(default 30 s), so such a
tenant costs one HEAD GET per TTL and a HEAD the first fold publishes is
picked up within that TTL. Other HEAD GET errors are not cached. A cached
absence counts againsthead_cache_capacitybut is not cached when the
cache is full of unexpired entries, and it never replaces or evicts a
present HEAD; a HEAD inserted into a full cache evicts the oldest absence
first. Folds read HEAD from the store and are unaffected. - A query audit record that finds the pipeline idle is written at once
(ADR-2509 decision 2, ADR-0062 amendment, issue #2539). The query-audit
pipeline used to hold a record for up to--audit-max-age(25 ms by default)
waiting for others to batch with, so sequential queries paid the full wait on
every response. A record is now written without waiting when nothing else is
queued, it was not submitted while a write was in progress, and the previous
record arrived at least--audit-max-ageearlier. Steady traffic arriving
more often than once per--audit-max-agebatches as before, and the pipeline
writes at most twice as many objects as before on any arrival pattern. Every
query still waits for its audit record to be durable before its response is
released, and a failed write still fails the query in--audit-mode required. - Compaction skips a log object whose
stream_attrsblob the writer refuses
(issue #2554). The writer refuses such a blob as of this release (issue #2548,
under Fixed), so a bucket holding an object an earlier version wrote with one
would fail every compaction and every maintenance tick of its shard.
Compaction instead leaves that object out of the merge, counts it once per
process on
ravel_maintain_compaction_inputs_skipped_total{reason="unwritable_stream_attrs"}
and warns once with its key. When at leastmin_compaction_inputsinputs
remain it merges them under a compaction record that does not name the object;
when fewer remain it publishes nothing and reports the bucket below the
minimum with the count of inputs left. The object stays in storage. The same
process does not read it again, and a later evaluation of a bucket left below
the minimum by its skipped objects returns before taking the bucket's claim.
Format migration still fails on such an object.
Fixed
- CI lints the workspace with every feature on at once (issue #1925). A new
all-featuresjob runscargo clippy --workspace --all-targets --all-features, so a feature-gated call site that only the union of features
reaches no longer compiles nowhere;scripts/gates.shdoes not run it,
because on a 4-core executor it is a second near-full compile (859 s cold)
beside the default-feature workspace clippy (about 710 s). - A Parquet table's footer read refuses a store that ignores
Range
(issue #2287). On a store without suffix reads,CREATE EXTERNAL TABLE
places its first footer read from the listed size and discards that read's
response when the listing was stale. The discarded response was never
length-checked, so an endpoint answering with the whole object held it
against a reservation sized for the requested range. A first footer read
that returns more bytes than its range now refuses the statement as a
corrupt file, naming it, before any retry. - A
CREATE EXTERNAL TABLEcut off by its deadline no longer leaks its
bucket-probe object (issue #2287). When the statement deadline expired
while the not-Ravel's-bucket probe was in flight, the probe was dropped
before it deleted the object it had written under
sys/pq-probe/in Ravel's own bucket, and that object stayed until a bucket
lifecycle rule removed it. The statement now deletes it from a best-effort
background task as it is dropped. A probe write that is reported as failed
after the object landed is deleted by the probe itself before it returns
(issue #2575, below). The background delete is not counted in the statement's
reported store cost. - A large S3 put under
--store-schedulingkeeps no more requests in
flight than the permits it holds (issue #2327). Aput()above the 16 MiB
multipart threshold took one scheduler permit and then uploaded up to four
parts at once below the scheduler. With upload integrity off, the class
handle now takes one permit plus as many free ones of its class as the
upload can use, without waiting for any, and the upload keeps at most that
many parts in flight. A class with no spare permit uploads the parts one at
a time. With upload integrity on (the--s3-upload-integritydefault), every
put is a single PUT and takes one permit. - Per-role credentials start a fresh AWS S3 bucket without a shared-credential
first boot (issue #2332). AWS answers a read of an absent key with 403
unless the caller may list that key, so the gateway, query and maintain
templates indeploy/iam/now grant a list on exactly the bootstrap keys each
process reads where absence is normal (such assys/tenancy,sys/gcand a
tenant's config record), and on nothing else. ravel-cliMaintain-credential data writes can go out under the tenant's
KMS key (issue #2363).maintain compact-bucket,compact-tenant,
migrateandcatalog foldtake ravel-server's--tenant-kms-configflag
and, when it is given, route their data writes through the tenant's key the
way the servers do; without the flag they still write under the bucket's
default encryption. A command never records a tenant's key: when the
tenant's key-epoch record is absent, or its current key differs from the
file's, it refuses before any write and tells the operator to start
ravel-server with the file first, since only ravel-server startup records a
configured or changed key. A--dry-runreads the record and refuses the
same way. The key-epoch record itself,maintain sweep's unnamed-since
markers,load's writes and the Admin control records stay under the bucket
default, and thecommit reconstructaccess-denied error now says so for
the Admin records.- The SQL endpoint's JSON output encodes the column types a Parquet table
produces (issue #2390). AnInt8,Int16,UInt16,Date32,Date64,
second, millisecond or microsecondTimestamp,Decimal128,LargeBinary
orBinaryViewcolumn used to fail a JSON response with 422execution
and the fixedinternal query engine errormessage; Arrow IPC was
unaffected. Integers are JSON numbers, every timestamp is an
integer count of nanoseconds since the epoch asTimestamp(ns)already was,
dates areYYYY-MM-DDstrings, a decimal is a string of its exact decimal
text, and binary values are lowercase hex. A timestamp with no i64
nanosecond count, a date outside years 0000 to 9999, or aDate64that is
not a whole day fails the response with 422executionnaming the column
and its type (issue #2514) rather than returning a wrong value. - A forged Parquet table manifest version above 2^32, or a manifest key
that names no version, no longer wedges a table (issue #2430). Manifest
versions are bounded at 2^32: DDL refuses to write a version above it with a
422. A version above the bound, and a.pqmkey under a table'sv/prefix
whose slot is not a version number (the wrong length, an extra path segment
such as.../v/q/v/<20 digits>.pqm, too large for au64, zero, or not
digits), is skipped by
every reader, counted and logged as a warning naming the table, excluded
fromravel-cli parquet sweep's pairing, and flagged by
ravel-cli parquet repair --tenant <t> --table <name>, whose--delete,
run under the Maintain credential, deletes exactly the flagged keys. A
forged version at or below the bound still blocks DDL, or serves as the
table, until an operator removes it withparquet repair --delete-version N
after checking the DDL audit log for a statement that wrote it. Narrowing
the Query role's manifest grant remains the root fix. ravel_server::startrefuses a flush cadence that leaves the flush
deferral cap at 0, not only the command line (issue #2438). A
ServerConfigbuilt in code with such a cadence, or with an idle delay past
theFLUSH_BOUND_SLACK_HOURSslack, now fails startup with the typed
FlushCadenceErrorand the same messageCli::validategives, instead of
starting and then refusing every write from the first deferred flush trigger.EXTRACT(field FROM expr)now plans, asdate_part. Previously it
failed to plan on every SQL surface (logs, spans, samples and Parquet
tables) withThis feature is not implemented: Extract not supported by ExprPlanner, which included ClickBench Q19's exact text.- A malformed columnar log batch is refused with a clear error instead of
corrupting or crashing (issue #2460).ColumnarLogBatch::validatenow runs
first inRlogWriter::push_columnarandLogIngestRouter::write_columnarand
refuses: a per-row field whose length is notnum_rows; a trace or span id
buffer that does not match its present rows; mismatched or repeated stream ids
and a stream reference past the end of the batch; a dynamic column whose
validity or cell count disagrees with the batch, or one of whose cells has a
type other than the column's; a dictionary list of the wrong length, with the
wrong number of ids, or with an id past its distinct values; and a dictionary
entry that differs from itsStrcell or non-List/MapBytescell. A
repeated(name, type)dynamic column, more than 4 GiB of text in one column,
and dictionary contents forList/Mapcells are not checked. - Keyed log and span dedup now finds its markers on S3, and a failed lookup
fails the keyed write instead of writing a duplicate (issue #2462). The
lookup listed a prefix ending mid-segment, which the S3 adapter extends with
/, so on S3 it never found a marker and a retry of an acknowledged keyed
write was stored twice. It now GETs the exact marker key for each hour of the
dedup window: the hour ahead, the current hour and the one before it together
first, so a hit there costs 3 GETs and about one round trip, then the other 23
hours at most 8 at a time (26 GETs on a miss at the default window). A probe
that fails with a store error other than not-found at a newer hour than any
marker found, or a lookup still running at half of the request's
ack_deadline, now fails the write with a retryable 503 /UNAVAILABLE
before its data is written, counted on
ravel_ingest_idempotency_lookup_failures_total; previously the gateway
logged a warning and wrote anyway. The lookup and the write share that one
ack_deadline, and the lookup may use only half of it, so it can no longer
leave the write without a budget: a strict write left none could time out
after its records were enqueued, flush durably with no marker, and be stored
again by the retry. Writes without a key do no lookup and keep the whole
ack_deadline. Every marker GET the lookup issues, hit or miss, is counted on
the newravel_ingest_idempotency_probe_gets_total, by signal. The gateway
template indeploy/iam/lists no idempotency prefix; it grants a list of
exactly the marker key so that on AWS S3 a probe of an absent marker answers
404, not 403. Apply the updated gateway policy before rolling out this version
on AWS: under the previous policy an absent marker answers 403, so every keyed
log and span write is refused with 503 until the policy is updated. - PromQL queries over many series per segment no longer spend time
quadratic in the series count finding each series' pages. - A high-cardinality
GROUP BYwith an exact integerAVGno longer holds
roughly 1.7x-2.6x the peak memory of the same query withCOUNT(*).
(Measured across 8 and 32 partitions, 1.5M groups, five--releaseruns each
on the same host before and after this fix.) The exact integerAVG
accumulator could not convert input rows to partial state, so
skip_partial_aggregationnever applied to it and the emitted partial state
sat in the exchange outside the SQL memory reservation. It now converts rows
to state: for aGROUP BYwhose cardinality crosses the configured
skip_partial_aggregationthreshold, the partialAggregateExec's
skipped_aggregation_rowsmetric is nonzero and results are unchanged, the
same behaviorCOUNTandSUMalready had (issue #2488, follow-up of #2367). - A large S3 whole-object read under
--store-schedulingkeeps no more
requests in flight than the permits it holds (issue #2493). A
GetRange::Fullread above the per-request body bound took one scheduler
permit and then fetched up to four ranges at once below the scheduler. The
read still takes one permit; once its first response shows ranges remain, it
takes free permits of its class until it holds one per range left to fetch, at
most four, without waiting for any, and keeps no more ranged requests in
flight than the permits it holds. A class with no spare permit fetches the
ranges one at a time. A read that fits in one response, and a ranged or suffix
read, holds one permit. - The SQL endpoint's JSON output encodes Parquet's remaining decimal,
Float16, time-of-day and duration columns (issue #2496). A
Decimal32,Decimal64orDecimal256(a Parquet DECIMAL whose embedded
Arrow schema names that width, or one of precision 39 or more),Float16,
Time32,Time64orDurationcolumn used to fail a JSON response with
422executionand the fixedinternal query engine errormessage. Every
decimal width is a string of its exact decimal
text asDecimal128already was, aFloat16follows the float rules
including the"NaN","+Inf"and"-Inf"strings, a time of any unit is
an integer count of nanoseconds since midnight, and a duration of any unit
is a signed integer count of nanoseconds. A time that is negative or a
whole day or more, or a duration with no i64 nanosecond count, fails the
response with 422executionnaming the column and its type (issue #2514)
rather than returning a wrong value. - The SQL endpoint's JSON output encodes interval, list and dictionary
columns of every layout (issue #2512). AnIntervalcolumn of any unit, a
LargeList,FixedSizeList,ListVieworLargeListViewcolumn, and a
Dictionarykeyed by any integer type other thanInt32used to fail a
JSON response with an error; Arrow IPC was unaffected. An interval is now an
object with all three integer fieldsmonths,daysandnanoseconds,
every list layout is a JSON array encoded likeList, and a dictionary cell
is its value. - PromQL over logs excludes records erased through a resource or scope
attribute (issue #2541).ravel_log_linesandravel_log_bytesnow match
pending erasure predicates against the merged resource, scope and record
view, as the SQLlogsscan does, so a predicate on an attribute such as
host.namedrops the matching records instead of counting them. The match
decodes each record'sstream_attrsblob, so a blob that does not decode
fails the query with a typed error rather than being counted or skipped. - The RLOG writer refuses a
stream_attrsblob the reader cannot decode
(issue #2548). A log flush now fails with a segment build error, instead of
writing a segment whose every merged-view read fails, when a resource or
scope attribute nests past the decoder's depth cap of 32 (which counts map
and value levels alike, about 15 nested maps, and now bounds lists too) or
when a scope name or version is not UTF-8. OTLP admission rejects an
attribute nested past that rule before it reaches the writer (the next
entry), so a sender cannot trigger this refusal with a deep attribute. - A log attribute the segment format cannot hold is rejected at OTLP
admission (issues #2554, #2553). A resource or scope attribute 16 to 99
kvlists deep or 32 to 99 arrays deep, or holding an array or kvlist of more
than 2^20 entries (for example 2^20 + 1 empty arrays, which the value-length
limit counts as zero bytes), used to be admitted and written into a segment
whose every merged-view read then failed. It is now rejected with
AttributeTooDeeplyNested(at most 15 kvlists around a value, an array level
costing half a kvlist level) or the newAttributeTooManyEntries, the rule
the RLOGstream_attrsdecoder applies, exported as
ravel_logseg::attr_value_fits_storage. A record attribute that fails it is
dropped on its own and its record kept. This narrows what is stored for record
attributes: one nested 16 to 32 kvlists deep, or 32 to 99 arrays deep, used to
be written and read back intact and is now dropped with
AttributeTooDeeplyNested. The RLOG writer holdsattrs_rawoverflow values
to the bounds of theattrs_rawdecoder, not to this stricter rule, so values
stored under the older bound stay readable and compaction and erasure rewrites
can write them again. Theattrs_rawdecoder's one new depth limit refuses a
list or map nested inside 128 lists and maps instead of overflowing the stack;
that is above the 100 levels OTLP ever admitted. A list or map value stored in
a dynamicBytescolumn is not checked by the writer and is read back as
bytes. ravel-cli tenant parquet-grant addno longer leaks its bucket-probe
object when the probe write is reported as failed (issue #2575). The
cleanup now lives inprobe_not_ravel_bucket, which deletes the object
before it returns on that path too, so every caller gets it, not only
CREATE EXTERNAL TABLE. The same function's drop guard also covers an
in-process cancellation, such as the statement deadline of
CREATE EXTERNAL TABLE. A process killed mid-probe still leaves the
object behind; the lifecycle rule onsys/pq-probe/bounds that.
Added
- A runnable ClickBench Parquet lane (issue #2055).
clickbench_parquet_bench
(inravel-bench,sql-latencyfeature) runs the 43 upstream statements
through a runningravel-serverover the upstream Parquet files, compares
every answer with datafusion-cli 54.1.0's, writes a report stamped with the
server's resolved settings, and exits 1 when the report misses the bar
pre-registered inbenchmarks/clickbench/parquet/prereg.toml. It reads the
bearer token from the environment variable--token-envnames, never from
the command line. With--concurrency-seconds, the report's concurrency
block carries queries per second over the measured window and two error
ratios, all errors and errors outside the registered failures, and the bar
is judged on the second; a concurrency phase that fails once started is
recorded in the report, which is still written, and the bench exits 1.
benchmarks/clickbench/parquet/make-reference.shwrites
the datafusion-cli reference and refuses any other datafusion-cli version.
The runbook is the "ClickBench Parquet lane" section of
docs/internal/clickbench.md. - DDL statements report their object-store cost (issue #2374).
CREATE EXTERNAL TABLEandDROP TABLEoverPOST /api/v1/sqlnow count
on/metricsin three families:ravel_sql_ddl_statements_totalby
kindandoutcome,ravel_sql_ddl_store_requests_totalbyphase
(grant,probe,snapshot,write) andop, and
ravel_sql_ddl_store_bytes_totalbyphase, each withtenant_hashfolded
like the per-query cost family. A failed statement reports the cost it
accrued before it failed. DDL cost stays out of theravel_query_*family
and the usage record. The families and the byte definition are in the
observability guide. /metricsnow exposesravel_cache_resident_entriesand
ravel_cache_resident_bytesforcache="catalog". Previously only the
fetch-family read cache reported live residency; the catalog byte cache's RAM
and disk tiers (ADR-0046) were tracked internally but never rendered.
What's Changed
- docs(runbook): expect all 43 reference statements to measure by @pmoust in #2455
- docs: add ADR-2425 ordinal maps use binary search by @pmoust in #2439
- refactor(segment): resolve exemplar series by binary search by @pmoust in #2449
- refactor(cli): resolve columnar stream refs by binary search by @pmoust in #2450
- refactor(logseg): resolve stream refs by binary search by @pmoust in #2454
- feat(bench): run the ClickBench Parquet suite through Ravel end to end by @pmoust in #2459
- test(sql): pin dedup positions under a pending erasure mask by @pmoust in #2464
- fix(iam): let each role see its absent bootstrap keys as absent by @pmoust in #2463
- fix(sql): plan EXTRACT(field FROM expr) as date_part by @pmoust in #2461
- feat(sql): decode directories once per segment on the striped logs route by @pmoust in #2471
- docs(adr): derive the ADR-1170 budget from available memory by @pmoust in #2474
- fix(server): refuse a zero deferral cap in the library entry too by @pmoust in #2470
- test(maintain): pin that a forced orphan pass still reclaims by @pmoust in #2481
- docs: correct ADR-2425's memory peaks, which a formula bug inflated by @pmoust in #2486
- fix(logseg): refuse a malformed columnar batch before anything reads it by @pmoust in #2487
- feat(bench): add the ClickBench Parquet bench binary, report and runbook by @pmoust in #2478
- docs: correct four stale claims about migrate, cursors and the gateway by @pmoust in #2490
- fix(object-store): bound a scheduled large put to its permits by @pmoust in #2494
- fix(sql): encode Parquet column types in SQL JSON output by @pmoust in #2495
- feat(server): derive the memory budget from available memory by @pmoust in #2492
- ci: build the workspace with every feature on by @pmoust in #2501
- feat(server): report DDL statement store cost on /metrics by @pmoust in #2503
- fix(object-store): bound a scheduled whole-object GET to its permits by @pmoust in #2507
- test(query): restore the log and span slice differentials by @pmoust in #2508
- fix(sql): JSON-encode Decimal256, Float16 and time-of-day columns by @pmoust in #2506
- fix(cli): route maintain data writes through the tenant KMS key by @pmoust in #2500
- fix(sql): let exact AVG skip partial aggregation; export catalog residency by @pmoust in #2513
- fix(parquet): length-check the discarded footer read and close #2287 nits by @pmoust in #2515
- test(bench): let cargo build ingest_bench for the stage breakdown test by @pmoust in #2531
- ci: free runner disk in the background on lanes that pull no image by @pmoust in #2527
- ci: run the quickstart build for server-only changes on main only by @pmoust in #2535
- ci: guard that published binaries build on the optimised profile by @pmoust in #2534
- fix(query): look up a series run's page ranges without scanning the plan by @pmoust in #2491
- docs(query): say where log erasure and slice decoding really run by @pmoust in #2533
- ci: run the feature lanes as a matrix, with tests under nextest by @pmoust in #2529
- docs: add ADR-2509 per-statement floor by @pmoust in #2537
- fix(bench): tighten the ClickBench Parquet comparator and close #2287 nits by @pmoust in #2516
- docs(adr): record that the quickstart job can veto a publish by @pmoust in #2542
- feat(sql): spill under --cache-dir by default and admit a tie-ordered Sort by @pmoust in #2497
- ci: split the workspace test run across four runners by @pmoust in #2530
- feat(query): time term in the cost-based rate and a pipelined ranged fast path by @pmoust in #2546
- fix(query): apply the merged-view erasure rule to log-derived series by @pmoust in #2550
- ci: build the sql, flight-sql and feature lanes on the ci profile by @pmoust in #2544
- docs(query): say the SQL surfaces read the pending erasure predicates by @pmoust in #2551
- fix(pqtable): bound manifest versions and add a repair command by @pmoust in #2511
- ci: give the jobs that moved to the ci profile new cache keys by @pmoust in #2559
- fix(logseg): validate stream_attrs blobs with the reader's decoder by @pmoust in #2557
- perf(maintain): flush an idle audit event without waiting max_age by @pmoust in #2560
- refactor(query): drop BudgetRefused's unread key and message by @pmoust in #2566
- fix(sql): encode interval, list and dictionary columns; name unencodable ones by @pmoust in #2568
- docs: add ADR-2467 row input encodes through the columnar builder by @pmoust in #2562
- test(sql): link most ravel-sql integration tests into one binary by @pmoust in #2571
- perf(catalog): list prefix-path shards concurrently, cache a missing HEAD by @pmoust in #2567
- test(sql): give grouped sum the NaN carve-out avg already has by @pmoust in #2572
- fix(sql): delete the bucket probe object a cut-off CREATE leaves behind by @pmoust in #2576
- test(server): prove the statement floor mechanisms reach SQL HTTP by @pmoust in #2582
- fix(ingest): probe idempotency markers by key and fail a refused lookup by @pmoust in #2482
- fix(maintain): skip a log object compaction cannot rewrite by @pmoust in #2579
- ci: run the workspace tests with optimised dependencies by @pmoust in #2552
- test(sql): pin the expression planner surface and its refused syntax by @pmoust in #2584
- docs(server): name the DDL footer reads under the fetch memory component by @pmoust in #2588
- fix(logseg): one attribute storage rule, checked at OTLP admission by @pmoust in #2586
- fix(server): give /mcp its own message for an unencodable SQL result by @pmoust in #2590
- feat(query): cost-based projection break-even at three request costs by @pmoust in #2578
- docs: say what a workspace gate run compiles of the SQL surface by @pmoust in #2573
- fix(object-store): give the Ravel-bucket probe its own drop guard by @pmoust in #2591
- chore(release): prepare 0.22.0 by @pmoust in #2598
Full Changelog: v0.21.0...v0.22.0
Downloads
The attached binaries were extracted unmodified from the signed container
images this release published, so each one is byte-identical to the file inside
the image you can pull. They are glibc-dynamic, built against Debian 12
(the runtime image is gcr.io/distroless/cc-debian12), so they need glibc 2.36
or newer. On an older distribution, run the container image instead:
docker pull ghcr.io/nofireai/ravel-server:0.22.0One binary per platform, named <binary>-<os>-<arch>:
ravel-server-linux-amd64,ravel-server-linux-arm64ravel-cli-linux-amd64,ravel-cli-linux-arm64ravel-operator-linux-amd64,ravel-operator-linux-arm64ravel-ingest-router-linux-amd64,ravel-ingest-router-linux-arm64
Verifying the downloads
SHA256SUMS covers every attached binary and symbol archive. Check the files
you downloaded, then verify SHA256SUMS itself against the signature and
certificate beside it. The signature is keyless, bound to this release
workflow's identity, so there is no key to obtain:
sha256sum --check --ignore-missing SHA256SUMS
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/NOFireAI/ravel/.github/workflows/publish-images.yml@refs/tags/v0.22.0' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
SHA256SUMSDebug symbols
The shipped binaries are stripped of debug info but keep their symbol table,
so a backtrace resolves function names with nothing extra installed. Line
numbers come from the matching symbols archive. Each binary records the
basename of its symbol file, so unpacking the archive next to the binary is
enough and no symbol-file step is needed:
tar -xzf ravel-debug-symbols-linux-amd64.tar.gz
gdb ./ravel-server-linux-amd64gdb looks for ravel-server.debug in the same directory (and in
.debug/), which is the name inside the archive. Do not rename the extracted
.debug files.