Moved a subquery to a CTE
− from (select * from stg_payments − where paid) p + from paid p
equivalentPublished columns are identical. Nothing downstream can change.
For dbt and Snowflake teams
Semiring checks dbt models as you write them: join fan-out, NULLs, column lineage, broken downstream columns. Then it runs them on a local Snowflake and reviews every pull request for what the change does to your data. Built for people, and for the agents writing SQL beside them.
In development. dbt + Snowflake first.
One loop, from first keystroke to merged pull request
Concept demo
Problems 2
fan-outline 9
revenue is over-counted. stg_payments and stg_shipments each match many rows per order_id, so every extra shipment repeats p.amount.
1 order × 2 payments × 2 shipments = 4 rows
nullableline 4
revenue is NULL for orders with no payment. fct_revenue.revenue_usd declares not_null.
No query was run. No rows were read.
Lineage fct_orders.revenue
sum(p.amount)provenamount_cents / 100provensourceDownstream 6 models · 8 columns
Terminal local
$ dbt build -s fct_orders+ -t local
Found 214 models, 611 data tests
1 of 9 OK table fct_orders ....... 0.14s
2 of 9 OK table fct_revenue ...... 0.09s
3 of 9 FAIL not_null revenue_usd ... 0.03s
4 of 9 OK table dim_customers .... 0.11s
…
Done. PASS=8 ERROR=1 TOTAL=9 1.8s
Ran on your laptop. No warehouse was resumed.
Agent
Fix the fan-out in fct_orders. Keep one row per order.
semiring criteria fct_ordersjoin stg_shipments: multiple_matchesedit models/marts/fct_orders.sql+13 −7semiring check fct_orders0 findings · one row per order_id, provendbt build -s fct_orders+ -t localPASS=9 in 2.1sAggregated payments and shipments to one row per order before joining. Unpaid orders now report 0, not NULL.
OpenFix revenue fan-out in fct_orders#482
✓ semiring / semantic review 7 of 214 models rebuilt, locally
| fct_orders | changed | revenue, shipments: changes the value provenone row per order_id proven |
|---|---|---|
| fct_revenue +5 | affected | revenue_usd and 7 more columns inherit the new value |
| 207 models | untouched | No path from this change. Not rebuilt. |
Counterexample · synthesized rows, run locallyorder_id 1042 base revenue 240.00 head revenue 120.00
Static checks
Semiring reads the compiled SQL of every dbt model and works out what each column is made of, which rows survive, and how many times each one can repeat. It answers from the code. Nothing is executed and no data leaves your warehouse.
I want this first| order_id |
|---|
| 1042 |
| order_id | amount |
|---|---|
| 1042 | 80 |
| 1042 | 40 |
| order_id | id |
|---|---|
| 1042 | S1 |
| 1042 | S2 |
| amount | id |
|---|---|
| 80 | S1 |
| 80 | S2 |
| 40 | S1 |
| 40 | S2 |
sum(amount) returns coalesce, case without else and empty aggregates, then checks it against your not_null tests.flatten. Each path is graded proven, approximate or not traceable, so you know how far to trust it.unique and not_null tests it can already back.Semantic review in CI
A forty-line SQL diff can be a harmless refactor or a silent change to revenue. Semiring compares the meaning of both versions, tells you which columns and rows can differ, and looks for a concrete row that proves it.
I want this firstMoved a subquery to a CTE
− from (select * from stg_payments − where paid) p + from paid p
equivalentPublished columns are identical. Nothing downstream can change.
Tightened a join
− left join stg_shipments s + inner join stg_shipments s
changedChanges which rows. Order 1042 has no shipment: present in base, missing in head.
Dropped a column
− o.customer_id,
brokendim_customers reads fct_orders.customer_id, which is no longer published.
Local Snowflake
A Snowflake-compatible endpoint backed by DuckDB. Your connector, your dbt profile and your Snowflake SQL, pointed at 127.0.0.1. The edit-run loop stops waiting on a warehouse, and CI stops paying for one to check that a model compiles and its tests pass.
I want this first# profiles.yml local: type: snowflake host: localhost port: 8080 protocol: http $ dbt build --target local Found 13 models, 6 seeds, 27 data tests, 3 unit tests 1 of 46 OK created sql table model metricflow_time_spine [0.12s] 2 of 46 OK created sql view model stg_customers … Finished running 7 table models, 27 data tests, 3 unit tests, 6 view models in 2.63 seconds. Done. PASS=43 WARN=0 ERROR=0 SKIP=0 NO-OP=3 TOTAL=46
dbt-snowflake and the SQL REST API connect unchanged.QUALIFY, FLATTEN, VARIANT paths, IFF, LISTAGG, MERGE, stages and COPY INTO. Snowflake's dialect, including the parts DuckDB does not speak.For coding agents
Agents write SQL that looks right. Without a compiler that understands joins and NULLs, the first thing to tell them otherwise is production. Semiring gives them the same answers it gives you, as tool calls, with no warehouse credentials.
I want this first$ semiring impact fct_orders.revenue --json
{
"column": "fct_orders.revenue",
"downstream": { "models": 6, "columns": 8 },
"affected": [
{ "column": "fct_revenue.revenue_usd",
"effect": "value", "grade": "proven" },
{ "column": "rpt_finance_daily.revenue",
"effect": "value+rows", "grade": "approximate" }
]
}
explain, impact, criteria, diff: each a CLI command with a JSON form, usable from Claude Code, Codex or Cursor.Desktop IDE
A desktop editor for dbt projects where the checks, the lineage, the local warehouse and the agent share one index of your project. The demo at the top of this page is the idea.
I want this firstFrom the prototype
Early access
Semiring is in development and none of it is generally available yet. Leave an email and pick what you would use. The answers decide the order we ship in, and we will write when there is something for you to try.
We will email you when there is something to try. Nothing else.
The name
A semiring is a set with two operations, ⊕ and ⊗. Relational queries have that shape: a union adds rows and a join multiplies them. Read SQL as algebra and questions like “can this join repeat a row?” or “can this column be NULL?” stop being matters of opinion. They can be computed.
The mark is the two operators fused into one star, under half a ring.