Skip to main content

semql

Pure-Python compiler from a semantic spec to backend SQL. Define cubes (dimensions, measures, time-dimensions, joins) once; emit correct, parameterised SQL for Postgres, ClickHouse, DuckDB, Snowflake, BigQuery, and the analytics engines Redshift, Trino and Databricks.

SQL Server, MySQL and Oracle ship as experimental / opt-in dialects: sqlglot transpiles their date_trunc / percentile to best-effort forms that aren't exercised in CI, so enable them deliberately by passing experimental_dialects() through the compiler's dialects= override and verify the SQL on a live instance. (Gap-filling time-spines — fill_nulls — are not yet implemented for any of the six new dialects.)

semql does no I/O: catalogs are Python data; the compiler returns SQL + bound params; running the SQL is the caller's job. Sibling packages add LLM-planner prompt fragments (semql-prompt), MCP exposure (semql-mcp) and ER diagrams (semql-erd).

Install

pip install semql

Quick start

from semql import (
    Dialect,
    Catalog,
    Cube,
    Dimension,
    Measure,
    SemanticQuery,
)

orders = Cube(
    name="orders",
    dialect=Dialect.POSTGRES,
    table="orders",
    alias="o",
    measures=[
        Measure(name="revenue", sql="{o}.amount", agg="sum", unit="currency"),
    ],
    dimensions=[
        Dimension(name="region", sql="{o}.region", type="string"),
    ],
)

catalog = Catalog([orders])
compiled = catalog.compile(
    SemanticQuery(measures=["orders.revenue"], dimensions=["orders.region"]),
)
# compiled.sql, compiled.params, compiled.columns, compiled.dialect

The {o} placeholder in a cube's sql is its alias; the compiler resolves it (along with {schema}-style context placeholders and {ctx.X} row-level-security placeholders) at compile time.

Semantic and executable contracts

Every successful compilation attaches compiled.analysis, including explicit coverage. Its frozen descriptors distinguish logical metrics and hidden operands, result grain/population, predicates, selection, time/null policies, declared catalog assumptions, and physical execution recipes. Complete semantic coverage does not certify source freshness, completeness, or key uniqueness.

node_id, output_id, population IDs, and binding slots are artifact-local references. Serialization preserves them; independent compilations need not use the same strings. Compare definitions with compare_analysis, not IDs, SQL, aliases, or output positions:

from semql import CatalogContext, compare_analysis, compile_query

revision = CatalogContext(namespace="reporting", semantic_revision="r17")
first = compile_query(query, catalog.as_dict(), catalog_context=revision)
second = compile_query(aliased_query, catalog.as_dict(), catalog_context=revision)
comparison = compare_analysis(first.analysis, second.analysis, scope="expression")
# comparison.outcome: "equivalent", "different", or "not_established"

The host owns the immutable catalog revision, including changes to catalog SQL, policies, and scope-function behavior. Without a revision, local analysis still works, but persisted/cross-process equivalence is not established. An explicit shared_catalog_snapshot=True attests a shared immutable snapshot in-process.

Expression comparison is not a promise of equal cohorts or result rows. scope="result" also compares grain, population, joins, selection, and time policy. Supply left_context and right_context as private mappings keyed by each artifact's semantic binding slots, or provide bound_context_equal after independently establishing that equality. Missing context yields not_established; values never appear in comparison diagnostics. Neither scope proves identical live data. Invalid/dangling/cyclic graphs raise ContractError; unknown required semantics or versions are never certified.

Policy-context slots cover catalog predicates, tenancy, security context, and evaluated scopes. Supply each slot's complete private request context, or attest that context equality explicitly; the same catalog revision alone does not prove equal authorized populations. Value-free predicates require no private binding. Logical assumptions constrain result comparison; physical merge-key obligations remain separate execution checks.

CompiledQuery.model_dump() / model_validate() preserve analysis, executable version, diagnostics, and binding requirements. Older payloads without analysis remain unavailable; recompile unsupported executable versions before execution. Artifact dumps include private params and are not safe diagnostic payloads. Use SemQLError.to_public_payload() for external errors; to_payload() remains lossless and potentially sensitive for trusted repair consumers.

Duplicate semantic projections reject. To display a metric twice, reuse its output in presentation. compile_plan() is a privileged host/optimizer entry point, not a client-facing alternative to SemanticQuery.

Alias-aware enrichment

enrich_all now requires the result's analysis and returns EnrichedResult:

from semql import ResolutionContext, enrich_all

