Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 373 additions & 0 deletions design-docs/gravitino-metric-view-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# Design of Metric View Support in Gravitino

## Background

Business metrics such as revenue, order count, and active users are shared semantic assets consumed by analytics, BI, and AI applications. When their definitions are kept only in individual semantic-layer tools or project files, discovery, ownership, version history, access control, and consistent reuse become fragmented. Gravitino therefore needs a governed metadata model that manages metric definitions alongside the data entities they reference.

Semantic-layer definitions are commonly authored and exchanged as YAML. That is convenient for authoring and interoperability, but a raw document does not provide Gravitino consumers with a typed API for datasets, relationships, fields, metrics, and AI context. This design introduces an OSI/Ossie-compatible structured representation while retaining the existing View lifecycle and governance model.

## Goals

- **Unified lifecycle.** Represent metric definitions as schema-scoped metadata and manage them through the existing View lifecycle.
- **Structured access.** Expose datasets, relationships, fields, metrics, AI context, and extensions through typed APIs.
- **Governance.** Apply View-level identity, authorization, ownership, audit, tags, policies, and version history to metric definitions.
- **Compatibility.** Preserve existing logical View behavior and provide explicit capability handling for connectors that do not support Metric Views.
- **Validation.** Define deterministic write-time checks and clear boundaries for catalog-dependent validation.

## Non-Goals

- **Non-OSI native models.** Compatibility with dbt, Cube, Databricks, Snowflake, or other non-OSI semantic definitions is outside this design.
- **Document authoring and conversion.** YAML parsing, formatting, conversion, and exact textual round trips are not server API contracts. External tools may provide best-effort stable serialization.
- **Compilation and execution.** Semantic query planning, SQL generation, engine execution, and engine-specific compatibility are separate work.
- **Materialization.** Metric caches, refresh policies, and materialized results are not defined here.
- **Continuous dependency maintenance.** Catalog-wide lineage, automatic revalidation after catalog changes, and transitive cycle analysis are not included.
- **Member-level authorization.** Datasets, fields, and metrics are governed as members of the enclosing Metric View rather than as independently authorized entities.

## Proposed Design

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few sections reviewers will likely ask for: storage impact (does the whole model live in view_version_info.representations JSON, and is there a size limit, given every alter copies it into a new version?); OpenAPI updates under docs/open-api; Python client scope; audit and event listeners; feature flag; entity cache interaction (design-docs/cache-improvement-design.md). Also worth defining OSI/Ossie on first use, and folding in a short rationale for this shape versus the superseded alternative.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The complete structured model is stored in the existing view_version_info.representations snapshot, and each alter creates a full immutable View version, as already described in the storage section. There is no Metric-View-specific size limit; it follows the existing View storage contract. The entity cache stores only the current View entity and uses the existing View invalidation path.

Python and REST are already in scope; OpenAPI updates, View events/listeners, and cache wiring follow the existing View implementation paths. I don’t think a separate feature flag is needed.

The background already explains why the structured model was chosen over opaque YAML. I agree that the first reference should define Apache Ossie as the project formerly known as Open Semantic Interchange (OSI).


### Object Model and Constraints

A Metric View is a specialized use of the existing View object under a metalake, catalog, and schema. It does not introduce a new top-level metadata object.

```text
metalake.catalog.schema
View (logical)
SQLRepresentation
View (metric)
MetricRepresentation
```

- **Containment and governance.** The enclosing Metric View is the governed object. Datasets, relationships, fields, metrics, AI context, and extensions are members of its representation.
- **Semantic identity.** A logical View defines fixed SQL computation and fixed output columns. A Metric View defines query-time semantic choices, so the two are distinct kinds of definitions.
- **Namespace.** Logical and Metric Views share the same schema-level View namespace and name rules; same-name objects cannot coexist (see Storage and Connector Behavior for conflict resolution).
- **Representation.** A Metric View contains exactly one `MetricRepresentation`. It cannot contain a SQL representation, and alter requests that change a View between logical and metric semantics are rejected.
- **Lifecycle and columns.** Metric Views reuse View create, list, load, alter, drop, and version operations. Their `columns` collection is always empty because the output schema is selected at query time.

### Representation Model

The upstream OSI document places its specification version beside an array of semantic models. The abbreviated form is:

