diff --git a/sidemantic/adapters/atscale_sml.py b/sidemantic/adapters/atscale_sml.py index 77c4dc75..70164a9c 100644 --- a/sidemantic/adapters/atscale_sml.py +++ b/sidemantic/adapters/atscale_sml.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import warnings from dataclasses import dataclass from pathlib import Path @@ -80,6 +81,7 @@ class MetricInfo: label: str | None description: str | None format_str: str | None + metadata: dict[str, Any] | None = None def _normalize_calc_method(method: str | None) -> str | None: @@ -135,6 +137,56 @@ def _parse_named_quantile(value: str | None) -> float | None: return None +def _parse_custom_quantiles(value: Any) -> list[float]: + """Parse a custom_quantiles array (SML v1.5) of numbers between 0 and 1.""" + if not isinstance(value, (list, tuple)): + return [] + quantiles: list[float] = [] + for item in value: + try: + numeric = float(item) + except (TypeError, ValueError): + continue + if 0 < numeric < 1: + quantiles.append(numeric) + return quantiles + + +# Anchored at both ends so only a SQL expression that is *entirely* a percentile +# (no prefix/suffix such as ``1 + PERCENTILE_CONT(...)``) is reclassified as a +# percentile metric on export; otherwise the prefix would be silently dropped. +_PERCENTILE_CONT_RE = re.compile( + r"\A\s*PERCENTILE_CONT\s*\(\s*(?P[0-9.]+)\s*\)\s*WITHIN\s+GROUP\s*\(\s*ORDER\s+BY\s+(?P.+?)\s*\)\s*\Z", + re.IGNORECASE | re.DOTALL, +) + + +def _percentile_column_from_sql(sql: str | None) -> str | None: + """Extract the ordered column from a PERCENTILE_CONT(...) WITHIN GROUP (ORDER BY col) expression.""" + if not sql: + return None + match = _PERCENTILE_CONT_RE.search(sql.strip()) + if not match: + return None + return match.group("column").strip() or None + + +def _percentile_quantile_from_sql(sql: str | None) -> float | None: + """Extract the quantile from a PERCENTILE_CONT(q) WITHIN GROUP (ORDER BY col) expression.""" + if not sql: + return None + match = _PERCENTILE_CONT_RE.search(sql.strip()) + if not match: + return None + try: + quantile = float(match.group("quantile")) + except (TypeError, ValueError): + return None + if 0 < quantile < 1: + return quantile + return None + + def _data_type_to_granularity(data_type: str | None) -> str | None: if not data_type: return None @@ -168,6 +220,7 @@ class AtScaleSMLAdapter(BaseAdapter): "metric_calc", "model", "composite_model", + "package", } _object_type_by_dir = { @@ -177,6 +230,7 @@ class AtScaleSMLAdapter(BaseAdapter): "dimensions": "dimension", "metrics": "metric", "models": "model", + "packages": "package", } def parse(self, source: str | Path) -> SemanticGraph: @@ -291,6 +345,7 @@ def _load_objects(self, files: list[Path]) -> dict[str, dict[str, dict[str, Any] "metric_calc": {}, "model": {}, "composite_model": {}, + "package": {}, } for file_path in files: @@ -330,9 +385,24 @@ def _register_object( if obj_type not in self._supported_object_types: return - unique_name = data.get("unique_name") - if unique_name: - objects[obj_type][unique_name] = data + # Package objects (external Git-repo references, SML packages/) key on + # `name` since they do not carry a `unique_name`. + key = data.get("unique_name") or (data.get("name") if obj_type == "package" else None) + if key: + objects[obj_type][key] = data + + # A standalone SML package file or a catalog may declare external + # Git-repo packages via a top-level `packages` list (the canonical + # package-file shape is `{version, packages: [{name, url, ...}, ...]}` + # with no `object_type`/`unique_name`). Register each nested entry so + # the references are tracked alongside single-package fixtures. + if obj_type in {"catalog", "package"}: + for package in data.get("packages") or []: + if not isinstance(package, dict): + continue + pkg_key = package.get("name") or package.get("url") + if pkg_key: + objects["package"].setdefault(pkg_key, package) def _resolve_datasets( self, datasets: dict[str, dict[str, Any]], connections: dict[str, dict[str, Any]] @@ -610,6 +680,7 @@ def _metric_from_definition(self, metric_def: dict[str, Any]) -> MetricInfo | No agg = None metric_type = None sql = None + metadata: dict[str, Any] | None = None if method in _CALC_METHOD_AGG_MAP: agg = _CALC_METHOD_AGG_MAP[method] @@ -619,8 +690,19 @@ def _metric_from_definition(self, metric_def: dict[str, Any]) -> MetricInfo | No if column: sql = f"SUM(DISTINCT {column})" elif method == "percentile": - quantile = _parse_named_quantile(metric_def.get("named_quantiles")) - if quantile == 0.5 and (metric_def.get("named_quantiles") or "").lower() == "median": + metadata = self._percentile_metadata(metric_def) + # custom_quantiles (SML v1.5) takes precedence over named_quantiles. + custom_quantiles = _parse_custom_quantiles(metric_def.get("custom_quantiles")) + if custom_quantiles: + quantile = custom_quantiles[0] + else: + quantile = _parse_named_quantile(metric_def.get("named_quantiles")) + is_median = ( + not custom_quantiles + and quantile == 0.5 + and (metric_def.get("named_quantiles") or "").lower() == "median" + ) + if is_median: agg = "median" sql = column else: @@ -648,8 +730,21 @@ def _metric_from_definition(self, metric_def: dict[str, Any]) -> MetricInfo | No label=metric_def.get("label"), description=metric_def.get("description"), format_str=metric_def.get("format"), + metadata=metadata, ) + @staticmethod + def _percentile_metadata(metric_def: dict[str, Any]) -> dict[str, Any] | None: + """Preserve SML v1.5 percentile fields that have no direct SQL mapping.""" + metadata: dict[str, Any] = {} + compression = metric_def.get("compression") + if compression is not None: + metadata["compression"] = compression + custom_quantiles = _parse_custom_quantiles(metric_def.get("custom_quantiles")) + if custom_quantiles: + metadata["custom_quantiles"] = custom_quantiles + return metadata or None + def _metric_from_metrical_attribute(self, metrical: dict[str, Any]) -> MetricInfo | None: if not metrical: return None @@ -668,6 +763,8 @@ def _metric_from_metrical_attribute(self, metrical: dict[str, Any]) -> MetricInf "description": metrical.get("description"), "format": metrical.get("format"), "named_quantiles": metrical.get("named_quantiles"), + "custom_quantiles": metrical.get("custom_quantiles"), + "compression": metrical.get("compression"), } return self._metric_from_definition(metric_def) @@ -689,6 +786,19 @@ def _metric_from_calc(self, calc_def: dict[str, Any]) -> MetricInfo | None: format_str=calc_def.get("format"), ) + @staticmethod + def _metric_from_info(info: MetricInfo) -> Metric: + return Metric( + name=info.name, + agg=info.agg, + sql=info.sql, + type=info.metric_type, + label=info.label, + description=info.description, + format=info.format_str, + metadata=info.metadata, + ) + def _apply_dimension_attrs_to_models( self, dimension_attrs: dict[str, list[DimensionAttr]], @@ -763,34 +873,14 @@ def _apply_metric_infos_to_models( for info in metrics: if model.get_metric(info.name): continue - model.metrics.append( - Metric( - name=info.name, - agg=info.agg, - sql=info.sql, - type=info.metric_type, - label=info.label, - description=info.description, - format=info.format_str, - ) - ) + model.metrics.append(self._metric_from_info(info)) if not dataset_to_model: for info in metric_infos.get("__global__", []): for model in graph.models.values(): if model.get_metric(info.name): continue - model.metrics.append( - Metric( - name=info.name, - agg=info.agg, - sql=info.sql, - type=info.metric_type, - label=info.label, - description=info.description, - format=info.format_str, - ) - ) + model.metrics.append(self._metric_from_info(info)) def _build_models_from_model_defs( self, @@ -893,34 +983,14 @@ def _attach_metrics_for_model( continue if model.get_metric(info.name): continue - model.metrics.append( - Metric( - name=info.name, - agg=info.agg, - sql=info.sql, - type=info.metric_type, - label=info.label, - description=info.description, - format=info.format_str, - ) - ) + model.metrics.append(self._metric_from_info(info)) for info in metric_infos.get("__global__", []): if not include_all and info.name not in metric_names: continue if model.get_metric(info.name): continue - model.metrics.append( - Metric( - name=info.name, - agg=info.agg, - sql=info.sql, - type=info.metric_type, - label=info.label, - description=info.description, - format=info.format_str, - ) - ) + model.metrics.append(self._metric_from_info(info)) def _apply_model_relationships( self, @@ -1216,6 +1286,13 @@ def _export_metric(self, metric: Metric, model: Model) -> dict[str, Any]: if metric.type and metric.type != "derived": return self._export_metric_calc(metric) + # Imported custom-quantile percentiles (SML v1.5) arrive as derived metrics with + # agg=None and a PERCENTILE_CONT(...) expression. Export them as percentile metrics + # so custom_quantiles/compression round-trip instead of degrading to a metric_calc. + percentile_def = self._export_percentile_metric(metric, model) + if percentile_def is not None: + return percentile_def + if metric.agg is None: return self._export_metric_calc(metric) @@ -1232,15 +1309,70 @@ def _export_metric(self, metric: Metric, model: Model) -> dict[str, Any]: if not metric.sql: return self._export_metric_calc(metric) + calculation_method = method_mapping.get(metric.agg, "sum") metric_def = { "unique_name": metric.name, "object_type": "metric", "label": metric.label or metric.name, - "calculation_method": method_mapping.get(metric.agg, "sum"), + "calculation_method": calculation_method, "dataset": model.name, "column": metric.sql, } + if calculation_method == "percentile": + metadata = metric.metadata or {} + custom_quantiles = metadata.get("custom_quantiles") + if custom_quantiles: + metric_def["custom_quantiles"] = list(custom_quantiles) + else: + metric_def["named_quantiles"] = "median" + if metadata.get("compression") is not None: + metric_def["compression"] = metadata["compression"] + + if metric.description: + metric_def["description"] = metric.description + if metric.format: + metric_def["format"] = metric.format + + return metric_def + + def _export_percentile_metric(self, metric: Metric, model: Model) -> dict[str, Any] | None: + """Export an imported percentile metric as a percentile metric definition. + + Imported SML v1.5 percentiles parse to derived metrics with ``agg=None`` and a + ``PERCENTILE_CONT(q) WITHIN GROUP (ORDER BY col)`` expression. Both custom-quantile + percentiles (``custom_quantiles: [0.75]``) and named percentiles (``named_quantiles: p90``, + which carry ``compression`` but no ``custom_quantiles``) are handled here. Without this, + ``_export_metric`` would emit a ``metric_calc`` and drop the percentile shape plus + ``custom_quantiles``/``compression``. Named percentiles are re-emitted as their numeric + ``custom_quantiles`` equivalent since the original name is not preserved on import. + """ + if metric.agg is not None: + return None + + column = _percentile_column_from_sql(metric.sql) + if column is None: + return None + + metadata = metric.metadata or {} + custom_quantiles = _parse_custom_quantiles(metadata.get("custom_quantiles")) + if not custom_quantiles: + quantile = _percentile_quantile_from_sql(metric.sql) + if quantile is None: + return None + custom_quantiles = [quantile] + + metric_def: dict[str, Any] = { + "unique_name": metric.name, + "object_type": "metric", + "label": metric.label or metric.name, + "calculation_method": "percentile", + "dataset": model.name, + "column": column, + "custom_quantiles": list(custom_quantiles), + } + if metadata.get("compression") is not None: + metric_def["compression"] = metadata["compression"] if metric.description: metric_def["description"] = metric.description if metric.format: diff --git a/tests/adapters/atscale_sml/test_export.py b/tests/adapters/atscale_sml/test_export.py index 53f1e7b1..1dfe6a9b 100644 --- a/tests/adapters/atscale_sml/test_export.py +++ b/tests/adapters/atscale_sml/test_export.py @@ -88,6 +88,208 @@ def test_export_simple_graph(self, tmp_path): assert "relationships" in model_def assert model_def["relationships"][0]["to"]["dimension"] == "customers" + def test_export_percentile_metadata_roundtrips(self, tmp_path): + """Percentile metric metadata (custom_quantiles, compression) exports.""" + orders = Model( + name="orders", + table="public.orders", + primary_key="order_id", + dimensions=[Dimension(name="order_id", type="numeric", sql="order_id")], + metrics=[ + Metric( + name="amount_p75", + agg="median", + sql="amount", + metadata={"custom_quantiles": [0.75], "compression": 10000}, + ), + Metric(name="amount_median", agg="median", sql="amount"), + ], + ) + + graph = SemanticGraph() + graph.add_model(orders) + + adapter = AtScaleSMLAdapter() + adapter.export(graph, tmp_path) + + with open(tmp_path / "metrics" / "amount_p75.yml") as f: + metric = yaml.safe_load(f) + assert metric["calculation_method"] == "percentile" + assert metric["custom_quantiles"] == [0.75] + assert metric["compression"] == 10000 + + with open(tmp_path / "metrics" / "amount_median.yml") as f: + metric = yaml.safe_load(f) + assert metric["calculation_method"] == "percentile" + assert metric["named_quantiles"] == "median" + + def test_imported_custom_percentile_roundtrips(self, tmp_path): + """An imported SML v1.5 custom_quantiles percentile re-exports as a percentile metric. + + Imported custom-quantile percentiles parse to derived metrics with agg=None and a + PERCENTILE_CONT(...) expression. Export must preserve custom_quantiles/compression + instead of degrading to a metric_calc that drops both fields. + """ + src = tmp_path / "src" + for sub in ("metrics", "models", "datasets"): + (src / sub).mkdir(parents=True) + (src / "atscale.yml").write_text( + yaml.safe_dump({"object_type": "catalog", "unique_name": "cat", "label": "cat"}) + ) + (src / "models" / "orders_model.yml").write_text( + yaml.safe_dump( + { + "object_type": "model", + "unique_name": "orders_model", + "label": "Orders", + "metrics": [{"unique_name": "amount_p75"}], + "dimensions": ["orders"], + } + ) + ) + (src / "datasets" / "orders.yml").write_text( + yaml.safe_dump( + { + "object_type": "dataset", + "unique_name": "orders", + "label": "Orders", + "sql": "select * from public.orders", + "columns": [{"name": "order_id"}, {"name": "amount"}], + } + ) + ) + (src / "metrics" / "amount_p75.yml").write_text( + yaml.safe_dump( + { + "object_type": "metric", + "unique_name": "amount_p75", + "label": "Amount P75", + "calculation_method": "percentile", + "dataset": "orders", + "column": "amount", + "custom_quantiles": [0.75], + "compression": 10000, + } + ) + ) + + adapter = AtScaleSMLAdapter() + graph = adapter.parse(src) + + out = tmp_path / "out" + adapter.export(graph, out) + + with open(out / "metrics" / "amount_p75.yml") as f: + exported = yaml.safe_load(f) + assert exported["object_type"] == "metric" + assert exported["calculation_method"] == "percentile" + assert exported["custom_quantiles"] == [0.75] + assert exported["compression"] == 10000 + assert exported["column"] == "amount" + + def test_imported_named_percentile_with_compression_roundtrips(self, tmp_path): + """An imported SML v1.5 named percentile carrying compression re-exports as a percentile. + + Named percentiles (e.g. named_quantiles: p90) with compression but no custom_quantiles + parse to derived metrics with agg=None and a PERCENTILE_CONT(...) expression, storing + compression in metadata only. Export must preserve the percentile shape and compression + instead of degrading to a metric_calc that drops both. The numeric quantile is re-emitted + via custom_quantiles since the original name is not preserved on import. + """ + src = tmp_path / "src" + for sub in ("metrics", "models", "datasets"): + (src / sub).mkdir(parents=True) + (src / "atscale.yml").write_text( + yaml.safe_dump({"object_type": "catalog", "unique_name": "cat", "label": "cat"}) + ) + (src / "models" / "orders_model.yml").write_text( + yaml.safe_dump( + { + "object_type": "model", + "unique_name": "orders_model", + "label": "Orders", + "metrics": [{"unique_name": "amount_p90"}], + "dimensions": ["orders"], + } + ) + ) + (src / "datasets" / "orders.yml").write_text( + yaml.safe_dump( + { + "object_type": "dataset", + "unique_name": "orders", + "label": "Orders", + "sql": "select * from public.orders", + "columns": [{"name": "order_id"}, {"name": "amount"}], + } + ) + ) + (src / "metrics" / "amount_p90.yml").write_text( + yaml.safe_dump( + { + "object_type": "metric", + "unique_name": "amount_p90", + "label": "Amount P90", + "calculation_method": "percentile", + "dataset": "orders", + "column": "amount", + "named_quantiles": "p90", + "compression": 10000, + } + ) + ) + + adapter = AtScaleSMLAdapter() + graph = adapter.parse(src) + + # Imported as a derived percentile with compression in metadata, no custom_quantiles. + metric = graph.models["orders_model"].get_metric("amount_p90") + assert metric.agg is None + assert metric.metadata["compression"] == 10000 + assert "custom_quantiles" not in metric.metadata + + out = tmp_path / "out" + adapter.export(graph, out) + + with open(out / "metrics" / "amount_p90.yml") as f: + exported = yaml.safe_load(f) + assert exported["object_type"] == "metric" + assert exported["calculation_method"] == "percentile" + assert exported["custom_quantiles"] == [0.9] + assert exported["compression"] == 10000 + assert exported["column"] == "amount" + + def test_prefixed_percentile_expression_not_reclassified(self, tmp_path): + """A derived metric whose SQL only contains a percentile sub-expression is not a percentile. + + Percentile detection must match the full SQL expression. An expression like + ``1 + PERCENTILE_CONT(...)`` must export as a metric_calc preserving the whole + expression, not as a percentile metric that drops the ``1 +`` prefix. + """ + prefixed_sql = "1 + PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY amount)" + orders = Model( + name="orders", + table="public.orders", + primary_key="order_id", + dimensions=[Dimension(name="order_id", type="numeric", sql="order_id")], + metrics=[ + Metric(name="weird_pct", type="derived", sql=prefixed_sql), + ], + ) + graph = SemanticGraph() + graph.add_model(orders) + + adapter = AtScaleSMLAdapter() + out = tmp_path / "out" + adapter.export(graph, out) + + with open(out / "metrics" / "weird_pct.yml") as f: + exported = yaml.safe_load(f) + # Must degrade to a metric_calc that preserves the full expression, not a percentile. + assert exported["object_type"] == "metric_calc" + assert exported["expression"] == prefixed_sql + assert "custom_quantiles" not in exported + def test_export_relationship_level_uses_dimension(self, tmp_path): orders = Model( name="orders", diff --git a/tests/adapters/atscale_sml/test_kitchen_sink.py b/tests/adapters/atscale_sml/test_kitchen_sink.py index 0584b692..26be9dcf 100644 --- a/tests/adapters/atscale_sml/test_kitchen_sink.py +++ b/tests/adapters/atscale_sml/test_kitchen_sink.py @@ -343,5 +343,82 @@ def test_geography_dimension_hierarchy(self, adapter, fixtures_dir): assert zip_code.parent == "City" +class TestAtScaleSMLNewerSpecFeatures: + """Tests for newer SML spec features. + + Covers percentile compression/custom_quantiles (v1.5), package object + type (external Git-repo references), catalog hidden_models (v1.2) and + description (v1.6), and model dataset_properties / overrides. + """ + + def test_percentile_custom_quantiles_and_compression(self, adapter, fixtures_dir): + """Percentile metric with custom_quantiles + compression (SML v1.5).""" + graph = adapter.parse(fixtures_dir) + order_model = graph.models["order_model"] + + amount_p75 = order_model.get_metric("amount_p75") + assert amount_p75 is not None + assert amount_p75.type == "derived" + # custom_quantiles takes precedence and drives the PERCENTILE_CONT SQL + assert amount_p75.sql == "PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY total_amount)" + # Both v1.5 fields are preserved in metadata + assert amount_p75.metadata is not None + assert amount_p75.metadata["custom_quantiles"] == [0.75] + assert amount_p75.metadata["compression"] == 10000 + + def test_existing_percentile_metrics_still_work(self, adapter, fixtures_dir): + """Median and p90 percentile metrics remain unchanged (backward compat).""" + graph = adapter.parse(fixtures_dir) + order_model = graph.models["order_model"] + + amount_median = order_model.get_metric("amount_median") + assert amount_median is not None + assert amount_median.agg == "median" + assert amount_median.sql == "total_amount" + + amount_p90 = order_model.get_metric("amount_p90") + assert amount_p90 is not None + assert amount_p90.sql == "PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY total_amount)" + + def test_package_object_parsed_without_error(self, adapter, fixtures_dir): + """A package object (external Git-repo reference) parses cleanly.""" + graph = adapter.parse(fixtures_dir) + # Packages do not materialize models, but must not break parsing. + assert "shared_dimensions" not in graph.models + assert "order_model" in graph.models + + def test_standalone_package_file_registers_entries(self, adapter, fixtures_dir): + """A canonical SML package file (`{version, packages: [...]}` in a + packages/ dir, with no object_type/unique_name) registers every nested + package reference, not just single-package or inline-catalog shapes.""" + files = adapter._collect_yaml_files(fixtures_dir) + objects = adapter._load_objects(files) + package_keys = objects["package"].keys() + # Nested entries from packages/shared_repos.yml are registered. + assert "shared_repo_dims" in package_keys + assert "shared_repo_metrics" in package_keys + # Existing single-package and inline-catalog references still register. + assert "shared_dimensions" in package_keys + assert "shared_metrics" in package_keys + + def test_catalog_new_fields_parse_without_error(self, adapter, fixtures_dir): + """Catalog hidden_models (v1.2), description (v1.6), dataset_properties, + and inline packages do not break parsing or hide referenced models.""" + graph = adapter.parse(fixtures_dir) + # hidden_models lists return_model; it is still importable as a model. + assert "return_model" in graph.models + assert "order_model" in graph.models + + def test_model_dataset_properties_and_overrides_parse(self, adapter, fixtures_dir): + """Model-level dataset_properties and overrides parse without error.""" + graph = adapter.parse(fixtures_dir) + order_model = graph.models["order_model"] + # Metrics, relationships, and aggregates are unaffected by the new fields. + assert order_model.get_metric("total_amount") is not None + assert len(order_model.pre_aggregations) == 1 + rel_names = {rel.name for rel in order_model.relationships} + assert "dim_customers" in rel_names + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/adapters/test_fixture_functionality_contracts.py b/tests/adapters/test_fixture_functionality_contracts.py index ecde29a4..87d3af02 100644 --- a/tests/adapters/test_fixture_functionality_contracts.py +++ b/tests/adapters/test_fixture_functionality_contracts.py @@ -79,6 +79,8 @@ "tests/fixtures/atscale_sml_kitchen_sink/model_internet_sales.yml", "tests/fixtures/atscale_sml_kitchen_sink/model_orders.yml", "tests/fixtures/atscale_sml_kitchen_sink/model_returns.yml", + "tests/fixtures/atscale_sml_kitchen_sink/package_shared_dims.yml", + "tests/fixtures/atscale_sml_kitchen_sink/packages/shared_repos.yml", "tests/fixtures/atscale_sml_kitchen_sink/row_security_country.yml", "tests/fixtures/cube/rbac_views.yaml", "tests/fixtures/holistics/ecommerce.dataset.aml", diff --git a/tests/fixtures/atscale_sml_kitchen_sink/atscale.yml b/tests/fixtures/atscale_sml_kitchen_sink/atscale.yml index 2fde4cc8..0fb38d6c 100644 --- a/tests/fixtures/atscale_sml_kitchen_sink/atscale.yml +++ b/tests/fixtures/atscale_sml_kitchen_sink/atscale.yml @@ -2,3 +2,15 @@ unique_name: kitchen_sink_catalog object_type: catalog label: Kitchen Sink Catalog version: 1.0 +description: Kitchen sink catalog exercising advanced SML features. +hidden_models: + - return_model +dataset_properties: + fact_orders: + allow_aggregates: true + allow_local_aggs: false +packages: + - name: shared_metrics + url: https://github.com/semanticdatalayer/shared-metrics.git + branch: main + version: commit:abcdef12 diff --git a/tests/fixtures/atscale_sml_kitchen_sink/metric_amount_p75.yml b/tests/fixtures/atscale_sml_kitchen_sink/metric_amount_p75.yml new file mode 100644 index 00000000..d35d2ade --- /dev/null +++ b/tests/fixtures/atscale_sml_kitchen_sink/metric_amount_p75.yml @@ -0,0 +1,8 @@ +unique_name: amount_p75 +object_type: metric +calculation_method: percentile +dataset: fact_orders +column: total_amount +custom_quantiles: + - 0.75 +compression: 10000 diff --git a/tests/fixtures/atscale_sml_kitchen_sink/model_orders.yml b/tests/fixtures/atscale_sml_kitchen_sink/model_orders.yml index cd547d9f..7b160de2 100644 --- a/tests/fixtures/atscale_sml_kitchen_sink/model_orders.yml +++ b/tests/fixtures/atscale_sml_kitchen_sink/model_orders.yml @@ -41,7 +41,14 @@ metrics: - unique_name: amount_stddev - unique_name: amount_median - unique_name: amount_p90 + - unique_name: amount_p75 - unique_name: avg_order_value +dataset_properties: + fact_orders: + create_hinted_aggregate: true +overrides: + total_amount: + query_name: order_total_amount aggregates: - unique_name: orders_rollup label: Orders Rollup diff --git a/tests/fixtures/atscale_sml_kitchen_sink/package_shared_dims.yml b/tests/fixtures/atscale_sml_kitchen_sink/package_shared_dims.yml new file mode 100644 index 00000000..0d4a5ceb --- /dev/null +++ b/tests/fixtures/atscale_sml_kitchen_sink/package_shared_dims.yml @@ -0,0 +1,5 @@ +object_type: package +name: shared_dimensions +url: https://github.com/semanticdatalayer/shared-dimensions.git +branch: main +version: tag:v2024.01 diff --git a/tests/fixtures/atscale_sml_kitchen_sink/packages/shared_repos.yml b/tests/fixtures/atscale_sml_kitchen_sink/packages/shared_repos.yml new file mode 100644 index 00000000..06dd369a --- /dev/null +++ b/tests/fixtures/atscale_sml_kitchen_sink/packages/shared_repos.yml @@ -0,0 +1,10 @@ +version: 1 +packages: + - name: shared_repo_dims + url: https://github.com/company/shared-dims.git + branch: main + version: tag:v2024.01 + - name: shared_repo_metrics + url: https://github.com/company/shared-metrics.git + branch: main + version: commit:abcdef12