Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e985426
feat(snowflake): import Cortex Analyst measures and enrichment keys
nicosuave Jun 13, 2026
9124e4b
Auto-update JSON schema
github-actions[bot] Jun 13, 2026
b7f0127
Fix Snowflake top-level metric qualification and round-trip export
nicosuave Jun 14, 2026
6a14c81
Skip unrepresentable Snowflake metrics and wire Cortex fields into na…
nicosuave Jun 14, 2026
b19d987
Coerce Snowflake sample_values to str and export graph-level metric s…
nicosuave Jun 14, 2026
f718051
Preserve graph-level metadata across native export round-trips
nicosuave Jun 14, 2026
6640a93
Preserve Snowflake relationship names and metric using_relationships
nicosuave Jun 14, 2026
2284417
Match owned Snowflake metrics by identity so same-named top-level met…
nicosuave Jun 14, 2026
55c8517
Accept Snowflake enrichment fields in the Rust native schema
nicosuave Jun 14, 2026
642cde3
Merge remote-tracking branch 'origin/main' into wf-snowflake-200
nicosuave Jun 14, 2026
a322a39
Merge Snowflake graph metadata during directory loading
nicosuave Jun 15, 2026
b68e9e2
Round-trip Snowflake non-additive metrics, private access, multi-file…
nicosuave Jun 15, 2026
71d5216
Keep Snowflake metric expressions resolvable across contexts
nicosuave Jun 15, 2026
7cf5af0
Defer Snowflake top-level metrics until all tables are parsed
nicosuave Jun 15, 2026
da115d3
Resolve Snowflake cross-file top-level metrics in the CLI loader
nicosuave Jun 15, 2026
87ea415
Detect metric-only Snowflake Cortex files in the directory loader
nicosuave Jun 15, 2026
c684a99
Allow tableless metrics in split Snowflake Cortex metrics files
nicosuave Jun 15, 2026
d20f85d
Route tableless Snowflake view-metric sidecars with top-level sections
nicosuave Jun 15, 2026
5ef649c
Route instruction-only Snowflake Cortex sidecars
nicosuave Jun 15, 2026
fca4603
Avoid pending-metric name collisions and merge non-OSI root metadata …
nicosuave Jun 15, 2026
0ffb3a8
Route and defer relationship-only Snowflake Cortex sidecars
nicosuave Jun 15, 2026
ef6e760
Prefer explicit Snowflake joins and route metric-key sidecars
nicosuave Jun 15, 2026
4db2970
Merge origin/main into update-snowflake-adapter
nicosuave Jun 15, 2026
7cb7c30
Merge origin/main into update-snowflake-adapter
nicosuave Jun 15, 2026
892f84e
Keep distinct same-target Snowflake relationships in directory loader
nicosuave Jun 15, 2026
976b382
Route named tableless Cortex view-metric sidecars to Snowflake
nicosuave Jun 15, 2026
3d47659
Fix Snowflake access override and SQL metadata frontmatter
nicosuave Jun 15, 2026
924caf0
Export Snowflake Cortex search service as single nested shape
nicosuave Jun 17, 2026
7245d1a
Merge origin/main into update-snowflake-adapter
nicosuave Jun 17, 2026
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
70 changes: 69 additions & 1 deletion sidemantic-rs/src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ pub struct SidemanticConfig {
pub sql_metrics: Option<String>,
#[serde(default)]
pub sql_segments: Option<String>,
/// Graph-level metadata payload (round-trips format-specific state such as OSI).
/// Graph-level metadata payload (round-trips format-specific state such as OSI,
/// and Snowflake Cortex top-level sections from the Python native export).
#[serde(default)]
pub metadata: Option<serde_json::Value>,
Comment thread
nicosuave marked this conversation as resolved.
}
Expand Down Expand Up @@ -132,6 +133,15 @@ pub struct DimensionConfig {
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub meta: Option<serde_json::Value>,
/// Alternative names (e.g. Snowflake Cortex Analyst, Cube).
#[serde(default)]
pub synonyms: Option<Vec<String>>,
/// Representative sample values for this dimension.
#[serde(default)]
pub sample_values: Option<Vec<String>>,
/// Linked Cortex Search service name (Snowflake Cortex Analyst).
#[serde(default)]
pub cortex_search_service_name: Option<String>,
Comment on lines +140 to +144

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Copy accepted enrichment fields into Rust core types

Fresh evidence in this patch is that Rust now accepts these native enrichment keys, but DimensionConfig::into_dimension() builds a core Dimension without sample_values or cortex_search_service_name slots, and the metric synonyms field added below is similarly not copied by MetricConfig::into_metric(). In the Rust/wasm load path for Python export-native output, Snowflake/Cortex enrichment parses successfully but disappears from the graph/catalog payload; add matching core fields and copy them, or preserve them under metadata.

Useful? React with 👍 / 👎.

pub format: Option<String>,
pub value_format_name: Option<String>,
pub parent: Option<String>,
Expand Down Expand Up @@ -190,6 +200,9 @@ pub struct MetricConfig {
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub meta: Option<serde_json::Value>,
/// Alternative names (e.g. Snowflake Cortex Analyst, Cube).
#[serde(default)]
pub synonyms: Option<Vec<String>>,
#[serde(default = "default_public")]
pub public: bool,
}
Expand Down Expand Up @@ -1492,6 +1505,61 @@ models:
);
}