```yaml
version: 0.2.0.dev0
semantic_model:
- name: sales_semantic_model
datasets:
- name: orders
source: sales.mart.orders
```

Gravitino maps the root `version` to `MetricRepresentation.osiVersion` and one `semantic_model` item to `semanticModel`. A three-part OSI dataset source maps to a `NameIdentifier`. View identity and lifecycle remain in the surrounding View object.

```text
MetricRepresentation
type: "metric"
osiVersion: string
semanticModel: MetricModel
```

The representation has three fields:

- `type`: The fixed value "metric" classifies the View as a Metric View.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

RepresentationDTO declares defaultImpl = SQLRepresentationDTO.class (common/src/main/java/org/apache/gravitino/dto/rel/RepresentationDTO.java:37), so an unrecognized type does not fail, it deserializes as a SQL representation. Any rolling upgrade where an older server or client sees type: "metric" hits this. Worth calling out removing defaultImpl or adding explicit unknown-type rejection. The same class sets @JsonIgnoreProperties(ignoreUnknown = true), which works against strict validation of the metric subtree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

defaultImpl was reasonable when SQL was the only representation type. With Metric View introducing a second subtype, missing or unknown type values will be rejected explicitly.

Metric View payloads will use strict validation, so unknown standardized fields in the Metric subtree will be rejected rather than ignored. Both behaviors will be covered by tests.

- `osiVersion`: A required string identifying the OSI profile used to interpret and validate the model. The initial supported value is `0.2.0.dev0`.
- `semanticModel`: The stable, structured Gravitino model exposed through public APIs.
- A Metric View contains exactly one `MetricRepresentation`.
- Its `columns` array is empty.
- It cannot contain a SQL representation.
- Both `osiVersion` and `semanticModel` are required.

#### MetricModel Schema

The canonical model follows the pinned OSI `0.2.0.dev0` profile. Fields marked with `?` are optional; all other fields are required. Names below use OSI wire-format spelling, while language bindings use idiomatic accessor names.

```text
MetricModel
name: string
description?: string
ai_context?: AIContext
datasets: Dataset[1..*]
relationships?: Relationship[]
metrics?: Metric[]
custom_extensions?: CustomExtension[]
```

- `MetricModel` contains at least one `Dataset`.
- Names in each collection follow the uniqueness and reference rules defined with the nested types below.

Dataset and field definitions:

```text
Dataset
name: string
source: NameIdentifier
primary_key?: string[]
unique_keys?: string[][]
description?: string
ai_context?: AIContext
fields?: Field[]
custom_extensions?: CustomExtension[]

Field
name: string
expression: Expression
dimension?: Dimension
label?: string
description?: string
ai_context?: AIContext
custom_extensions?: CustomExtension[]
```

- `Dataset` names are unique within `MetricModel`.
- `Field` names are unique within each `Dataset`.
- Internal field references resolve within the model.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This appears to contradict the next bullet, "It does not infer source-column references from field or metric expressions." Expressions are opaque per-dialect strings, so there is nothing to resolve without parsing them. Either drop the claim or define which structured fields count as an internal reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

“Internal field references” is ambiguous and could imply expression parsing, so I’ll remove that bullet. The following rules already define the validation boundary: relationship endpoints reference existing Datasets, while primary_key, unique_keys, from_columns, and to_columns are checked against the columns exposed by the referenced Table or logical View. Field and metric expressions remain opaque.

- Each `source` is a `NameIdentifier` with namespace `[catalog, schema]`; `source.name` identifies a `Table` or `View`, and the enclosing object supplies the metalake.
- For `Table` and logical `View` sources, Gravitino validates columns explicitly declared in `primary_key`, `unique_keys`, `from_columns`, and `to_columns` against the source schema. It does not infer source-column references from field or metric expressions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the meaning of "the enclosing object supplies the metalake"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How do you differentiate a table or a view only by the name identifier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What is the meaning of "the enclosing object supplies the metalake"?

It means that Dataset.source stores catalog.schema.name, Gravitino resolves it within the Metric View's metalake. Cross-catalog references within that metalake are allowed, while cross-metalake references are not supported.

How do you differentiate a table or a view only by the name identifier?

We do not need Dataset.source to distinguish between a Table and a View. We only need to verify that the referenced entity exists. During validation, Gravitino can call loadTable first and, if no Table is found, call loadView.

