semantic layer as a service

Semantic specs in.
Complex SQL out.

Your code or your agent sends a structured spec: measures, dimensions, filters, cohorts. 0sql resolves it against your governed model and returns one correct SQL statement for your warehouse, in microseconds. Nothing is guessed, nothing is invented, nothing is executed.

0sql never connects to your warehouse, so your data never leaves your network. The same spec and model always produce the same statement, with row-level security already compiled in. That is the security, determinism and reliability your application inherits the day it stops assembling SQL by hand.

Need the whole stack?Strata is this planner with execution, caching, dashboards, self-service and AI agents already around it. strata.do
request
curl -s https://app.0sql.io/projects/tpcds/branches/main/sql \
  -H "Authorization: Bearer zqk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "spec": {
      "projections": [
        {"field": "Web Net Paid"},
        {"field": "Web Sold Date"},
        {"field": "Net Paid"}
      ]
    }
  }'
shorthand
zsql sql --expr "web net paid, web sold date, net paid"
request.js
const res = await fetch('https://app.0sql.io/projects/tpcds/branches/main/sql', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    spec: { projections: [{ field: 'Web Net Paid' }, { field: 'Web Sold Date' }, { field: 'Net Paid' }] },
  }),
});
const { sql } = await res.json();
response sql, adapter postgres, planned in 70 µs
response.sql
WITH ag29130fae5cf6548d6ddb1bd37298b407 AS (
SELECT
	sum(T0."ws_net_paid") AS "msr501e4a8",
	T0."ws_sold_date_sk" AS "dimed56b67"
FROM
	web_sales T0
GROUP BY
	T0."ws_sold_date_sk"
), ag7098b0d0901f2eb16d14f9356f0bb2a0 AS (
SELECT
	T0."ss_sold_date_sk" AS "dimed56b67",
	sum(T0."ss_net_paid") AS "msr621f67c"
FROM
	store_sales T0
GROUP BY
	T0."ss_sold_date_sk"
)
SELECT
	A0.msr501e4a8 AS "Web Net Paid",
	COALESCE(A1.dimed56b67, A0.dimed56b67) AS "Web Sold Date",
	A1.msr621f67c AS "Net Paid"
FROM
	ag29130fae5cf6548d6ddb1bd37298b407 A0
	FULL OUTER JOIN ag7098b0d0901f2eb16d14f9356f0bb2a0 A1
		ON A1.dimed56b67 = A0.dimed56b67

Two measures from two fact tables, one conformed date. Each fact is aggregated at its own grain and stitched with a full outer join. Nothing double counts, and nobody modeled that query.

In

Business terms

Measures and dimensions by the names your business already uses, filters as plain values, formulas over [Measure]@m, cohorts as segments. As JSON, or as one shorthand line. Plus the security context of whoever is calling.

Out

One SQL statement

For the project's warehouse dialect, with joins, aggregations, security filters and masks already applied. Or an error naming what could not be resolved.

Never

A connection to your data

0sql holds the shape of your warehouse, never its contents. No credentials, no connection, no rows, no results, no cached answers. The statement comes back to you and runs inside your own network.

Model. Deploy. Request. Run.

Four steps, three of them yours. The semantic model is YAML in git; a branch deploys as a branch.

  1. 01 · model

    Tables, fields and joins in YAML

    Name a measure once. Declare joins with cardinality. The planner derives every reachable combination; you never write a query.

    models/sales/tbl.web_sales.yml
    name: Web Sales
    physical_name: web_sales
    datasource: warehouse
    cost: 100
    
    fields:
      - type: measure
        name: Web Net Paid
        data_type: decimal
        expression:
          sql: sum(ws_net_paid)
    models/sales/rel.sales.yml
    datasource: warehouse
    
    web_sales_item:
      left: Web Sales
      right: Item
      sql: left.ws_item_sk = right.i_item_sk
      cardinality: many_to_one
  2. 02 · deploy

    One command, per git branch

    Secrets are stripped before upload. The service validates the model, forms the universes, runs your tests and answers with counts and warnings.

    terminal
    $ zsql deploy
    deploy tpcds (6120 bytes) as tpcds/main (the production branch) to https://app.0sql.io
    deployed tpcds/main: 6 tables, 41 fields, 7 joins, 11 paths, 1 policies
    PASSED  category by net paid
    tests: 1 passed, 0 failed
  3. 03 · request

    A spec, or one line of shorthand

    Decorators for month grains and period-over-period, filters that land in WHERE or HAVING by themselves.

    POST /projects/tpcds/branches/main/sql
    {
      "spec": {
        "projections": [
          {"field": "Date", "decorators": [{"type": "truncate", "grain": "month"}], "order_by": "asc"},
          {"field": "Web Net Paid", "decorators": [{"type": "temporalize", "transform": "month_over_month"}]}
        ],
        "filters": [{"field": "Web Net Paid", "predicate": "greater_than", "value": "100"}]
      }
    }
    the same, as shorthand
    zsql sql --expr "month(date) asc, mom(web net paid), web net paid > 100"
  4. 04 · run

    In your code, on your warehouse

    Top five categories by revenue: a ranking CTE joined back, no window-function guesswork. Hand it to your driver.

    response.sql
    WITH ag5c9bb8d550197b3a219ae50f0b427905 AS (
    SELECT
    	T1."i_category" AS "dim30d09b7",
    	sum(T0."ws_net_paid") AS "msr0c0d023"
    FROM
    	web_sales T0
    	JOIN item T1
    		ON T0.ws_item_sk = T1.i_item_sk
    GROUP BY
    	T1."i_category"
    ORDER BY
    	sum(T0."ws_net_paid") desc
    LIMIT 5
    )
    SELECT
    	sum(T0."ws_net_paid") AS "Web Net Paid",
    	T1."i_category" AS "Category"
    FROM
    	web_sales T0
    	JOIN item T1
    		ON T0.ws_item_sk = T1.i_item_sk
    	INNER JOIN ag5c9bb8d550197b3a219ae50f0b427905 AS A2
    		ON T1."i_category" = A2.dim30d09b7
    GROUP BY
    	T1."i_category"

