Skip to content

Latest commit

 

History

History
86 lines (86 loc) · 120 KB

File metadata and controls

86 lines (86 loc) · 120 KB
VersionDatesCommitsHighlights
v2.30.0'26 Sep 139
  • External concept mappings: OBML artefacts link into a business ontology (#449). A model, data object, dimension, measure or metric may carry externalConceptMappings: a concept as a compact or full IRI, a SKOS-style relation (exact, close, broader, narrower, related; broader means the external concept is the broader one) and optional justification, source, ontologyVersion, confidence and comment. A top-level ontology.prefixes block declares the prefixes compact IRIs expand with; rdf, rdfs, owl, skos and xsd are built in. The resolver expands every concept and reports bad prefixes, bad IRIs, duplicates and conflicts as structured errors with source spans. Mappings are descriptive metadata: compiled SQL, join paths and planner warnings are identical with and without them. The JSON schema, contract manifest, OBSL ontology and OSI converter (osi-orionbelt 0.3.1) all carry them, so OBML to OSI to OBML is lossless, and extends / inherits fragments merge prefixes with a conflict error rather than a silent rewrite. Five review rounds went into making fragments exactly as strict as a top-level document.
  • The RDF graph states the links (#450). Every mapping becomes a direct skos:exactMatch / closeMatch / broadMatch / narrowMatch / relatedMatch triple from the artefact to the expanded IRI, plus an obsl:ExternalConceptMapping resource at a deterministic IRI (slug plus a content hash, so two targets with the same label cannot collide) whenever provenance was authored. The model prefixes and skos are bound on the graph, the mapping vocabulary is embedded so the graph stays self-contained, and external ontologies are referenced by IRI, never imported. /graph and /sparql see the links immediately.
  • Discovery over REST (#451). GET .../concept-mappings lists every mapping with the artefact it sits on and the expanded IRI, filtered by concept, namespace, relation and types; .../namespaces reports which external namespaces a model links into, most used first; .../unmapped lists the artefacts still without a mapping. The schema and per-artefact describe responses carry each artefact's mappings. Backed by a model-local index built on demand, so model load is unchanged.
  • A guide, and a demo that links into a real ontology (#452, #453). docs/guide/concept-mappings.md explains the model ontology versus an external one, why mappings never affect SQL, the IRI rules, the relation vocabulary and its direction, and how mappings surface over REST, RDF/SPARQL and OSI. The commerce model the public playground loads now declares a synthetic commerce glossary plus schema.org, GoodRelations and FIBO prefixes and maps its high-value artefacts, and a test runs every SPARQL example in the OBSL guide against the demo's exported graph, so the documentation cannot drift from the graph.
  • SPARQL in the playground (#454). A SPARQL tab runs read-only SELECT and ASK over the loaded model's OBSL graph from a gallery of seven ready-to-run examples, through the API when one is configured and in-process when not. The editor is ACE with its SPARQL grammar, vendored like vis-network and mermaid so nothing comes from a CDN; Gradio's own editor has no SPARQL mode. Its word-boundary-less keyword regexes were patched in the vendored bundle (AS inside ASK, STR inside STRSTARTS). A wrongly named ORDER BY variable used to be silently ignored; the algebra is now walked after execution and every variable used but never bound is reported as a warning.
  • Business rules in OBML, compiled to the query that reports their findings (#455). A top-level rules block declares rules as conditions over dimensions, measures and metrics without SQL: comparisons in the query filter shape, all / any / not composition, and references that inline another rule. A rule over dimensions only is row-level and compiles to a WHERE predicate; one over a measure or metric is aggregate, declares its grain and compiles to HAVING, so multi-fact rules reuse the CFL planner. classification and eligibility rules describe members; validation and constraint rules state an invariant whose violations the query returns. References must match level and grain and form a DAG, an aggregate rule may compare only dimensions of its grain, and every problem is a structured error with a source span. Rules are obsl:Rule in the graph, ride through OSI, and can carry concept mappings. The commerce demo ships six.
  • Rules run, and a tab to run them from (#456, #457). POST .../rules/{name}/evaluate runs one rule through the same cache-aware pipeline as query/execute; POST .../rules/evaluate runs every rule, or a subset, into a report with per-rule status, counts, sample rows and errors, never hiding a failure. The Business Rules tab lists the model's rules with statistics, selects a rule by clicking its row (a dropdown filled after load validated its value against choices a restarted server no longer knew, and raised on every pick), shows the selected rule's OBML with its mapping provenance on request, and tests one rule or all. Along the way every block that ends a tab (findings, SPARQL results, SQL output, ER diagram, ontology graph) is now sized to the window by measurement rather than by fixed viewport offsets that only added up on tall screens, maximising a table lifts the cap, and Render Graph lays the ontology out anew on every click.
v2.29.0'26 Sep 113
  • TLS on the REST surface, the last one that could not encrypt itself (#447). API_TLS_CERT + API_TLS_KEY make the API serve HTTPS and API_TLS_CLIENT_CA additionally requires a client certificate, through the same loader and the same rules as the two wire surfaces: both settings or neither, a client CA refused without a server certificate, every error naming API_TLS_* rather than another surface's settings. 2.28.0 had scoped TLS to pgwire and Flight on the implicit reasoning that REST is always behind something that terminates it - but that applied to pgwire too, which shipped TLS anyway precisely because there is not always a proxy, so leaving REST out was an inconsistency rather than a decision and the plan recorded no rationale for it. uvicorn takes paths rather than PEM bytes, which is the third shape of the same material and why ListenerTLS carries both. Defaults are unchanged, so no existing deployment moves. Verified with curl rather than only in unit tests: HTTPS served and validated against the issuing CA, plain HTTP refused, an untrusted CA refused, and under mutual TLS a client with no certificate refused while one signed by the CA is served.
  • The CLI can present a client certificate (#446). --client-cert / --client-key / --ca-cert let obsl --server reach an API that requires mutual TLS, which it previously could not connect to at all - a wall rather than a degradation. With API_TLS_CLIENT_CA that is OBSL itself; it equally satisfies a gateway in front. Two defects were found in review and both were subtle: building the context with ssl.create_default_context() meant adding --client-cert silently changed which authorities the server was checked against, because httpx falls back to certifi while ssl's default is OpenSSL's own store; and the paths were validated expanded but used unexpanded, so ~/ca.pem passed the existence check and failed inside OpenSSL. Certificate material now loads when the flags resolve, so a path that is missing, unreadable or not what it claims names the setting and the file.
  • Every TLS setting is in the configuration reference (#447). docs/reference/configuration.md says .env.template holds every option, and neither carried API_TLS_* - but neither carried PGWIRE_TLS_* or FLIGHT_TLS_* either. 2.28.0 shipped TLS on two surfaces without adding a single variable to the file that claims to list them all, so an operator reading the reference would have concluded OBSL cannot do TLS at all. All nine are now in both places, grouped by surface.
  • The comparison pages cover transport security, DuckDB and the CLI (#445). Six pages had zero mentions of TLS between them, which for the self-hostable option compared against hosted products reads as absence. Every competitor cell is verified rather than inferred, and the Cube finding is source-level rather than a docs gap: its SQL API answers N to every SSLRequest, since SSLResponse is declared const CODE: u8 = b'N'. The DuckDB row is deliberately not a win claim - Cube emulates the same pg_catalog surface, so ATTACH may well work against it, untested by us and undocumented by them. The CLI row corrects an earlier draft that called dbt's the strongest: both query from a terminal.
v2.28.2'26 Sep 111
  • Zoned timestamps and times advertised an OID that meant something else (#443). pg_attribute translates DuckDB's internal type ids to Postgres OIDs with a CASE whose ELSE passes the id through unchanged, and two types were missing from it: DuckDB's TIMESTAMPTZ is 32, which Postgres uses for pg_ddl_command, and its TIMETZ is 34, which is not a type OID at all. Both went out dressed as Postgres OIDs. The shadow pg_type had declared 1184 and 1266 correctly the whole time; nothing pointed at them. Latent rather than live - no shipped model uses either - but both are legal OBML and timestamp_tz is a first-class time dimension type, and zoned timestamps are the norm on Postgres and Snowflake.
  • A sweep over the whole type surface, so this class cannot recur (#443). Three defects of one shape had now shipped, each a type whose id or modifier translated for some values and silently fell through for others, and none was visible because the test models only ever used string, int, float and date - the suite tested the types the fixtures happened to use. TestEveryDataTypeMaps asserts for every DataType plus both decimal widths that the advertised OID is one the shadow pg_type declares and that the modifier is either -1 or a correctly packed decimal, so a new DataType cannot be added without mapping it. It reads back through the emulator rather than re-applying the translation expressions: the first version did the latter and was worth less than it looked, catching a wrong CASE but not a correct CASE the view forgot to apply, which is one of the two defects it exists for. Verified by reverting each fix in turn and watching the right assertion fail.
v2.28.1'26 Sep 101
  • A DECIMAL measure could not be read over the DuckDB ATTACH route (#441). The catalog advertised numeric correctly and then attached a type modifier in the wrong encoding: pg_attribute translated the type id from DuckDB's numbering to Postgres's and passed atttypmod straight through, and the two engines pack it differently - DuckDB uses precision * 1000 + scale (18002 for DECIMAL(18,2)), Postgres uses ((precision << 16) | scale) + 4 (1179654). Decoded the Postgres way, 18002 reads as DECIMAL(0, 78): zero total digits, 78 decimal places, so no real value fits and each was refused with a conversion error naming a string that was entirely valid - Could not convert string "1936466.31" to DECIMAL(0,78). Browsing the catalog worked throughout, because the modifier only matters once DuckDB allocates a vector to hold values, so the failure read as bad data rather than a bad type. The per-query RowDescription had always packed the Postgres encoding, which is why psql and every BI tool were unaffected, as were Flight/ADBC and REST; only the catalog path disagreed with a rule the codebase already knew. It shipped because no fixture had a decimal measure - every measure in the test models was a float, so the suite never produced a NUMERIC column at all, and most real models have one. Both pgwire suites now carry a decimal variant, the catalog tests name 18002 explicitly so a regression is unmistakable, and the client suite reproduces the original failure against the real extension.
v2.28.0'26 Sep 102
  • DuckDB queries the layer as an attached catalog (#439). ATTACH 'host=... dbname=<model>' AS obsl (TYPE postgres) in a plain shell mounts the model as obsl.<model>.model, so governed measures join to local Parquet and CSV and land in CREATE TABLE AS with no OBSL-specific client - the capability adbc_scanner already offered over Flight SQL, addressed as a table rather than a string in a table function. Four gaps stood in the way and not one of them raised an error: the extension enumerates a catalog with several statements in one simple-query message and the surface answered only the first; it joins pg_type.typnamespace to pg_namespace.oid, and there was neither the column nor a pg_catalog row to land on, so the whole enumeration matched nothing; it probes with table-less SELECTs the semantic translator has no model to resolve; and it asks for a row count by projecting SELECT NULL FROM t, which read as a literal projection and was rejected. ATTACH reported success and the model was simply absent. Found by pointing the real extension at a real listener, which is what the integration suite now does. One client setting is required, SET pg_use_text_protocol = true, because the default reads with binary COPY.
  • The row count had to be made to agree with SELECT * (#439). Answering SELECT NULL from the dimensions alone drops the fact table from the query, so a dimension value with no facts behind it - a customer who has never ordered - becomes a row: count(*) returned 3 where SELECT * returned 2. Every column of the model is projected instead, which is what SELECT * expands to, so the two agree by construction. The regression test asserts count(*) against len(SELECT *) rather than a literal, since agreeing with it is the whole requirement.
  • TLS on both wire surfaces, mutual TLS included (#438). PGWIRE_TLS_CERT + PGWIRE_TLS_KEY make the listener answer S to an SSLRequest and upgrade the socket where it always answered N; FLIGHT_TLS_CERT + FLIGHT_TLS_KEY serve grpc+tls instead of grpc. Until now the Flight surface had authentication and no transport security, so an API key crossed the wire in clear text. Both settings or neither: one alone refuses to start rather than falling back to plaintext, because a deployment that reads as encrypted and is not is worse than one that does not come up. Client trust was measured rather than read from driver docs, against Python ADBC, DuckDB's adbc_scanner and the Flight SQL JDBC driver: all three are refused rather than downgraded when told to trust nothing, and two traps came out of it - from DuckDB the adbc_connect MAP key must be a literal, and from JDBC trustStore does nothing without useSystemTrustStore=false beside it. The loader is shared, in orionbelt.service.tls, because pgwire is core and Flight is an optional extra; each surface still names its own settings in every error, including the one for a key bind-mounted root-owned 0600 into an image that runs as non-root.
  • Multi-statement simple queries (#439). One Query message carrying several statements now runs each in order and replies with one result set per statement, as Postgres does. The splitter is a scanner rather than split(";"), since a semicolon inside a string, an escape string, a quoted identifier, a dollar-quoted body, a line comment or a nested block comment is data and not a boundary. Comment-only fragments are dropped and routing sees the cleaned statement, because a word inside a discarded comment (-- from the dashboard) otherwise steers it: SELECT 1 followed by that comment was sent to the semantic translator, which has no model for it.
v2.27.2'26 Sep 091
  • The ER diagram rendered as its own source text (#436). Gradio 6 parses a mermaid fence into a div and stops there; Gradio 5 rendered it. So the gradio 6 bump shipped a UI whose ER Diagram tab showed 129 lines of mermaid source collapsed into one paragraph, and whose zoom control was silently dead alongside it, polling for an svg nothing would ever produce. The bundle is now vendored under ui/static and inlined, exactly as vis-network already is for the ontology graph - which matters twice over, because /ui's CSP allows scripts from 'self' and inline only, so a CDN import would have been blocked in the API-mounted mode while working in bare Gradio. The UMD build is the vendored one: mermaid.esm.min.mjs is 30 KB and loads its own chunks at runtime.
  • The action buttons spanned the row and wrapped their labels (#436). Compile SQL had Gradio's default scale so it absorbed the row's spare width, and a 140px min_width was narrower than "Validate Model" renders. All three are scale=0 at a width that fits their label, with nowrap so a narrow button cannot break across two lines again.
  • A browser test suite, because no server-side assertion could see either (#436). Every other test here asserts what the server sends; both regressions were in what the browser does with it, and the markup was correct at every layer already tested. pytest -m ui drives a real Gradio app in a real Chromium, in both serving modes - bare Gradio and the API's /ui, which differ in the way that matters since only one has a CSP. Verified against both defects rather than assumed: reverting the loader fails the standalone tests, and putting the CDN import back fails the embedded one. CI installs chromium so the suite asserts rather than skipping green.
v2.27.1'26 Sep 093
  • CommandGetTables ignored include_schema, so DuckDB could not list tables (#433). Flight SQL defines two response shapes for that command - four columns, or five with the serialised table schema appended - chosen by a flag on the request. OBSL always answered with five. Every client tested until now happened to ask for the five-column form, so it went unnoticed; DuckDB's adbc_scanner extension asks for four and the driver rejects the endpoint outright. The flag is parsed now and both the streamed table and the schema advertised in FlightInfo follow it. Same class as the two catalog defects the ADBC conformance harness found in #382, and caught the same way: by driving the server with a client nobody had pointed at it before.
  • DuckDB is a documented client of the layer (#434). With the adbc_scanner community extension a plain DuckDB shell connects to the Flight SQL surface, and adbc_scan turns a governed query into a relation - so measures join to local tables, aggregate, and land in CREATE TABLE AS, Arrow the whole way with no conversion at either end. The recipe is asserted like every other sample on that page, against a live server. Two caveats are stated rather than buried: adbc_scanner is a community extension, and adbc_connect wants a filesystem path to the Flight SQL driver library rather than a package name.
v2.27.0'26 Sep 0944
  • One cache entry meant two different things depending on which surface wrote it (#429). REST, pgwire and Flight share a result cache - same key, same blob - but not a representation. REST and pgwire encoded rows that had already been serialised, storing a timestamp as an ISO string, a date as a string and binary as base64; Flight stored its Arrow table verbatim, so the same three columns were timestamp[us], date32 and binary. Whichever surface ran a query first decided what the others read back, and each read path only understood its own writer's convention. The driver's table is what gets stored now, and a hit is materialised through the same serialiser a miss is, so a hit returns what its miss returned cell for cell - including the ADBC opaque NUMERIC that is a string in Arrow and a Decimal in a row. format=arrow returns the warehouse's types rather than ISO strings under an envelope that called them datetimes, and reconcile_to_declared works on a hit, where a row-backed rebuild had made it a silent no-op. KEY_VERSION moves 4 to 5; v4 entries miss once and age out.
  • The Flight executor guessed its Arrow types from the first few rows (#428). It built every result by pulling tuples out of a PEP 249 cursor and inferring types from the values in the first batch, so the one surface whose protocol is made of record batches was the one that never asked a driver for Arrow: a Postgres or Dremio result that arrived as Arrow was torn down into Python objects and rebuilt with re-inferred types. Measured on a real DuckDB cursor, CAST(1.50 AS DECIMAL(18,2)) was sized decimal128(3, 2) from the value and now keeps decimal128(18, 2). Also fixed: ob_duckdb called DuckDB's deprecated fetch_arrow_table, warning on every Flight query once the executor began calling it.
  • ob-dremio executes over ADBC Flight SQL (#427). The driver was the only one that spoke a wire protocol by hand - a pyarrow.flight client building descriptors, calling get_flight_info + do_get, and threading the bearer token through every RPC. Dremio serves Flight SQL natively, so adbc-driver-flightsql is its driver and the protocol code is deleted rather than replaced, with the PEP 249 surface and every Arrow type unchanged. ? parameters now bind, where the statement used to reach Dremio with its placeholders intact and come back as a Calcite RexDynamicParam error. One statement per execution, deliberately: ADBC skips re-preparing when the SQL is unchanged and Dremio then answers the second execution with the first one's rows, silently. 111/111 live against a container.
  • Arrow Flight SQL statistics, documented rather than implemented (#426). ADBC 1.1 defines GetStatistics and client planners use it. The flightsql driver refuses both entrypoints in the client, before a request reaches the wire, because Flight SQL carries no statistics command - so nothing a server implements can answer them. Pinned by assertions rather than skipped, so a future driver that starts answering fails the suite and reopens the question.
v2.26.0'26 Aug 2514
  • An expression that could not be read compiled to a reference to itself (#359, #364). A computed column whose body failed to parse fell back to a ColumnRef carrying its display name, so the model loaded, sql_valid came back true, and the database rejected a column no table has. Reachable through ordinary SQL: ||, INTERVAL, EXTRACT(... FROM ...). The parser also invented a missing closing parenthesis on a call, which does not fail - it moves what the call wraps, so ROUND(x, 2 * 100 returned a different number from ROUND(x, 2) * 100. Both are errors now, carrying the parser's own message and the column's path.
  • ORDER BY on a computed dimension named a table the outer query did not have (#358). Four passes wrap the planner's SELECT in a CTE and rebuilt ORDER BY from the column form only, so a computed dimension's expression was inlined a second time where its table is out of scope. All eight dialects. Every compile now also checks that the outermost query names only tables its own FROM binds, reported as OUT_OF_SCOPE_TABLE.
  • A period-over-period query aggregated rows its filter excluded (#365, #366). The filters reached the date-range CTE and nothing else, so the spine covered the filtered extent while every measure summed every row: 710.00 where the same query without the metric returned 10.00. pop_base and date_range now read one derived table carrying the query's own join tree and filters. Multi-fact PoP, which emitted SQL naming a table its FROM lacked, is refused.
  • A time-grained dimension lost its declared type, differently per engine (#369). resultType: date became TIMESTAMP on DuckDB and timestamptz on PostgreSQL, whose date_trunc resolves to the zoned overload - so which month a row belonged to depended on the session zone. A dimensionsExclude query was worse off still, comparing day pairs under a column labelled by the month.
  • cast and to_number, and the simple CASE form (#355, #375, #360). Casting was the one operation an OBML expression could not express at all. cast(x, 'type') takes an OBML type and pins ties away from zero; to_number(x) pins NULL for text that is not a number on all eight engines, which is what a value read out of JSON needs. Both matrices were measured on live engines rather than read: MySQL answers 0 for cast('abc', 'double'), and Dremio has no TRY_CAST at all.
  • An integer AVG is exact on DuckDB (#316). The last engine whose average drifted and the only one with no exact division to rewrite to - every route through / returns DOUBLE - so the average is assembled from integer arithmetic instead, with ties away from zero, matching PostgreSQL at 2.365 and -2.365.
  • Two cast rendering defects and their residue (#356, #357, #361, #362). ClickHouse wrapped an overflowing integer cast where every other engine raises, returning -294967296 for a true 4000000000, and MySQL rendered TIMESTAMP and TINYINT(1) as cast targets its CAST does not accept.
v2.25.1'26 Aug 213
  • round(2.5) was 2 on a PostgreSQL or MySQL float column (#351). All three of ClickHouse, PostgreSQL and MySQL round ties to even for their float type and away from zero for their decimal type, and all three document both halves, so 2.25.0's ClickHouse-only rewrite fixed one engine of three. round(x, 2) over a float column raised outright on PostgreSQL, which has no round(double precision, integer) at all. PostgreSQL now casts to its unbounded numeric; MySQL and ClickHouse add half of the last kept place and truncate, which needs no conversion, so a decimal operand stays exact and a float stays a float.
  • Three regressions in what 2.25.0 shipped for ClickHouse (#351). Its rewrite dragged every Decimal through a Float64, turning 12345678901234567.885 into 1.2345678901234568e16; a large float came back wrong, round(1e19) answering 9999999999999999539; and round of an infinity raised where it had returned inf. Checked against DuckDB as the oracle across the ties, 1e19, 1e20, an infinity, negative digit counts and a DECIMAL(65, 30) at its 29th and 30th place.
  • A ClickHouse FixedString was read by storage rather than by value (#352, #353). ClickHouse pads to the declared width with NUL bytes that count as content, so a FixedString(50) holding 'Books' answered 50 to length, came back from upper still carrying 45 of them, and made ends_with(x, 'ks') false; replace and split_part raised. 13 of the 15 catalog string functions disagreed with the same characters held as a String. TPC-DS types its CHAR columns this way, following ClickHouse's own published DDL, so it is how a real schema arrives. The catalog now marks which arguments hold text and ClickHouse reads those through toString.
  • The tests could not see either bug, for the same reason (#351, #353). The execution matrix passes string and numeric literals, and a literal is neither a float to PostgreSQL nor a FixedString to ClickHouse, so it was exercising the one type each engine already got right. Both groups now run against real typed columns on every vendor.
v2.25.0'26 Aug 2050
  • A repeated column is queryable as a data object (#342, #344, #346, #348). nestedIn declares that an object takes its rows by unnesting a parent's ARRAY<STRUCT> column, which is the shape Google Cloud and AWS CUR use where FOCUS defines a scalar. Seven dialects unnest natively and Dremio reads a code fallback with a warning saying which source ran. The keys stay data, so "which label keys does our spend carry?" is a group-by rather than a column per key. An unnest multiplies the row that contains it, so a parent measure grouped by a nested dimension is deduplicated on the parent primaryKey while a measure on the array keeps every element: measured on the demo, a naive unnest overstates one label's spend by 51%. What no plan can express is refused with a named reason rather than answered wrongly.
  • A portable scalar-function catalog, 39 entries in five groups (#300, #301, #302, #304, #307, #308). string, numeric, conditional, date/time and json. Where engines disagree about the answer rather than the spelling, the answer is pinned and the odd engine is rewritten: round(2.5) is 3 including on ClickHouse, whose ties go to even; trunc(-1.9) is -1 including on Databricks, which has no numeric truncation; greatest(1, NULL, 3) is NULL everywhere. Every entry is executed against live engines and asserted against its documented value. expressionMode: portable turns an uncatalogued call into an error rather than a silent engine dependency.
  • Breaking: one number on every engine (#315, #318, #320, #325, #326, #335, #336). A zero divisor was inf on DuckDB, an error on three engines and NULL on MySQL; it is NULL everywhere now, and div and log are pinned the same way. A measure outgrowing its type returned 9999999999999999.99 as an ordinary row on MySQL and now cannot reach the overflow. An integer AVG drifted past fifteen digits on five engines and is exact on the four with a route to it, by three routes that do not transfer. A HAVING on a windowed value was evaluated before the window, silently returning the wrong rows.
  • What a model could not previously state (#291, #292, #293, #294 to #299). measure defaultValue says what an aggregate over nothing reads as, which standard SQL and ClickHouse answer differently. required: true on a join emits an INNER JOIN, so a nullable key is stated in the model rather than filtered in every query. A computed column can read another data object's column, and an exists subquery filter resolves against the target's joins. filterContext measures are computed through metrics, filtered on, and planned against their own fact under a multi-fact plan; all three were refused before.
  • Checked rather than asserted (#306, #309, #343, #347, #349). The TPC-DS sweep is re-run and published: 39 of 40 exact on DuckDB and 37 of 40 on ClickHouse, every remaining difference traced to a reference variant, zero unexplained. A FinOps model on the FOCUS specification ships with an end-to-end notebook. The model reference's YAML examples, unreadable since v2.6.1 because a chore stripped their indentation, are restored and verified to parse; the function catalog gets its own page.
v2.24.1'26 Aug 053
  • The converter changes in 2.24.0 never reached PyPI. osi-orionbelt carries its own version and none of the six converter changes bumped it, so it stayed at 0.1.2, which was already published; pypi-publish.yml sets skip-existing: true on that job (so a release where the converter did not change does not fail) and cannot tell that apart from a forgotten bump, so the tag build uploaded nothing and reported success. The converter is now 0.2.0, a major-for-pre-1.0 bump since 2.24.0 removed OBMLtoOSIOntology and validate_osi_ontology from its public API, and the extras declare osi-orionbelt>=0.2 instead of leaving it unpinned. Nothing crashed while this was wrong, it degraded silently: on a PyPI install of the osi extra, a model using the new anchor: field was reported schema-invalid by the converter's advisory validation and anchor was dropped on an OBML to OSI to OBML round trip, and the sqlglot metric decomposer, key-derived relationship cardinality, Apache Ossie datatype and non-SQL-expression fix were all absent. Both builds reported __version__ 0.1.2, so the version string could not distinguish them. Only PyPI installs of the osi / flight / flight-duckdb-only extras were affected; the Docker images and the Cloud Run demo build from workspace source and always had the 2.24.0 code.
  • CI fails a converter change the release could not publish. scripts/check_osi_version_bump.py runs on every pull request: when the packaged converter source changed against the merge base, the version in its pyproject.toml must not already be on PyPI, which is exactly the condition that turns skip-existing into a silent no-op. The rule is "not already published" rather than "changed in this PR", so further work on an already-bumped, unreleased version needs no second bump. It also requires pyproject.toml and __init__.py to agree. Confirmed against the real defect: restoring the 2.24.0 converter version files makes it exit 1.
  • The 2.24.0 changelog gained the entries it was missing. Six converter commits (#246 to #251) shipped with no CHANGELOG entry, including the breaking removal of the OSI-ontology emit. Two entries also illustrated the cross-fact refusal with count_distinct(Returns.[Return ID], Sales.[Sale ID]) and corr(Returns.Qty, Sales.Amount), which are not refusals: Returns declares a many-to-one join to Sales on the demo commerce model, so one leg reaches both and the pairing works. Both now use Purchases and Sales, which share dimensions but never join, and say explicitly that a declared join makes the pairing work.
v2.24.0'26 Aug 0433
  • Measures sourced from the "one" side of a join are no longer silently overcounted (#258, #262, #263, #264, #265, #266, #268). Joining Sales to Products repeats each product row once per sale, so SUM(Products.Stock On Hand) grouped by Sales.Region counted every product once per sale it appeared in, with no error and no warning. Such measures are now aggregated in their own CTE over rows deduplicated on the source object's primaryKey, then joined back onto the query grain, and the query carries a FAN_TRAP_RISK warning. Mixed-grain expressions (extended price) and multiplicity-insensitive aggregations are left untouched. A measure that could previously only be queried alongside a many-side measure now re-anchors on the common root and plans as an ordinary star; total: true composes with a deduplicated measure instead of being refused; and ACR stops advertising measures the dedup guards would refuse.
  • Metrics, HAVING, cumulative and window wrappers compose with deduplicated measures (#269, #273). The grain-dedup pass splits an inlined metric expression back into per-component columns and rebuilds the formula in the outer projection, so a metric over a deduplicated component is computed rather than refused. HAVING predicates naming a deduplicated measure move to the outer WHERE, where they mean exactly what HAVING would. Cumulative and window metrics take their base measure by alias from the CTE beneath them instead of re-deriving it from fact tables that are no longer in scope.
  • A measure expression reading two independent facts now resolves to a defined grain (#276). SUM({[Sales].[Amount]} * {[Returns].[Quantity]}) had no defined value and named a table the plan never joined. Three rules settle it in order: a declared join path wins; otherwise the new anchor: field names the grain, with every unreachable fact conformed to the key it shares with the anchor and joined many-to-one; otherwise the shared key, with a CONFORMED_GRAIN_ASSUMED warning. Facts sharing several dimensions raise ANCHOR_REQUIRED_AMBIGUOUS_KEY rather than picking one. anchor propagates to the JSON schema, contract manifest, Ossie roundtrip, and the new obsl:anchorGrain predicate.
  • Ordered aggregates and two-column statistics work in multi-fact queries (#270, #272, #275). A withinGroup sort key is now projected into the leg that owns the measure and NULL-padded across its siblings, so a multi-fact LISTAGG returns the same sequence as the single-fact plan instead of being reordered or refused. corr, covar_pop, covar_samp, regr_slope and regr_intercept are re-applied over the leg's own argument pair rather than folded into the count-distinct concat trick that made them meaningless. Arguments that straddle facts stay refused, and a multi-column count_distinct spanning two facts now raises instead of answering 0. Also fixed: every listagg measure in a multi-fact query compiled to SQL that failed at execution (numeric padding over a text column), a declared delimiter was dropped, a withinGroup column's data object was never joined, and derived metrics may now reference other derived metrics at any depth.
  • Ossie converter fidelity (#246, #247, #248, #249, #250, #251). Fields adopt the Apache Ossie datatype with a schema drift-check in CI, relationship cardinality is inferred from primary_key / unique_keys, the regex metric decomposer was replaced with sqlglot, non-SQL field expressions no longer leak into the column code, and seeding generates per-vendor SQL scripts. Breaking: the OSI-ontology emit is removed (OBMLtoOSIOntology, validate_osi_ontology, the obml-to-osi --ontology flag, and the include_ontology API field, which now answers 410 Gone); the OBSL RDF ontology is a separate core feature and is untouched.
v2.23.1'26 Jul 224
  • Period-over-period and cumulative metrics now execute on every dialect (#241, #242). A batch of dialect codegen bugs, each surfaced by executing the full measure/metric surface against a real engine: quarter/week period-over-period date arithmetic crashed or produced invalid SQL on Dremio (INTERVAL '-1' QUARTER is not a valid Calcite qualifier), Postgres, Databricks and ClickHouse; Dremio miscompiled the previousValue comparison (reading a self-joined decimal's bytes as the output date); MTD/YTD grain-to-date metrics emitted a hardcoded DATE_TRUNC that MySQL lacks and BigQuery spells differently; BigQuery rejected time-grain dimensions because DATE_TRUNC got a quoted string date-part instead of the bare keyword; and Snowflake could not resolve the spine CTEs (bare references vs quoted declarations) and rejected its constant-less GENERATOR(ROWCOUNT). MySQL, BigQuery, Snowflake, DuckDB were already correct where not listed.
  • Per-vendor measure/metric execution sweeps (#242, #244). New integration tests run every measure and metric against each engine — local Postgres/MySQL/ClickHouse/DuckDB testcontainers, live Dremio via pgwire, live BigQuery/Snowflake/Databricks, and a pyspark-gated local Spark run for the Databricks dialect — asserting the compiled SQL actually executes, closing the gap that let the fixes above ship undetected. The Databricks seed also gains a bulk PUT + read_files path (#243).
v2.23.0'26 Jul 214
  • High-precision DECIMAL preserved across every surface (issue #136). Values past float's ~15-16 significant digits no longer round (123456789012345678.90 came back as ...680.00) because the executor stopped casting Decimal to float. Two consumer-visible shape changes: raw REST JSON delivers DECIMAL cells as exact decimal strings, a fixed value-independent contract documented on columns[].type; and Arrow Flight advertises governed DECIMAL as DECIMAL(p, s) instead of DOUBLE, so BI tools over JDBC/ODBC see the precise type. pgwire NUMERIC (already exact since #116), the shared cache, and value-formatted/TSV output keep their shape and are now exact. MCP relays REST results, so its consumers see the same string shape.
  • Arrow Flight catalog probes fixed under sqlglot 30 (#237). sqlglot 30 renamed the Select FROM arg key, so information_schema / pg_catalog discovery queries were misrouted and returned a malformed result, breaking BI-tool schema discovery (DBeaver, Tableau) over Flight SQL.
  • Driver test suites now run in CI (#239). The root testpaths excluded drivers/*, so the Flight and ClickHouse suites were never exercised and had silently rotted (the sqlglot 30 break above; ClickHouse tests drifted from the driver's query_arrow migration, realigned in #238). Each driver package now runs from its own directory.
v2.22.2'26 Jul 195
  • OSI converter dimension round-trip fidelity (osi-orionbelt 0.1.2). The OSI field name is the physical column code, so a round trip renamed every dimension to its code and could trip the collision fallback; the name is now preserved in an extension and restored, making the fallback foreign-OSI only. OBML allows N dimensions over one column but OSI is one-per-field, so extras were dropped silently; they are now preserved (with a warning) and rebuilt, each carrying its own synonyms and vendor extensions. Malformed extension payloads are filtered rather than crashing the converter on an unhashable key.
  • obsl convert surfaces input schema violations. The local and --server convert paths now report OSI/OBML input schema errors (advisory; the conversion still runs), matching the REST convert endpoints.
  • label is no longer authorable on dimensions/measures/metrics. A vestige of the old list-keyed-by-label format that the resolver silently ignored (identity is the mapping key); authoring it now fails validation, consistent with dataObject/column. No shipped model used it; the resolved identity field was renamed label to name internally.
v2.22.1'26 Jul 161
  • The 2.22.0 UI image could not start. import gradio raised ModuleNotFoundError: No module named 'requests', so the UI container exited on boot. Gradio below 5.50 eagerly imports its CLI from __init__, which chains to import requests, and nothing in the ui extra provides it. Only the UI image was affected; the API, Flight, PyPI packages and the live deployment were fine throughout. The ui extra now floors gradio at 5.50, restoring the gradio 5.50.0 + pydantic 2.12.3 pair that 2.21.1 shipped: the old >=5.0 floor let a Dependabot group update resolve gradio down to 5.23.1 to keep a pydantic bump that gradio 5.50 caps out.
  • CI imports the UI against the deps the image ships. Every other job installs --all-extras --all-groups, which pulls mkdocs-material and with it requests, so a UI that cannot import in its own shipped venv passed all of CI; nothing started a UI container before the release tag.
v2.22.0'26 Jul 153
  • Python 3.14 support. All three Docker images (API, UI, Flight) build on python:3.14-slim and 3.14 joined the CI matrix; snowflake-connector-python and pyarrow were raised to versions that ship cp314 wheels, since the locked ones had none and the slim image failed building them from source.
  • sqlglot 30. Constraint moved >=26,<27 to >=30,<31. sqlglot 30 renamed the Select args colliding with Python keywords and made exp.Expr the common base class; the OBSQL translator and pgwire subquery flattening were updated to match. No behavior change, but installs pinning sqlglot 26 must move. pyarrow widened to <26 and structlog to <27.
  • OSI converter roundtrip fidelity (osi-orionbelt 0.1.1). Metrics whose SQL referenced physical codes rather than display names were dropped on the return trip; colliding dimension names silently overwrote each other; fields with spaces in the display name emitted invalid OBML with no LOSSY warning; and validate_osi crashed on malformed input instead of reporting schema errors.
v2.21.1'26 Jul 131
  • Arrow Flight SQL result cache restored. Flight's cache read/write called a result-codec API removed in 2.20.0, so every Flight query silently missed the cache and never stored a result; Flight now uses the current data-only codec and shares one entry per compiled query with REST and pgwire.
  • Flight cache hits keep the advertised schema. Empty / all-null results no longer stream null-typed columns on a hit: Flight preserves the exact Arrow schema on write and casts a hit back to the schema advertised in FlightInfo.
  • Cache key + TTL derivation shared across surfaces via a single layer-clean service module, replacing three copies that could drift (internal refactor, no behavior change).
v2.21.0'26 Jul 073
  • Cross-session content-addressed model cache. Identical OBML loaded into different sessions now compiles once and is shared under a stable content-derived model_id, instead of each session recompiling its own copy under a random id. In admin-curated mode the curated model is compiled once for the process and every user session references it; shared models are refcounted and evicted once no session uses them. Because model_id is part of the result-cache key, identical model plus SQL now shares result-cache entries across sessions too.
  • Single-model shortcut resolution scoped to protected sessions. In admin-curated single-model mode the top-level shortcut endpoints ignore transient user sessions, so a duplicated per-session copy of the curated model no longer triggers a spurious "multiple models loaded across sessions" error.
v2.20.0'26 Jul 063
  • Ontology Graph rendered from the OBSL ontology. The UI graph is now a rendering of the exported RDF (single source of truth), so it and the ontology can't drift; synthesized row-count measures appear in it, and an Export Onto button downloads the Turtle. New toolbar: rotate-left/right controls and a clearer PNG export.
  • New ontology predicates. obsl:anchoredTo (grain-anchored counts, a new measure source form) and obsl:referencesColumn (columns an expression measure reads, distinct from declared-columns[] obsl:sourceColumn). Ontology, SHACL, and spec updated together; exported graphs stay SHACL-valid.
  • Cache stores row data + column schema only. The response envelope (sql, explain, timing, cached, columns) is rebuilt fresh per request, so a cache hit reports the cache read time (not the stale DB time) on every surface and empty/all-null column types survive. format=arrow now returns a length-prefixed frame (JSON envelope + gzip'd Arrow data); JSON/TSV unchanged.
v2.19.0'26 Jul 053
  • Auto-synthesized row-count measures. Every countable dataObject yields a grain-anchored row-count measure whose name equals its label (default "Sales Count"), governed like any declared measure: referenced by name in select.measures, never as an ad-hoc COUNT(*), and a dataObject is never FROM-able. Knobs: countable/countLabel per object and exposeCounts/countLabelPattern per model; a declared measure of the same name overrides synthesis. Counts flow through discovery, the BI catalog, metric references, and Artefacts Composability Resolution, and their knobs roundtrip through OSI.
  • Security: two pgwire SQL-injection fixes. Numeric text bind parameters are now strictly parsed per type OID and re-rendered as canonical literals instead of being spliced raw; the $N placeholder scanner skips comments and dollar-quoted strings so a placeholder can no longer be substituted inside them.
v2.18.2'26 Jul 041
  • Format-independent result cache. The cache stores raw, locale-neutral rows keyed on the query alone, so raw JSON, value-formatted JSON, TSV, and Arrow share one entry; formatting is applied on delivery. format=arrow now honors format_values (display strings baked into the IPC blob); raw format=arrow is unchanged.
  • Faster cache hits. Hit/miss counters accumulate in memory and flush lazily (no per-hit metadata write under the lock); the payload read and gzip+Arrow decode are offloaded off the event loop (REST and oneshot); raw format=arrow hits are served via zero-copy byte-passthrough.
v2.18.1'26 Jul 031
  • Fix: playground UI failed with "No module named pyarrow". The UI decodes query results over the Arrow IPC transport, but pyarrow was declared only in the flight extras, so the [ui] install and the UI Docker image lacked it. pyarrow is now a dependency of the ui extra.
v2.18.0'26 Jul 033
  • Arrow IPC result format. /v1/query/execute negotiates format=arrow (or the Arrow stream Accept header), returning a self-describing Arrow IPC stream with the result envelope (row count, execution time, cache status, timezone, column types/formats) in the schema metadata; JSON and TSV are unchanged and gzip is honored.
  • Arrow-backed result cache, harmonized across surfaces. The cache now stores Arrow IPC + gzip instead of Parquet, and REST, pgwire, and Flight share a single entry keyed on the data source, so identical queries reuse results regardless of surface.
  • Interactive Query Results in the playground. Click a dimension value to add a WHERE filter, a measure/metric value to add a HAVING filter, or a null dimension cell to add an IS NULL filter; filters are additive and toggle off, with a Clear-filters button that leaves query-defined filters enforced. Per-column headers run server-side ORDER BY (asc/desc/clear) with the active direction highlighted, and a Jump-to navigator scrolls the model editor to any section or artefact. Every interaction rewrites the query YAML and re-executes over Arrow transport.
v2.17.1'26 Jun 281
  • Result cache scoped to the data source, not the session. The cache key was keyed on session_id, so two sessions running identical SQL against the same (global per-dialect) database connection never shared a cached result. The key now uses a datasource identity (the dialect today; gains the tenant/principal when per-tenant connections land) and KEY_VERSION is bumped to 3, so REST and pgwire/Flight share entries across sessions. Storage groups by datasource (in-place migration of the legacy session_id column); delete_session becomes delete_datasource; session close no longer purges the shared cache. Old on-disk entries age out via the version bump.
v2.17.0'26 Jun 285
  • obsl command-line interface. A local-first CLI, registered as a third console script alongside orionbelt-api and orionbelt-ui. Commands: validate, compile, execute, describe, diagram, graph, convert, dialects. Local commands run in-process through the same compiler / parser / converter the REST API uses, so a model can be linted and its SQL previewed with zero infrastructure (CI-friendly: validate exits non-zero on invalid models). With --server (and optional --api-key), compile / execute run against a deployed server's curated model through the /v1/query/* shortcuts (no model upload, so governed single-model deployments are respected). Queries come from a JSON/YAML document (-q) or an OBSQL string (--sql); results render as table / json / csv / tsv, data on stdout and notes on stderr. Adds typer.
  • Docker Hub + PyPI publish workflows. Version tags (vX.Y.Z) and manual dispatch now build and push all three images (multi-platform amd64/arm64, semver-tagged) to Docker Hub, and publish both packages (osi-orionbelt first, then orionbelt-semantic-layer) to PyPI via Trusted Publishing (OIDC, no stored token). The release flow no longer publishes images or packages locally; pushing the release tag triggers both workflows.
  • Breaking: Docker Hub images renamed to orionbelt-semantic-layer-{api,ui,flight} (was orionbelt-{api,ui,flight}); update any docker pull / compose references (Cloud Run service names unchanged). Published images no longer bake the demo DuckDB dataset by default (the API image still bakes it when the seed is present, so the one-command local demo keeps working).
  • Public build/release tooling under scripts/. scripts/build_demo_duckdb.py and a public scripts/release.sh (everything except the Cloud Run deploy) now live in the repo, so a clean clone can build the demo seed and cut a release without the private infra repo.
v2.16.0'26 Jun 2315
  • OBML contract manifest (schema/obml-contract.yml). A drift-checked inventory of every OBML enum / class / field (camelCase alias, JSON-schema exposure, ontology property, OSI round-trip); CI fails if the Pydantic models, JSON schema, or ontology drift from it.
  • JSON Schema validation at the API boundary. Model-load and query endpoints plus the MODEL_FILES preload validate payloads against the published schema (422 on violation). The schema is now camelCase-only; loosely-coerced payloads (snake_case keys, string version, uppercase enums) are rejected.
  • Architecture hardening. Explicit compiler pass pipeline; a sessions.py service layer; per-request app runtime; five large modules split into focused submodules (drift snapshots byte-identical). New CI gates: dependency-direction, broad-except, RawSQL containment, compiler invariants, coverage floors.
v2.15.0'26 Jun 181
  • DECIMAL columns report as NUMERIC over pgwire (#116). Decimal measures/metrics were coarsened to FLOAT8 and lost scale on display (574585.00 -> 574585.0, large values -> 1.6E7); now reported as NUMERIC(precision, scale) from the model's declared dataType across RowDescription, catalog metadata, and encoded values.
  • Dremio federation hardening. Hides DuckDB's internal main schema from BI-tool browsers (pg_tables / pg_views shadows); flattens Dremio's nested view/filter pushdown (constant-folded equality, CAST projections) so saved views and filtered queries compile, bailing when an outer filter would cross an inner LIMIT.
  • Compiler fixes. HAVING filters on period-over-period metrics are applied (were dropped); clearer plain-language errors for incompatible-artefact combinations.
  • Governed views in the Dremio demo. The bootstrap creates a governed Space of curated views; the pgwire database is the orionbelt brand catalog with the model as a schema.
v2.14.0'26 Jun 161
  • Artefacts Composability Resolution (ACR). New composables endpoint: given the query so far (or one or more named anchors), it returns which dimensions / measures / metrics can still be added and compile, plus cflMeasures / cflMetrics for artefacts combinable only via the Composite Fact Layer. Reuses the planner's directed join-graph reachability, so every suggestion is guaranteed to compile. Top-level /v1/composables shortcut auto-resolves a single session/model.
  • Guided query building in the UI. The Gradio playground highlights composable artefacts in the pickers as you edit the query (check mark, with (via CFL) for cross-fact candidates); highlighting never hides artefacts, so independent analyses stay discoverable.
v2.12.0'26 Jun 143
  • Unified authentication across every surface. One AUTH_MODE selector (none / api_key / oidc) governs REST, Arrow Flight SQL, Postgres wire, the Gradio UI, and MCP. Off by default, so the public demo and local dev are unchanged; production sets AUTH_MODE=api_key + API_KEYS (comma-separated, rotated by overlap). Startup fails fast on empty or sub-16-char keys; oidc is reserved for a later release and rejected loudly until then.
  • REST API-key auth. Every /v1 endpoint requires a valid key when auth is on; X-API-Key (configurable) and Authorization: Bearer both work. Missing -> 401 with WWW-Authenticate, invalid -> 403. /health, docs, and /ui stay open, and /health reports auth_mode so clients can detect the requirement.
  • Flight + pgwire on the shared key store. Flight validates the handshake credential against the same keys; pgwire requires the key as a password, defaulting to SCRAM-SHA-256 (never sends the key on the wire), with cleartext opt-in via PGWIRE_AUTH_MODE=password. Legacy FLIGHT_AUTH_MODE=token keeps working one release with a deprecation warning.
  • UI credential forwarding. The Gradio UI reads OBSL_API_KEY and forwards it on every REST call (browser users never see it), logging a clear startup error when a key is required but missing. New authentication guide in the docs.
v2.11.0'26 Jun 1314
  • Filter pushdown through Postgres-federation BI tools (Dremio). Dremio's connector wraps the virtual model table in a derived table and lifts the predicate out (SELECT ... FROM (SELECT ... FROM model) WHERE ...), which the OBSQL translator rejected as a subquery. The pgwire translator now flattens that trivial wrapper, so dimension (WHERE) and measure (HAVING) filters execute through federation; non-matching SQL is left untouched.
  • Result cache now serves the pgwire surface. The freshness-driven cache was wired only into the REST handlers, so pgwire / Flight bypassed it. Extracted a shared orionbelt.api.query_cache service that REST and pgwire both use, so repeated pgwire queries hit the cache. Catalog / metadata probes are never cached. Arrow Flight has a separate streaming path and is tracked separately.
  • Period-over-period: multiple comparison offsets per query. One query can combine PoP metrics with different offsets (month-over-month + year-over-year), each with its own prior-period self-join over one shared date spine. Also fixed the PoP self-join alias (prev is reserved in Dremio) so PoP works on the Dremio dialect.
  • Cross-fact metrics no longer leak component columns. A CFL query selecting a derived/ratio metric (Return Rate, Gross Margin) projected the underlying component measures as extra result columns, which Dremio's fixed-schema federation rejected. The outer SELECT now projects only the requested columns.
  • Cleaner federated catalog + metric formulas. In admin-curated mode the pgwire/BI catalog exposes only curated models (no transient session schemas), and the _metrics_metadata view's formula column is populated again (it read a renamed-away attribute).
  • Dremio "semantic sidecar" demo (demo/dremio). One-command stack (MinIO + Dremio OSS + OrionBelt single-model + Gradio) showing Dremio federating into OrionBelt over pgwire while OrionBelt pushes execution back via Arrow Flight, with a raw-Parquet-vs-governed comparison. CI actions bumped to Node 24.
v2.10.0'26 Jun 128
  • osi-orionbelt converter package. The bidirectional OBML <-> OSI converter is now a standalone, pip-installable package (Apache-2.0), developed in-repo as a uv workspace member under packages/osi-orionbelt and published to PyPI. Ships a single osi-orionbelt command with obml-to-osi / osi-to-obml subcommands (mirroring osi-dbt) and vendors its schemas so it builds and validates standalone.
  • OSI conversion is now an optional extra. osi-orionbelt is no longer a hard dependency; a bare install stays lean and /convert returns 503 when it is absent. Install via orionbelt-semantic-layer[osi]; the flight deploy extras bundle it so shipped API images keep working. Converter force-include and Dockerfile COPY lines removed.
  • Third-party vendor extensions preserved. OSI custom_extensions from vendors the converter does not handle internally (SNOWFLAKE, DBT, SALESFORCE, GOODDATA) round-trip verbatim at model, dataset, field, and measure/metric levels; an OBML dimension's foreign extensions surface on its OSI field.
  • Converter vendor identity. OBML -> OSI tags OrionBelt-proprietary payloads as ORIONBELT (was COMMON); OSI -> OBML stashes OSI-native fields OBML cannot hold under OSI (was OBSL). Legacy COMMON/OBSL still read. Non-breaking output change.
  • UI: Export as OSI. The export button shows clean copyable OSI YAML in the relabelled preview box and downloads model.osi.yaml, instead of prepending a status banner into the YAML.
v2.9.0'26 Jun 114
  • OSI ontology export. OBML models can be exported to the OSI ontology layer (EntityType/relationship concepts in ontology.json) alongside the core-spec export. New include_ontology flag on POST /v1/convert/obml-to-osi and GET /v1/sessions/{id}/models/{mid}/osi; the response gains ontology_yaml + ontology_validation as a separate, individually-valid document. Default off, fully backward-compatible. Importer deferred while OSI is at 0.2.0.dev0.
  • Model-level settings.defaultLocale. A model can declare a BCP-47 default locale for result value formatting (thousand/decimal separators) on /query/execute?format_values=true. Resolution: explicit ?locale= then settings.defaultLocale then DEFAULT_LOCALE env. Roundtrips through OSI losslessly.
  • OBML JSON schema realigned with the model. Added top-level name (the schema had been rejecting valid multi-model OBML); removed the vestigial rich locale object and measure.functions (unimplemented since the initial commit); trimmed measure-filter definitions to the implemented surface. Added a regression guard.
v2.8.0'26 Jun 025
  • Session-scoped OSI model endpoints. POST /v1/sessions/{id}/models/from-osi converts OSI YAML to OBML and loads it into the session's model store (returns the model summary plus conversion warnings and OSI input validation); GET /v1/sessions/{id}/models/{mid}/osi exports a loaded model back to OSI YAML. Distinct from the existing stateless /v1/convert/* transforms. New ModelStore.get_raw() returns a deep copy of a model's raw OBML.
  • OSI converter now packaged into the wheel. The converter lived in repo-root osi-obml/ and was only found via repo-root or /app paths, so a non-editable wheel raised ModuleNotFoundError from the /convert and new OSI endpoints. It is now bundled under orionbelt/_osi_obml/ via hatch force-include; verified on a clean wheel install.
  • Mermaid ER diagram label clipping fixed. Every label clipped its last character because Mermaid measured column widths with the theme font while the browser painted a wider cascaded font. Pinned a local-only font stack in the diagram's themeVariables.fontFamily and on the rendered ER text.
v2.7.10'26 Jun 011
  • Reference schema endpoints 500 on every non-editable install. /v1/reference/schemas/obml and /v1/reference/schemas/query returned HTTP 500 "Schema file is missing from this deployment" on the PyPI wheel and Docker / Cloud Run. The loader resolved the JSON Schema files via parents[4]/schema, which only points at the repo root in a source / editable layout; in an installed wheel it lands in site-packages and the files were never shipped there (packages = ["src/orionbelt"] excluded the repo-root schema/ dir). Tests passed because they run editable. The schemas are now bundled into the wheel under orionbelt/schema/ via hatch force-include and loaded through importlib.resources, with a source-tree fallback for editable checkouts. Regression test added.
v2.7.9'26 May 271
  • Colab notebook still broken on v2.7.8. Different failure: /v1/query/execute returned HTTP 503 "ob-flight-extension package is not installed" on every query cell. v2.7.8 dropped ob-flight from the notebook's _REQUIRED map on the assumption the quickstart only queries via REST and never uses Flight - wrong: db_executor.py imports ob_flight.db_router.get_credentials unconditionally for every dialect including DuckDB. v2.7.8 unbroke API startup but moved the failure two cells later. Restored ob-flight; PyPI now has 2.6.1 with the cache= kwarg the API expects, so the install resolves cleanly.
  • Notebook smoke workflow had been masking Colab regressions. The existing job ran inside the uv workspace which always installs every drivers/* package locally, so the test environment never matched what Colab gets from pip install. Added a second job notebook-pypi-equivalent: build OBSL wheel from PR source, install it + side packages STRICTLY from PyPI into a plain python -m venv (no uv, no workspace), execute the notebook end-to-end, assert every code cell ran cleanly (mermaid.ink transient 503s filtered as the only allowed exception). This is the gate that would have caught v2.7.7 and v2.7.8 before shipping.
v2.7.8'26 May 271
  • Colab quickstart hotfix (#96). v2.7.7 release surfaced a kwarg drift that had been shipping since v2.4.0: OBSL calls start_flight_background(cache=..., cache_config=...) but PyPI's ob-flight-extension is stuck at 2.1.0 (the local source bumped to 2.6.1 with the kwargs but was never republished). CI passed because it built the local 2.6.1; Colab crashed because pip pulled PyPI 2.1.0. v2.7.5 / v2.7.6 install-cell bugs had masked it; v2.7.7's fixes (#87 / #94) finally got Colab far enough to hit the real TypeError. Two-part fix: drop ob-flight-extension from the notebook's _REQUIRED map (the quickstart only queries via REST, never Flight SQL) and catch TypeError in the lifespan as a defensive guard against future signature drift. Verified end-to-end in a fresh Colab-mimicking venv. 3 new tests (2 static contract + 1 runtime regression).
v2.7.7'26 May 271
  • GROUP BY ALL on the 5 supporting OLAP dialects (#91). New DialectCapabilities.supports_group_by_all flag, advertised via /v1/dialects. Snowflake, Databricks, DuckDB, BigQuery, ClickHouse emit GROUP BY ALL instead of the explicit column list when no ROLLUP / CUBE modifier is requested — equivalent SQL, much shorter on queries with computed dimensions (e.g. GROUP BY date_trunc('year', "Sales"."salesdate"), date_trunc('month', "Sales"."salesdate") collapses to GROUP BY ALL). Postgres, MySQL, Dremio unchanged. ClickHouse retains its trailing WITH ROLLUP / WITH CUBE form for modifier paths. 27 new tests, 35 drift snapshots regenerated, live-verified against Snowflake, BigQuery, Databricks, ClickHouse.
  • aggregation: measure for Databricks Metric Views (#92). New AggregationType.MEASURE enum value (with agg / aggregate aliases). Compiler emits MEASURE("<measure_label>") and skips column resolution; the engine resolves the aggregation by name via its metric-view machinery. Only Databricks accepts the aggregation; the other 7 dialects (including Snowflake, which uses the separate SEMANTIC_VIEW(...) construct instead) raise UnsupportedAggregationError. Model validator forbids columns:, expression:, filters:, total: true on a delegated measure. Propagated to JSON schema enum, ontology + SHACL, OSI converter roundtrip (via the COMMON custom_extension), and ChatGPT Action OpenAPI. 22 + 5 new tests.
  • Colab notebook smoke workflow no longer dies inside uv venv (#94). The install cell's _REQUIRED map mapped ob_flight_extension as the import name, but the actual module shipped by the distribution is ob_flight; find_spec always returned None on CI, forcing the pip fallback path. uv-managed venvs do not include pip by default, so the fallback died with No module named pip and cascaded into NameErrors. Fixed the _REQUIRED map and added uv pip install pip to the notebook workflow as a backstop. 2 new contract tests.
v2.7.6'26 May 271
  • Five v2.7.5 review follow-ups closed in one bundle. #88 fix notebook_setup.show_yaml() TypeError (_indentless kwarg mismatch — one-char rename + the missing helper-call test that would have caught it). #89 UI silent fallback to bundled YAML on transient /v1/settings failure — killed _cached_settings, added retry-with-backoff for Cloud Run cold starts, distinguish API unreachable from API in self-service mode. #87 Colab notebook smoke workflow failed on every PR — idempotent install cell (find_spec check, no -q) so CI's pre-installed working tree wins over PyPI. #85 JSON schemas drifted from Pydantic — root additionalProperties: false moved out of properties block, timeGrain enum trimmed, removed dimension.group and measure.reduceToRelationDimensionality, added grouping to query-schema.json. #84 RDF exporter silently dropped every v2.7.5 ontology addition (CustomExtension / ModelExample / WithinGroup / numClass / delimiter / hasWithinGroup / …) — exporter now emits them; bidirectional drift guard locks the loop. 4 new test files, ~30 new tests. Full suite 2322 passed, 157 skipped.
v2.7.5'26 May 263
  • Fix flaky test_ob_clickhouse_driver::test_obml_derived_metric — the ob-clickhouse driver correctly returns Decimal for decimal-typed metric columns (preserving precision is the driver contract), but the test asserted via pytest.approx(<float>), which raised TypeError against the live ClickHouse testcontainer. Now stays in the Decimal domain end-to-end.
  • Ontology drift audit + fill missing OBML properties (#82). Six OBML fields had silently drifted out of ontology/obsl.ttl across v2.2–v2.6 releases. Added CustomExtension, ModelExample, WithinGroup classes; new properties numClass / primaryKey / delimiter / via / vendor / extensionData / exampleName / exampleDescription / exampleQuery / intentTag / withinGroupOrder / hasWithinGroup / hasCustomExtension / hasExample. Matching SHACL shapes in obsl.shacl.ttl. New tests/unit/test_ontology_drift.py drift guard introspects every OBML modeling class and asserts each field maps to an obsl:* property — the rule no longer depends on memory.
  • OBSQL [NOT] EXISTS translation. v2.7.0 added EXISTS / NONEXISTS to QueryObject but OBSQL rejected the syntax — WHERE EXISTS (SELECT 1 FROM "OrderItems") came back with UNSUPPORTED_SQL_FEATURE even though the QueryObject layer fully supported it. compiler/sql_translator.py now translates [NOT] EXISTS (SELECT 1 FROM <DataObject> [WHERE <preds>]) into QueryFilter(op=exists/nonexists, subquery=Subquery(...)). Outer subject column is derived from SELECT's first dim / measure. EXISTS body is constrained: no JOIN / GROUP BY / ORDER BY / LIMIT / HAVING / nested EXISTS. Documented in obsql_reference.py. 12 unit tests + 1 live pgwire round-trip test on the Dremio compose stack (direct OBSL pgwire — Dremio's federation parser can't see OBSL's nested data objects so the EXISTS body's FROM "Sales" never reaches OBSL via that path; this is a Dremio-relay limitation, not an OBSL bug).
v2.7.4'26 May 261
  • Precedence-aware SQL emitter — generated SQL is no longer deeply over-parenthesized. Fixes #79. The emitter in dialect/base.py wrapped every BinaryOp / IsNull / Between / InList / UnaryOp in (...) regardless of operator precedence — a typical anti-join with a computed dimension produced nine layers of nested parens. compile_expr now takes an internal _parent_prec hint and each operator wraps only when its precedence is strictly less than its parent's required level; atoms (literals, column refs, function calls, CAST, CASE ... END) never wrap; the clause root passes prec 0 so the outermost expression never picks up a redundant outer wrap. Non-associative ops (comparisons, LIKE, -, /) wrap any equal-precedence child to avoid invalid chained comparisons. 26 new precedence tests, Tier 2 drift snapshots regenerated, no semantic-correctness tests changed. Full suite 2238 passed, 154 skipped.
v2.7.3'26 May 261
  • Computed-column CASE WHEN ... END expressions silently compiled to 'CASE'. Fixes #77. The recursive-descent parser in compiler/expr_parser.py tokenised CASE as a bare identifier; _parse_factor treated bare identifiers as Literal.string(...) and silently dropped the trailing WHEN ... THEN ... ELSE ... END. A measure over CASE WHEN {Default Status} NOT IN ('11','14') THEN {Credit Exposure Amount} ELSE 0 END compiled to SUM('CASE') — a regulatory-quality bug for Anacredit-style risk metrics. Now computed-column expressions support CASE WHEN ... THEN ... [WHEN ...]* [ELSE ...] END, [NOT] IN (...), [NOT] BETWEEN ... AND ..., IS [NOT] NULL, [NOT] LIKE. Strict parsing: unconsumed tokens, missing ), unterminated CASE, WHEN without THEN, IN without (, BETWEEN without AND, IS without NULL all raise. AST nodes (CaseExpr / InList / Between / IsNull) already had codegen on every dialect — only the parser was missing the syntax. 28 new tests; full suite 2212 passed, 154 skipped.
v2.7.2'26 May 261
  • Strict OBML / QueryObject parsing — typos no longer slip through. Validator used to silently drop unknown keys: a measure with filtter: validated clean and compiled to SQL with no filter applied. Now every OBML object (dataObjects / columns / joins / dimensions / measures / metrics / filters / filterContext / grain / settings / examples / …) and every QueryObject surface (QueryObject / QueryFilter / Subquery / QuerySelect / QueryOrderBy / UsePathName) rejects unknown keys with the new UNKNOWN_PROPERTY error code and a "did you mean?" suggestion derived from the model's real fields — no flag to bypass. Implementation: extra="forbid" on every Pydantic model in models/semantic.py + models/query.py; allowlist check at every resolver parse site; FastAPI RequestValidationError handler translates Pydantic extra_forbidden into the same UNKNOWN_PROPERTY response shape REST clients get from the OBML side. Fixes #75. 21 new tests; full suite 2184 passed, 154 skipped.
v2.7.1'26 May 251
  • Hotfix: Gradio UI broken in admin-curated mode under v2.7.0. v2.7.0's MODEL_FILE removal dropped the legacy "auto-preload model into every new REST session" behaviour AND stopped returning model_yaml in /v1/settings; the bundled UI hadn't migrated to GET /v1/models, so its compile/execute flow tried to upload into a fresh user session and 403'd ("Single-model mode: model upload is disabled"). v2.7.1 restores both legs of the v2.6 contract only for single-MODEL_FILES deployments/v1/settings.model_yaml re-exposed; POST /v1/sessions re-seeds each new user session with the protected model. Multi-model deployments unchanged.
  • Cloud Armor rule #106 tightened. The LLM-API-recon deny rule's regex inadvertently caught OBSL's legitimate GET /v1/models alongside the OpenAI /v1/chat/completions probes it was meant to block. Tightened in orionbelt-infra so /v1/models reaches the API; OpenAI/Anthropic/Ollama recon paths remain denied.
v2.7.0'26 May 254
  • exists / nonexists filter operators — first-class primitive for "this row has (or doesn't have) a matching row in a related data object", compiled as a correlated EXISTS (SELECT 1 FROM …) subquery. Drives regulatory data-quality rules, coverage / anti-join reports, and any parent-has-child check. New subquery: payload names the target data object (the planner walks the model's existing joins: to derive correlation predicates — join columns are not restated), with optional pathName: to pin a secondary join and optional filter: list restricting target rows. WHERE only — HAVING is rejected with INVALID_FILTER_OPERATOR (correlation subject is out of scope after GROUP BY). Portable across all 8 dialects.
  • MODEL_FILE env var removed — deprecated since v2.4.0. Replace with MODEL_FILES=<path> (single-entry list is the direct equivalent). Startup logs a clear deprecation warning if the legacy name is still set, since pydantic-settings silently ignores unknown env vars.
  • Shortcut routes see MODEL_FILES-protected sessions/v1/schema, /v1/query/sql, /v1/query/execute, and friends now consult list_protected_session_ids() in addition to __default__ + user sessions. Pre-fix: any admin-curated model loaded via MODEL_FILES returned 404 from the unscoped endpoints.
  • physical_tables tracks EXISTS subquery targets — cache TTL / freshness invalidation now reflects every table the SQL reads. Pre-fix: an EXISTS OrderItems filter looked like a single-fact query for Orders, and child-table updates couldn't invalidate cached results.
  • query-schema.json enforces EXISTS payload shape — new allOf/if-then-else block: exists/nonexistssubquery required + value forbidden; every other op ⇒ subquery forbidden. Matches the runtime Pydantic validator.
  • Semantic Sidecar positioning propagated into the MkDocs site.
  • Test coverage strengthened: 15 new tests (shortcuts on protected sessions, EXISTS physical-tables tracking, HAVING+EXISTS rejection, MODEL_FILE deprecation warning, JSON schema constraints, raw-mode + EXISTS, nested-subquery-filter target tracking). Full suite 2161 passed, 154 skipped.
v2.6.1'26 May 241
  • Derived → window correctnessMoM Delta = {[Revenue]} - {[Revenue Prior Month]} now wraps the window even when only the derived metric is selected (or when both are selected together). Before: no LAG emitted in the lone-derived case; in the both-selected case the substitution baked the inner aggregate into the outer expression yielding Revenue - Revenue. After: "Revenue" - LAG("Revenue", 1) OVER (...) inlined at the outer SELECT regardless of which combination is selected. New "deferred derived metric" (DDM) path in compiler/window_wrap.py
  • CFL guard for two-column statistical aggregatescorr / covar_* / regr_* measures in a multi-fact query no longer compile garbage CORR(CAST(f0 AS VARCHAR) || ...) via the concat-count path. New UnsupportedAggregationForCFLError (inherits UnsupportedAggregationError so existing router catches translate it transparently) fires before the multi-fact branch. Single-fact queries with the same measures continue to compile via the star planner
  • Expression-based stat aggregate validationaggregation: corr with expression: "{a} + {b}" now rejected at model-load (collapsed to one scalar arg, producing invalid CORR((a + b))). Single-column stat aggs (stddev etc.) still accept expression: since STDDEV(<scalar>) is valid
  • Window CTE applies base measure dataType — a LAG over a decimal(18, 2) measure now operates on the cast value, mirroring cumulative_wrap
  • ORDER BY on window / cumulative / PoP metric resolves to the wrapped alias instead of the base measure expression — ORDER BY "Revenue Prior Month" DESC no longer silently rewrites to ORDER BY "Revenue" DESC
  • OSI converter emits all columns of multi-column measures — derived metrics referencing two-column corr measures no longer export wrong OSI SQL with only the first column
  • Calcite-compatibility shim extension — idempotent wraps (SUM / MIN / MAX / AVG / MEDIAN) now accepted on metrics and on non-matching measures so Dremio / Spark / Flink can satisfy "expression must be aggregated" via SUM(<metric>) et al. without mirroring each measure's declared aggregation. COUNT / COUNT(DISTINCT) remain restricted to matching-aggregation acceptance. AGG() and AGGREGATE() recognised as portable aliases for MEASURE(). New "Dremio SQL Runner — catalog flip & Calcite quirks" section in docs/guide/postgres-wire-bi-tools.md
  • Test coverage strengthened: tightened the previously-loose test_window_metric_compose_with_derived assertion to a literal "Revenue" - LAG( pattern match (reviewer-flagged); 12 new shim-rule tests; CFL rejection test (positive + negative); stat-aggregate-arity rejection tests; OSI multi-col derived-metric serialization test. Full suite 2105 passed, 154 skipped
v2.6.0'26 May 235
  • Trend Analysis primitives — four additive surface extensions for FP&A / finance workloads: partitionBy on MetricType.CUMULATIVE (per-dimension rolling windows like 12-month MA per country); new MetricType.WINDOW covering RANK / DENSE_RANK / ROW_NUMBER / NTILE / LAG / LEAD / FIRST_VALUE / LAST_VALUE; 9 statistical aggregates on Measure.aggregation (stddev, stddev_pop, variance, var_pop, corr, covar_pop, covar_samp, regr_slope, regr_intercept) with arity validation + dialect-gap rejection (MySQL drops correlation/covariance/regression; BigQuery + ClickHouse drop linear regression — hard error, no silent fallback); composition over DERIVED metrics for MA crossovers, MoM deltas, etc. New compiler/window_wrap.py runs after cumulative_wrap so window functions can rank cumulative outputs. New guide docs/guide/trend-analysis.md
  • OSI v0.2.0.dev0 compatibility — converter emits version: "0.2.0.dev0" to track the upstream OSI spec evolution from v0.1.1: primary_key first-class (composite supported, declaration order preserved); unique_keys first-class (lossless roundtrip via OBSL-vendor customExtensions since OBML has no native concept); field label first-class (round-trip via OBSL-vendor customExtensions); top-level dialects / vendors informational arrays; new MAQL dialect + GOODDATA vendor in the known-enum tuples; legacy _normalize_legacy_v01() reader promotes pre-v0.2 customExtensions payloads before parsing, so existing OSI v0.1.1 inputs continue to load. Vendored schema refreshed to upstream main; scripts/refresh-osi-schema.sh keeps it in sync
  • OSI input validation on POST /v1/convert/osi-to-obml — new optional input_validation field on ConvertResponse carries Draft 2020-12 schema errors + semantic errors for the source OSI document. Advisory by default — the endpoint still returns 200 with the converted output even when input fails strict v0.2 validation (the legacy v0.1 shim still produces correct OBML)
  • BREAKING (output format) — OBML → OSI emits OSI v0.2.0.dev0. Downstream consumers pinning to v0.1.1 will reject v2.6 output. Migration: parse with any v0.2-aware reader, or use the legacy v0.1 shim on the reverse direction (the converter still reads v0.1 inputs)
  • Propagated: OBML JSON Schema (new metric fields + WindowFunctionKind enum + statistical aggs); OBML reference markdown; OBSL ontology (new obsl:WindowMetric class + 6 datatype properties); MkDocs (new trend-analysis guide + model-format / osi guide refresh + comparison-doc refresh across dbt / Cube / LookML / Malloy / AtScale). Tests: +72 (43 trend-analysis unit + 9 OSI v2.6 roundtrip + 16 OSI v0.2 compat + 4 convert-endpoint integration). Full suite 2081 passed, 159 skipped
v2.5.0'26 May 197
  • Postgres wire protocol surface (pgwire) — fourth surface alongside REST / Flight / MCP. Native postgres://-protocol endpoint (port 5433, configurable) so any psql / pgjdbc / BI tool client talks to OBSL directly. Steps 1–4 land the hello-world handshake, semantic-SQL routing through SemanticRouter, pg_catalog / information_schema emulation via an embedded DuckDB catalog, and the extended-query protocol (Parse / Bind / Describe / Execute / Sync). New env vars PGWIRE_ENABLED / PGWIRE_PORT / PGWIRE_AUTH_MODE / PGWIRE_MAX_CONNECTIONS
  • Tableau Desktop end-to-end compatibility over pgwire / pgjdbc — every layer Tableau exercises during connect + dashboard query: Bind.result_formats honoured per column (binary FLOAT8 for pgjdbc's binary-transfer set), result-column alias preservation (SUM(x) AS "sum:x:ok" survives translator + compiler via a sqlglot-based router rewrite), shadow _obsl_pg_attribute / _obsl_pg_type TEMP views translate DuckDB internal type IDs to real Postgres OIDs (DOUBLE = DuckDB 23 → PG 701, BIGINT = 14 → PG 20, DATE = 15 → PG 1082), binary parameter decoding for the JDBC connect-check temp-table dance (INT2/4/8, BOOL, FLOAT4/8, TEXT/VARCHAR/NAME/BPCHAR, BYTEA), SQL rewrites for CREATE [LOCAL|GLOBAL] TEMP TABLE and SELECT … INTO TEMP TABLE, stub _pg_expandarray macro for getPrimaryKeys, canned locale + version probes (SHOW lc_collate family, SELECT current_catalog / current_role / session_user), and silent stripping of Tableau's HAVING (COUNT(1) > 0) tautology
  • Fixed: CFL leg joins tables referenced by measure-filter expressions — a measure with a filter on a sibling dim table expanded into the CFL leg as CAST(CASE WHEN Products.Category = 'Electronics' THEN Sales.Amount END) but the leg's FROM only carried the measure source + dim joins (missing FROM-clause entry for table "Products"); planner now scans own-measure expressions for table refs and adds reachable ones to leg_required before computing the common-root + join path
  • Fixed: ob-postgres driver classifies ADBC PyArrow types — ADBC returns cursor.description[i].type_code as PyArrow DataType + OpaqueType objects (NUMERIC wraps as OpaqueType(type_name='numeric')) not OID integers. The legacy isinstance(type_code, int) check fell through to STRING, surfacing every NUMERIC measure as TEXT and breaking Tableau's SUM. New _classify_type_code handles the Arrow DataType paths, OpaqueType repr-substring matching, and the OID-integer legacy psycopg2 path
  • Fixed: executor recognises ADBC OpaqueType in _arrow_type_to_hint — same root cause as the driver fix, in the executor's Arrow-fast-path type detection. pa.types.is_decimal returns False for OpaqueType; we now inspect type_name + map numeric/money/decimal → number, temporal → datetime, bytea → binary. Added a _PG_OID_TO_HINT table for the PEP-249 fallback (psycopg2/3 populates type_code with raw Postgres OIDs)
v2.4.0'26 May 1542
  • OrionBelt Semantic QL (OBSQL) — third surface in the OBSL / OBML / OBSQL trio. BI-style SQL against a per-model virtual table, translated to QueryObject and compiled through the existing pipeline. Bare-label form, MEASURE("…") marker (matches Snowflake SEMANTIC_VIEW + Databricks metric views), aggregate-wrap matching, WHERE-on-measure auto-routes to HAVING, no-FROM mode (single-model connection), raw mode via qualified "DataObject"."column" refs, OFFSET / NULLS FIRST/LAST. New REST endpoints /v1/query/semantic-ql[/compile]; new guide at docs/guide/semantic-ql.md
  • WITH ROLLUP / WITH CUBE first-class — QueryObject.grouping enum, GROUP BY ROLLUP/CUBE(...) + GROUPING(dim) AS _g_dim flag columns; ClickHouse emits trailing-modifier form; auto-order with NULLS FIRST default so subtotals + grand totals sort to the top of BI pivot tables. Real-DB tests across Postgres / ClickHouse / MySQL / DuckDB
  • Multi-model addressing — new MODEL_FILES=path1.yaml,path2.yaml,... env var pre-loads N models, each addressable via the standard Flight SQL "Database" field (Connection.setCatalog() / gRPC database header). New GET /v1/models discovery; per-model dialect via OBML settings.defaultDialect. MODEL_FILE deprecated, removal scheduled for v2.5.0
  • BREAKING: hard-block raw SQL + DDL/DML — removed env flags FLIGHT_ALLOW_RAW_SQL + FLIGHT_ALLOW_DATA_OBJECT_SQL; OBSL is read-only by design. New catalog mode answers SHOW / DESCRIBE / information_schema / pg_catalog / scalar probes from the model in-process via _handle_catalog_sql() — never touches the warehouse. New error codes RAW_SQL_REJECTED, WRITE_OPERATION_REJECTED
  • BREAKING: result cache KEY_VERSION bumped to 2 — hashes on session_id + model_id + dialect + compiled SQL instead of QueryObject JSON. OBSQL / QueryObject / OBML YAML all share one cache key per compiled SQL; pre-v2.4.0 entries are invalidated
  • Deterministic caching — auto-order on LIMIT without ORDER BY (engines return any N rows); non-deterministic SQL bypass for RAND() / NOW() / CURRENT_DATE / TABLESAMPLE via new cache/determinism.py. New NoCacheReason.NON_DETERMINISTIC_SQL surfaces in ttl_source
  • Flight extension wired into the result cache — semantic + OBML YAML queries participate in the same cache as REST /query/execute; OBFlightServer.__init__ accepts cache + cache_config. Catalog metadata views split: dimensions / measures / metrics for BI column pickers; _dimensions_metadata / _measures_metadata / _metrics_metadata for introspection (PoP and cumulative time_dimension/window/grain_to_date/time_grain surfaced)
  • OBSQL CLI at examples/obsql.py — pyarrow-based smoke-test for the Arrow Flight SQL surface. --model/-m for multi-model selection (gRPC database header), --list for catalog discovery via /v1/models
  • Reference endpoints for LLM / MCP / BI tool discovery — GET /v1/reference index; /v1/reference/obml + /v1/reference/obsql markdown grammar references; /v1/reference/schemas/{name} JSON Schema with application/schema+json content-type
  • Commerce battery across 8 dialects — shared parquet fixtures + battery runner; new live tests for BigQuery / Databricks / Snowflake with skip-if-exists data caching (BIGQUERY_RESEED/DATABRICKS_RESEED/SNOWFLAKE_RESEED force reload). ClickHouse / MySQL / Postgres tests migrated to the shared battery
  • Fixed: cache-key _normalize_sql collapsed whitespace inside quoted regions (distinct strings served each other's rows); SessionManager _is_expired() ignored session.protected (admin-loaded sessions deleted at TTL); Flight _cache_put_table wrote bare parquet instead of parquet_codec.encode envelope (cross-surface cache miss); Flight CommandGetTables/Columns ignored table_name_filter_pattern + protobuf field 1 (catalog) so DBeaver showed mixed dim/measure rows under each metadata view; star-planner GROUPING() referenced SELECT alias instead of group-by expression (Postgres / Snowflake / BigQuery rejected); CFL own-leg measure cast aligned with sibling NULL padding (resolves ClickHouse UNION ALL Variant typing); Databricks _ABSTRACT_TYPE_MAP override emits STRING for CFL NULL-padding (Databricks rejects bare VARCHAR); UI execute button + dialect refresh on page load (Cloud Run cold-start fix); Flight catalog SQL precomputes pa.Table at get_flight_info + do_action so JDBC clients see real schema (DBeaver no longer shows one result column); Ctrl-C left port 8815 bound (added server.wait()); MySQL NULLS FIRST/LAST only emits IS NULL workaround when it disagrees with MySQL default; CommandGetSqlInfo populated so DBeaver / Tableau no longer show Server: ?
v2.3.1'26 May 111
  • MySQL CAST(string) no longer emits invalid CHAR(65535) or silently-truncating CHAR(255)_compile_cast is now length-aware: VARCHAR lengths above 255 collapse to plain CHAR, lengths ≤ 255 are preserved (DDL paths still use the wider VARCHAR type)
  • Vendor-execution drift normaliser only coerces strings matching canonical Decimal form (no leading zeros, no scientific notation) — zero-padded IDs ("00123") and exponent-form strings ("1e3") stay distinct so a cross-vendor key-handling regression can't pass row-set equality
  • Tier 2 metadata gate uses sys.executable -m pytest instead of uv run pytest, so the snapshot pointer-validity check runs in any environment that runs the test suite (CI containers without uv on PATH)
v2.3.0'26 May 1010
  • Two-tier integration test framework: Tier 1 correctness ratifies query results via independent paths (aggregation invariance, hand-SQL reference, pandas baseline, metric algebra, CFL split, filter additivity, hierarchical rollup); Tier 2 drift snapshots compiled SQL + canonical-sorted rows per query (DuckDB exec + 8-dialect compile-only + metadata gate). Pure-OBML query files in tests/integration/correctness/queries/ with sidecar corpus.yaml manifest
  • Phase A vendor-execution sweep: every corpus query × DuckDB / Postgres 16 / MySQL 8 / ClickHouse via testcontainers, gated by pytest -m docker. 60/60 pass, zero xfails
  • Cumulative metrics now respect declared dataType — Cumulative Sales / Rolling 30 Day Sales emit CAST(... AS DECIMAL(p, s)) on both the inner cumulative_base CTE and the outer windowed aggregate
  • HAVING auto-includes referenced measures — pre-scans HAVING (recursively across QueryFilterGroup), adds any unseen measure to resolved.measures before base-object selection, and drops it from the final SELECT so the user only sees what they asked for
  • CFL NULL-pad type matches source column for COUNT-style aggregates — strict-typed engines (Postgres / MySQL / ClickHouse) now accept UNION ALL of NULL::TEXT with text-ID columns; numeric SUM/AVG keep the existing outer-CAST-target alignment
  • ClickHouse Decimal division precision: operands widened to Decimal(38, 14) so ratios survive (Return Rate no longer truncates to 0.03)
  • ClickHouse Decimal CAST rounds, not truncates: round(x, S) wrapped before CAST(... AS Decimal(P, S)) for cross-vendor parity
  • MySQL CAST(VARCHAR) translated to CAST(CHAR) at cast time (DDL paths keep VARCHAR); DECIMAL(38, 14) operand widening for ratio precision
  • Demo metric Rolling 30 Day Sales now decimal(18, 0) (was decimal(18, 2)) — smoothed metric where cent precision is engine-variant noise
  • Operator manual at docs/guide/correctness-and-drift-tests.md; dev quick-reference at tests/integration/README.md; CI lint job fixed (pyarrow mypy override)
v2.2.1'26 May 093
  • Bundled demo model rewritten with business-friendly spaced names; common base measures use short forms (Total Sales, Total Returns, Total Purchases, Total Shipments) and derived metrics follow suit (Return Rate, Average Sale, Cumulative Sales, MTD/YTD Sales); generic dims coexist with role-playing variants via via: to keep cross-fact queries unambiguous
  • Demo model pins settings.defaultDialect: duckdb so the UI dropdown auto-selects DuckDB on load
  • UI: Execute Query snaps the SQL Dialect dropdown to the API's effective execution dialect (from /v1/settings) before running, so previewing alternate-dialect SQL via Compile doesn't accidentally execute against the wrong engine
  • Fixed: ER diagram per-attribute right-edge clipping (CSS font-size override no longer fights Mermaid's column-width measurement)
  • Fixed: ER diagram attribute identifiers are camelCased from the business label (Sales ID → SalesID) with the spaced label rendered as the attribute's comment column; entity names with spaces are double-quoted so Mermaid renders them verbatim; join labels keep their business names
v2.2.0'26 May 0513
  • POST /v1/oneshot/batch — load (or reference) a model and run N independent queries in one round trip; partial-failure-by-default, fail-fast option, per-query and whole-batch timeouts
  • Model load deduplication: identical OBML bytes in the same session reuse the existing model_id (skips parse/validate/OBSL graph)
  • Freshness-driven result cache (file backend, off by default): TTL derived from each touched dataObject's refresh: contract; ETL heartbeat invalidates every dependent cached query in one call
  • New refresh: block on dataObjects (mode: static/scheduled/heartbeat/unknown) — round-trips through OSI and declared in the OBSL ontology
  • POST /v1/heartbeat (bearer-auth) — invalidate by physical database.schema.table
  • GET /v1/cache/stats, POST /v1/cache/sweep, POST /v1/cache/clear for observability and manual control
  • UI: side-by-side API Settings + Cache Stats panel with Refresh / Sweep / Clear; Query Results annotates each execution with (cache) or (database)
  • execution_time_ms on cache hits now reports actual fetch+decode wall time, not the persisted DB run time
  • Fixed: Gradio UI was capturing query_execute=False because mount in create_app() ran before the lifespan hook initialised deps.py globals
  • Cache stats timestamps unified in UTC; sweep cadence default raised from 15 min to 1 day (lazy TTL on read keeps freshness correct)
v2.0.1'26 Apr 27
  • /v1/settings now returns version and api_version (single-call feature negotiation)
  • Docs: Coalesce section moved next to Dimensions / Time Grain Override (above Measures)
v2.0.0'26 Apr 27
  • BREAKING: many-to-one joins are now strictly forward-only — reverse traversal raises UNREACHABLE_REQUIRED_OBJECT (declare bridges as many-to-many; see examples/movies.obml.yml)
  • BREAKING: CFL legs honor per-dimension via — role-playing dims no longer leak across UNION ALL legs
  • BREAKING: Postgres renderer emits DECIMAL(p,s) instead of NUMERIC(p,s)
  • BREAKING: sqlparse dependency removed (sqlglot now formats all SQL)
  • Query-level coalesce dimensions: select.dimensions accepts {coalesce: [...], as: alias} — merges role-playing dims with COALESCE
  • primaryKey column property: PK marker in ER diagram, obsl:primaryKey triple in OBSL graph; composite keys supported
  • API now returns sqlglot-pretty SQL on every compile/execute response — readable by default for AI agents and MCP
  • Vertically responsive Gradio UI (dvh-based) across all tabs
  • Ontology Graph tab: vis-network visualization with toggleable layers and node spacing slider
  • Cloud Armor rules block /ui/gradio_api/info, /ui/monitoring, /ui/openapi.json on the public demo
  • main branch protection enabled across all OrionBelt repos
v1.8.2'26 Apr 25
  • Release notes pending
v1.8.1'26 Apr 242
  • Fix CFL NULL padding type mismatch: use source column abstractType instead of measure resultType for UNION ALL legs (fixes PostgreSQL errors with COUNT_DISTINCT on string columns)
  • Fix UI dropdown pre-selection: pickers no longer auto-select first value
v1.8.0'26 Apr 224
  • Grain override: per-measure `grain:` with FIXED/RELATIVE modes, compiled as window functions OVER (PARTITION BY ...)
  • Filter context: per-measure `filterContext:` with CTE isolation, LEFT/CROSS JOIN strategies
  • OBSL ontology: 12 new properties (grain*, filterContext*, owner, dataType, format)
  • OSI roundtrip for grain and filterContext via custom_extensions
  • Dedicated guide page with examples (percent of total, parent total, unfiltered grand total)
  • 112 new tests (40 grain + 59 filter context + 13 OSI roundtrip)
v1.7.1'26 Apr 223
  • OSI converter roundtrip: full preservation for settings, owner, dataType, column metadata, dimension properties, metric format (22 new tests)
  • Docs favicon and MkDocs version pin fix
v1.7.0'26 Apr 205
  • Data types & numerical precision: CAST wrapping, type registry, dialect mapping, precision clamping
  • Timezone settings: `defaultTimezone` (IANA), `allowUtcFallback`, naive timestamp coercion, ISO 8601 serialization
  • HAVING on metrics: alias expansion for Postgres compatibility
  • Model settings in TPC-H example and sales model fixtures
  • All pre-existing mypy errors fixed
v1.6.2'26 Apr 191
  • Query execution in Gradio UI: "Execute Query" button + "Query Results" tab (visible when QUERY_EXECUTE=true)
  • Docker UI instructions in README
  • Gradio mount log message in embedded mode
  • Codex code review note in CLAUDE.md
v1.6.1'26 Apr 183
  • `model_json` input: load/validate endpoints accept JSON objects (no YAML escaping needed for LLMs)
  • Auto-parse stringified JSON in `model_json` field
  • Verbose 422 error messages with inline error codes/details for MCP consumers
v1.6.0'26 Apr 183
  • Extends/inherits model composition: deep-merge models via `extends_yaml` and `inherits_model_id`
  • Comprehensive malformed expression ref detection: 16 bracket patterns for metric and measure expressions
  • UI query pickers (dimension, measure/metric, column) with intelligent YAML insertion
  • UI editor toolbar: clear, undo, redo buttons on CodeMirror editors
v1.5.1'26 Apr 164
  • OBSL measure filter expression: `obsl:filterExpression` datatype property on measures (ontology, SHACL, spec, example updated)
  • Silently skip unreachable filters: both static and query-time filters on unreachable data objects are ignored instead of erroring
v1.5.0'26 Apr 166
  • Static model filters: top-level `filters:` YAML key with mandatory WHERE conditions, all operators (OBML + SQL-style), auto-join extension
  • ISO 8601 date/timestamp support in static and query-time filters (bare YAML dates, timestamps with timezone offsets)
  • Filter deduplication: skip query-time WHERE filters identical to static filters
  • OSI roundtrip preservation of static filters via custom_extensions
  • JSON Schema: staticFilterOperator enum (30 operators), typed value/values fields
  • Schema API: filters field in GET /schema response
v1.4.0'26 Apr 122
  • Session hardening: absolute max-age (SESSION_MAX_AGE_SECONDS), global session cap (MAX_SESSIONS → 429), per-session model cap (MAX_MODELS_PER_SESSION → 429)
  • Per-IP rate limiting on POST /sessions (SESSION_RATE_LIMIT, default 10/min)
  • 410 Gone for expired sessions (distinct from 404 Not Found)
  • expires_at / max_expires_at in session responses for proactive client refresh
  • Session lifecycle structured logging (create, expire, close, purge)
  • Default session purge respects single-model mode flag
v1.3.0'26 Apr 01 – 1013
  • OBSL-Core 0.1 RDF graph export (Turtle) and read-only SPARQL API (SELECT/ASK)
  • OWL axioms in OBSL: disjointness, functional properties, inverse properties
  • Extended metric profile: CumulativeMetric, PeriodOverPeriodMetric classes
  • obsl:synonym property (replaces SKOS alignment)
  • OBSL Turtle download button in Gradio UI
  • OBML reference endpoint (/v1/reference/obml)
  • OBSL guide page (docs/guide/obsl.md)
  • Fix Colab notebook: Mermaid via mermaid.ink, zombie subprocess cleanup, session-based model loading
  • Remove dead code (ErrorResponse, load_model_directory, _cleanup_session)
  • Rename OBSL/ ontology directory to ontology/
v1.2.2'26 Mar 281
  • Fix 4 bugs from code review: Flight state sync after auto-detection, shortcut 409 in single-model mode, test skip guards for optional packages, stateless /v1/validate
v1.2.1'26 Mar 273
  • Fix 11 bugs from deep code review (CR-01 through CR-11): reversed joins, SQL injection in table refs, default session purge, assert removal, filter value validation, duplicate YAML keys, recursive DFS, PoP fallback
  • Publish 11 packages to PyPI (orionbelt-semantic-layer + 10 drivers)
  • Prepare driver packages: add LICENSE, README, fix author name and license field
v1.2.0'26 Mar 22 – 2412
  • Bump version to 1.2.0
  • DuckDB integration tests for query execution layer
  • UnsupportedAggregationError and expose dialect limitations in API
  • cumulative metric support to OSI ↔ OBML converter
  • PostgreSQL integration tests via testcontainers
  • MySQL and ClickHouse integration tests via testcontainers
  • ob-* PEP 249 driver integration tests against real databases
  • infra/ from repo and gitignore it
  • period-over-period (PoP) metrics with 4-CTE date spine architecture
  • OSI converter support for period-over-period metrics
  • filtered measures with CASE WHEN wrapping and ratio metrics
v1.1.0'26 Mar 17 – 2231
  • Bump version to 1.1.0
  • Arrow Flight SQL execute support for all 7 database drivers
  • Update docs for Arrow support across all 7 drivers
  • Docker Hub pull instructions to README
  • TPC-H quickstart notebook, model, and Docker Hub badges
  • Update Docker Hub badge link to repositories page
  • redundant Docker-ready badge (Docker Hub badge covers it)
  • Link Docker Hub section heading to repositories page
  • OBML validator namespace, pluralize TPC-H data objects, make database/schema optional
  • QUERY_EXECUTE from FLIGHT_ENABLED for REST query execution
  • ob_flight __init__.py lazy to avoid pyarrow.flight import on db_router use
  • quickstart notebook: show SQL on execute, render mermaid ER dark
  • TPC-H composite join, cross-object measures, and notebook enhancements
  • NULL padding in CFL for dialects supporting UNION ALL BY NAME
  • quickstart notebook link to README Quick Start section
  • qualified DataObject.Column references in WHERE filters
  • OBML-only properties in OSI custom_extensions for roundtrip
  • description property to all OBML model objects and update OSI converter
  • filter groups (AND/OR/NOT) and relax dimensionsExclude validation
  • cumulative metrics (running total, rolling window, grain-to-date)
  • MySQL dialect and ob-mysql PEP 249 driver
v1.0.0'26 Mar 16 – 1714
  • Release v1.0.0: change license from Apache 2.0 to BSL 1.1
  • v1.0.0: model discovery API, query explain, /v1/ routing, owner field, prod hardening
  • Accept DISABLE_SESSION_LIST=true (403) in cloud run tests
  • split SQL/Explain panel in UI with detailed CFL leg explanations
  • DB-API 2.0 drivers, Arrow Flight SQL server, and query execution endpoint
  • Rename .env.example to .env.template with full settings, add MODEL_DIR for Docker volume mounts
  • ob-bigquery DB-API 2.0 driver for BigQuery (7th dialect)
  • ob-bigquery to driver tables in README.md and docs/drivers.md
  • Arrow Flight SQL and DB-API 2.0 badges and feature entries
v0.8.0'26 Mar 11 – 1611
  • single-model mode, /settings endpoint, and bump to 0.8.0
  • dimensionsExclude flag, dimension-only queries, and CFL fixes
  • dimensionsExclude feature and dimension-only query improvements
  • junction table fanout, remove -- prefix from UI errors
  • -- comment prefix to warnings in SQL output
  • BigQuery and DuckDB/MotherDuck dialect implementations
v0.7.0'26 Mar 103
  • OSI conversion REST API endpoints and bump to 0.7.0
  • Copy osi-obml converter into API Docker image
  • osi-obml from UI Docker image, move jsonschema to main deps
v0.6.0'26 Mar 02 – 1017
  • v0.6.0: Extract MCP server to separate repo, add OBML reference endpoint
  • architecture diagram to README and fix table formatting
  • structured error details for query compilation failures
  • Mermaid download buttons (.md and .png) to ER Diagram tab
  • numClass property to DataObjectColumn for LLM aggregation hints
  • numClass to YAML example on docs landing page
  • numClass to all numeric columns in UI example model
  • numClass only on numeric columns (int/float)
  • unused sqlalchemy and alembic dependencies
  • Update numClass descriptions and add numClass to README example
  • Simplify README Cloud Run and ER diagram sections
  • Rename Cloud Run section to API and UI Live Demo Hosting
  • synonyms property for LLM hints and map directly in OSI converter
  • jsonschema to ui extra for OSI validation in Gradio
  • Format API error details as readable list in UI compile output
  • QueryResolver god-class and remove dead code
  • Update OSI spec version from 1.0 to 0.1.1
v0.5.0'26 Feb 24 – 2718
  • Bump to v0.5.0: OSI import/export UI, CFL common root, customExtensions
  • osi-obml converter files, list OSI feature in README
  • order_by/filter fields, fix CFL filters and ORDER BY
  • /ui redirect from API: fixed via LB path rule instead
  • request body size limits via Cloud Armor and streaming middleware
  • structured errors from load_model, fix mutable defaults in schemas
  • load balancer configuration reference
  • CSP for Swagger UI and ReDoc doc endpoints
  • FastMCP from 2.x to 3.x
v0.4.0'26 Feb 21 – 2413
  • Bump to v0.4.0: expression syntax {[DataObject].[Column]}, fix docs, add deploy script
  • Mark resultType as informative only in schema, MCP reference, and docs
  • version badge to README and docs link to Gradio UI header
  • OBML single source of truth section to CLAUDE.md
  • Cloud Armor WAF policy export and apply script
  • Update demo link to point to Gradio UI
  • Set demo UI link to dark theme by default
  • link to ralforion.com on footer logo
  • resultType optional with sensible defaults
  • API security and split Docker images for Cloud Run
  • /robots.txt endpoint to stop crawler 404s
v0.3.0'26 Feb 19 – 2112
  • dialect-specific LISTAGG, ANY_VALUE, MEDIAN, MODE aggregations (v0.3.0)
  • version badge below TOC sidebar in MkDocs site
  • Hide version badge in left sidebar, show only in TOC sidebar
  • version logging to server startups and MCP registry metadata
  • Rename package to orionbelt-semantic-layer in pyproject.toml
  • MCP server registry metadata and ignore token files
  • Dockerfile for Cloud Run deployment with automated integration tests
  • Cloud Run integration test script and public API URL to README
  • Rewrite CLAUDE.md with commands, pipeline diagram, and architecture details
  • fanout detection for CFL multi-fact queries
  • query JSON schema, upload buttons, and improve Gradio UI layout
  • Gradio UI at /ui inside FastAPI when ui extra is installed
v0.2.1'26 Feb 18 – 194
  • ER diagram visualization with Mermaid (v0.2.1)
  • Update README with ER diagram feature, screenshot, and API endpoint
  • SQL Compiler screenshot to README
  • Update README with API-first messaging and rename screenshots
v0.2.0'26 Feb 181
  • secondary joins with usePathNames query support (v0.2.0)
v0.1.0'26 Feb 161
  • Initial commit: OrionBelt Semantic Layer
1372026-02-16 – 2026-03-24