- Metric View sources validate direct existence only.
- Inline query sources are not supported; register the query as a logical View and reference that View through a `NameIdentifier`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain more about the meaning here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For example, instead of storing SELECT * FROM sales.orders WHERE status = 'active' directly in Dataset.source, create a logical View named sales.mart.active_orders with that SQL and set Dataset.source to sales.mart.active_orders. Raw SQL is not stored directly in the Metric View.

I will add the above explanation to the doc.

- Catalog unavailability is treated as a retriable validation failure.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since member-level patches are unsupported, changing a description means a full replaceView, which revalidates every source, so one unreachable catalog blocks a metadata-only edit. Worth deciding whether source validation is skippable (view marked unvalidated) or config gated, and stating 503 rather than 400 for the retriable case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Introducing validation state would add unnecessary complexity. Since writes are expected to be infrequent, revalidating sources on each write is an acceptable trade-off and ensures that persisted Metric Views remain validated.

I agree that transient catalog unavailability should return 503, while invalid definitions should remain 400.


Relationship and metric definitions:

```text
Relationship

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Relationship has no cardinality or join type, so fan-out and double-counting cannot be planned safely; Metric has no result type, aggregation semantics, or declared grain; Dimension carries only is_time, with no granularity, and there is no default time dimension. If these are upstream OSI limits, saying so and naming custom_extensions as the interim extension point would set expectations.

Separately, field names are unique per Dataset and metric names per model, but consumers reference both in one query namespace: worth a cross-collection uniqueness rule or a precedence definition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are limitations of the pinned OSI profile. Relationship implies from as the many side and to as the one side, while the other semantics mentioned are not currently standardized by OSI. Gravitino follows this metadata model, and query planning is outside the scope of this design.

I don’t think cross-collection uniqueness belongs here. Fields are scoped to a Dataset, metrics to the SemanticModel, and any shared query namespace or precedence should be defined by the downstream query compiler.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fine to follow the pinned profile. The from = many, to = one convention is useful and not obvious from the schema; worth stating it in the doc so consumers do not have to infer it.

name: string
from: string
to: string
from_columns: string[1..*]
to_columns: string[1..*]
ai_context?: AIContext
custom_extensions?: CustomExtension[]

Metric
name: string
expression: Expression
description?: string
ai_context?: AIContext
custom_extensions?: CustomExtension[]
```

- `Relationship` and `Metric` names are unique within `MetricModel`.
- Each relationship endpoint references an existing `Dataset`.
- `from_columns` and `to_columns` are non-empty and have equal length.
- Each metric expression satisfies the `Expression` rules below.

Supporting types:

```text
Expression
dialects: DialectExpression[1..*]

DialectExpression
dialect: Dialect
expression: string

Dimension
is_time?: boolean

AIContext = string | { instructions?: string, synonyms?: string[],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The string-or-object union is awkward for Java and Python bindings and for the OpenAPI schema, and the trailing ... in the object form is undefined. Suggest normalizing on write (accept a bare string, store it as instructions) so reads see one shape, and stating whether unknown keys inside AIContext are retained or rejected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The union and extensible object are defined by the pinned OSI schema. A bare string is not specified as equivalent to instructions, so normalizing it would introduce additional semantics and lose the original shape. #12386 models both forms explicitly.

I agree that the trailing ... is unclear. I’ll clarify that unknown keys in the object form are allowed and retained losslessly, matching OSI additionalProperties: true.

examples?: string[], ... }

CustomExtension
vendor_name: string
data: string

Dialect = "ANSI_SQL" | "SNOWFLAKE" | "MDX" | "TABLEAU"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This creates a second dialect vocabulary alongside the existing Dialects type (trino, spark, hive, flink) in the same View object, with different naming conventions and different extensibility. A closed enum also makes every new OSI dialect a Gravitino API change plus a hard write-time rejection. Suggest an open string with well-known constants, and a sentence on what a consumer does when its dialect is absent from an Expression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#12386 already models the dialect as an open String with MetricDialects well-known constants rather than an enum, so new values do not require changing the API type. Dialects identifies logical View SQL representations, while MetricDialects follows OSI expression dialects.

Gravitino does not interpret or compile these expressions. Dialect selection and fallback are consumer-specific; a downstream consumer may reject a dialect it cannot interpret.

| "DATABRICKS" | "MAQL" | "BIGQUERY"
```