#[test]
fn test_native_contract_accepts_snowflake_enrichment_fields() {
// Native YAML produced by Python `export-native` after a Snowflake import
// carries root `metadata`, dimension synonyms/sample_values/cortex search,
// and metric synonyms. The Rust native loader must accept (not reject) it.
let yaml = r#"
metadata:
snowflake:
verified_queries:
- name: total revenue
custom_instructions: Prefer revenue.
models:
- name: orders
table: orders
dimensions:
- name: status
type: categorical
synonyms: [state]
sample_values: ["1001", "1002"]
cortex_search_service_name: status_search
metrics:
- name: revenue
agg: sum
sql: amount
synonyms: [total revenue]
"#;

let config: SidemanticConfig = serde_yaml::from_str(yaml).unwrap();

assert_eq!(
config.metadata.as_ref().unwrap()["snowflake"]["custom_instructions"],
"Prefer revenue."
);

let dim = &config.models[0].dimensions[0];
assert_eq!(dim.synonyms.as_deref(), Some(&["state".to_string()][..]));
assert_eq!(
dim.sample_values.as_deref(),
Some(&["1001".to_string(), "1002".to_string()][..])
);
assert_eq!(
dim.cortex_search_service_name.as_deref(),
Some("status_search")
);

let metric = &config.models[0].metrics[0];
assert_eq!(
metric.synonyms.as_deref(),
Some(&["total revenue".to_string()][..])
);

// The config must still convert into the internal model without error.
config.into_parts().unwrap();
}

