Extension versions and compatibility¶
Every DATUS payload may declare the version it was written against in v,
and list in requires the keys an older engine must not drop — both are
described with the envelope. Any engine
reports the version it implements: dosi info, GET /v1/capabilities, or
dosi_engine.DATUS_EXT_VERSION. This page is the policy behind that number
and the record of what each version added.
Versioning policy¶
Every key this engine reads is registered with the version that introduced it
and what it costs a consumer to ignore it. The registry is built into the
engine and is the source of truth: the version gate, the basic-mode warning
text, dosi info and GET /v1/capabilities all read it at runtime, so none of
them can drift from it. The change log below is
written by hand against that registry.
Ignore-impact levels, ordered by increasing danger — note that failing loudly ranks safer than quietly returning different numbers:
| Impact | Meaning |
|---|---|
inert |
Presentation metadata the engine never consumed. Results are identical. |
degraded |
No wrong numbers — the request fails loudly instead (a structured error, or a query name that stops resolving). |
documented |
Numbers change, but predictably and in a documented direction (LEFT vs INNER, NULL vs 0). |
silent |
Numbers are wrong and the caller cannot tell. Must be listed under requires. |
Three shipped keys are silent: measure (D-MEASURE, 1.4), time
(D-FORMAT, 1.6) and cardinality (D-CONFORM, 1.8) — see the
change log. Every other key is degraded or documented, so
basic mode remains coherent for models that use none of them. A model that
uses one must list it under requires, which is what stops a
datus-mode engine that does not implement the key (an older Dosi, another
vendor's reader of the envelope) from silently computing a different number:
it fails at the version gate with datus_ext_key_required instead. Basic mode
is a different case — it never reads the payload at all, requires included,
and only warns ignored_vendor_extension; a model with a silent key should
not be run in basic mode.
When to bump:
- MINOR — purely additive: a new key, or a new accepted value for an
existing key that no old payload could have used. Old payloads keep working
unchanged; a new payload on an old engine gets
datus_ext_version_aheadand loses only the new key. A new key whose impact issilentis still a minor bump, but the producer must list it underrequires. - MAJOR — only when an existing key changes meaning, is renamed, or is
withdrawn. Every payload declaring an older
vis then rejected per changed key, with an instruction for updating the YAML. - Neither — engine-internal work that changes no key's contract. A version with no key behind it would make the registry's invariants vacuous.
Change log¶
Each released minor and the keys it added. The machine-readable form of this
table is capabilities().history — dosi info --format json or
GET /v1/capabilities.
| Version | Date | Keys added | Impact if ignored |
|---|---|---|---|
| 1.0 | 2026-07-11 | join_type (D-JOIN), fill_nulls_with (D-FILL) |
documented, documented |
| 1.1 | 2026-08-02 | time_dimension (D-TIME), time_granularity (D-GRAIN), dataset (D-DATASET) |
degraded, documented, degraded |
| 1.3 | 2026-08-09 | (none — in-family growth of window) |
(inherits window's documented) |
| 1.4 | 2026-08-12 | derive (D-DERIVE M1), measure (D-MEASURE) |
degraded, silent |
| 1.5 | 2026-08-26 | params (D-PARAM M1) |
degraded |
| 1.6 | 2026-09-02 | time (D-FORMAT) |
silent |
| 1.7 | 2026-09-11 | is_dimension (D-DIM) |
documented |
| 1.8 | 2026-09-16 | cardinality (D-CONFORM) |
silent |
| 1.9 | 2026-09-16 | (none — in-family growth of window) |
(inherits window's documented) |
| 1.10 | 2026-09-23 | lod (D-LOD; also the lod family of derive) |
degraded |
| 1.11 | 2026-10-07 | (none — in-family growth of lod) |
(inherits lod's degraded) |
| 1.12 | 2026-10-25 | concept_params (D-PARAM, on the mapping document); params grows onto the field carrier |
degraded |
- 1.0 (2026-07-11): initial —
D-JOIN(relationshipjoin_type, implemented: the join is emitted as INNER or LEFT accordingly),D-FILL(metricfill_nulls_with, implemented 2026-07-12: the metric output is COALESCEd; overrides the S-FAN-6 count default, any metric kind). - 1.1 (2026-08-02): time & attribution —
D-TIME(dataset/metrictime_dimension, implemented: a dataset's primary time dimension and a metric's own, the reservedmetric_timequery name, per-branch time-range fallback),D-GRAIN(fieldtime_granularity, implemented: finer-grain requests →grain_too_fine),D-DATASET(metricdataset, implemented:COUNT(*)home attribution). Basic-modeignored_vendor_extensionwarnings now cover all five carrier objects (previously only Relationship and Metric). - 1.1 (2026-08-04, no version bump): the envelope version gate itself —
vis now read and enforced (envelope),requiresis introduced, each key carries a declared ignore-impact, and the whole registry is published throughdosi info,GET /v1/capabilities, anddosi_engine.DATUS_EXT. No key was added or changed, so the version stays at 1.1. Payloads without av— which is what the Datus agent emits today — are unaffected. - 1.2 (2026-08-06): window metrics —
D-WINDOW(metricwindow, implemented: theWindowStagestep in--explain; pop/rolling/cumulative sugars + general offset/frame form, cumulative reset with scan expansion + post-window trim; ignore-impactdocumented— the metric falls back to its plain base aggregate; spec inwindow-extension.md). Initially DuckDB-executed; the all-dialect enablement (per-dialect offset-shift lowerings, verified on live warehouses) landed as a follow-up. In-family additions (same minor, fail-closed on engines predating them): mixed families / mixed shifts / mixed resets-with-start relaxed, andrequire_full_windowon finite frames (2026-08-06) — seewindow-extension.md#restrictions. - 1.3 (2026-08-09): window families & functions — grows the
windowkey in-family, no new registry row (capabilities().historystill ends at 1.2; the version signals the payload dialect). Additions: therankfamily (row_number / rank / dense_rank / ntile / percent_rank / cume_dist over the metric value), thevaluefamily (first_value / last_value / nth_value), offsetdirection: "forward"(LEAD — next-period reference), frameorder/partition/units/secondmodifiers (value-ordered and RANGE frames, partition modes, two-argument inputs), and registry levels W2 (stddev_pop/samp,var_pop/samp) + W3 (covar_pop/samp,corr). Every addition fails closed on a 1.2 engine: unknown top-level family keys fall into the exactly-one-family error, unknown in-family keys are rejected, unknown function names fail enum parse — and a declared"v": "1.3"trips the version gate besides. Spec:window-extension.md(family sections + function registry). - 1.4 (2026-08-12): derived metrics & measure naming —
D-DERIVEM1 (derivekey, implemented:filter+composefamilies, metric references resolved with cycle detection, the expression equivalence gate, compile-time conformed dimensions, and derive metadata on every host surface;reagg/ cross-datasetfilterpredicates /window.baseare reserved and rejectnot_implementednaming their milestone — notewindow.basepreviously fell into the silently-ignored unknown-key bucket and now fails closed) andD-MEASURE(measurekey, implemented: explicit measure naming, signature-keyed dedup and collision checks unchanged — the fix for the non-ASCII stem collision, and the firstsilent-impact key: emit it underrequires). M2 (2026-08-19, in-family — no new keys, no version bump):window.baseflips from reserved to functional (windows over leaf / filter metrics), compose accepts window members (stage-tier lowering: arithmetic over window output) and filter members (flatten tier). A 1.4 payload using these shapes compiles on an M2 engine and fails closed (not_implemented/ rejection) on the M1 engine — never a silent misread. Nested compose (in-family — no new keys, no version bump): a compose member may itself be a compose, expanded inline at compile time. An older engine rejects the shape with the sameinvalid_datus_extensionit always did, so this too fails closed. The metric stays on the flatten tier, andderive_membersbecomes the transitive leaf list with folded coefficients; a window anywhere in the tree keeps the combinationnot_implemented. - 1.6 (2026-09-02): time storage formats —
D-FORMAT(fieldtimeblock, implemented: the column is parsed per dialect on the grouping side, and filter bounds are folded into the stored format before the SQL is built, with the grain inferred from the layout).time.granularityjoins the flattime_granularityas an equal spelling — no deprecation, but the two may not disagree. The secondsilent-impact key: emit it underrequires, so a datus-mode engine that lacks it fails at the version gate (basic mode never readsrequires; see the envelope). A consumer that drops it does not fail loudly; it assumes a native date column and, for a month- or year-stored one, returns an empty result with no diagnostic. See D-FORMAT for configuration and limitations. Scope: string and integer layouts plus the native temporal types;epoch_seconds/epoch_millisare deferred becausefrom_unixtimereads the session timezone and that needs its own round.
In-family addition (2026-09-08, still 1.6): time.format accepts continuous ordered fixed-width
calendar prefixes with arbitrary fixed literal text. Filtering still folds
date bounds to stored constants; grouping extracts components into a canonical
date/time before dialect parsing. Existing layouts remain compatible. Older
Datus engines reject new layouts rather than silently interpret them.
- 1.7 (2026-09-11): field roles — D-DIM (field is_dimension,
implemented: read by list_dimensions and by
attribution's candidate list). The model can finally say that a column is a
measurement, an identifier, free text or PII, so it stops being offered as a
grouping dimension. It is a recommendation signal only: an explicitly named
group_by is honored either way and no compiled SQL changes, hence
documented impact and no requires entry. Where the key is absent the
engine infers the role from model structure alone — keys, relationship
columns and numerically aggregated columns — and never probes the warehouse.
See D-DIM.
- 1.8 (2026-09-16): conformed fact-to-fact relationships — D-CONFORM
(cardinality key on a Relationship, implemented:
conformed classes in the join graph, per-branch
resolution of group-by / filter / time-range columns and of metric_time
to each fact's own spelling, merge on the output name). A many_to_many
edge is never joined; only its paired columns are shared. The third
silent-impact key: emit it under requires — without it the edge is a
many→one join and duplicate-sensitive aggregates fan out. See
D-CONFORM.
- 1.9 (2026-09-16): a ranked scorecard — grows the window key in-family
the way 1.3 did, so there is no new registry row. Four additions driven by
one shape.
frame.scope: "partition" states the frame as the entire partition instead
of an offset from the current row, lowering to <func>(…) OVER (PARTITION BY
…) with no ORDER BY and no frame clause. It is the second window function
beside a rank — "how many rows am I ranked against" — which a ranked
scorecard needs and which no combination of the existing keys could express:
count with preceding: "unbounded" is a running count, and units:
"range" still stops at the current row's peer group. scope is mutually
exclusive with preceding, reset, require_full_window, units and
order, each of which only describes an ordered frame; writing one is
rejected rather than ignored.
window.base widens: a ratio or a compose can now be the series a window
reads, so ranking a rate or taking its period-over-period is expressible. A
cross-fact compose's series only becomes a value after the branch merge, so
the window stage sits on the merge — the branches aggregate separately and
their CTEs ride beside the stage's base. Such a base has a shared time axis
only when the facts' dates are paired (D-CONFORM); otherwise
the families that need one refuse on conformance.
Fails closed on a 1.8 engine twice over: it rejects the new keys inside
frame and order as unknown, and the payload's "v": "1.9" trips the
version gate.
See window-extension.md.
Nested windows and NULL placement complete the shape. order.nulls:
"first" | "last" pins where NULLs sort in a window ordering — left unset,
each warehouse applies its own default and they disagree (DuckDB and
PostgreSQL last ascending, MySQL-family and StarRocks first), which a ratio
base turns from theoretical into routine. The generator emits the clause
only where it differs from the target's default, and where a dialect has no
such clause at all (MySQL, TiDB) the placement lowers to a leading
CASE WHEN … IS NULL sort key — so the rendering varies by dialect and the
meaning does not. And window.base goes one step further than a ratio: it
accepts a compose over another window's output, so a second window can read
the first one's. Each
level lowers to one more SQL stage; depth is capped at 3 and level 2+ must
declare partition. The guards that matter run across levels, not within
one: the output trim stays outside the outermost stage, and a
partition-global metric at any level refuses to share a query with a
scan-expanding one at any other. Also in-family: a stage-tier compose may
have a compose member (it stays a member rather than being inlined, since
on that tier each member is its own series).
- 1.10 (2026-09-23): levels of detail — D-LOD, one grammar on two
carriers: the lod key on a Field and the lod family of the existing
derive key on a Metric (implemented).
On a field, fixed(base, dims…) makes a per-entity aggregate groupable,
filterable and re-aggregatable through an ordinary query (the LodRollup
step in --explain; a D-FORMAT rollup key keeps its storage
encoding and is parsed once, outside). When the view needs more than the
rollup — a dimension outside its keys, a row-level field reading the value,
several key families — the broadcast shape (LodBroadcast) joins the detail
rows with one derived table per key family; an aggregate over a pinned
value counts each of its keys once per group. No keys is the table-scoped
FIXED, broadcast onto every row.
On a metric, include groups the inner stage by the query's own group-by
plus the declared dimensions and collapses it with the declared outer
aggregate; the grain and any D-FORMAT parse apply inside. exclude
attaches a coarser aggregation of the same fact to the view's rows — LEFT
JOIN on the dimensions they share, CROSS JOIN on none — recomputed rather
than windowed so a COUNT(DISTINCT) base is right too. fixed is the
field's declaration hosted on a metric so it can attach to the view: a
table-scoped or key-scoped total computed before the dimension filter,
which is what Tableau's {SUM([Sales])} is as a denominator and what
exclude() cannot mean (LOD metrics). A conformed pairing
whose endpoints are two facts' own fixed fields aligns them on one axis:
each fact rolls up for itself and the branches merge, the facts are never
joined.
Filter placement follows Tableau's order of operations. The where clause
is a dimension filter — applied to a fixed rollup's output when it names
only the rollup's keys, to the detail rows otherwise, never inside the
rollup — while time_range is the context filter that scopes every rollup.
A row-level field that reads only D-LOD values and constants aggregates once
per key and can filter a reaggregation at that key grain. A calculated
classification such as customer_margin >= 5000 AND customer_orders >= 6
therefore filters the fixed values before they are reaggregated. If the
predicate reads a different fixed key family, it is evaluated where that
family is available, before the grid is reduced.
Basic mode drops a D-LOD field (aggregate_field_dropped) and fails closed
on a D-LOD metric, whose expression is the nested aggregate its
declaration describes (nested_aggregate), rather than computing one level
of it. See Understand LOD.
- 1.11 (2026-10-07): levels of detail across datasets — grows the lod
key in-family, no new registry row. A field's fixed base may aggregate
another dataset than the field's own when every key corresponds to a column
of it through a declared relationship: a many→one edge between the two in
either direction, a many→one path from the base to the host, or a D-CONFORM
pairing (a rollup over a scan of its own). The rollup is LEFT JOINed onto the host's rows on plain
equality and a count reads 0 where nothing matched, so > 0, = 0 and
>= n are the semi-join, the anti-join and a threshold; the context layer
reaches the rollup through the base's own edges or the pairing, and a
conjunct it cannot read there is left out (across datasets).
A 1.10 engine fails closed: it refuses the base as aggregating another
dataset.
- 1.12 (2026-10-25): parameters on fields and derived subtypes. params
grows onto the field carrier — a field expression's :name holes, the
field.literal slot, bound by patching the field for the whole query and
echoed under bindings (field parameters) — and
concept_params lands on the Ossie mapping document, declaring the
placeholders of a derived subtype's
derived_by rule (subtype thresholds). Alongside, the
semantics contract gains S-ATTR-3′: a CASE WHEN <guard> THEN <arg> END
aggregate may read, in its guard, one dataset the arg's dataset reaches
many→one — which is what lets a D-DERIVE filter on a fact metric be
written where: "Customer is PremierCustomer". An older engine fails
closed: concept_params is an unknown key, a :name in a field is not a
column, and "v": "1.12" trips the version gate.
Not yet implemented (roadmap)¶
The queued RFCs are all additive, so each lands as one minor bump with its own registry rows. Order may change; every entry occupies a minor of its own.
| Minor | RFC | Keys | Carrier | Impact | requires? |
|---|---|---|---|---|---|
| ≥1.12 | rfc-semi-additive-metrics §4 |
semi_additive |
Metric | silent — an end-of-period balance gets summed across snapshots |
yes |
| ≥1.12 | rfc-minimal-declarations §3 D-DISTINCT-STATE |
distinct_state |
Field / Dataset | silent — a materialized bitmap column is aggregated as if it were an ordinary column |
yes |
| ≥1.7 | rfc-derived-metrics M3-M4 |
derive reagg + the general cross-dataset filter (window.base and compose window/filter members shipped in-family at 1.4/M2; the one-dimension guard shipped at 1.12 as S-ATTR-3′) |
Metric | degraded/error — reserved shapes reject not_implemented today, never silently compute |
no |
| — | window-extension.md rank/share/streak families, W2+ functions |
future window shapes (in-family additions, fail-closed on older engines) |
Metric | degraded |
no |
Three silent keys have shipped: measure (D-MEASURE) at 1.4, time
(D-FORMAT) at 1.6 and cardinality (D-CONFORM) at 1.8. If you author
models against a Datus agent that emits
silent-impact keys, keep an eye on the requires list it produces: that is
what stops an older engine from quietly returning wrong numbers, and it only
works because requires itself shipped at 1.1 — ahead of the first key that
needs it.