Security, determinism, reliability

Three properties an application cannot easily give itself, and the reason to put a semantic layer between your product and your warehouse. The planner behind this API has run in production at Netflix scale for years inside Strata.

Your data never leaves your network

0sql is given the shape of your warehouse, not its contents: table names, column expressions, join conditions and security policies. It holds no credential, opens no connection and reads no row. You deploy a model, you send a spec, you get a statement back, and you run that statement yourself, inside your own perimeter, against your own database. Nothing a customer of yours ever typed, and no number anyone ever read, passes through us.

your network

  • your application
  • your warehouse, and the rows in it
  • your credentials
  • every result anyone sees
spec + context one SQL statement

0sql

  • your semantic model
  • names, expressions, joins, policies
  • the planner
  • no connection, no credentials, no rows

Specs and security contexts are planned in memory and are never stored or logged. The only thing kept between requests is the model you deployed. See what is stored.

Row-level security in the SQL

Policies live in the model. Each request carries a security context from your auth system. A policy that fires becomes a WHERE on the context dimension, or a CASE mask. A branch with policies refuses to plan without a context.

request
{
  "spec": {"projections": [{"field": "Country"}]},
  "context": {
    "email": "tank@matrix.com",
    "groups": [
      {"name": "CC-TMNT", "tags": ["call_center_id:TMNT"]},
      {"name": "CC-AVCL", "tags": ["call_center_id:AVCL"]}
    ]
  }
}
response.sql
SELECT
	T0."cc_country" AS "Country"
FROM
	call_center T0
WHERE
	LOWER(T0."cc_call_center_id") IN ('tmnt', 'avcl')

The same spec always returns the same SQL

Byte for byte, on every deploy and every replica. Equal-cost join routes and merge order are settled by field name rather than by row id, so planning is not a coin flip. Pin a statement with a test and it runs on every deploy: a model edit that would change the SQL your application depends on fails the deploy instead of surprising you in production.

tests/revenue_by_category.yml
name: revenue by category
projections:
  - Category
  - Net Paid
assert_sql: |
  SELECT
  	T1."i_category" AS "Category",
  	sum(T0."ss_net_paid") AS "Net Paid"
  FROM
  	store_sales T0
  	JOIN item T1
  		ON T0.ss_item_sk = T1.i_item_sk
  GROUP BY
  	T1."i_category"

The planner is held to byte-for-byte parity with the engine that runs Strata, case by case, on every build.

Complex measures, at request time

Calculations reference measures with [Name]@m, dimensions with [Name]@d, and each other by alias. Conditional counts, ratios across fact tables, window functions over a calculation: the planner places each at the right node.

request
{
  "spec": {
    "projections": [
      {"field": "Category"},
      {"alias": "Actives", "calculation": true, "data_type": "integer",
       "sql": "count(distinct [Category]@d)"},
      {"alias": "Retention Rate", "calculation": true,
       "sql": "[Actives] / nullif(first_value([Actives]) over (order by [Category]@d), 0)"}
    ]
  }
}
response.sql
SELECT
	A0.dim30d09b7 AS "Category",
	count(distinct A0.dim30d09b7) AS "Actives",
	count(distinct A0.dim30d09b7) / nullif(first_value(count(distinct A0.dim30d09b7)) over (order by A0.dim30d09b7), 0) AS "Retention Rate"
FROM
	(
SELECT
	T0."i_category" AS "dim30d09b7"
FROM
	web_sales_sum T0
GROUP BY
	T0."i_category"
) A0
GROUP BY
	A0.dim30d09b7

Cohort beside baseline, one row