enriched = enrich_all(rows, catalog, ResolutionContext(), analysis=compiled.analysis)
rows = enriched.rows
analysis = enriched.analysis

Lookup inputs are matched by qualified semantic identity, so aliases work and equal labels never collapse distinct keys. Original rows are not mutated; original semantic references and grain remain intact. Attached fields carry lookup provenance in analysis.enrichments. Masked keys are not enriched, and attachments cannot overwrite an existing output.

What lives in the box

Surface Module
Cube / Measure / Dimension / TimeDimension / Join semql.model
SemanticQuery / Filter / TimeWindow / CompareWindow semql.spec
Catalog wrapper (validation, compile entry) semql.catalog
Compiler — sqlglot AST → dialect SQL semql.compile
Collect-all static validator semql.validate
Reflection cubes (catalog_cubes, ...) semql.introspect
Planner / router prompt fragments semql-prompt (sibling package)
Dialect strategies + sqlglot dialect adapter semql.backend, semql.dialect
Visualisation decision (chart type, axes, formats) semql.visualize
is_read_only_statement post-hoc SQL guard semql.safe
Structured error hierarchy semql.errors

Features

  • Compare windows — CompareWindow(mode="previous_period") wraps the inner query in current / prior CTEs joined via FULL OUTER JOIN and emits {m}_current / {m}_prior / {m}_delta / {m}_pct_change columns per measure.
  • Temporal model — time dimensions group by second / minute / hour / day / week / month / quarter / year; a type="date" time dimension drops sub-day grain and timezone shifts; per-cube timezone makes date_trunc tenant-correct and transpiles per dialect (AT TIME ZONE, CONVERT_TIMEZONE, ClickHouse's native arg, …); per-cube week_start (monday default, or sunday) sets the week bucket boundary consistently across dialects.
  • Explicit raw SQL — every hand-written fragment (Measure.sql, Join.on, Cube.base_predicate, …) is wrapped in a RawSQL marker at validation: when raw SQL is used, the model says so.
  • Tenancy — per-cube NONE (default; honestly unscoped), SCHEMA ({tenant_schema} substituted from the identity's tenant, required when declared) or DISCRIMINATOR (compiler wraps the source in a subquery with one bound WHERE predicate per tenancy_columns entry, so composite tenant keys need no workaround). tenant is first-class on AuthContext; Catalog(strict_tenancy=True) rejects any cube left with no isolation, scope, or required_roles.
  • Row-level security — Cube.security_sql AND-composes with tenancy inside the isolation subquery; {ctx.X} placeholders bind as parameters, never inline as literals.
  • MCP-ready — build_planner_prompt_fragment(catalog.as_dict()) (in semql-prompt) produces the planner system-prompt fragment; semql-mcp wraps it as a server.
  • Pluggable backends — DialectStrategy Protocol lets out-of-tree Snowflake / BigQuery adapters slot in without forking the compiler.

Philosophy

See PHILOSOPHY.md at the repo root. Highlights:

  • The emitted SQL must be readable by the engineer debugging a production incident at 2am.
  • Compile errors beat runtime errors; runtime errors beat wrong results.
  • compile() fails at the first problem; validate() collects them all.
  • Catalogs are data; the META cubes expose the catalog through the same compiler path a normal query takes.

Status

Pre-v1. The shape is stable, but minor names / fields may move before the v1 contract locks. Tests pin every public behaviour the README documents.

Metadata

Release files for semql 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for semql 0.8.0
File Size Uploaded Provenance
semql-0.8.0.tar.gz 279.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for semql 0.8.0
File Interpreter ABI Platform
semql-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 583.6 kB

Release files / semql-0.8.0.tar.gz

Download URL semql-0.8.0.tar.gz
Size 279.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c4704236f92e924be059bf34badaf6a1c6c2371e8a9784c3d3ce88b5cb3999e4
BLAKE2b-256 checksum
How to use checksums
219b9112986c5e975ecba9c03dd37ef7d71fcd4c60c6220c8382a03cbf3bc844
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / semql-0.8.0-py3-none-any.whl

Download URL semql-0.8.0-py3-none-any.whl
Size 303.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9c2b45fd423d76c7e8f8eb1e415149a067429edfe050034536a721708cf77bf8
BLAKE2b-256 checksum
How to use checksums
4f9933a60e56416cc95654d1405ce9499e904ca07cbc7fc59f9ca04a81e57ab2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Security for semql 0.8.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for semql-0.8.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page