- Each `Expression` contains at least one `DialectExpression`.
- Every dialect entry uses a supported `Dialect`.
- Every dialect entry has a non-empty `expression`.
- `Dimension`, `AIContext`, and `CustomExtension` values satisfy the structures above.

The required `MetricModel.name` is independent of the enclosing View name. This preserves semantic-model identity across imports and View renames.

Every supported `custom_extensions` array is retained losslessly. Unknown standardized fields are rejected until the declared OSI profile supports them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Unknown standardized fields are rejected until the declared OSI profile supports them."

Is this the OSI's behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. For standardized OSI model objects, the schema defines the supported fields and sets additionalProperties to false. Vendor-specific data remains supported through explicitly defined extension points such as custom_extensions.

See the OSI schema definition.


Create and alter reject an unsupported `osiVersion` before validating or persisting `semanticModel`. The REST API returns HTTP 400 with the requested and supported versions, and creates no View or View version.

Once an `osiVersion` has been accepted for persistence by a released Gravitino version, later releases retain read support for it. Write support may be removed only through a major-version compatibility change with a documented migration path. Unknown stored versions fail explicitly and are never silently coerced.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's hard to support multi-version write. We can revisit the restrictions when we want to bump the supported versions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The original purpose of osiVersion was to preserve interpretability if the OSI specification changes field semantics across versions. Each Metric View would be bound to the OSI version under which it was defined, allowing clients and validators to select the correct interpretation.

After reconsideration, I think introducing this multi-version mechanism now is premature. If OSI introduces an incompatible semantic change in the future, Gravitino can add explicit version metadata then and treat definitions without it as using the initial semantics. I will simplify the current design by removing osiVersion.


**Implementation note:**

Gravitino selects a versioned OSI validation profile by `osiVersion`. Each profile pins the upstream JSON Schema and adds only version-specific projection or semantic rules beyond the shared validator. Compatible OSI versions may reuse the same validator implementation, while incompatible changes require a new profile. Every new OSI version must still be explicitly registered in a Gravitino release; unregistered versions are rejected. Adapters required to read previously accepted versions are retained for backward compatibility.

- All representation and model checks run on `create` and `alter` before a View or View version is persisted.
- Validation checks direct references only.
- Transitive dependency and cycle correctness are not checked; a cyclic definition may be persisted and later rejected by a downstream consumer.
- Catalog changes do not trigger automatic revalidation.
- Catalog-wide revalidation is excluded because it would require a dependency index and potentially global impact analysis.

### Usage

Metric Views reuse the existing View lifecycle. Their columns are always empty, and create, list, load, alter, and drop operations use the existing View APIs.

#### Supported Alter Operations

Metric Views support the existing `ViewChange` operations:

- `rename`: Renames the enclosing View only; `MetricModel.name` is unchanged. The target name must be available in the shared View namespace.
- `setProperty`: Adds or replaces a View-level property.
- `removeProperty`: Removes a View-level property.
- `replaceView`: Atomically replaces the View body. The `columns` must remain empty, exactly one `MetricRepresentation` must remain, and changing between metric and logical semantics is rejected.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

replaceView has no expected-version or ETag precondition, so read-modify-write of the whole model means two concurrent editors silently lose one. Versioning already exists in view_version_info, so an optional expectedVersion is cheap. If the risk is accepted deliberately, worth saying so.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

view_version_info is internal history; its version is not exposed through View or the update APIs, so expectedVersion would require a broader Java, REST, and Python contract change.

For the initial design, I propose retaining the existing View last-write-wins behavior. Optimistic concurrency can be added consistently to the generic View API later if needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agreed, last-write-wins is the right scope for the initial design. Worth one line in the doc saying so, since member-level patches are excluded and every edit is a full read-modify-write.


Member-level patch operations are not supported; changes to datasets, relationships, fields, or metrics require replacing the complete `MetricModel`.

Metric Views do not use `defaultCatalog` or `defaultSchema` because dataset sources use `NameIdentifier`; both values must be `null` in create and `replaceView` requests.

#### Java API

The Java API uses immutable builders for the structured definition and the existing ViewCatalog lifecycle methods:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Python API should also be supported, which is even more important.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok, let me add it.


