Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/native-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ The suite currently covers:
- Parameter interpolation in query filters.
- Pre-aggregation routing shape and DuckDB execution against seeded rollup tables.
- Semantic SQL rewrite cases for single-model and relationship queries.
- Query-local table calculations on the Rust SQL compiler path, including Rust-only DuckDB result coverage.
- Query-local table calculations for the shared Python/Rust subset. Python applies these after fetching rows;
Rust compiles them into SQL window expressions.
- Native `.sql` definition files.
- Native SQL frontmatter model definitions.
- YAML `sql_metrics` and `sql_segments` blocks.
Expand Down Expand Up @@ -112,6 +113,15 @@ The default Rust runner loads every manifest fixture, asserts `expected/validati

The `adbc-exec` Rust runner executes every query with `expected_result` or `rust_expected_result` through DuckDB ADBC, using the fixture seed SQL and result columns from the manifest. Any Rust-only expected output must include `rust_only_reason`. It is enabled in CI after installing the DuckDB ADBC driver.

Table-calculation fixture contract:

- Shared table calculations may use `percent_of_total`, `percent_of_previous`, `running_total`, `rank`, `row_number`, or `moving_average`.
- Shared calculations should include deterministic query `order_by` when row order affects the result.
- Python evaluates shared calculations with `TableCalculationProcessor` after query execution.
- Rust evaluates shared calculations by compiling them into SQL expressions.
- Rust-only table calculation types (`dense_rank`, `difference`, `lead`, `lag`) must use `rust_expected_result` and `rust_only_reason`.
- Python-only post-query table calculation types (`percent_of_column_total`, `percentile`) stay out of shared native fixtures until Rust supports them.

## Adding Fixtures

Add the narrowest fixture that proves one semantic behavior. Avoid kitchen-sink fixtures unless the behavior itself is cross-feature interaction.
Expand Down
47 changes: 46 additions & 1 deletion docs/native-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ The native format has two source forms:

The native format is the runtime contract. External formats such as LookML, MetricFlow, Hex, Rill, Malloy, Omni, Superset, GoodData, Snowflake Cortex, ThoughtSpot, Holistics, Tableau, AtScale SML, BSL, Yardstick, and Graphene GSQL should be converted into this format by Python importers before they are expected to run through the Rust native runtime.

## Rust Loader Scope

The Rust runtime and Rust CLI directory loader intentionally have a smaller direct
input surface than Python:

- `.yml` / `.yaml`: native Sidemantic YAML or Cube YAML.
- `.sql`: native Sidemantic SQL definition files.

They do not auto-detect LookML, MetricFlow/dbt manifests, Hex, Rill, Malloy,
Omni, Superset, GoodData, Snowflake Cortex, ThoughtSpot, Holistics, Tableau,
AtScale SML, BSL, Yardstick, or other external source formats. Convert those
formats through the Python CLI/API first, then load the exported native YAML/SQL
with the Rust runtime.

## Versioning

Current native format version: `1`.
Expand Down Expand Up @@ -61,6 +75,18 @@ Top-level sections:
| `metrics` | No | Graph-level metrics. Rust assigns these to exactly one owning model when possible. |
| `parameters` | No | Graph-level parameters for templates and query-time substitution. |

Top-level metrics are graph-scoped in the Python runtime. The Rust runtime does not
store a separate graph-metric namespace at execution time; it assigns each top-level
metric to one owning model by resolving explicit model references, metric dependencies,
entity dimensions, or a single-model project fallback. If Rust cannot infer exactly
one owner, loading fails. Portable native files should therefore make top-level metric
dependencies explicit, for example `orders.total_revenue` rather than `total_revenue`
when multiple models define the same local metric name. Dotted top-level metric names
are allowed and are resolved by exact metric name before `model.metric` parsing.

Top-level parameters remain graph-scoped in both runtimes. Query APIs interpolate
parameter values before SQL compilation.

## Models

Models describe physical or logical query sources.
Expand Down Expand Up @@ -94,6 +120,12 @@ At least one of `table`, `sql`, or `source_uri` should be present unless the mod
| `pre_aggregations` | No | List of pre-aggregation definitions. |
| `default_time_dimension` | No | Time dimension to add by default when the query needs time grouping. |
| `default_grain` | No | Default time grain for the default time dimension. |
| `auto_dimensions` | No | Python auto-discovery flag. Rust accepts `false` for compatibility and rejects `true` because it does not perform schema discovery. |

Canonical CLI-authored files should use `metrics` and `sql`. The native loaders
also accept compatibility input aliases: model-level `measures` for `metrics`,
dimension/metric `expr` for `sql`, and metric `measure` for `sql`. Exports use
canonical field names.

Single-column primary key:

Expand Down Expand Up @@ -332,8 +364,21 @@ relationships:
| `primary_key_columns` | Conditional | Explicit target-column list. |
| `through` | For many-to-many | Junction model. |
| `through_foreign_key` | For many-to-many | Source-to-through key. |
| `through_foreign_key_columns` | For many-to-many | Explicit source-to-through key columns. |
| `related_foreign_key` | For many-to-many | Through-to-target key. |
| `sql` | No | Custom join SQL using runtime placeholders where supported. |
| `related_foreign_key_columns` | For many-to-many | Explicit through-to-target key columns. |
| `sql` | No | Custom join SQL using `{from}` and `{to}` runtime placeholders. |

For CLI-authored native files, prefer explicit `foreign_key` and `primary_key`
fields. Omitted keys are still supported for compatibility: `many_to_one`
defaults the source key to `{name}_id`, while `one_to_many` and `one_to_one`
default the related-side key to `id`; omitted `primary_key` resolves to the
target model's declared primary key when building graph joins.

When `sql` is present, Python and Rust use it instead of the FK/PK-generated
predicate. `{from}` is replaced with the source model's runtime alias and `{to}`
with the target model's runtime alias. Reverse graph traversal swaps the
placeholders automatically.

Relationship types:

Expand Down
2 changes: 1 addition & 1 deletion docs/runtime-feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This matrix documents current product support for native Sidemantic projects. It
| Conversion metrics | Yes | Yes, fixture-covered compile | No dedicated fixture yet | No dedicated fixture yet |
| Retention metrics | Yes | Yes, fixture-covered compile | No dedicated fixture yet | No dedicated fixture yet |
| Cohort metrics | Yes | Yes, fixture-covered compile | No dedicated fixture yet | No dedicated fixture yet |
| Table calculations | Post-query processing | Yes, Rust fixture-covered compile and Rust-only result coverage | No dedicated fixture yet | No dedicated fixture yet |
| Table calculations | Yes, shared fixture post-query result parity | Yes, shared fixture SQL/window result parity | No dedicated fixture yet | No dedicated fixture yet |
| Pre-aggregation routing | Yes | Yes, fixture-covered compile | No dedicated fixture yet | No dedicated fixture yet |
| Semantic SQL rewrite | Yes | Native subset, fixture-covered | Native subset target | Narrow subset |
| DuckDB execution | Yes | Via ADBC, fixture result parity in CI | Native DuckDB process | No |
Expand Down
2 changes: 1 addition & 1 deletion docs/rust-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,4 @@ cd sidemantic-rs && cargo test --test native_fixtures

CI runs these in the `Native Compatibility` job.

The shared fixture suite currently includes executable coverage for basic models, joins, fanout-safe symmetric aggregation, many-to-many joins, parameters in filters, embedded SQL definitions, SQL frontmatter definitions, default time dimensions, segments, derived/ratio metrics, and pre-aggregation routing. Table calculations have Rust-only DuckDB result coverage because Python does not accept `table_calculations` in the native query API yet. `source_uri` is covered as a validation-only load fixture and query compilation rejects it until a concrete table or SQL source is provided.
The shared fixture suite currently includes executable coverage for basic models, joins, fanout-safe symmetric aggregation, many-to-many joins, parameters in filters, embedded SQL definitions, SQL frontmatter definitions, default time dimensions, segments, derived/ratio metrics, table calculations, and pre-aggregation routing. `source_uri` is covered as a validation-only load fixture and query compilation rejects it until a concrete table or SQL source is provided.
45 changes: 45 additions & 0 deletions scripts/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,54 @@
"""Generate JSON Schema from Pydantic models for YAML editor support."""

import json
from copy import deepcopy
from pathlib import Path

from sidemantic import Dimension, Metric, Model, Parameter, Relationship, Segment


def add_native_relationship_aliases(schema: dict) -> dict:
"""Expose native YAML relationship aliases that map to Python API fields."""
properties = schema.setdefault("properties", {})

if "foreign_key" in properties and "foreign_key_columns" not in properties:
foreign_key_columns = deepcopy(properties["foreign_key"])
foreign_key_columns["title"] = "Foreign Key Columns"
foreign_key_columns["description"] = "Explicit source-column list (alias for foreign_key)"
properties["foreign_key_columns"] = foreign_key_columns

if "primary_key" in properties and "primary_key_columns" not in properties:
primary_key_columns = deepcopy(properties["primary_key"])
primary_key_columns["title"] = "Primary Key Columns"
primary_key_columns["description"] = "Explicit target-column list (alias for primary_key)"
properties["primary_key_columns"] = primary_key_columns

if "sql" not in properties:
properties["sql"] = {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"description": "Custom join SQL using {from} and {to} runtime placeholders",
"title": "Sql",
}

return schema


def patch_relationship_schemas(schema: dict) -> None:
"""Patch every embedded Relationship schema emitted by Pydantic."""
if not isinstance(schema, dict):
return
if schema.get("title") == "Relationship":
add_native_relationship_aliases(schema)
for value in schema.values():
if isinstance(value, dict):
patch_relationship_schemas(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
patch_relationship_schemas(item)


def generate_schema() -> dict:
"""Generate JSON Schema for sidemantic YAML files."""
# Get schemas from pydantic models
Expand Down Expand Up @@ -51,6 +94,8 @@ def generate_schema() -> dict:
},
}

patch_relationship_schemas(schema)

return schema


Expand Down
9 changes: 9 additions & 0 deletions sidemantic-rs/examples/parity_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum Request {
#[serde(default)]
order_by: Vec<String>,
limit: Option<usize>,
offset: Option<usize>,
#[serde(default)]
ungrouped: bool,
#[serde(default)]
Expand Down Expand Up @@ -137,6 +138,7 @@ fn handle(request: Request) -> sidemantic::Result<Response> {
segments,
order_by,
limit,
offset,
ungrouped,
skip_default_time_dimensions,
dialect,
Expand All @@ -153,6 +155,9 @@ fn handle(request: Request) -> sidemantic::Result<Response> {
if let Some(limit) = limit {
query = query.with_limit(limit);
}
if let Some(offset) = offset {
query = query.with_offset(offset);
}
let mut generator = SqlGenerator::new(&graph);
if let Some(dialect) = dialect {
generator = generator.with_dialect(parse_dialect(&dialect)?);
Expand Down Expand Up @@ -654,6 +659,10 @@ fn metric_aggregation_name(aggregation: Option<&Aggregation>) -> &'static str {
Some(Aggregation::Min) => "min",
Some(Aggregation::Max) => "max",
Some(Aggregation::Median) => "median",
Some(Aggregation::Stddev) => "stddev",
Some(Aggregation::StddevPop) => "stddev_pop",
Some(Aggregation::Variance) => "variance",
Some(Aggregation::VariancePop) => "variance_pop",
Some(Aggregation::Expression) | None => "sum",
}
}
Expand Down
Loading
Loading