A semantic-layer transpiler: one neutral IR, many dialects.
Built for benchouse.ai, the independent leaderboard for analytics agents, and open-sourced for everyone.
Where sqlglot translates across SQL
dialects, semglot translates across semantic-layer dialects (dbt
semantic models, Snowflake Cortex, Snowflake semantic views, and more) through
one neutral intermediate representation (IR).
You point it at a source layer, pick a target dialect, and it writes the equivalent layer out. Because everything routes through the IR, adding a dialect adds every conversion into and out of it, not just one.
A source is read into the IR; a target is written from it. dbt is
both, so dbt to dbt is a lossless round-trip.
| Dialect | Source | Target |
|---|---|---|
dbt |
✓ | ✓ |
cortex |
✓ | |
snowflake-semantic-view |
✓ | |
supersimple |
✓ | |
nao-yaml |
✓ | |
nao-context-rules |
✓ |
Adding a dialect is small, self-contained work: implement the Dialect interface
(Parse, Emit, or both) and register it, and every conversion to and from it
comes for free. Missing one you need? Please
open an issue or PR.
go install github.com/benchouse/semglot/cmd/semglot@latest
# or, from a clone:
go build -o semglot ./cmd/semglotbuild transpiles a source semantic layer into a target dialect. Builds are
configured with named profiles in semglot.yaml:
# semglot.yaml
profiles:
catalog:
source: ./models
target-dialect: snowflake-semantic-view
output: ./out
database: ANALYTICS
model-name: catalogGiven a small dbt table:
# models/schema.yml
models:
- name: dim_product
description: Product dimension.
columns:
- name: product_id
description: Product surrogate key.
data_type: number
constraints:
- type: primary_key
- name: category
description: Product category.
data_type: varchar
- name: title
description: Product title.
data_type: varcharrun the profile:
semglot build --profile catalogsemglot writes out/definition.md with the create statement:
create or replace semantic view CATALOG
tables (
DIM_PRODUCT as ANALYTICS.MAIN.DIM_PRODUCT primary key (PRODUCT_ID) comment='Product dimension.'
)
dimensions (
DIM_PRODUCT.PRODUCT_ID as dim_product.PRODUCT_ID comment='Product surrogate key.',
DIM_PRODUCT.CATEGORY as dim_product.CATEGORY comment='Product category.',
DIM_PRODUCT.TITLE as dim_product.TITLE comment='Product title.'
)
;--profile <name>selects a profile from the config. Required.--config <path>points at the config file. Defaults to./semglot.yaml.
Anything a target dialect can't express is reported rather than dropped silently
(e.g. a NOTES.md sidecar listing metrics that don't map).
A profile is a complete, self-contained build. Every field:
# semglot.yaml
profiles:
view_prod:
source: ./models # required. dbt source dir, or a list of dirs
source-dialect: dbt # optional. default: dbt
target-dialect: snowflake-semantic-view # required
output: ./out/view # required. directory to write into
database: ANALYTICS # required for Snowflake targets (cortex, snowflake-semantic-view)
schema: SEM # optional. default: MAIN (schema of the source tables)
view-schema: SEM_VIEWS # optional. schema for the emitted semantic-view object; defaults to schema
model-name: catalog # optional. default: source dir name
description: Curated view. # optional- Each profile is independent: there is no shared or inherited config. Staging and
production are two profiles that differ only in
databaseandoutput. - Omitted optional fields take defaults:
source-dialectisdbt,schemaisMAIN, andmodel-nameis the source directory name. buildfails clearly when the config is missing or unparseable, the--profileis not found, a required field (source,target-dialect,output) is absent, or a Snowflake target has nodatabase.
Every dialect maps to the same neutral IR, but targets differ in how much of it
they can express. This is what each target emits today (dbt is currently
the only source).
| Feature | dbt |
cortex |
snowflake-semantic-view |
supersimple |
nao-yaml |
nao-context-rules |
|---|---|---|---|---|---|---|
| Tables | ✓ | ✓ | ✓ | ✓ | ~ | |
| Columns | ✓ | ✓ | ✓ | ✓ | ✓ | ~ |
| Time dimensions | ✓ | ✓ | ~ | ✓ | ✓ | ~ |
| Descriptions | ✓ | ✓ | ✓ | ✓ | ~ | ✓ |
| Data types | ✓ | ✓ | ✓ | |||
| Primary keys | ✓ | ✓ | ✓ | ✓ | ||
| Relationships | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Metrics (aggregations) | ✓ | ✓ | ✓ | ✓ | ~ | ✓ |
| Ratio & derived metrics | ✓ | ✓ | ✓ | ~ | ✓ | ✓ |
| Synonyms | ~ | ✓ | ≈ | ≈ | ||
| Enums / allowed values | ✓ | ~ | ≈ | ≈ | ✓ | ✓ |
✓ structured · ≈ rolled up as text in a description or comment · ~ partial · blank not emitted.
Where a target has no native slot for something, semglot degrades rather than drops it:
≈means the value survives only as text inside a parent's description or comment (Values: …), not as a structured field. Enums and synonyms fold this way when the target has no native slot for them.cortexis the exception: it has a native synonyms field and keeps enum sample values.supersimpleemits division ratios; other arithmetic is deferred to aNOTES.mdsidecar.nao-yamlis a flat, model-global document, so it has no table grouping.nao-context-rulesis prose, so it lists only elements that carry a description or synonyms.
MIT. See LICENSE.