Repository navigation
First public release, and the baseline that later releases' upgrade, rollback and
compatibility checks run against. WeftDB is pre-beta: the API will change, and the
version number says so. This release ships as pre-built binaries on the GitHub release
only; the library crates are not published to crates.io (build them from the v0.1.0
tag).
The Changed, Fixed and Security entries are for anyone who built WeftDB from source
before this release. A bare "0.1" in them means splimes 0.1.
Known limitations
- Pre-beta. The HTTP API, the Rust API, the configuration and the on-disk format can
change in any 0.x release. Nothing is covered by a stability promise before 1.0. - No authentication, authorization or TLS.
weft-serverserves an unauthenticated,
plain-HTTP API; see
SECURITY.md.
Do not expose it to an untrusted network: keep the default loopback bind, or put it
behind something that terminates TLS and authenticates callers. - Crash consistency is incomplete. A
2xxfrom a storage ingest survives a crash of
theweft-serverprocess, but not necessarily a power loss or an operating-system
crash: the index commit is fsynced, the segment frame it points at is not.
Maintenance (reconcile, split, squash, compaction) still rewrites committed frames in
place, so a crash during a rewrite can tear one. And two seals into one aspect at the
same moment can be given the same segment id, so that one replaces the other.
docs/design/crash-consistency.md
lists the 43 windows it verified. This release closes one of them, the deleted MVCC
log (see Fixed). The durability work after it closes or mitigates the rest, though a
legacy (rows-mode) database keeps some of them by design. - The next release changes the store layout. It moves a store to layout v2, in
place, and the upgrade is one-way: once a newer release has written to a store, do not
run v0.1.0 on it again. Before upgrading, stopweft-serverand copy the whole store
directory;POST /api/v1/storage/backupcopies only the control plane, not the
segment frames. - The Linux binaries need glibc 2.34 or newer, and
weft-tuineeds 2.39 or newer.
They are built on Ubuntu 24.04. Ubuntu 22.04, Debian 12 and RHEL 9 runweft-server
andweft-benchbut notweft-tui; on an older glibc, build from source. - The macOS and Windows binaries are not signed. Gatekeeper and SmartScreen warn
before running them. Check each archive againstSHA256SUMSinstead. - The bit-sliced codec is off in the release binaries. None of them is built with
bitsliced-codec, so they never write the bit-sliced value codec
(WEFT_SEGMENT_TRANSPOSED_MAX_OVERHEADis ignored, with a warning) and cannot read a
segment written with it; for a store that has such segments, buildweft-serverfrom
source with--features bitsliced-codec.experimental-codecsis compiled into
weft-benchonly, for its advisory size estimates; nothing writes those codecs to
disk.
Added
- Interpolation as a first-class query. Ask for any resolution and get back a
continuous series, reconstructed with the spline method you choose (linear,
quadratic, cubic, polynomial), computed on SIMD/parallel CPU or GPU (wgpu). - Provenance labelling. Every returned point is marked
raw,interpolatedor
extrapolated, so a synthetic value is never silently mistaken for an observed one. - Declared precision. Values are logically
BigDecimal. Each aspect declares a
physical encoding (F64,F32,ScaledI64,ScaledI128,Decimal128,
BigDecimalText) and an error bound; a value the encoding cannot represent within
that bound is rejected rather than quietly rounded. - Typed columnar segment store (
.weftseg) on the measurement hot path, with
bit-packing and delta-of-delta timestamp coding (an opt-in bit-sliced value codec sits
behind thebitsliced-codecfeature; see Changed), over a Turso (libSQL) control
plane for catalog, metadata and the segment index. - Downsampling with mergeable partial reductions (
.weftpartsidecars), including
time-weighted averages, over an epoch-aligned bucket grid. - Apache Arrow interchange — sealed segments to
RecordBatchand back, plus Arrow
IPC and Parquet bytes. - HTTP API (
weft-server) covering health/readiness, ingest, query, interpolation,
storage management, backup/restore and a restore drill, with OpenTelemetry tracing. - Terminal UI (
weft-tui) for exploring and administering an instance. - Weft-Bench (
weft-bench), a reproducible, correctness-gated benchmark harness. - Analytics pipeline (
weft-orchestration) chaining batching, pattern extraction,
event detection, correlation and signal generation. - Pre-built
weft-server,weft-tuiandweft-bencharchives for Linux (x86_64 and
aarch64), macOS (x86_64 and arm64) and Windows (x86_64), with aSHA256SUMSfile,
attached to the GitHub release. - Interpolation grids requested over HTTP are capped at 10,000,000 points per
request by default (weft_server::MAX_INTERPOLATE_OUTPUT_POINTS), on every
/api/v1/interpolate*endpoint; setWEFT_MAX_INTERPOLATE_POINTSto change it. The
variable is read once at startup, and a value that is not a positive integer stops
weft-serverfrom starting. A larger grid is a400that names its size and the
limit, refused before anything is allocated; before, a fine resolution over a long
range tried to allocate all of it. weft-servercalibrates the interpolation backends at startup. Once the listener
is bound, it runssplimes::calibrate()once in the background on a blocking thread:
that starts the GPU if there is one, times the single-thread, rayon and GPU backends,
and sets whereBackend::Autoswitches between them. It takes several seconds and
never delays serving or fails startup; until it finishes, requests interpolate on the
CPU with splimes' default thresholds. The adapter (or why there is none) and the
thresholds are logged. A CPU/software adapter (llvmpipe, lavapipe, WARP) is not
calibrated unlessWEFT_GPU_CALIBRATE=force;WEFT_GPU_CALIBRATE=0skips calibration.
Without it, splimes 1.0 never uses the GPU.- Weft-Bench calibrates like
weft-server. An interpolation run (line protocol or
--synthetic) callssplimes::calibrate()once before anything is timed, skipping a
CPU/software adapter as the server does (it has no counterpart to
WEFT_GPU_CALIBRATE=force), so the numbers come from the engine the server runs on
that machine;--no-gpu-calibrateturns it off. The calibration, the GPU and
Backend::Auto's thresholds are printed and recorded in the report's
metadata.engine(bench schema v16) and its HTML view. - Third-party attribution. A root
NOTICE, and aTHIRD-PARTY-NOTICESfile shipped in
theweft-physical-typeandweft-reducepackages, credit the two Apache-2.0 projects
whose code is adapted here: the Chimp/Chimp128 codecs (from the authors' reference
implementation) and the DDSketch quantile sketch (from Datadog's sketches-java), with the
upstream NOTICE text and the license. The Chimp128 docs no longer credit DuckDB as the
source. - Contribution terms. Every pull request takes one of two routes, the contributor's
choice: a DCO sign-off (git commit -s) on every commit, or the WeftDB Individual
Contributor License Agreement
(CLA.md, version 1,
adapted from the Apache Software Foundation's ICLA with Justin Icenhour as the
recipient), signed once by a pull request comment. Thecontribution-termscheck
passes a pull request when either holds. Contributions are licensed
MIT OR Apache-2.0. See
CONTRIBUTING.md. Inputs::register_dictionary_if_absentregisters a dictionary as
set_dictionary_metadatadoes unless a complete registration of that name exists,
checking inside its own write transaction, and returns whether it registered it. A
registration that cannot be read counts as existing and is left alone, and a write
that loses an MVCC conflict is tried once more after a short random delay. The check
sees registrations committed before its transaction began: one committed while it
runs, like a second writer registering the same new dictionary at the same moment, is
not seen, both are written, and reads pick the newest.
Changed
- Crates renamed for publication. The core crate is now
weftdb(wasdatabase,
a name already taken on crates.io) and the pipeline crate isweft-orchestration
(wasdatabase_orchestration). Import paths change accordingly:
use database::…becomesuse weftdb::…, anduse database_orchestration::…
becomesuse weft_orchestration::…. - The workspace builds on stable Rust (MSRV 1.95). The
#![feature(stmt_expr_attributes)]gate is gone, so nightly is no longer required to
build, test or depend on any crate. Onlycargo fmtstill uses nightly, because
rustfmt.tomlsets nightly-only options. splimesmoved to its own repository (basic-automation/splimes)
and is now a crates.io dependency. It is released on its own schedule, and a stable
WeftDB waits on a stable splimes.- splimes 0.1 → 1.0. WeftDB depends on
splimes = "1", resolved to 1.0.0 from
crates.io. splimes' migration guide
lists the results that change; through WeftDB:- large inputs keep their method (0.1 swapped cubic for quadratic from 2,500 input
points, and cubic and quadratic for linear above 5,000); Polynomial(1 | 2 | 3, b)stays a polynomial, so it extends outside the data
instead of holding flat, and with too few points a polynomial steps down to degree
n − 1. 0.1 stepped down toCubic/Quadratic/Linearwith four input points or
fewer, and with five or more kept the requested degree and failed;- a polynomial degree above 8 is an error instead of being capped at 8;
- a zero-span range returns its one point:
POST /api/v1/interpolatewith a single
input point and no explicit range answers200with that point (it was a500); - points that coincide with an input return that input's value exactly, and
interpolatedBigDecimals are the shortest decimal that round-trips; - time is exact to the nanosecond at every resolution, duplicate timestamps keep the
last value, and a leap second is the same instant as the start of the next second; - results no longer depend on whether the GPU ran them. With a GPU present, 0.1's
auto_interpolate(behindanalyze_range,analyze_point, compression and every
weft-serverinterpolation endpoint) ran a call on it when the larger of its input
count and grid size was 100–999, or either reached 50,000. Its GPU path evaluated a
polynomial of degree 0 or 4–7 one degree higher than the CPU did (both capped at
n − 1and 8) and, inf64, ignored any bounds factor other than 1.0, so those
calls returned different polynomial values from the CPU's, and on a GPU without
f64support it computed inf32. 1.0 computes the same method, inf64, on every
backend; Backend::Autouses the GPU only once the program has started it, and only above
the calibrated thresholds (see the startup calibration above); without calibration
every interpolation runs on the CPU.
- large inputs keep their method (0.1 swapped cubic for quadratic from 2,500 input
- No interpolation runs on an async worker. splimes 1.0 is synchronous;
weftdb's
analyze_point,analyze_rangeand compression, everyweft-serverinterpolation
endpoint, and the Weft-Bench WeftDB adapter run it on tokio's blocking pool
(Interpolator::run_async, splimes'tokiofeature). 0.1'sasyncfunctions
computed on the calling worker. - Downsample buckets don't move. splimes 1.0 removed
Resolution::to_baseand the
SECONDS_IN_*constants; the newweft_reduce::bucket_indexreproduces 0.1's index
exactly, including its rounding toward zero before 1970, so bucket edges and the keys
of persisted.weftpartsidecars are unchanged.Resolution::differenceis replaced
byweftdb::units_between, with the same results. - Breaking for Rust users:
weftdb::Resolutionandweftdb::Splineare
#[non_exhaustive]. They are re-exports of splimes' types, which 1.0 marks
non-exhaustive so new variants can arrive in minor releases: an exhaustivematchon
either needs a_arm. Their removed 0.1 helpers go with them: use
Resolution::step()/step_nanos()forto_step()and theSECONDS_IN_*constants,
weftdb::units_betweenfordifference,weft_reduce::bucket_indexforto_base,
andSpline::min_points()fornumber_of_points_required().Resolution::round()
andto_step_base()need no replacement: a step is one unit at every resolution, so
to_step_base()was always1andround()returned its input unchanged (or, at
nanosecond resolution outside 1677–2262, an error).Spline::pre_check()has no
direct replacement:Spline::validate()checks a polynomial's parameters, and the
engine checks the range and steps down when there are too few points (or, under
Interpolator::exact(true), returnsInsufficientPoints). - Stored spline methods are validated when they are loaded. A dictionary created
withAspectStructure::new_dictionary, or registered by a pipeline through
weft_orchestration::load_dictionary(which stored no constraints before this
release; see Fixed), persists its step interpolation as theSpline's text
(steps_interpolationin thedictionary_constraintstable of
<aspect>/dictionaries/<dictionary>.db). splimes 1.0'sSpline::from_strvalidates what it parses, so a stored
Polynomialwith degree 0, a degree above 8, or a negative or non-finite bounds
factor, which 0.1 accepted, no longer loads:get_dictionary_metadatareturns
Database error: Invalid interpolation format: invalid polynomial degree 9: must be between 1 and 8(or… invalid bounds factor -1: must be finite and not negative),
andload_dictionarylogs that error as a warning ("Failed to read dictionary
metadata; leaving the stored registration as it is") and carries on with the
pipeline's own constraints, without touching what is stored. Until this releaseget_dictionary_metadatacould
not read any stored dictionary (see Fixed), so this is the first release that reads,
and so checks, a stored method at all. To fix it, stop WeftDB and rewrite the stored
value with Turso's shell (tursodb, notsqlite3: the file is in Turso's MVCC
journal mode), e.g.UPDATE dictionary_constraints SET steps_interpolation = 'Polynomial(degree: 8, bounds_factor: None)' WHERE steps_interpolation = 'Polynomial(degree: 9, bounds_factor: None)'; 0.1 capped any degree above 8 at 8.
Degree 0 and invalid bounds factors have no exact 1.0 equivalent: choose a degree
from 1 to 8 and a finite, non-negative bounds factor, orNone.new_dictionary
now checks the method withSpline::validate()and refuses one that would not load,
e.g.Invalid step interpolation for dictionary 'd': invalid polynomial degree 9: must be between 1 and 8; it stored any method before.set_dictionary_metadata, and so
load_dictionarywhen it registers a dictionary, refuses one the same way. Resolutionnames parse case-insensitively (Hours,HOURS), for example in
WEFT_SEGMENT_PARTIAL_BASE, which ignored anything but lowercase before.weft-serverprovenance comes from splimes. Thekindof each interpolated
point (JSON, CSV, Arrow, Parquet, and the point query) is splimes'PointKind, with
the sameraw/interpolated/extrapolatedtokens, andweft_server::PointKind
is now a re-export of it. It agrees with the classificationweft-serverdid itself
except at a leap second: an input at23:59:60.5and a grid instant at00:00:00.5
the next day are one POSIX instant, so that point is nowraw(it wasextrapolated
orinterpolated).- Invalid polynomial parameters are a
400. Apolynomialspline with degree 0, a
degree above 8, or a negativebounds_factorused to be accepted, and the
interpolation endpoints usually answered200with a result (0.1 capped a degree
above 8 at 8). Not always: with 5 todegreeinput points a degree above 8 was a
500(0.1's failed step-down, above), and on a machine without a usable GPU, a
request whose larger of input count and grid size was 100–999 or 50,000 and up ran
0.1's SIMD path, where degree 0 was a500and a bounds factor below −0.5 panicked
once it extrapolated non-constant data, dropping the connection without a response.
splimes 1.0 rejects them, and the request is now a400naming the problem, e.g.
invalid polynomial degree 9: must be between 1 and 8. (JSON cannot carry a
non-finite bounds factor; such a body is a400from the JSON parser before splimes
sees it.) - Interpolation errors map to HTTP status by kind. An input or extrapolated value
beyondf64's range and an oversized grid are400s too; any other engine error
(a GPU failure, a panicked blocking task) stays a500, as every engine error was
before. - Store-wide maintenance carries on past a failing aspect. Every store-wide sweep
(reconcile, overlap merge, squash and compaction, from the background daemon or
POST /api/v1/storage/reconcile) stopped at the first aspect whose pass failed, on a
torn or truncated frame for example, and so never maintained the aspects after it in
name order. Each aspect's failure is now its own: the endpoint answers200and lists
the aspects that failed infailed: [{aspect, error}](it answered500), the daemon
logs oneWARNline per failed aspect, and both count them in
weft_reconcile_failed_passes_total. Only an unreadable aspect list is still a500.
For Rust callers,ReconcileSweep,HotColdSweep,OverlapSweepandSquashSweep
gainfailed: Vec<(String, anyhow::Error)>and are no longerCopy,Clone,
PartialEqorEq. - Turso control plane upgraded 0.6 → 0.8.
⚠️ This is one-way: once 0.8 writes a
store, its MVCC log is v3 and an older WeftDB can no longer open it. Back up the
control plane before upgrading. .weftpartsidecars usepostcardinstead ofbincode(frame v3). Sidecars
written by older versions are ignored: the segment is decoded instead, and the
sidecar is rebuilt on the next seal. No data migration is needed.weft-bench --spline poly:Nrejects a degree outside 1–8 when the arguments are
parsed, naming the limit. It accepted any degree before, and with splimes 1.0 such a
run would only have failed once the engine rejected it.- All dependencies updated to their latest major versions, including wgpu 30,
Arrow/Parquet 60 and OpenTelemetry 0.33. - Advisory codecs moved behind the
experimental-codecsfeature (weft-physical-type).
⚠️ Breaking for code that calls them: thefloatcodecmodule (Gorilla-XOR, Chimp,
Chimp128, Elf andbest_f64_*),ColumnEncoding::{gorilla_f64_bytes, best_f64_bytes, best_f64_codec}and the FIRE forecaster (fire_*) now need
features = ["experimental-codecs"]. None of them was ever written to disk, so stored
segments are unaffected. The feature is off by default, outside the semver promise, and
pending patent review. - The opt-in transposed value codec is now the
bitsliced-codecfeature
(weft-physical-type, forwarded byweftdbandweft-server), pending patent review.
⚠️ A store that enabled it withWEFT_SEGMENT_TRANSPOSED_MAX_OVERHEADmust be built with
bitsliced-codecto read those segments: without the feature, reading one fails with the
newWeftSegError::CodecNotEnabled(which names the feature) instead of decoding, and the
variable is ignored with a warning. For library callers, without the feature
FrameOptions::transposed_max_overheadis accepted but silently ignored
(write_segment_withandwrite_paged_segment_withemit the size-selected codec), and
WeftSegErrorgains theCodecNotEnabledvariant, which breaks exhaustive matches on it.
Thetranspose_bitpack_*primitives,TRANSPOSE_TILE,
ColumnEncoding::{transposed_value_bytes, transposed_overhead, best_value_codec_transposed}
andweftseg::write_value_column_transposedneed the feature too. The default
configuration never wrote this codec, so a store that never set the variable (or
FrameOptions::transposed_max_overhead) is unaffected. The docs now call it a bit-sliced
(bit-plane-major) layout; it is not the FastLanes layout they used to name. - WeftDB is dual-licensed under MIT OR Apache-2.0, at your option, from this release on.
Every crate declareslicense = "MIT OR Apache-2.0", and the repository root and every
crate shipLICENSE-MITandLICENSE-APACHEin place ofLICENSE. The release archives
carry both files,NOTICEand theTHIRD-PARTY-NOTICESfiles. Earlier commits remain
available under MIT, as they were published. The portions adapted from Chimp and
DDSketch stay under Apache-2.0 whichever option you choose. - The MIT license text (now
LICENSE-MIT) readsCopyright (c) 2025-2026 Justin Icenhour,
naming the individual copyright holder for both years of the project.
Fixed
- A restart lost a legacy database's committed metadata. The first open of a
legacy (rows-mode) database in a process deleted its Turso MVCC log,<file>.db-log.
Under MVCC that log holds every committed transaction until a checkpoint copies it
into the main file, and WeftDB's checkpoints do not run under MVCC, so for
metadata.dbit was usually the only copy: every commit made before a restart (the
database row, subjects, aspects, the unbatched queue) was lost, and
Database::existingthen failed withno such table: database. A non-empty log is
now kept, and Turso replays it at open; only an empty one is removed. - Interpolation responses report the method that ran.
splinein the
/api/v1/interpolateand/api/v1/interpolate/ilpJSON responses and in the point
query is documented as the method actually used, but echoed the requested one. With
fewer distinct timestamps than a method needs, splimes steps down (Cubic→
Quadratic→Linear,Polynomial(d, b)→Polynomial(n − 1, b)), so acubic
request over three points ran a quadratic and still saidCubic; it now says
Quadratic. The CSV, Arrow and Parquet outputs carry no method field. The
interpolate.enginetrace span keeps the requested method insplineand now
records the one that ran aseffective_spline. get_dictionary_metadataandlist_dictionariesfailed for every stored
dictionary. Both selected anupdated_atcolumn that thedictionary_metadata
table does not have (Failed to query dictionary metadata: … no such column: updated_at), andget_dictionary_metadatathen readsteps_count, anINTEGER
column, as text. They read the table as it is now: the count as an integer (text is
still accepted), and a dictionary without steps asNone. The metadata
get_dictionary_metadatacaches is keyed by aspect as well as name, so two aspects'
dictionaries of the same name no longer share it.- A pipeline's dictionaries lost their steps and variabilities, and gained a metadata
row on every load.set_dictionary_metadata, which
weft_orchestration::load_dictionaryregisters a pipeline's dictionaries with,
inserted only thedictionary_metadatarow. The steps and variabilities were never
stored,get_dictionary_metadata(which needs the constraints row) never found the
dictionary, and so everyload_dictionary(two perPipeline::extract_patterns)
registered it again, with another row and a new id. It now writes the whole
registration and replaces any earlier one of that name, since the tables cannot carry
a unique constraint;AspectStructure::new_dictionarywrites the same way, so creating
a dictionary twice no longer leaves two.load_dictionaryregisters a dictionary only
when none is registered, under theDictionary's own id, through the new
Inputs::register_dictionary_if_absent: it checks again inside its write, so a
registration committed afterload_dictionaryread none and before that write began
(an explicitset_dictionary_metadatawith tuned constraints, say) is kept instead of
replaced with the pipeline's, and a write that loses an MVCC conflict to another load
healing the same rows is tried once more. A dictionary name that cannot name a file is an
error fromload_dictionary.get_dictionary_metadata
now answersOk(None)for a dictionary with no database yet (it was an error,
Dictionary '…' does not exist for aspect '…'), and a registration it cannot read
is left alone instead of registered again. Of several rows for one name,
get_dictionary_metadatareads the newest that has constraints (it read whichever
came first), so a dictionary an earlier version registered from a pipeline reads as
unregistered and is registered again, with its constraints, on its next load, which
also deletes its extra rows. get_dictionary_metadatareturned no variabilities. It always answeredNone,
althoughnew_dictionarystores them. It now reads them back in the order they were
stored; an empty list stores nothing, and so reads back asNone.list_dictionariesread only a dictionary named "default". It opened the
aspect's dictionary named "default", so it failed for an aspect without one
(Dictionary 'default' does not exist for aspect '…') and otherwise listed only that
dictionary. Each dictionary is its own<aspect>/dictionaries/<name>.db; it now lists
all of them, by name, each with its steps and variabilities (it returned empty
constraints), and the list is no longer cached, so it does not go stale when a
dictionary is added. Only regular files are listed: a symlink, which it followed and
so opened (setting the target's journal mode and creating tables in it), a directory
or another entry named*.dbis skipped. Every regular*.dbfile there is still
opened as a dictionary, so keep backups out of that directory or under another
extension. A dictionary whose stored registration does not parse, such as one with a
stored step method splimes rejects, is logged as a warning and left out instead of
failing the whole listing; a read that fails (I/O, a query, an MVCC conflict) still
fails it, so a dictionary is never left out only because it could not be read.- A dictionary file without its tables could not be registered. A dictionary's file
can exist before its tables, for example wheninsert_pattern_into_dictionaryopened
it first, which creates no tables. Once the file was open in the process nothing created
them, soget_dictionary_metadatafailed withno such table: dictionary_metadata,
load_dictionaryonly warned and never registered the dictionary, and
list_dictionariesfailed for the whole aspect. Such a file holds no registration, so
it now reads as unregistered (Ok(None)), and registering the dictionary creates the
tables. - A metadata read could cache a registration that had just been replaced. A
get_dictionary_metadatawhose snapshot predated aset_dictionary_metadatacommit
could store the old registration after the write had invalidated it, and the old one
was then served for up to the ten-minute cache lifetime. A read now caches what it read
only if no write on the sameDatabaseinvalidated the entry since it began
(DatabaseCache::generation,invalidate_generationandstore_if_generation). A
registration written throughAspectStructure::new_dictionary, anotherDatabase
handle or another process still does not invalidate the cache, and can be served
stale until the entry expires. - Commit failures were silently ignored. A control-plane write that lost an MVCC
conflict was reported as success. Commit errors now reach the caller, and the
transaction is rolled back. - A write that lost an MVCC conflict reported a failed rollback instead. Turso rolls
a transaction back itself when one of its statements loses a write-write conflict, so
theROLLBACKafter the failed statement fails (cannot rollback - no transaction is active), and control-plane writes (registering or creating a dictionary, inserting a
pattern, removing an event, …) returned that error,Rollback failed: …, in place of
the conflict. They now return the statement's error and log the failed rollback as a
warning.weftdb::error::is_transient_mvcc_erroralso matches Turso'sBusy,
BusySnapshotand write-write conflict errors anywhere in an error's chain (it
matched only WeftDB's ownTransientMvccError), so it recognises the conflict as
retryable where the returned error keeps Turso's error in its chain: from registering
or creating a dictionary (set_dictionary_metadata,register_dictionary_if_absent,
AspectStructure::new_dictionary) and from a failed commit. The other control-plane
writes still return the statement's error as text only, which it does not recognise. - Quadratic GPU interpolation failed on GPUs without f64 support (Apple Silicon,
most integrated GPUs, Windows WARP). The f32 fallback shader did not parse, so wgpu
panicked.
Security
-
Aspect names can no longer point outside the segment store. An aspect's name is
part of the file names of its.weftsegframes and.weftpartsidecars, and it was
not checked, so a client ofweft-servercould declare a name that made sealing,
reconciling or compacting create, overwrite or delete those files outside the store's
segments/directory. Names are now validated wherever they are declared or turned
into a path (weftdb::aspect_name::validate): at most 160 bytes, no/or\, no
control characters, no leading., no trailing.or space, and not a Windows device
name (CON,NUL,COM1,CONIN$, …, also with an extension or a:suffix).
Bidirectional controls, line and paragraph separators and invisible formatting
characters are refused as well, so a name cannot display as a different one in
listings and logs (the zero-width joiner and non-joiner, which some scripts need, are
still accepted). On
Windows,<,>,:,",|,?and*are refused too, since such a name could
never be sealed there.POST /api/v1/storage/aspectsanswers400for an invalid
name, and every frame path is also checked to be a direct child ofsegments/. An
aspect already declared under such a name was never safe to use: sealing, reading or
maintaining it now fails with a typedweftdb::InvalidAspectNameerror (a400from
the ingest, read, reconcile, squash and compact endpoints), and the store-wide
maintenance sweeps list it infailedand count it in
weft_reconcile_failed_passes_totalwhile still maintaining every other aspect.
Listing it and reading its schema or stats, which touch no files, still work. Names
that differ only in letter case or Unicode normalisation are still accepted and can
share frame files on a case-insensitive or normalising filesystem; that is a collision
insidesegments/, not a way out of it, and the planned encoded frame names remove it. -
Dictionary names can no longer point outside the aspect's
dictionaries/
directory. Each dictionary is its own<aspect>/dictionaries/<name>.db, and the name
was not checked, so../xreached<aspect>/x.dband an absolute name such as/tmp/x
replaced the whole path;get_dictionary_metadatathen created that file and its
tables, andnew_dictionaryandset_dictionary_metadatawrote to it. Dictionary
names now follow the aspect-name rules (weftdb::dictionary_name::validate), checked by
the dictionary path builders, so every dictionary operation (new_dictionary,
Aspect::dictionary,set_dictionary_metadata,insert_pattern_into_dictionary,
get_dictionary_metadata,get_dictionary_db,get_dictionary_patterns) refuses
such a name with a typedweftdb::InvalidDictionaryNameerror before any file is
touched.Config::aspect_dictionaries_db_pathandConfig::dictionary_pathreturn
Result<String>(they returned the path), andlist_dictionariesskips a.dbfile
whose name is not a valid dictionary name, with a warning. Noweft-serverendpoint
takes a dictionary name.Breaking for existing data: a dictionary an earlier version created under a name
these rules now refuse (a leading., a trailing.or space, a Windows device name
on any OS such asaux,con,com1ornul.x, a control, bidirectional or
invisible formatting character, or more than 160 bytes) can no longer be reached.
Every operation on it returnsInvalidDictionaryName,load_dictionaryfails the
pipeline run that uses it, andlist_dictionariesleaves it out with a warning.
Nothing is deleted. To recover one, stop everything that uses the database, rename
<aspect>/dictionaries/<name>.db, and the<name>.db-logand<name>.db-walfiles
beside it if present, to a valid name, then set that name in thenamecolumn of the
dictionary'sdictionary_metadatarows, in thedictionary_namecolumn of the
pipeline_dictionariesrows in the aspect'spipeline.db, and in the pipeline
configuration (PipelineRunConfig,PipelineBuilder) that names it. -
API callers can no longer make backup retention delete the daemon's snapshots.
WEFT_BACKUP_KEEPretention keeps the newestbackup-<digits>directories by their
embedded timestamp, andPOST /api/v1/storage/backupcreated directories in that same
form, both for a?label=in it and for every unlabelled backup, so a caller could fill
the retained set and get the daemon's genuine snapshots pruned. That form is now
reserved for the backup daemon: a?label=in it is a400, and an unlabelled backup
is now namedmanual-<unix_millis>. A label also may no longer start or end with.
(Windows drops a trailing dot from a directory name, so such a label could still land
on a reserved name there); the restore drill's?label=follows the same rule.
Snapshots taken through the endpoint are therefore never counted or pruned by
retention; this is a behaviour change for unlabelled backups, which used to be pruned,
so remove them by hand when no longer needed. Retention also ignores a
generated-looking directory whose stamp is more than 24 hours past the current clock or
past the directory's own modification time, which no daemon snapshot ever is: it is
not counted and not removed, and each listing logs a warning naming it. A directory
planted through the API before this release keeps its planting time as its
modification time, so a far-future stamp stays ignored after the clock reaches it, as
long as the directory is not modified and its filesystem reports modification times.
One stamped less than 24 hours past its planting looks like a snapshot from a skewed
clock and is counted, but no longer outranks new snapshots a day after it was
planted. Remove such directories by hand. Anyone who can write to the backup directory
directly can still affect retention; this closes the API path. -
Web pages can no longer drive a loopback-bound
weft-server. With no
authentication, the default127.0.0.1bind was the only protection, but a page open
in a browser on the same machine could still send requests that need no CORS
preflight (enough to trigger maintenance, backups and ingest), or reach the API
through DNS rebinding. While bound to a loopback address (including the IPv4-mapped
[::ffff:127.0.0.1]), the server now answers only requests addressed tolocalhost,
127.0.0.0/8or[::1](421 Misdirected Requestotherwise,/healthand/ready
included) and refuses a state-changing request whoseOriginis not a loopback origin
(403). Clients that send noOrigin, such ascurl, are unaffected. Pages served by
another local web server (any loopback origin, on any port) are still trusted until
authentication lands. Health probes must send a loopbackHostsuch aslocalhostor
127.0.0.1, as every HTTP/1.1 client does; an HTTP/1.0 probe that sends noHostnow
gets421, so configure it to send one or setWEFT_ALLOW_ANY_HOST=1. Behind a local
reverse proxy that forwards a differentHost, or the non-loopbackOriginof a
browser UI it fronts, setWEFT_ALLOW_ANY_HOST=1. Non-loopback binds are unchanged
and remain unauthenticated. -
Cleared the
crossbeam-epoch(RUSTSEC-2026-0204) andh2(RUSTSEC-2026-0258)
advisories, replaced the unmaintainedbincode(RUSTSEC-2025-0141), and removed the
unsoundlru0.16 (RUSTSEC-2026-0253) by disabling turso's unused full-text search.
The archives attached to this release were built from commit b9a8ee424e5b9df84b2f3d5b904a0a7301565541.