```java
NameIdentifier ident = NameIdentifier.of("mart", "sales_metrics");
Dataset orders =
Dataset.builder()
.withName("orders")
.withSource(NameIdentifier.of("sales", "mart", "orders"))
.build();

MetricModel model =
MetricModel.builder()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shall we let users to specify the version number if we want to support multiple version?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I will remove the version number in current design. see #12360 (comment)

.withName("sales_semantic_model")
.withDatasets(List.of(orders))
.build();

MetricRepresentation representation =
MetricRepresentation.builder()
.withOsiVersion("0.2.0.dev0")
.withSemanticModel(model)
.build();

View created =
catalog.createMetricView(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This contradicts the premise that existing View APIs manage both kinds; load, alter, and drop below and the whole REST section use the generic resource. Suggest createView(ident, comment, new Column[0], new Representation[] {representation}, null, null, Map.of()).

Related: the design never says how a caller detects a Metric View. A Representation.TYPE_METRIC constant next to TYPE_SQL (api/src/main/java/org/apache/gravitino/rel/Representation.java:29, whose javadoc still says SQL is the only supported type) and a View.metricRepresentation() accessor mirroring sqlFor(dialect) (View.java:97) would help. Empty columns cannot be the discriminator: ViewCreateRequest.validate() already permits empty columns for logical views (ViewCreateRequest.java:110).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

createMetricView is only a Java convenience method. It delegates to the generic createView API while enforcing the Metric View constraints: empty columns, exactly one MetricRepresentation, and no default catalog or schema. The REST API and the remaining View lifecycle stay generic.

I agree that detection should be explicit. #12386 adds Representation.TYPE_METRIC and View.metricRepresentation(); empty columns are not used as the discriminator. I’ll update the design example accordingly.

ident, "Sales metric definitions", List.of(representation),
null, null, Map.of());
```

```java
View loaded = catalog.loadView(ident);
NameIdentifier[] views = catalog.listViews(Namespace.of("mart"));

MetricModel updatedModel =
MetricModel.builder()
.withName("sales_semantic_model")
.withDescription("Updated sales model")
.withDatasets(List.of(orders))
.build();
MetricRepresentation updatedRepresentation =
MetricRepresentation.builder()
.withOsiVersion("0.2.0.dev0")
.withSemanticModel(updatedModel)
.build();

View updated =
catalog.alterView(
ident,
ViewChange.replaceView(
new Column[0],
new Representation[] {updatedRepresentation},
null, null, "Updated sales metric definitions"));

boolean dropped = catalog.dropView(ident);
```

#### REST API

REST uses the existing View resources. Create supplies an empty columns array and one Metric representation:

```http
POST /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views
{
"name": "sales_metrics",
"comment": "Sales metric definitions",
"columns": [],
"representations": [
{
"type": "metric",
"osiVersion": "0.2.0.dev0",
"semanticModel": {
"name": "sales_semantic_model",
"datasets": [
{ "name": "orders", "source": { "namespace": ["sales", "mart"], "name": "orders" } }
]
}
}
]
}
```

List, load, alter, and drop use the same resource:

```http
GET /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views
GET /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views/sales_metrics

PUT /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views/sales_metrics
{
"updates": [
{
"@type": "replaceView",
"columns": [],
"representations": [
{
"type": "metric",
"osiVersion": "0.2.0.dev0",
"semanticModel": {
"name": "sales_semantic_model",
"description": "Updated sales model",
"datasets": [
{ "name": "orders", "source": { "namespace": ["sales", "mart"], "name": "orders" } }
]
}
}
],
"comment": "Updated sales metric definitions"
}
]
}

DELETE /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views/sales_metrics
```

### Storage and Connector Behavior

- **Source of truth.** Metric Views are stored only in the Gravitino EntityStore. Logical Views remain stored by their underlying catalogs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two things here.

The store-only read path does not exist today. ViewOperationDispatcher.internalLoadView starts with c.doWithViewOps(v -> v.loadView(ident)) and throws NoSuchViewException if the catalog lacks the object (core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java:397); listViews (line 84), dropView, and alterView follow the same pattern. So a Metric View stored only in the EntityStore is unreachable through all of them. Worth stating where the branch lives (dispatcher short circuit vs. a Gravitino-managed view-ops impl selected by Capability.Scope.VIEW), whether a Metric View can be created in an Iceberg/HMS-backed schema, and what happens on schema/catalog drop cascade.