#[test]
fn test_parse_many_to_many_relationship_fields() {
let yaml = r#"
Expand Down
138 changes: 138 additions & 0 deletions sidemantic-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
"Dimension": {
"description": "Dimension (attribute) definition.\n\nDimensions are used for grouping and filtering in queries.",
"properties": {
"cortex_search_service_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Linked Cortex Search service name (Snowflake Cortex Analyst)",
"title": "Cortex Search Service Name"
},
"dax": {
"anyOf": [
{
Expand Down Expand Up @@ -147,6 +160,22 @@
"title": "Public",
"type": "boolean"
},
"sample_values": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Representative sample values for this dimension",
"title": "Sample Values"
},
"sql": {
"anyOf": [
{
Expand Down Expand Up @@ -176,6 +205,22 @@
"description": "Supported granularities for time dimensions",
"title": "Supported Granularities"
},
"synonyms": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Alternative names for this dimension",
"title": "Synonyms"
},
"type": {
"description": "Dimension type",
"enum": [
Expand Down Expand Up @@ -812,6 +857,22 @@
"description": "N-step funnel filter expressions (overrides base_event/conversion_event)",
"title": "Steps"
},
"synonyms": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Alternative names for this measure/metric",
"title": "Synonyms"
},
"time_offset": {
"anyOf": [
{
Expand Down Expand Up @@ -2013,6 +2074,22 @@
"description": "N-step funnel filter expressions (overrides base_event/conversion_event)",
"title": "Steps"
},
"synonyms": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Alternative names for this measure/metric",
"title": "Synonyms"
},
"time_offset": {
"anyOf": [
{
Expand Down Expand Up @@ -2129,6 +2206,19 @@
"Dimension": {
"description": "Dimension (attribute) definition.\n\nDimensions are used for grouping and filtering in queries.",
"properties": {
"cortex_search_service_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Linked Cortex Search service name (Snowflake Cortex Analyst)",
"title": "Cortex Search Service Name"
},
"dax": {
"anyOf": [
{
Expand Down Expand Up @@ -2273,6 +2363,22 @@
"title": "Public",
"type": "boolean"
},
"sample_values": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Representative sample values for this dimension",
"title": "Sample Values"
},
"sql": {
"anyOf": [
{
Expand Down Expand Up @@ -2302,6 +2408,22 @@
"description": "Supported granularities for time dimensions",
"title": "Supported Granularities"
},
"synonyms": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Alternative names for this dimension",
"title": "Synonyms"
},
"type": {
"description": "Dimension type",
"enum": [
Expand Down Expand Up @@ -2938,6 +3060,22 @@
"description": "N-step funnel filter expressions (overrides base_event/conversion_event)",
"title": "Steps"
},
"synonyms": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Alternative names for this measure/metric",
"title": "Synonyms"
},
"time_offset": {
"anyOf": [
{
Expand Down
27 changes: 27 additions & 0 deletions sidemantic/adapters/sidemantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"models",
"metrics",
"parameters",
"metadata",
Comment thread
nicosuave marked this conversation as resolved.
"sql_metrics",
"sql_segments",
}
Expand Down Expand Up @@ -79,6 +80,9 @@
"label",
"metadata",
"meta",
"synonyms",
"sample_values",
"cortex_search_service_name",
"format",
"value_format_name",
"parent",
Expand Down Expand Up @@ -118,6 +122,7 @@
"periods",
"retention_granularity",
"granularity",
"synonyms",
"inner_metrics",
"entity_dimensions",
"having",
Expand Down Expand Up @@ -387,6 +392,11 @@ def parse(self, source: str | Path) -> SemanticGraph:
validate_native_format_version(data)
reject_unknown_fields(data, ROOT_FIELDS, "root", source_path=source_path)

# Preserve graph-level metadata (e.g. Snowflake Cortex top-level sections).
graph_metadata = data.get("metadata")
if isinstance(graph_metadata, dict):
graph.metadata.update(graph_metadata)
Comment on lines +409 to +411

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect metadata-only native sidecars

When this accepts root metadata, a YAML file containing only version/metadata becomes a valid native graph-metadata file when parsed directly, but the CLI directory path never routes that sidecar to SidemanticAdapter: load_from_directory() only chooses native for models or _looks_like_native_sidemantic_yaml(), and that helper still requires metrics/parameters/sql. In CLI-first projects that split passthrough metadata into a native sidecar, the file is silently skipped and the metadata is lost; update the directory detector to recognize metadata-only native files, at least when version is present.

Useful? React with 👍 / 👎.


# Parse models
for model_def in data.get("models") or []:
model = self._parse_model(model_def, source_path=source_path)
Expand Down Expand Up @@ -484,6 +494,9 @@ def export(self, graph: SemanticGraph, output_path: str | Path) -> None:
if graph.parameters:
data["parameters"] = [self._export_parameter(parameter) for parameter in graph.parameters.values()]

if graph.metadata:
data["metadata"] = graph.metadata
Comment thread
nicosuave marked this conversation as resolved.

output_path.parent.mkdir(parents=True, exist_ok=True)

with open(output_path, "w") as f:
Expand Down Expand Up @@ -561,6 +574,9 @@ def _parse_model(self, model_def: dict, *, source_path: Path | None = None) -> M
parent=dim_def.get("parent"),
metadata=dim_def.get("metadata"),
meta=dim_def.get("meta"),
synonyms=dim_def.get("synonyms"),
sample_values=dim_def.get("sample_values"),
cortex_search_service_name=dim_def.get("cortex_search_service_name"),
window=dim_def.get("window"),
)
dimensions.append(dimension)
Expand Down Expand Up @@ -781,6 +797,7 @@ def _parse_metric(
"value_format_name",
"drill_fields",
"non_additive_dimension",
"synonyms",
Comment thread
nicosuave marked this conversation as resolved.
"meta",
"public",
]:
Expand Down Expand Up @@ -928,6 +945,12 @@ def _export_model(self, model: Model) -> dict:
dim_def["metadata"] = dim.metadata
if dim.meta:
dim_def["meta"] = dim.meta
if dim.synonyms:
dim_def["synonyms"] = dim.synonyms
if dim.sample_values:
dim_def["sample_values"] = dim.sample_values
if dim.cortex_search_service_name:
dim_def["cortex_search_service_name"] = dim.cortex_search_service_name
Comment thread
nicosuave marked this conversation as resolved.
if dim.format:
dim_def["format"] = dim.format
if dim.value_format_name:
Expand Down Expand Up @@ -966,6 +989,8 @@ def _export_model(self, model: Model) -> dict:
measure_def["metadata"] = measure.metadata
if measure.meta:
measure_def["meta"] = measure.meta
if measure.synonyms:
measure_def["synonyms"] = measure.synonyms
if not measure.public:
measure_def["public"] = measure.public
if measure.format:
Expand Down Expand Up @@ -1089,6 +1114,8 @@ def _export_metric(self, measure: Metric, graph) -> dict:
result["metadata"] = measure.metadata
if measure.meta:
result["meta"] = measure.meta
if measure.synonyms:
result["synonyms"] = measure.synonyms
if not measure.public:
result["public"] = measure.public

Expand Down
Loading
Loading