Skip to main content
Version: Next

Semantic provider metadata contract

The optional interfaces in superset_core.semantic_layers.metadata let a provider use a host-owned catalog store. This SDK defines the boundary; it does not supply storage, authorization, endpoints or a refresh UI. Existing providers need no new overrides.

Opting in​

SemanticLayer.supports_metadata_refresh(configuration) defaults to False. Overrides must determine support without construction, network requests or cache writes. SemanticLayer.metadata_refresh defaults to None; an opted-in provider returns a MetadataRefreshAdapter. The property must return the same instance for the layer's lifetime; views and runtime-schema discovery use that bound instance. If support is declared but the instance is absent, an enabled host must reject the configuration rather than silently using legacy cache keys.

The host authorizes and resolves the stored connection before constructing the provider. It calls adapter.bind(store, deadline=deadline) once, before discovery. Rebinding raises MetadataRefreshError("configuration"); an unbound refresh raises MetadataRefreshError("unsupported"). Invalid or exhausted bind deadlines raise MetadataRefreshError("deadline") before binding and leave the adapter unbound.

Full and custom views use the bound store. The adapter's instance get_runtime_schema(runtime_data=None) returns the existing schema shape using that same store, avoiding the legacy classmethod's unbound construction path. Unsaved configuration previews cannot publish into a stored connection namespace. A bound store failure must not silently fall back to provider-local caching. Existing unbound preview/classmethod discovery remains a provider responsibility; it never publishes into the stored connection namespace. The in-memory example below demonstrates the bound path, not a complete preview implementation.

One bound scope contains one catalog covering that connection's supported runtime configurations. A provider needing different catalogs per runtime database/schema must not opt in until its scope can satisfy that requirement.

Discovery must be identical for every user of that connection scope. Providers whose discovery depends on per-user OAuth credentials, impersonation or another user-specific identity must not declare supports_metadata_refresh; the shared catalog has no per-user dimension. Query authorization and result-cache user/RLS identity remain separate host responsibilities.

Data exchanged​

TypeFields / signatureMeaning
CatalogLoaderCallable[[float], str]Acquisition within a host-supplied absolute monotonic deadline returning provider-validated canonical JSON; valid empty data is allowed
CatalogSnapshotpayload: str, cache_token: str, observed_at: strImmutable observation and its captured scope-qualified identity
MetadataRefreshResultstatus: Literal["changed", "unchanged"], snapshot: CatalogSnapshotConfirmed publication, not just completed acquisition
MetadataSnapshotStore.read(fetch: CatalogLoader, *, deadline: float) -> CatalogSnapshotValid observation or bounded coordinated acquisition
MetadataRefreshAdapter.bind(store: MetadataSnapshotStore, *, deadline: float) -> NoneBind once and capture the host operation's discovery deadline
MetadataRefreshAdapter.refresh(*, deadline: float) -> MetadataRefreshResultPass the caller's budget to the bound store
MetadataSnapshotStore.refresh(fetch: CatalogLoader, *, deadline: float) -> MetadataRefreshResultBypass a hit and confirm a fresh publication

The provider validates and interprets its payload. The host owns scope, expiry, authorization and publication. These records do not validate vendor JSON or implement coordination. observed_at is a UTC RFC3339 source-observation time, not entry creation, expiry or an ordering authority. CatalogSnapshot rejects non-string fields and empty tokens/timestamps with fixed messages that do not echo input. Timestamp syntax remains the producer's responsibility; this SDK record does not parse RFC3339 or validate canonical JSON.

Every successful publication gets a new nonempty opaque cache_token, including unchanged discovery. Cache hits retain the captured token. Equal discovery data cannot prove equal metric definitions if the upstream selector omits complete query semantics. changed includes installation without a prior observation.

SemanticView.metadata_cache_token defaults to None for legacy views. A bound opted-in view returns the token captured with its parsed members. Hosts reject missing tokens in that mode and include the captured token in metadata-dependent compatibility and result-cache keys, preserving view/query/access identities. An old result must never be labelled with a token fetched later. Host/provider implementations must enforce and test these obligations.

Failures and concurrency​

