fitzee commented on code in PR #45395:
URL: https://github.com/apache/superset/pull/45395#discussion_r4330796381
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
Review Comment:
**Non-blocking (vacuous).** The fault lane passes on any exception that is
not an SDK refusal or completeness error, including `TypeError`,
`AttributeError`, `KeyError` or `AssertionError` from a provider bug or a
broken fixture. With the example's fault path changed to `raise
TypeError("'NoneType' object is not subscriptable")`, all fault cases pass.
Consider letting the fixture declare the expected fault type, for example
`{"get_table:fault": (callable, ConnectionError)}`, or at least rejecting
`TypeError`, `AttributeError`, `NameError` and `AssertionError` here.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
+ error.value,
+ (SemanticQueryRejectedError, SemanticResultCompletenessError),
+ )
+
+ def test_advertised_capabilities(
+ self,
+ semantic_view: SemanticView,
+ semantic_capability_cases: Mapping[str, tuple[SemanticQuery,
pa.Table]],
+ ) -> None:
+ """Every advertised feature has a result-level witness in this lane."""
+ feature: SemanticViewFeature | str
+ for feature in semantic_view.features:
+ name: str = (
+ feature.value if isinstance(feature, SemanticViewFeature) else
feature
+ )
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_capability_cases[name]
+ assert expected.num_rows > 0
+ assert query.order or expected.num_rows == 1, (
+ f"{name}: multi-row fixtures need explicit order"
+ )
+ if name == "ROW_OFFSET":
+ assert query.offset is not None
+ assert query.offset > 0
+ assert query.limit is not None
+ assert query.limit > 0
+ elif name in {"GROUP_LIMIT", "GROUP_OTHERS"}:
Review Comment:
**Non-blocking (looser).** Only `ROW_OFFSET` and `GROUP_*` witnesses are
checked for actually using the feature. For `ADHOC_COLUMN_EXPRESSIONS` and
`ADHOC_EXPRESSIONS_IN_ORDERBY`, any plain query satisfies the lane, so "every
advertised feature has a result-level witness" does not hold for them. An
`AdhocExpression` should be required in `dimensions` or `order` respectively.
Also, a single test loops over every feature, so the first failing feature
hides the others. Parametrizing over `SemanticViewFeature`, with a skip when
the feature is not advertised, would report each one separately.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
Review Comment:
**Non-blocking (stricter than the contract).** `pa.Table.equals` compares
column order and field nullability. The SDK only promises "one column per
selected member name" (`types.py`, `view.py`), and the host selects by name
(`cache_transform.py`: `results.select(columns)`). A provider that returns
metrics before dimensions, or marks null-free columns `nullable=False`, fails
all 6 query cases and the capability case even though the host accepts it.
The nullability failure is also hard to diagnose because the message prints
identical data:
```
AssertionError: identity: expected {'category': [None, 'A', 'B'], 'total':
[3, 7, 8]}, got {'category': [None, 'A', 'B'], 'total': [3, 7, 8]}
```
Suggest comparing `result.results.select(expected.column_names)` after
normalizing nullability (or comparing types and values per column), and
including `schema` in the failure message. The same applies to line 207.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
+ error.value,
+ (SemanticQueryRejectedError, SemanticResultCompletenessError),
+ )
+
+ def test_advertised_capabilities(
+ self,
+ semantic_view: SemanticView,
+ semantic_capability_cases: Mapping[str, tuple[SemanticQuery,
pa.Table]],
+ ) -> None:
+ """Every advertised feature has a result-level witness in this lane."""
+ feature: SemanticViewFeature | str
+ for feature in semantic_view.features:
+ name: str = (
+ feature.value if isinstance(feature, SemanticViewFeature) else
feature
+ )
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_capability_cases[name]
+ assert expected.num_rows > 0
+ assert query.order or expected.num_rows == 1, (
+ f"{name}: multi-row fixtures need explicit order"
+ )
+ if name == "ROW_OFFSET":
+ assert query.offset is not None
+ assert query.offset > 0
+ assert query.limit is not None
+ assert query.limit > 0
+ elif name in {"GROUP_LIMIT", "GROUP_OTHERS"}:
+ assert query.group_limit is not None
+ if name == "GROUP_OTHERS":
+ assert query.group_limit.group_others
+ assert semantic_view.get_table(query).results.equals(expected),
name
+
+ def test_metadata_generation_and_unbound_bypass(
+ self,
+ semantic_make_view: Callable[[str | None], SemanticView],
+ semantic_change_catalog: Callable[[], None],
+ ) -> None:
+ """Changed generations and unbound calls cannot reuse a stale catalog.
+
+ The change fixture must add a member on each call. Factories and
transport
+ stay provider-owned; uncached legacy providers may report a None token.
+ """
+ first: SemanticView = semantic_make_view("generation-a")
+ before: tuple[set[str], set[tuple[str, str | None]]] = _catalog(first)
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ second: SemanticView = semantic_make_view("generation-b")
+ after: tuple[set[str], set[tuple[str, str | None]]] = _catalog(second)
+ assert after != before
+ assert second.metadata_cache_token in {None, "generation-b"}
+ # A captured view must never claim a later discovery token.
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ unbound: SemanticView = semantic_make_view(None)
Review Comment:
**Non-blocking (stricter than the contract).** No host path constructs a
provider without a token. `SemanticLayer.from_configuration` takes
`cache_token: str` (required, not optional), and its only host caller
(`execution.py`, the `bind` method) always passes `layer.metadata_generation`.
The "unbound" obligation exists only in this kit and in the sentence this PR
adds to `semantic-layer-cache-clear.md`.
As a result, adapters have to invent a tokenless path.
preset-io/superset-shell#5607 already calls `from_configuration(...,
cache_token=None)` against the `str` annotation. Either drop the `None` leg
(keep the two-generation freshness and no-relabel checks), or make the SDK
signature `str | None` and document when the host uses it.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
+ error.value,
+ (SemanticQueryRejectedError, SemanticResultCompletenessError),
+ )
+
+ def test_advertised_capabilities(
+ self,
+ semantic_view: SemanticView,
+ semantic_capability_cases: Mapping[str, tuple[SemanticQuery,
pa.Table]],
+ ) -> None:
+ """Every advertised feature has a result-level witness in this lane."""
+ feature: SemanticViewFeature | str
+ for feature in semantic_view.features:
+ name: str = (
+ feature.value if isinstance(feature, SemanticViewFeature) else
feature
+ )
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_capability_cases[name]
+ assert expected.num_rows > 0
+ assert query.order or expected.num_rows == 1, (
+ f"{name}: multi-row fixtures need explicit order"
+ )
+ if name == "ROW_OFFSET":
+ assert query.offset is not None
+ assert query.offset > 0
+ assert query.limit is not None
+ assert query.limit > 0
+ elif name in {"GROUP_LIMIT", "GROUP_OTHERS"}:
+ assert query.group_limit is not None
+ if name == "GROUP_OTHERS":
+ assert query.group_limit.group_others
+ assert semantic_view.get_table(query).results.equals(expected),
name
+
+ def test_metadata_generation_and_unbound_bypass(
+ self,
+ semantic_make_view: Callable[[str | None], SemanticView],
+ semantic_change_catalog: Callable[[], None],
+ ) -> None:
+ """Changed generations and unbound calls cannot reuse a stale catalog.
+
+ The change fixture must add a member on each call. Factories and
transport
+ stay provider-owned; uncached legacy providers may report a None token.
+ """
+ first: SemanticView = semantic_make_view("generation-a")
+ before: tuple[set[str], set[tuple[str, str | None]]] = _catalog(first)
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ second: SemanticView = semantic_make_view("generation-b")
+ after: tuple[set[str], set[tuple[str, str | None]]] = _catalog(second)
+ assert after != before
+ assert second.metadata_cache_token in {None, "generation-b"}
+ # A captured view must never claim a later discovery token.
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ unbound: SemanticView = semantic_make_view(None)
+ assert _catalog(unbound) != after
+ assert unbound.metadata_cache_token is None
+
+ def test_imports_and_static_schema(
+ self, semantic_provider_class: tuple[str, str]
+ ) -> None:
+ """Import in a fresh interpreter; reject host imports and schema
I/O."""
+ import os
+ import subprocess
+ import sys
+
+ # Fixed interpreter/code; provider metadata is argv, never shell text.
+ completed: subprocess.CompletedProcess[str] = subprocess.run( # noqa:
S603
+ [
+ sys.executable,
+ "-c",
+ "from superset_core.semantic_layers.testing import
_check_isolation; "
+ "import sys; _check_isolation(*sys.argv[1:])",
+ *semantic_provider_class,
+ ],
+ env={**os.environ, "PYTHONPATH": os.pathsep.join(sys.path)},
+ capture_output=True,
+ text=True,
+ timeout=30,
+ check=False,
+ )
+ assert completed.returncode == 0, completed.stdout + completed.stderr
+
+ # TODO: add the default chronological-order case after its SDK rule lands.
Review Comment:
**Nit.** This TODO is stale. #45389 landed the rule, and it is host-side:
`with_default_order` fills `order` before dispatch, and the `SemanticQuery`
docstring says a `get_table` call "receives an empty `order` only when no
temporal dimension is selected". The provider-side obligation is just "honour
the explicit order", which `order_limit`/`grain` already cover. The same
applies to the "Default chronological ordering will be added after that SDK
rule lands" sentence in `semantic-layer-provider-testing.md`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]