"Logical Views remain stored by their underlying catalogs" also conflicts with the accepted logical view design, which defines a fully Gravitino-managed tier for catalogs with no native view support (design-docs/gravitino-logical-view-management.md:152). Gravitino-stored logical Views already exist, so the merge and conflict rules should be defined against the existing capability tiers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Most of this is already covered by the lifecycle, storage, and listing sections.

I’ll clarify that Metric Views are managed independently of the underlying catalog’s View capability, align the Logical View wording with the existing capability tiers, and add the schema/catalog cascade semantics. The concrete dispatcher routing will remain an implementation detail.

- **Listing.** The server merges authorized catalog-backed logical Views with authorized Gravitino-managed Metric Views into the existing View listing.
- **Connector capability.** A connector that does not support Metric Views filters them from `listViews` and returns an explicit unsupported-Metric-View error for a direct `loadView`. Generic REST and Java View APIs continue to expose them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The server performs the listing, so this needs a mechanism: a client capability header, a query parameter, or connector-side filtering after fetch. Without it, Trino and Spark see View objects with empty columns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I consider this an implementation detail within the Gravitino connectors. The list API can expose a simple view-type filter, and the Gravitino Trino and Spark connectors can request logical Views only. Direct Metric View loads can likewise be rejected by the connector based on the representation type.

The design already defines the required behavior, so I don’t think it needs to prescribe the exact filtering mechanism.

- **Namespace conflicts.** Create checks both storage sources. If an external client later creates a same-name logical View directly in the catalog, list and load report a conflict and select neither object. The external operation must rename or remove its object; ownership, versions, tags, and policies remain attached to the Gravitino Metric View and never transfer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This failure mode worries me: an external client creating a same-name view in the catalog silently disables loadView for a governed Gravitino object, including the operations an owner needs to resolve it. Does the conflict fail the whole listViews call or just that entry, and how does an owner rename or drop the Metric View while it is conflicting? I would keep the Gravitino-managed object authoritative and loadable with a conflict indicator instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional fail-closed behavior, not a silent failure. Gravitino returns an explicit namespace-conflict error and tells the user to rename or drop the externally created catalog View. listViews also fails explicitly rather than hiding either object.

The conflict cannot be created through Gravitino because create and rename validate the shared namespace; it only occurs after an out-of-band catalog change. Making the managed object authoritative would hide that inconsistency, while the design encourages Gravitino to remain the unified metadata entry point.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail-closed makes sense, and the cascade and Gravitino-managed wording addresses the rest. One thing worth stating explicitly in the bullet: that drop and rename of the Gravitino-managed Metric View still work while a conflict exists, so an owner can resolve it from the Gravitino side rather than only out-of-band in the catalog.

- **Persistence and history.** Each stored View version contains `type`, `osiVersion`, and the structured `semanticModel`. Every successful alter creates an immutable View version. Existing logical View records require no migration.

### Authorization and Governance

- Reuse the existing View privileges for create, select, alter, drop, and owner management, together with View-level audit, tag, and policy behavior.
- Apply governance metadata to the enclosing Metric View and its immutable versions; semantic-model members do not have independent privileges.
- Catalog reference validation proves that a source exists, but it does not grant data access to the caller.
- A compiler or execution engine must authorize referenced data separately using the effective query caller.

## Development Plan

- **Add API types.** Implement `MetricRepresentation` and structured model DTOs in Java, REST, and Python without coupling `MetricModel.name` to the enclosing View name, with compatibility fixtures.
- **Implement lifecycle and persistence.** Add create, load, alter, drop, immutable versioning, and EntityStore persistence while preserving the shared View namespace.
- **Implement catalog exposure.** Merge View listings, add connector capability handling, and enforce deterministic namespace-conflict behavior.
- **Implement OSI version profiles and validation.** Pin OSI `0.2.0.dev0` as the initial profile, register supported versions, reject unregistered versions, retain read support for previously accepted versions, and add document-local consistency and direct `NameIdentifier` source and column checks.
- **Integrate governance.** Reuse View authorization, ownership, audit, tag, and policy paths and verify that conflicts cannot redirect governance operations.
- **Complete verification and documentation.** Add end-to-end tests for lifecycle, versioning, validation, merged listing, connector behavior, governance, and compatibility; replace the unreleased document-representation prototype before publication.
Loading