What Actually Happened When I Deployed Apache Ossie to Databricks, Snowflake, and dbt
This is part two of a series on Apache Ossie. Part one covered the concept: why every platform built its own semantic layer, and why more than fifty organizations, direct competitors among them, backed one shared interchange format for the definitions layer. It ended with a promise: take the same model, unmodified, and see what actually happens when you run it through real converters against real platforms.
So that’s what this post is. One semantic model, three targets, all three deployed against real accounts and queried live, not just converted and eyeballed.
Worth setting expectations before diving in: the Apache Ossie repository itself hasn’t shipped a stable release since entering the Apache Incubator, everything here runs against pre-release code. That’s the honest explanation behind a good chunk of what follows, not every gap below is a design flaw, several are just what “pre-release” actually looks like in practice.
The model
Deliberately small: two datasets, one relationship, three metrics.
version: 0.2.0.dev0
semantic_model:
- name: sales_demo
description: Minimal orders/customers demo model for the OSI meetup talk
datasets:
- name: customers
source: ossie_demo.main.customers
primary_key:
- customer_id
description: One row per customer
fields:
- name: customer_name
expression:
dialects:
- dialect: ANSI_SQL
expression: "UPPER(customer_name)"
- dialect: SNOWFLAKE
expression: "INITCAP(customer_name)"
- dialect: DATABRICKS
expression: "LOWER(customers.customer_name)"
label: Customer Name
datatype: String
- name: customer_segment
expression:
dialects:
- dialect: ANSI_SQL
expression: customer_segment
label: Customer Segment
datatype: String
dimension:
is_time: false
ai_context:
instructions: Splits customers into Enterprise and SMB. Sales commissions and the CEO's board deck are both based on this.
- name: orders
source: ossie_demo.main.orders
primary_key:
- order_id
description: One row per order, references customers via customer_id
fields:
- name: order_id
expression:
dialects:
- dialect: ANSI_SQL
expression: order_id
datatype: Integer
- name: customer_id
expression:
dialects:
- dialect: ANSI_SQL
expression: customer_id
datatype: Integer
- name: order_date
expression:
dialects:
- dialect: ANSI_SQL
expression: order_date
label: Order Date
datatype: Date
dimension:
is_time: true
- name: order_amount
expression:
dialects:
- dialect: ANSI_SQL
expression: order_amount
label: Order Amount
datatype: Decimal
ai_context:
synonyms:
- amount
- order total
- order value
relationships:
- name: orders_to_customers
from: orders
to: customers
from_columns:
- customer_id
to_columns:
- customer_id
metrics:
- name: total_revenue
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(order_amount)
description: Total revenue across all orders
datatype: Decimal
ai_context:
synonyms:
- revenue
- sales
- name: order_count
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(*)
description: Number of orders
datatype: Integer
- name: avg_order_value
expression:
dialects:
- dialect: ANSI_SQL
expression: AVG(order_amount)
description: Average order value
datatype: Decimal
ai_context:
synonyms:
- AOV
- average order value
- average purchase value
instructions: This is the number the CEO checks every Monday.
customer_name gets three separate expressions, one per SQL dialect, deliberately, to see which one each converter actually reaches for. ai_context shows up three times, in two shapes: a plain synonyms list on order_amount and total_revenue, free-text instructions alone on customer_segment, and both together on avg_order_value. instructions is the field the spec describes as guidance for AI tools, the kind of context a human would otherwise explain out loud, and it turns out to matter a lot for what follows.
Same two tables and three metrics went to Databricks, Snowflake, and dbt. Everything below is what came back.
Databricks
Of the three converters, this is the one whose conversion logic itself never produced a wrong result. ossie-databricks flattens both datasets into one Unity Catalog Metric View: one fact table, a joins block for the rest, every field collapsed into a single dimensions list. That doesn’t mean the deploy was friction-free, just that every bit of friction traced back to the source model, not the tool.
Flattening creates a name collision. orders.customer_id (the foreign key) and customers.customer_id (the primary key) both want to become a dimension named customer_id once Databricks flattens everything into one namespace, and Metric Views require unique dimension names, full stop. The fix: drop customer_id from customers.fields, keep it only in primary_key, enough for Databricks to derive rely.at_most_one_match on the join without a naming conflict.
With that one line changed, the deploy is real, against an actual workspace. customers and orders already exist by this point, created and populated with synthetic rows earlier in the same script:
CREATE OR REPLACE VIEW ossi.test.sales_demo_metrics
WITH METRICS
LANGUAGE YAML
AS $$
...
$$
Then queried with plain SQL:
SELECT customer_segment, MEASURE(total_revenue), MEASURE(avg_order_value)
FROM ossi.test.sales_demo_metrics
GROUP BY customer_segment
customer_segment total_revenue avg_order_value
enterprise 950.75 316.92
smb 40.00 40.00
Real numbers, worth flagging up front because it means the disagreements below are all about metadata, not about whether the aggregation itself is correct.
ai_context.synonyms survives; instructions has nowhere to go. Metric Views have a native synonyms: field per dimension and measure, and the converter maps ai_context.synonyms straight onto it: confirmed in the generated YAML and confirmed again after a real deploy, both total_revenue’s and avg_order_value’s synonyms showed up correctly. instructions, the free-text field, has no equivalent slot in the Metric View spec at all. The converter’s own code is blunt about it: “Object-form ai_context is handled separately (synonyms map natively; instructions/examples are dropped).” Not a bug, just a ceiling: Metric Views were designed around discoverability synonyms for Genie and AI/BI tooling, not open-ended guidance text.
Snowflake
ossie-snowflake produces Snowflake’s own Semantic View YAML, and SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML builds the object directly from it: no stage, no Cortex Analyst call required, just a native, SQL-queryable object. base_table still needs pointing at wherever the tables actually get deployed rather than the placeholder path baked into the demo model, the same routine substitution Databricks needs too, unremarkable on its own.
The one genuine gap: Snowflake’s native schema wants metrics nested inside the table they aggregate over, the same way it already scopes dimensions and facts to a table, not sitting in a flat list of their own. ossie-snowflake emits them as a flat top-level list instead, sibling to tables/relationships, because the OSI source doesn’t attach a table to a metric in the first place: metrics live at the semantic-model level, not on any one dataset, so the converter has nothing to attribute them with. Submit that flat list as-is and Snowflake rejects it outright with Unsupported expression in the definition of derived metric <NAME>, regardless of aggregate function or dialect, confirmed against Snowflake’s own AI-suggested-metric output, which nests metrics per table by default. The fix: decide which table owns them yourself and nest them there before deploying, fine for three order-level metrics here, a real limitation for a model with metrics spanning multiple tables. Databricks never runs into this: not because it attributes metrics correctly, but because flattening everything into one view leaves no table-scoped structure to attribute them to in the first place.
With both fixed, the deploy is real too: warehouse, database, and schema created by the script itself, no pre-existing resources needed, tables created, then the Semantic View itself, straight from the prepared YAML:
CALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML(
'ossie_demo.public',
$$
...
$$
)
Then queried live:
SELECT * FROM SEMANTIC_VIEW(
ossie_demo.public.sales_demo
METRICS total_revenue, order_count, avg_order_value
DIMENSIONS customer_segment
)
Same numbers as Databricks: enterprise $950.75 / 3 orders / $316.92 average, smb $40.00 / 1 / $40.00, and this time order_count’s bare COUNT(*) works cleanly too.
Snowsight Workspaces also has a native Ossie-file upload with no Python conversion step at all, but it only accepts the older version: 0.1.1, not ossie-snowflake’s 0.2.0.dev0.
dbt
Tested against dbt-core==1.12.3, a snapshot of that one version, not a verdict on where dbt’s Ossie support ends up. The semantic layer in dbt is MetricFlow. Unlike Databricks and Snowflake, whose Ossie support each lives in a small standalone converter package that just works, dbt’s equivalent, apache-ossie-dbt (converters/dbt in the apache/ossie repo), has real problems at every layer.
The bugs. Its own CLI (ossie-dbt ossie-to-msi) crashes on every input with a Pydantic v1/v2 mismatch, even on the package’s own documented Python API example, copy-pasted verbatim. Worth reading as “broken right now,” not a structural finding. Call the converter underneath it directly instead, bypassing the CLI, and it runs cleanly: zero exceptions, zero conversion issues reported. The bugs are in what it produces, not in whether it runs:
- No
agg_time_dimensionanywhere, even though the source hasdimension.is_time: truesitting right there onorder_date. MetricFlow, the query engine underneath, requires this for every query regardless of grouping. order_countgets attached to the wrong semantic model entirely (customersinstead oforders), because the converter resolves ownership by finding a real column name in a metric’s SQL expression, and a bareCOUNT(*)has none to find, exactly the kind of gap part three gets into.- No MetricFlow time spine configured at all, since the converter works on an isolated Ossie file with no visibility into the dbt project it’ll actually run inside.
A fourth issue looked like a bug and turned out to be the same conflict from the Databricks section again, just between a different pair this time: customers came out with zero entities, because this converter only turns a primary_key column into an entity if it’s also declared as a plain field on that dataset, and customer_id had been dropped from customers.fields to satisfy Databricks’ flattening. Same field, opposite needs, same as before, just Databricks versus apache-ossie-dbt this time instead of Databricks versus dbt-core’s native loader. Fixed by adding customer_id back as a field, on dbt’s copy of the model only.
Getting it to actually run. All three real bugs are patchable by editing the compiled manifest directly:
uv run mf query --metrics total_revenue,avg_order_value --group-by customer_id__customer_segment
customer_id__customer_segment total_revenue avg_order_value
-------------------------------- --------------- -----------------
smb 40 40
enterprise 950.75 316.917
Matching Databricks and Snowflake exactly, down to the cent. order_count doesn’t come back clean even patched: its COUNT(*) compiles to SUM(CASE WHEN * IS NOT NULL THEN 1 ELSE 0 END), which DuckDB rejects outright, a code-generation issue underneath the metadata fix. The same bare COUNT(*) worked cleanly on Snowflake, so this one’s specific to dbt’s code generation, not something inherent to the metric itself. And the patch doesn’t stick: any dbt command that recompiles, or re-running the converter, wipes it clean again.
On ai_context specifically: dbt drops all of it. Not just instructions, everything, synonyms included. The converter parses the field just fine, ai_context: Optional[OSIAIContext] = None sits right there in its own models, but nothing in the actual conversion logic ever reads it, confirmed by grepping the output manifest for any trace of a synonym. Databricks and Snowflake at least keep synonyms; dbt keeps none of it. Given how new this package is, and how fast it’s still moving, I’d genuinely expect that gap to close before this stays true for long: it’s the kind of loose end that’s easy to fix once someone notices it’s worth carrying forward.
What holds across all three
Zoom out from the individual bugs, and the pattern is consistent enough to be the actual headline: ai_context.synonyms is the one piece of AI-facing metadata every converter treats as a first-class citizen; instructions and examples aren’t a first-class citizen anywhere yet. Databricks keeps synonyms and drops the rest by design. Snowflake’s working path does the same. dbt drops all of it, synonyms included, simply because nothing downstream reads the field yet. Three independent implementations, three different reasons, the same net result: write a synonym list and it survives; write anything richer, and today, it doesn’t travel anywhere.
Two smaller patterns worth naming too. custom_extensions (the spec’s other escape hatch, for vendor-specific metadata that doesn’t need to mean anything outside that vendor) is silently dropped by every converter tested here, the same way instructions is; nothing in this model round-trips a custom_extensions block back out the other side. And is_time gets handled three different ways: Snowflake keeps time dimensions in their own time_dimensions: block, Databricks flattens them into regular dimensions with no time marker left at all, and dbt’s converter doesn’t act on the field reliably at all, even though MetricFlow underneath needs an aggregation time dimension for every query. Three vendors, three shapes, for a concept every one of them cares about.
The core spec is small enough that fifty-plus organizations, direct competitors among them, could agree to sign onto it. That’s a real accomplishment, and the numbers above prove the basic mechanism works: one file, converted three different ways, produces the same revenue number every time. But the smallness has a cost, and it’s visible in exactly the field built for the moment this whole project is supposed to matter most: grounding an AI agent’s answer. Right now, that field is the thinnest-supported part of the spec, everywhere I looked.
Everything above, the model, both deploy scripts, the fixes, is in ossie_example, if you want to run it yourself.
Next: where this project goes from here, and what an Ossie path into Power BI specifically would need to look like.