diff --git a/sidemantic/adapters/superset.py b/sidemantic/adapters/superset.py index 654cfc63..73e2adb3 100644 --- a/sidemantic/adapters/superset.py +++ b/sidemantic/adapters/superset.py @@ -1,5 +1,6 @@ """Superset adapter for importing/exporting Apache Superset datasets.""" +import json from pathlib import Path from typing import Any @@ -20,6 +21,16 @@ class SupersetAdapter(BaseAdapter): - Columns → Dimensions - Metrics → Metrics - main_dttm_col → Time dimension + + Superset metadata that has no first-class Sidemantic equivalent is preserved + under the ``meta`` payload (namespaced under ``superset``) so it survives a + Superset → Sidemantic → Superset roundtrip: + + - Dataset: ``catalog`` (multi-catalog qualifier), ``currency_code_column``, + ``folders`` (column/metric folder organization). + - Column: ``advanced_data_type``, ``python_date_format``, ``datetime_format``. + - Metric: ``currency`` (``{symbol, symbolPosition}``), ``d3format`` (also + mapped to ``Metric.format``), ``warning_text``. """ def parse(self, source: str | Path) -> SemanticGraph: @@ -68,9 +79,12 @@ def _parse_dataset(self, file_path: Path) -> Model | None: if not table_name: return None - # Get table reference + # Get table reference, qualified by optional catalog and schema. + # Superset supports multi-catalog datasets via a top-level `catalog` key. + catalog = dataset.get("catalog") schema = dataset.get("schema") - table = f"{schema}.{table_name}" if schema else table_name + table_parts = [part for part in (catalog, schema, table_name) if part] + table = ".".join(table_parts) # Get SQL for virtual datasets sql = dataset.get("sql") @@ -96,6 +110,31 @@ def _parse_dataset(self, file_path: Path) -> Model | None: if metric: metrics.append(metric) + # Preserve dataset-level Superset metadata that has no first-class + # Sidemantic equivalent so it survives a roundtrip. + superset_meta: dict[str, Any] = {} + if catalog is not None: + superset_meta["catalog"] = catalog + # `currency_code_column` enables dynamic per-row currency formatting. + # Real Superset exports nest it under `extra.currency_code_column`; some + # flattened payloads put it top-level. Accept both, preferring top-level. + # Superset also serializes `extra` as a JSON string, so parse that too. + extra = dataset.get("extra") + if isinstance(extra, str): + try: + extra = json.loads(extra) + except (TypeError, ValueError): + extra = None + currency_code_column = dataset.get("currency_code_column") + if currency_code_column is None and isinstance(extra, dict): + currency_code_column = extra.get("currency_code_column") + if currency_code_column is not None: + superset_meta["currency_code_column"] = currency_code_column + if dataset.get("folders") is not None: + superset_meta["folders"] = dataset.get("folders") + + meta = {"superset": superset_meta} if superset_meta else None + return Model( name=table_name, table=table if not sql else None, @@ -104,6 +143,7 @@ def _parse_dataset(self, file_path: Path) -> Model | None: primary_key=primary_key, dimensions=dimensions, metrics=metrics, + meta=meta, ) def _parse_column(self, col_def: dict[str, Any], main_dttm_col: str | None) -> Dimension | None: @@ -145,6 +185,15 @@ def _parse_column(self, col_def: dict[str, Any], main_dttm_col: str | None) -> D # Get label from verbose_name label = col_def.get("verbose_name") + # Preserve column-level Superset metadata that has no first-class + # Sidemantic equivalent so it survives a roundtrip. + superset_meta: dict[str, Any] = {} + for key in ("advanced_data_type", "python_date_format", "datetime_format"): + if col_def.get(key) is not None: + superset_meta[key] = col_def.get(key) + + meta = {"superset": superset_meta} if superset_meta else None + return Dimension( name=column_name, type=dim_type, @@ -152,6 +201,7 @@ def _parse_column(self, col_def: dict[str, Any], main_dttm_col: str | None) -> D label=label, granularity=granularity, description=col_def.get("description"), + meta=meta, ) def _parse_metric(self, metric_def: dict[str, Any]) -> Metric | None: @@ -205,6 +255,21 @@ def _parse_metric(self, metric_def: dict[str, Any]) -> Metric | None: # Get label from verbose_name label = metric_def.get("verbose_name") + # d3format is Superset's display format string (D3 number format). + # Map it to the Sidemantic `format` field and also preserve it (along + # with currency/warning_text) under meta so it survives a roundtrip. + d3format = metric_def.get("d3format") + + superset_meta: dict[str, Any] = {} + if d3format is not None: + superset_meta["d3format"] = d3format + if metric_def.get("currency") is not None: + superset_meta["currency"] = metric_def.get("currency") + if metric_def.get("warning_text") is not None: + superset_meta["warning_text"] = metric_def.get("warning_text") + + meta = {"superset": superset_meta} if superset_meta else None + return Metric( name=metric_name, type=metric_type, @@ -212,6 +277,8 @@ def _parse_metric(self, metric_def: dict[str, Any]) -> Metric | None: sql=sql if sql else None, label=label, description=metric_def.get("description"), + format=d3format, + meta=meta, ) def export(self, graph: SemanticGraph, output_path: str | Path) -> None: @@ -254,6 +321,8 @@ def _export_dataset(self, model: Model) -> dict[str, Any]: Returns: Dataset definition dictionary """ + superset_meta = (model.meta or {}).get("superset", {}) + dataset: dict[str, Any] = { "table_name": model.name, "description": model.description, @@ -261,14 +330,44 @@ def _export_dataset(self, model: Model) -> dict[str, Any]: "sql": model.sql, } - # Extract schema from table name if present + # Extract catalog/schema from the (catalog.)?(schema.)?table reference. + # Superset supports a top-level `catalog` qualifier for multi-catalog + # datasets, so a 3-part name maps to catalog.schema.table. if model.table and "." in model.table: parts = model.table.split(".") - dataset["schema"] = parts[0] - dataset["table_name"] = parts[1] + if len(parts) >= 3: + dataset["catalog"] = parts[-3] + dataset["schema"] = parts[-2] + dataset["table_name"] = parts[-1] + elif superset_meta.get("catalog") is not None: + # A preserved catalog with a two-part reference means the schema + # was originally null, so the qualifier is catalog.table, not + # schema.table. Keep schema null and restore the catalog below. + dataset["table_name"] = parts[-1] + else: + dataset["schema"] = parts[-2] + dataset["table_name"] = parts[-1] elif model.table: dataset["schema"] = None + # Restore preserved catalog if not derivable from the table reference. + if "catalog" not in dataset and superset_meta.get("catalog") is not None: + dataset["catalog"] = superset_meta["catalog"] + + # Dataset-level currency formatting metadata and folder organization. + # Emit `currency_code_column` both top-level and nested under `extra` + # (where real Superset stores it) so the export is consumable by Superset + # and still round-trips through this adapter. + if superset_meta.get("currency_code_column") is not None: + dataset["currency_code_column"] = superset_meta["currency_code_column"] + extra = dataset.get("extra") + if not isinstance(extra, dict): + extra = {} + extra["currency_code_column"] = superset_meta["currency_code_column"] + dataset["extra"] = extra + if superset_meta.get("folders") is not None: + dataset["folders"] = superset_meta["folders"] + # Find main datetime column main_dttm_col = None for dim in model.dimensions: @@ -312,6 +411,12 @@ def _export_dataset(self, model: Model) -> dict[str, Any]: if dim.description: col_def["description"] = dim.description + # Restore preserved column-level Superset metadata. + dim_meta = (dim.meta or {}).get("superset", {}) + for key in ("advanced_data_type", "python_date_format", "datetime_format"): + if dim_meta.get(key) is not None: + col_def[key] = dim_meta[key] + columns.append(col_def) if columns: @@ -352,6 +457,17 @@ def _export_dataset(self, model: Model) -> dict[str, Any]: if metric.description: metric_def["description"] = metric.description + # Restore preserved metric-level Superset metadata. d3format prefers + # the preserved raw value, falling back to the mapped `format`. + metric_meta = (metric.meta or {}).get("superset", {}) + d3format = metric_meta.get("d3format", metric.format) + if d3format is not None: + metric_def["d3format"] = d3format + if metric_meta.get("currency") is not None: + metric_def["currency"] = metric_meta["currency"] + if metric_meta.get("warning_text") is not None: + metric_def["warning_text"] = metric_meta["warning_text"] + metrics.append(metric_def) if metrics: diff --git a/tests/adapters/superset/test_fixtures.py b/tests/adapters/superset/test_fixtures.py index a33bc1ca..168d2bc4 100644 --- a/tests/adapters/superset/test_fixtures.py +++ b/tests/adapters/superset/test_fixtures.py @@ -385,10 +385,11 @@ def test_all_models_loaded(self, graph): assert "project_management" in graph.models assert "video_game_sales" in graph.models assert "cleaned_sales_data" in graph.models + assert "revenue_by_region" in graph.models def test_total_model_count(self, graph): """All fixture files produce models.""" - assert len(graph.models) == 11 + assert len(graph.models) == 12 def test_virtual_and_physical_datasets(self, graph): """Both virtual and physical datasets coexist.""" diff --git a/tests/adapters/superset/test_metadata.py b/tests/adapters/superset/test_metadata.py new file mode 100644 index 00000000..cf467820 --- /dev/null +++ b/tests/adapters/superset/test_metadata.py @@ -0,0 +1,319 @@ +"""Tests for Superset adapter - extended dataset/column/metric metadata. + +Covers Apache Superset dataset import/export fields that have no first-class +Sidemantic equivalent and are preserved under ``meta['superset']``: + +- Dataset: ``catalog`` (multi-catalog qualifier), ``currency_code_column``, + ``folders`` (column/metric folder organization). +- Column: ``advanced_data_type``, ``python_date_format``, ``datetime_format``. +- Metric: ``currency`` (``{symbol, symbolPosition}``), ``d3format``, ``warning_text``. +""" + +import json +import tempfile +from pathlib import Path + +import pytest +import yaml + +from sidemantic.adapters.superset import SupersetAdapter +from sidemantic.core.semantic_graph import SemanticGraph + +# ============================================================================= +# MULTI-CATALOG FIXTURE PARSING +# ============================================================================= + + +class TestMultiCatalogParsing: + """Tests for the multi_catalog_revenue.yaml fixture.""" + + @pytest.fixture + def graph(self): + adapter = SupersetAdapter() + return adapter.parse("tests/fixtures/superset/multi_catalog_revenue.yaml") + + @pytest.fixture + def model(self, graph): + return graph.models["revenue_by_region"] + + def test_model_loads(self, graph): + assert "revenue_by_region" in graph.models + + def test_catalog_in_table_reference(self, model): + """catalog.schema.table is preserved as a 3-part qualified table.""" + assert model.table == "analytics_catalog.finance.revenue_by_region" + + def test_catalog_in_meta(self, model): + assert model.meta["superset"]["catalog"] == "analytics_catalog" + + def test_currency_code_column_in_meta(self, model): + assert model.meta["superset"]["currency_code_column"] == "iso_currency" + + def test_folders_in_meta(self, model): + folders = model.meta["superset"]["folders"] + assert isinstance(folders, list) + assert folders[0]["name"] == "Money" + assert folders[0]["type"] == "folder" + child_names = [c["name"] for c in folders[0]["children"]] + assert "total_revenue" in child_names + assert "avg_revenue" in child_names + + def test_column_advanced_data_type(self, model): + region = model.get_dimension("region") + assert region.meta["superset"]["advanced_data_type"] == "country" + + def test_column_date_formats(self, model): + report_date = model.get_dimension("report_date") + assert report_date.meta["superset"]["python_date_format"] == "%Y-%m-%d" + assert report_date.meta["superset"]["datetime_format"] == "%Y-%m-%d" + + def test_metric_d3format_maps_to_format(self, model): + total_revenue = model.get_metric("total_revenue") + assert total_revenue.format == "$,.2f" + assert total_revenue.meta["superset"]["d3format"] == "$,.2f" + + def test_metric_currency(self, model): + total_revenue = model.get_metric("total_revenue") + currency = total_revenue.meta["superset"]["currency"] + assert currency["symbol"] == "EUR" + assert currency["symbolPosition"] == "suffix" + + def test_metric_warning_text(self, model): + total_revenue = model.get_metric("total_revenue") + assert total_revenue.meta["superset"]["warning_text"] == "Preliminary figures, subject to revision" + + def test_metric_without_metadata_has_no_superset_meta(self, model): + count = model.get_metric("count") + # No d3format/currency/warning_text -> no superset meta payload. + assert count.meta is None + assert count.format is None + + +# ============================================================================= +# DATASET-LEVEL METADATA ON THE ORDERS FIXTURE +# ============================================================================= + + +class TestOrdersMetadata: + """The orders.yaml fixture carries currency_code_column, folders, and + column/metric metadata (no catalog, to keep its table reference 2-part).""" + + @pytest.fixture + def model(self): + adapter = SupersetAdapter() + graph = adapter.parse("tests/fixtures/superset/orders.yaml") + return graph.models["orders"] + + def test_table_reference_unchanged(self, model): + assert model.table == "public.orders" + + def test_currency_code_column(self, model): + assert model.meta["superset"]["currency_code_column"] == "currency_code" + + def test_folders(self, model): + folders = model.meta["superset"]["folders"] + names = [f["name"] for f in folders] + assert "Revenue" in names + assert "Attributes" in names + + def test_column_advanced_data_type(self, model): + customer_id = model.get_dimension("customer_id") + assert customer_id.meta["superset"]["advanced_data_type"] == "internet_address" + + def test_column_date_formats(self, model): + created_at = model.get_dimension("created_at") + assert created_at.meta["superset"]["python_date_format"] == "%Y-%m-%d %H:%M:%S" + assert created_at.meta["superset"]["datetime_format"] == "%Y-%m-%dT%H:%M:%S" + + def test_metric_currency_and_warning(self, model): + revenue = model.get_metric("total_revenue") + assert revenue.meta["superset"]["currency"]["symbol"] == "USD" + assert revenue.meta["superset"]["warning_text"] == "Excludes refunded orders" + + +# ============================================================================= +# NESTED extra.currency_code_column (authentic Superset export layout) +# ============================================================================= + + +class TestNestedCurrencyCodeColumn: + """Real Superset datasets nest ``currency_code_column`` under ``extra`` rather + than at the top level. The international_sales.yaml fixture uses that layout; + the value must be preserved on import and survive a roundtrip.""" + + @pytest.fixture + def model(self): + adapter = SupersetAdapter() + graph = adapter.parse("tests/fixtures/superset/international_sales.yaml") + return graph.models["international_sales"] + + def test_nested_currency_code_column_imported(self, model): + assert model.meta["superset"]["currency_code_column"] == "currency_code" + + def test_nested_currency_code_column_roundtrip(self, model): + adapter = SupersetAdapter() + graph = SemanticGraph() + graph.add_model(model) + with tempfile.TemporaryDirectory() as tmpdir: + adapter.export(graph, tmpdir) + with open(Path(tmpdir) / "international_sales.yaml") as f: + exported = yaml.safe_load(f) + # Emitted in the authentic Superset location (nested under extra). + assert exported["extra"]["currency_code_column"] == "currency_code" + + reparsed = adapter.parse(Path(tmpdir) / "international_sales.yaml") + reloaded = reparsed.models["international_sales"] + assert reloaded.meta["superset"]["currency_code_column"] == "currency_code" + + +# ============================================================================= +# JSON-STRING extra payload (Superset serializes `extra` as a JSON string) +# ============================================================================= + + +class TestJsonStringExtra: + """Superset commonly serializes the dataset ``extra`` field as a JSON string + (e.g. ``extra: '{"currency_code_column": "ccy"}'``) rather than a mapping. + ``currency_code_column`` nested inside such a payload must still be imported + and survive a roundtrip; otherwise dynamic currency formatting is dropped.""" + + def _parse_dataset_dict(self, adapter, tmpdir, dataset): + path = Path(tmpdir) / "json_extra.yaml" + path.write_text(yaml.dump(dataset)) + return adapter.parse(path).models["json_extra"] + + def test_json_string_extra_currency_imported(self): + adapter = SupersetAdapter() + dataset = { + "table_name": "json_extra", + "extra": json.dumps({"currency_code_column": "ccy"}), + "columns": [{"column_name": "id", "type": "INT"}], + } + with tempfile.TemporaryDirectory() as tmpdir: + model = self._parse_dataset_dict(adapter, tmpdir, dataset) + assert model.meta["superset"]["currency_code_column"] == "ccy" + + def test_malformed_json_string_extra_ignored(self): + adapter = SupersetAdapter() + dataset = { + "table_name": "json_extra", + "extra": "{not valid json", + "columns": [{"column_name": "id", "type": "INT"}], + } + with tempfile.TemporaryDirectory() as tmpdir: + model = self._parse_dataset_dict(adapter, tmpdir, dataset) + assert model.meta is None or "currency_code_column" not in model.meta.get("superset", {}) + + def test_json_string_extra_currency_roundtrip(self): + adapter = SupersetAdapter() + dataset = { + "table_name": "json_extra", + "extra": json.dumps({"currency_code_column": "ccy"}), + "columns": [{"column_name": "id", "type": "INT"}], + } + with tempfile.TemporaryDirectory() as tmpdir: + model = self._parse_dataset_dict(adapter, tmpdir, dataset) + + graph = SemanticGraph() + graph.add_model(model) + out_dir = Path(tmpdir) / "out" + adapter.export(graph, out_dir) + with open(out_dir / "json_extra.yaml") as f: + exported = yaml.safe_load(f) + assert exported["extra"]["currency_code_column"] == "ccy" + + reloaded = adapter.parse(out_dir / "json_extra.yaml").models["json_extra"] + assert reloaded.meta["superset"]["currency_code_column"] == "ccy" + + +# ============================================================================= +# EXPORT TESTS +# ============================================================================= + + +class TestMetadataExport: + """New metadata fields are emitted on export.""" + + @pytest.fixture + def exported(self): + adapter = SupersetAdapter() + graph = adapter.parse("tests/fixtures/superset/multi_catalog_revenue.yaml") + with tempfile.TemporaryDirectory() as tmpdir: + adapter.export(graph, tmpdir) + with open(Path(tmpdir) / "revenue_by_region.yaml") as f: + yield yaml.safe_load(f) + + def test_catalog_exported(self, exported): + assert exported["catalog"] == "analytics_catalog" + assert exported["schema"] == "finance" + assert exported["table_name"] == "revenue_by_region" + + def test_currency_code_column_exported(self, exported): + assert exported["currency_code_column"] == "iso_currency" + + def test_folders_exported(self, exported): + assert isinstance(exported["folders"], list) + assert exported["folders"][0]["name"] == "Money" + + def test_column_metadata_exported(self, exported): + region = next(c for c in exported["columns"] if c["column_name"] == "region") + assert region["advanced_data_type"] == "country" + + report_date = next(c for c in exported["columns"] if c["column_name"] == "report_date") + assert report_date["python_date_format"] == "%Y-%m-%d" + assert report_date["datetime_format"] == "%Y-%m-%d" + + def test_metric_metadata_exported(self, exported): + total_revenue = next(m for m in exported["metrics"] if m["metric_name"] == "total_revenue") + assert total_revenue["d3format"] == "$,.2f" + assert total_revenue["currency"]["symbol"] == "EUR" + assert total_revenue["currency"]["symbolPosition"] == "suffix" + assert total_revenue["warning_text"] == "Preliminary figures, subject to revision" + + +# ============================================================================= +# ROUNDTRIP TESTS +# ============================================================================= + + +class TestMetadataRoundtrip: + """All new metadata survives Superset -> Sidemantic -> Superset.""" + + @pytest.fixture + def reparsed(self): + adapter = SupersetAdapter() + graph = adapter.parse("tests/fixtures/superset/multi_catalog_revenue.yaml") + with tempfile.TemporaryDirectory() as tmpdir: + adapter.export(graph, tmpdir) + graph2 = adapter.parse(Path(tmpdir) / "revenue_by_region.yaml") + yield graph2.models["revenue_by_region"] + + def test_catalog_roundtrip(self, reparsed): + assert reparsed.table == "analytics_catalog.finance.revenue_by_region" + assert reparsed.meta["superset"]["catalog"] == "analytics_catalog" + + def test_currency_code_column_roundtrip(self, reparsed): + assert reparsed.meta["superset"]["currency_code_column"] == "iso_currency" + + def test_folders_roundtrip(self, reparsed): + folders = reparsed.meta["superset"]["folders"] + assert folders[0]["name"] == "Money" + assert {c["name"] for c in folders[0]["children"]} == {"total_revenue", "avg_revenue"} + + def test_column_metadata_roundtrip(self, reparsed): + region = reparsed.get_dimension("region") + assert region.meta["superset"]["advanced_data_type"] == "country" + report_date = reparsed.get_dimension("report_date") + assert report_date.meta["superset"]["python_date_format"] == "%Y-%m-%d" + assert report_date.meta["superset"]["datetime_format"] == "%Y-%m-%d" + + def test_metric_metadata_roundtrip(self, reparsed): + total_revenue = reparsed.get_metric("total_revenue") + assert total_revenue.format == "$,.2f" + assert total_revenue.meta["superset"]["d3format"] == "$,.2f" + assert total_revenue.meta["superset"]["currency"] == {"symbol": "EUR", "symbolPosition": "suffix"} + assert total_revenue.meta["superset"]["warning_text"] == "Preliminary figures, subject to revision" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/adapters/superset/test_roundtrip.py b/tests/adapters/superset/test_roundtrip.py index 3e27f0e0..610b8cfd 100644 --- a/tests/adapters/superset/test_roundtrip.py +++ b/tests/adapters/superset/test_roundtrip.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +import yaml from sidemantic.adapters.superset import SupersetAdapter @@ -74,5 +75,42 @@ def test_superset_roundtrip_metric_properties(): assert_metric_equivalent(m1, m2) +@pytest.mark.parametrize( + "dataset_extra, expected_table", + [ + ({"catalog": "warehouse", "schema": None}, "warehouse.events"), + ({"catalog": "warehouse", "schema": "public"}, "warehouse.public.events"), + ({"schema": "public"}, "public.events"), + ({}, "events"), + ], +) +def test_superset_catalog_schema_roundtrip(dataset_extra, expected_table): + """Test catalog/schema qualifiers survive a Superset roundtrip. + + A catalog-only (schema null) reference must not be re-emitted with the + catalog duplicated into ``schema`` (regression: ``cat.table`` -> ``cat.cat.table``). + """ + dataset = { + "table_name": "events", + "columns": [{"column_name": "id", "type": "INT"}], + **dataset_extra, + } + + adapter = SupersetAdapter() + + with tempfile.TemporaryDirectory() as tmpdir: + input_path = Path(tmpdir) / "events.yaml" + input_path.write_text(yaml.dump(dataset)) + + graph1 = adapter.parse(input_path) + assert graph1.models["events"].table == expected_table + + out_dir = Path(tmpdir) / "out" + adapter.export(graph1, out_dir) + graph2 = adapter.parse(out_dir / "events.yaml") + + assert graph2.models["events"].table == expected_table + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/fixtures/superset/multi_catalog_revenue.yaml b/tests/fixtures/superset/multi_catalog_revenue.yaml new file mode 100644 index 00000000..2ad4ae66 --- /dev/null +++ b/tests/fixtures/superset/multi_catalog_revenue.yaml @@ -0,0 +1,101 @@ +table_name: revenue_by_region +main_dttm_col: report_date +description: Multi-catalog revenue dataset spanning catalog.schema.table +catalog: analytics_catalog +schema: finance +sql: null +cache_timeout: null +filter_select_enabled: true +currency_code_column: iso_currency +version: 1.0.0 + +folders: + - uuid: aaaaaaaa-0000-0000-0000-000000000001 + type: folder + name: Money + description: Currency-formatted measures + children: + - uuid: aaaaaaaa-0000-0000-0000-000000000002 + type: metric + name: total_revenue + - uuid: aaaaaaaa-0000-0000-0000-000000000003 + type: metric + name: avg_revenue + +metrics: + - metric_name: count + verbose_name: Records + metric_type: count + expression: COUNT(*) + description: Number of revenue records + d3format: null + extra: null + warning_text: null + + - metric_name: total_revenue + verbose_name: Total Revenue + metric_type: sum + expression: SUM(revenue) + description: Total revenue + d3format: "$,.2f" + currency: + symbol: EUR + symbolPosition: suffix + extra: null + warning_text: Preliminary figures, subject to revision + + - metric_name: avg_revenue + verbose_name: Average Revenue + metric_type: avg + expression: AVG(revenue) + description: Average revenue per record + d3format: ".2f" + currency: + symbol: GBP + symbolPosition: prefix + extra: null + warning_text: null + +columns: + - column_name: id + verbose_name: ID + is_dttm: false + is_active: true + type: INTEGER + groupby: true + filterable: true + expression: null + description: Primary key + + - column_name: report_date + verbose_name: Report Date + is_dttm: true + is_active: true + type: DATE + groupby: true + filterable: true + expression: null + description: Reporting date + python_date_format: "%Y-%m-%d" + datetime_format: "%Y-%m-%d" + + - column_name: region + verbose_name: Region + is_dttm: false + is_active: true + type: VARCHAR + advanced_data_type: country + groupby: true + filterable: true + expression: null + description: Sales region + + - column_name: revenue + verbose_name: Revenue + is_dttm: false + is_active: true + type: NUMERIC + groupby: false + filterable: true + expression: null + description: Revenue amount diff --git a/tests/fixtures/superset/orders.yaml b/tests/fixtures/superset/orders.yaml index c9a38f86..c117a45a 100644 --- a/tests/fixtures/superset/orders.yaml +++ b/tests/fixtures/superset/orders.yaml @@ -5,8 +5,26 @@ schema: public sql: null cache_timeout: null filter_select_enabled: true +currency_code_column: currency_code version: 1.0.0 +folders: + - uuid: 11111111-1111-1111-1111-111111111111 + type: folder + name: Revenue + description: Revenue metrics + children: + - uuid: 22222222-2222-2222-2222-222222222222 + type: metric + name: total_revenue + - uuid: 33333333-3333-3333-3333-333333333333 + type: folder + name: Attributes + children: + - uuid: 44444444-4444-4444-4444-444444444444 + type: column + name: status + metrics: - metric_name: count verbose_name: COUNT(*) @@ -23,8 +41,11 @@ metrics: expression: SUM(amount) description: Sum of order amounts d3format: "$,.2f" + currency: + symbol: USD + symbolPosition: prefix extra: null - warning_text: null + warning_text: Excludes refunded orders - metric_name: avg_order_value verbose_name: Average Order Value @@ -51,6 +72,7 @@ columns: is_dttm: false is_active: true type: VARCHAR + advanced_data_type: internet_address groupby: true filterable: true expression: null @@ -65,7 +87,8 @@ columns: filterable: true expression: null description: Order creation timestamp - python_date_format: null + python_date_format: "%Y-%m-%d %H:%M:%S" + datetime_format: "%Y-%m-%dT%H:%M:%S" - column_name: status verbose_name: Order Status