A segment is a population keyed on a dimension. Apply it to the whole query, or to one measure, so a cohort's number sits next to everyone's in the same result. No correlated subquery, no second request.

request
{
  "spec": {
    "projections": [
      {"field": "Product Name"},
      {"field": "Web Net Paid", "alias": "Book Buyers' Web Net Paid"},
      {"field": "Net Paid"}
    ],
    "segments": [{
      "name": "Book Products",
      "keys": ["Product Name"],
      "filters": [{"field": "Category", "predicate": "equals", "value": "books"}],
      "apply_to": ["Book Buyers' Web Net Paid"]
    }]
  }
}

The segment CTE joins inside the Web Net Paid aggregation only; Net Paid is untouched. See the SQL.

Refusals, not wrong numbers

A measure asked for by a dimension its fact cannot reach is an error with a class and a message, not a fan-out. Misspelled fields resolve by trigram similarity when one clearly leads, and the response says so under corrections.

422
{
  "error": {
    "class": "Planner::ResolutionError",
    "message": "No universe can resolve the query: 'Store Name' is not reachable from a fact that holds 'Web Net Paid'."
  }
}
  • /explain returns the node tree, join routes, strategies and microsecond timings.
  • /explore returns the dimensions and measures a query can still add and stay resolvable.
  • /fields?q= finds fields by name, synonym or similarity, for your pickers and your agents.

zsql

A CLI that stays out of the engine

zsql scaffolds projects, deploys branches, runs tests and opens a repl. It plans nothing itself: every line goes to the service, so the SQL you see in the terminal is the SQL your application gets.

  • zsql init · new table · new relation scaffold a project and its YAML.
  • zsql check · deploy --watch · test validate, deploy the checked-out branch, assert SQL.
  • zsql sql · explain · explore · repl shorthand in, SQL out, with corrections and timings.
  • zsql fields · tables discover what a branch holds, for your pickers and your agents.
CLI reference
terminal
$ zsql
tpcds/main on https://app.0sql.io: 6 tables, 41 fields, 7 joins, 1 policies
zsql> category top 5 by web net paid, web net paid
-- datasource: Warehouse
WITH ag5c9bb8d550197b3a219ae50f0b427905 AS (
  …
LIMIT 5
)
SELECT
  sum(T0."ws_net_paid") AS "Web Net Paid",
  T1."i_category" AS "Category"
…
-- round trip 38 ms
zsql> .explore category, net paid ? customer
customer_id     dim  Customer Id     Customer        ~1.0
customer_city   dim  Customer City   Customer        ~0.6
-- can add 31 dimensions, 9 measures · server 212 us
zsql> departmnt, net paid
departmnt → Category (~0.8)
…

Whatever composes the spec

A spec is data, so anything that can build a JSON object can drive the planner: a query builder, a scheduler, a notebook, an agent. None of them need to know SQL, and all of them get the same joins, the same grain and the same security.

AI agents

Let the model choose fields, not SQL. The agent reads the catalogue from /explore and emits a spec; the planner decides the joins, the grain and the security. A combination the model cannot have is refused with a reason, so a hallucination becomes an error instead of a confident wrong number. Point it at /docs/llms.txt.

Embedded analytics

Ship a query builder in your product without writing a SQL generator or a row-level security layer. Your UI emits specs, your backend runs the statement, and each tenant sees only their rows because the context says so.

BI and data apps

A governed measure layer for a notebook, an internal tool or a new BI front end. One definition of revenue, every dialect, every surface.

Platform teams

Model in git, review in pull requests, deploy per branch, test the SQL in CI. Give every team a read-only query key and keep the credentials to yourself.

One model, thirteen dialects

The adapter on the datasource decides quoting, date functions, join support and CTE strategy. Hot and cold tiers, partitions and aggregate tables route each request to the cheapest table that can answer it.

plan time
~70 µs
per request, release build
warehouses
13
one model, one request, every dialect
rows we can see
0
no connection, no credentials, no logs, no cache
measure kinds
5
standard, compound, snapshot, inclusion, exclusion

Need the whole stack?

0sql is the layer. Strata is the platform.

0sql returns SQL and stops there, because you are building the product around it. If what you want is the product, Strata is the same planner with everything else already attached: query execution across federated engines, aggregate awareness and hot tiers, dashboards that are good by default, self-service for non-technical users, and delivery through exports, subscriptions and Google Sheets.

It is also where the agents already live. Strata ships AI analytics over the same governed model, so a non-technical user or an agent can ask broad, cross-domain questions and get answers that are correct by construction, without you writing the retrieval layer, the chart layer or the guardrails.

The model format is the same one, so this is not a decision you have to get right today. A semantic model you write for 0sql is a semantic model Strata can run.

Stop generating SQL. Start declaring it.

Get a key, install zsql, model three tables, deploy, send your first spec. The quickstart walks through every step with the SQL that comes back.

Free while we work out whether this should exist. What that means.