MetadataRefreshErrorCategory is the typed set accepted by MetadataRefreshError: unsupported, configuration, in_progress, configuration_changed, upstream, invalid_payload, deadline, unavailable and indeterminate. Producers pass these categories, not raw vendor messages or credentials; the constructor rejects unknown values with a fixed error that does not echo them. Hosts must not expose exception cause chains. Providers translating sensitive vendor errors should use raise MetadataRefreshError("upstream") from None. HTTP status codes and localized text belong to the host.

  • configuration_changed: the authorized connection scope/configuration changed during the operation; discard the candidate rather than publish under obsolete authority.
  • invalid_payload: acquisition or a stored observation cannot satisfy the provider's catalog schema; never publish partial metadata or an error as an empty catalog.
  • unavailable: host-owned metadata storage or coordination cannot be reached or used; upstream instead identifies failure to acquire metadata from the provider's remote service.

The host establishes one finite absolute float deadline using time.monotonic() in the calling process before waiting or acquisition. It passes that value explicitly to adapter.bind(store, deadline=deadline), adapter.refresh(deadline=deadline), store.read(fetch, deadline=deadline) and store.refresh(fetch, deadline=deadline). The adapter forwards it to the store, and the store passes it to fetch(deadline) unchanged. Multiple calls within the same operation share this deadline through publication; neither the store nor nested acquisition may mint a new budget. Implementations use remaining_budget to reject non-finite, expired or implausibly large deadlines before I/O, including cache reads. The protocol signatures do not execute these checks on behalf of implementations. The provider checks the remaining time before each upstream operation and uses the tighter of that budget and its own transport limit. Exhaustion raises MetadataRefreshError("deadline") before further I/O; nested calls must not restart the budget. Checking only after an unbounded request is insufficient. Actual transport and publication budget enforcement require host/provider tests.

The existing layer/view and runtime-schema discovery signatures stay compatible. Ordinary from_configuration(configuration) construction remains deadline-free. The host hands the operation's deadline to bind; the adapter captures it for full/custom-view and runtime-schema discovery, passing it unchanged to store reads. A new operation requires a fresh provider/adapter instance and a new bind, not rebinding or renewing an existing instance's discovery budget. The explicit store/refresh parameters remain required. The example exercises normal provider construction followed by this bind-time handoff.

Use remaining_budget(deadline) before I/O and publication. It returns the positive remaining seconds and raises MetadataRefreshError("deadline") for non-finite, expired or implausibly large budgets. The SDK ceiling MAX_METADATA_BUDGET_SECONDS = 300.0 permits slow metadata discovery while catching common time.time()/monotonic clock confusion; it is not a default or a reason to extend the caller's budget. Longer requests must be split into bounded operations. Hosts/providers may use tighter limits. For a host with an injected monotonic clock, remaining_budget(deadline, now=clock()) uses that same clock without introducing host types into the SDK.

This value is neither a duration nor a UTC timestamp. Do not serialize it across processes, use it as cache expiry, or use it to order publications. A deadline cannot cancel arbitrary synchronous code and never replaces a publication fence.

An active explicit refresh can report in_progress. Cold readers wait/re-read within a finite deadline or fail safely. A delayed writer cannot replace a later accepted observation. Failure never installs an error as an empty catalog or renews freshness. Unconfirmed publication reports indeterminate; a timeout proves neither success nor rollback. These guarantees require real host backend tests.

Separate host cache controls and diagnostics​

Catalog invalidation, independent compatibility invalidation, selected-query result refresh and read-only cache inspection are host responsibilities. No extra provider methods are required. Catalog invalidation retires both older writer authority and derived-cache validity. Compatibility-only invalidation need not fetch metadata; result refresh does not imply catalog refresh.

Inspection reports recorded entry creation and expiry separately from source observation. It must not fetch, fill or renew entries. Missing legacy timestamps remain unknown. Cache payloads, credentials and raw keys are not diagnostic fields. Storage instrumentation, access enforcement and UI presentation are not functionality supplied by these SDK records.

Compatibility and verification​

No new abstract methods are added to the existing layer or view bases. Providers importing this API must require a released SDK containing it: a disabled feature cannot make a missing import safe. Fleet-wide freshness requires a compatible host/provider fleet and a coordinated store. Upstream query-version pinning and vendor-cache clearing are not promised.

tests/unit_tests/semantic_layers/metadata_contract_test.py contains a minimal legacy provider and an opted-in in-memory example. It checks captured observations, duplicate-label/raw-ID preservation, immutable records, field validation, explicit shared deadlines, error categories and imports without host dependencies in a fresh isolated subprocess. The test file is collected by the standard unit-test CI lane. The sequential example does not establish multiworker or backend correctness.