bennychow commented on code in PR #11041:
URL: https://github.com/apache/iceberg/pull/11041#discussion_r3244179464


##########
format/view-spec.md:
##########
@@ -322,3 +453,142 @@ 
s3://bucket/warehouse/default.db/event_agg/metadata/00002-(uuid).metadata.json
   } ]
 }
 ```
+
+### Materialized View Example
+
+Imagine the following operation, which creates a materialized view that 
precomputes daily event counts:
+
+```sql
+USE prod.default
+```
+```sql
+CREATE MATERIALIZED VIEW event_agg_mv (
+    event_count COMMENT 'Count of events',
+    event_date)
+COMMENT 'Precomputed daily event counts'
+AS
+SELECT
+    COUNT(1), CAST(event_ts AS DATE)
+FROM events
+GROUP BY 2
+```
+
+The materialized view metadata JSON file looks as follows:
+
+```
+s3://bucket/warehouse/default.db/event_agg_mv/metadata/00001-(uuid).metadata.json
+```
+```json
+{
+  "view-uuid": "b2a12651-3038-4a72-8a31-5027ab84da35",
+  "format-version" : 1,
+  "location" : "s3://bucket/warehouse/default.db/event_agg_mv",
+  "current-version-id" : 1,
+  "properties" : {
+    "comment" : "Precomputed daily event counts"
+  },
+  "versions" : [ {
+    "version-id" : 1,
+    "timestamp-ms" : 1573518431292,
+    "schema-id" : 1,
+    "default-catalog" : "prod",
+    "default-namespace" : [ "default" ],
+    "summary" : {
+      "engine-name" : "Spark",
+      "engine-version" : "3.4.1"
+    },
+    "representations" : [ {
+      "type" : "sql",
+      "sql" : "SELECT\n    COUNT(1), CAST(event_ts AS DATE)\nFROM 
events\nGROUP BY 2",
+      "dialect" : "spark"
+    } ],
+    "storage-table" : {
+      "namespace" : [ "default" ],
+      "name" : "event_agg_mv__storage"
+    }
+  } ],
+  "schemas": [ {
+    "schema-id": 1,
+    "type" : "struct",
+    "fields" : [ {
+      "id" : 1,
+      "name" : "event_count",
+      "required" : false,
+      "type" : "int",
+      "doc" : "Count of events"
+    }, {
+      "id" : 2,
+      "name" : "event_date",
+      "required" : false,
+      "type" : "date"
+    } ]
+  } ],
+  "version-log" : [ {
+    "timestamp-ms" : 1573518431292,
+    "version-id" : 1
+  } ]
+}
+```
+
+After a refresh operation, the storage table's snapshot summary contains the 
`refresh-state` property.
+The following is an example of the `refresh-state` JSON value stored in the 
snapshot summary of the storage table:
+
+```json
+{
+  "view-version-id" : 1,
+  "refresh-start-timestamp-ms" : 1573518435000,
+  "source-states" : [ {
+    "type" : "table",
+    "namespace" : [ "default" ],
+    "name" : "events",
+    "uuid" : "d4a10b5c-1e8a-4b72-9d67-3f4a8c9e1b2d",
+    "snapshot-id" : 6148331192489823102
+  } ]
+}
+```
+
+## Appendix B: What counts as a dependency
+
+The dependencies of a materialized view are determined by parsing the view 
query:
+
+- **Base Iceberg tables** in the dependency graph are recorded by 
`snapshot-id`.
+- **Iceberg views** in the dependency graph are recorded by `version-id`. A 
view's own dependencies are transitively dependencies of the materialized view 
and appear as additional entries in `source-states`.
+- **Intermediate materialized views** in the dependency graph are treated as 
their storage tables and recorded by the storage table's `snapshot-id`. Their 
own freshness is established recursively from their `refresh-state`.
+
+### Example

Review Comment:
   @wmoustafa asked if I could propose some description for Appendix B which is 
one possibility for how a producer and consumer could use the refresh state to 
record and evaluate freshness.
   
   This example explains how a producer and consumer can work together to 
record and evaluate freshness.  This example assumes:
   
   - Querying materialized view must be equivalent to querying base tables 
directly
   - Materialized view contains only Iceberg tables
   - Materialized view can be built on top of other materialized views 
   - Additional work on consumer to recursively evaluate materialized view 
freshness



##########
format/view-spec.md:
##########
@@ -322,3 +453,142 @@ 
s3://bucket/warehouse/default.db/event_agg/metadata/00002-(uuid).metadata.json
   } ]
 }
 ```
+
+### Materialized View Example
+
+Imagine the following operation, which creates a materialized view that 
precomputes daily event counts:
+
+```sql
+USE prod.default
+```
+```sql
+CREATE MATERIALIZED VIEW event_agg_mv (
+    event_count COMMENT 'Count of events',
+    event_date)
+COMMENT 'Precomputed daily event counts'
+AS
+SELECT
+    COUNT(1), CAST(event_ts AS DATE)
+FROM events
+GROUP BY 2
+```
+
+The materialized view metadata JSON file looks as follows:
+
+```
+s3://bucket/warehouse/default.db/event_agg_mv/metadata/00001-(uuid).metadata.json
+```
+```json
+{
+  "view-uuid": "b2a12651-3038-4a72-8a31-5027ab84da35",
+  "format-version" : 1,
+  "location" : "s3://bucket/warehouse/default.db/event_agg_mv",
+  "current-version-id" : 1,
+  "properties" : {
+    "comment" : "Precomputed daily event counts"
+  },
+  "versions" : [ {
+    "version-id" : 1,
+    "timestamp-ms" : 1573518431292,
+    "schema-id" : 1,
+    "default-catalog" : "prod",
+    "default-namespace" : [ "default" ],
+    "summary" : {
+      "engine-name" : "Spark",
+      "engine-version" : "3.4.1"
+    },
+    "representations" : [ {
+      "type" : "sql",
+      "sql" : "SELECT\n    COUNT(1), CAST(event_ts AS DATE)\nFROM 
events\nGROUP BY 2",
+      "dialect" : "spark"
+    } ],
+    "storage-table" : {
+      "namespace" : [ "default" ],
+      "name" : "event_agg_mv__storage"
+    }
+  } ],
+  "schemas": [ {
+    "schema-id": 1,
+    "type" : "struct",
+    "fields" : [ {
+      "id" : 1,
+      "name" : "event_count",
+      "required" : false,
+      "type" : "int",
+      "doc" : "Count of events"
+    }, {
+      "id" : 2,
+      "name" : "event_date",
+      "required" : false,
+      "type" : "date"
+    } ]
+  } ],
+  "version-log" : [ {
+    "timestamp-ms" : 1573518431292,
+    "version-id" : 1
+  } ]
+}
+```
+
+After a refresh operation, the storage table's snapshot summary contains the 
`refresh-state` property.
+The following is an example of the `refresh-state` JSON value stored in the 
snapshot summary of the storage table:
+
+```json
+{
+  "view-version-id" : 1,
+  "refresh-start-timestamp-ms" : 1573518435000,
+  "source-states" : [ {
+    "type" : "table",
+    "namespace" : [ "default" ],
+    "name" : "events",
+    "uuid" : "d4a10b5c-1e8a-4b72-9d67-3f4a8c9e1b2d",
+    "snapshot-id" : 6148331192489823102
+  } ]
+}
+```
+
+## Appendix B: What counts as a dependency
+
+The dependencies of a materialized view are determined by parsing the view 
query:
+
+- **Base Iceberg tables** in the dependency graph are recorded by 
`snapshot-id`.
+- **Iceberg views** in the dependency graph are recorded by `version-id`. A 
view's own dependencies are transitively dependencies of the materialized view 
and appear as additional entries in `source-states`.
+- **Intermediate materialized views** in the dependency graph are treated as 
their storage tables and recorded by the storage table's `snapshot-id`. Their 
own freshness is established recursively from their `refresh-state`.
+
+### Example
+
+The query under examination:
+
+- `A` (the materialized view being refreshed): `SELECT ... FROM B JOIN C ON 
...`
+- `B` (regular view): `SELECT ... FROM E JOIN D ON ...`
+- `C` (materialized view): `SELECT ... FROM F JOIN G ON ...`
+- `D` (materialized view): `SELECT ... FROM H WHERE ...`
+- `E`, `F`, `G`, `H`: base Iceberg tables
+
+`A`'s dependencies are `B`, `C`, and `D`. `B` is a regular view; its own 
dependencies (`E` and `D`) are transitively dependencies of `A`. `C` and `D` 
are materialized views; they appear in `A`'s `source-states` as their storage 
tables.
+
+```
+A [MV — being refreshed]
+├── B [VIEW]                            <-- recorded in A: version-id
+│   ├── E [TABLE]                       <-- recorded in A: snapshot-id
+│   └── D [MV]                          <-- recorded in A: storage-table 
snapshot-id
+│       ┄┄┄┄┄┄ recursive boundary ┄┄┄┄┄┄
+│       └── H [TABLE]                   (D's dependency; verified via D's 
refresh-state)
+└── C [MV]                              <-- recorded in A: storage-table 
snapshot-id
+    ┄┄┄┄┄┄ recursive boundary ┄┄┄┄┄┄
+    ├── F [TABLE]                       (C's dependency; verified via C's 
refresh-state)
+    └── G [TABLE]                       (C's dependency; verified via C's 
refresh-state)
+```
+
+`A`'s `source-states`:
+
+| type    | name          | recorded id        |
+|---------|---------------|--------------------|
+| `view`  | `B`           | `version-id: 5`    |
+| `table` | `E`           | `snapshot-id: 101` |
+| `table` | `C` (storage) | `snapshot-id: 12`  |
+| `table` | `D` (storage) | `snapshot-id: 14`  |
+
+`F`, `G`, and `H` do not appear in `A`'s `source-states` directly; they belong 
to `C` and `D`'s dependency sets and are reached recursively through `C` and 
`D`'s refresh states.
+
+A consumer establishes `A`'s freshness by checking each entry in 
`source-states` against the current catalog state. For `C` and `D`, the 
consumer compares the recorded storage-table snapshot to the current snapshot, 
then recurses into their `refresh-state` to verify each is itself fresh.

Review Comment:
   For C and D, it would be really nice if the consumer could know up front 
whether the table was a base table or storage table.



##########
format/view-spec.md:
##########
@@ -190,92 +190,93 @@ The table identifier for the storage table that stores 
the precomputed results.
 ### Storage table metadata
 
 This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
-The property "refresh-state" is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
 
 | Requirement | Field name      | Description |
 |-------------|-----------------|-------------|
 | _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
 
 #### Freshness
 
-A materialized view is "fresh" when the storage table adequately represents 
the result of the view query at the current state of its dependencies.
-Since different systems define freshness differently, it is left to the 
consumer to evaluate freshness based on its own policy.
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
 
-**Consumer behavior:**
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
 
-When evaluating freshness, consumers:
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
 
-- May apply time-based freshness policies, such as allowing a staleness window 
based on `refresh-start-timestamp-ms`.
-- May compare the `source-states` list against the states loaded from the 
catalog to verify the producer's freshness interpretation.
-- May parse the view definition to implement more sophisticated policies.
-- When a materialized view is considered stale, can fail, refresh inline, or 
treat the materialized view as a logical view.
-- Should not consume the storage table as it is when the materialized view 
doesn't meet the freshness criteria.
+##### Producer flexibility
 
-**Producer behavior:**
-
-Producers should provide the necessary information in the [refresh 
state](#refresh-state) such that consumers can verify the logical equivalence 
of the precomputed data with the query definition.
-Different producers may have different freshness interpretations, based on how 
much of the refresh state's dependency graph should be evaluated.
-Some producers expect the entire dependency graph to be evaluated and 
therefore include source MV dependencies. Other producers may only expect 
dependencies in the MV's SQL to be evaluated and therefore do not include 
dependencies of source MVs.
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
 
 When writing the refresh state, producers:
 
-- Should provide a sufficient list of source states such that consumers can 
determine freshness according to the producer's intent. If the producers intent 
is such that it doesn't rely on the source-states to determine freshness, it 
may provide an empty list.
-- If the source state cannot be determined for all objects (for example, for 
non-Iceberg tables or non-deterministic functions) may leave the source states 
list empty.
-- If a stored object is reachable through multiple paths in the dependency 
graph (diamond dependency pattern), all distinct source states have to be 
included in the list.
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.
+- **May** leave `source-states` empty (e.g., when sources are non-Iceberg or 
freshness is determined by a mechanism outside this spec).
+
+A snapshot whose refresh state violates a `Must` rule is invalid; consumers 
may treat it as if it had no `refresh-state`.
+
+##### Consumer options
+
+Consumers may use any combination of the following to assess the storage table:
+
+- **Recency policy.** Accept the storage table when 
`refresh-start-timestamp-ms` falls within a staleness window. A recency policy 
bounds data age but does not establish freshness.
+- **Trust the recorded `source-states`.** Compare each entry against the 
current catalog state — `snapshot-id` for tables, `version-id` for views, 
optionally recursive verification for intermediate materialized views recorded 
by their storage tables. Also confirm that the recorded `view-version-id` 
equals the materialized view's current `view-version-id`.
+- **Verify by parsing the view query.** Derive the dependency set from the SQL 
and confirm every dependency is covered by `source-states` and matches the 
current state. Treat any uncovered dependency as undetermined.
+
+If a consumer's assessment passes, it reads from the storage table; otherwise 
it evaluates the view query in place of the storage table.
 
 #### Refresh state
 
-The refresh state record captures the dependencies in the materialized view's 
dependency graph.
-These dependencies include source Iceberg tables, views, and materialized 
views.
+The refresh state record captures the dependencies in the materialized view's 
dependency graph. Each dependency is recorded in `source-states` as either a 
`table` entry (a base table or an intermediate materialized view's storage 
table) or a `view` entry.

Review Comment:
   I'd prefer "upstream" MV over a "base" MV.  I think "base" should be 
reserved for tables.



##########
format/view-spec.md:
##########
@@ -160,7 +178,120 @@ Each entry in `version-log` is a struct with the 
following fields:
 | _required_  | `timestamp-ms` | Timestamp when the view's 
`current-version-id` was updated (ms from epoch) |
 | _required_  | `version-id`   | ID that `current-version-id` was set to |
 
-## Appendix A: An Example
+#### Storage Table Identifier
+
+The table identifier for the storage table that stores the precomputed results.
+
+| Requirement | Field name     | Description |
+|-------------|----------------|-------------|
+| _required_  | `namespace`    | A list of strings for namespace levels |
+| _required_  | `name`         | A string specifying the name of the table |
+
+### Storage table metadata
+
+This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+
+| Requirement | Field name      | Description |
+|-------------|-----------------|-------------|
+| _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
+
+#### Freshness
+
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
+
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
+
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
+
+##### Producer flexibility

Review Comment:
   I can see how this heading has parallel structure with the next heading so 
how about:
   
   - Producer: Recording Refresh State
   - Consumer:  Evaluating Refresh State



##########
format/view-spec.md:
##########
@@ -160,7 +178,120 @@ Each entry in `version-log` is a struct with the 
following fields:
 | _required_  | `timestamp-ms` | Timestamp when the view's 
`current-version-id` was updated (ms from epoch) |
 | _required_  | `version-id`   | ID that `current-version-id` was set to |
 
-## Appendix A: An Example
+#### Storage Table Identifier
+
+The table identifier for the storage table that stores the precomputed results.
+
+| Requirement | Field name     | Description |
+|-------------|----------------|-------------|
+| _required_  | `namespace`    | A list of strings for namespace levels |
+| _required_  | `name`         | A string specifying the name of the table |
+
+### Storage table metadata
+
+This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+
+| Requirement | Field name      | Description |
+|-------------|-----------------|-------------|
+| _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
+
+#### Freshness
+
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
+
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
+
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.

Review Comment:
   There are too many different use cases for freshness requirements.  Producer 
decides the full set of possibilities based on what it puts into the refresh 
state.  Consumer has the flexibility to decide if the included refresh state is 
sufficient for its freshness requirement.



##########
format/view-spec.md:
##########
@@ -160,7 +178,120 @@ Each entry in `version-log` is a struct with the 
following fields:
 | _required_  | `timestamp-ms` | Timestamp when the view's 
`current-version-id` was updated (ms from epoch) |
 | _required_  | `version-id`   | ID that `current-version-id` was set to |
 
-## Appendix A: An Example
+#### Storage Table Identifier
+
+The table identifier for the storage table that stores the precomputed results.
+
+| Requirement | Field name     | Description |
+|-------------|----------------|-------------|
+| _required_  | `namespace`    | A list of strings for namespace levels |
+| _required_  | `name`         | A string specifying the name of the table |
+
+### Storage table metadata
+
+This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+
+| Requirement | Field name      | Description |
+|-------------|-----------------|-------------|
+| _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
+
+#### Freshness
+
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
+
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
+
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
+
+##### Producer flexibility
+
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
+
+When writing the refresh state, producers:
+
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.
+- **May** leave `source-states` empty (e.g., when sources are non-Iceberg or 
freshness is determined by a mechanism outside this spec).
+
+A snapshot whose refresh state violates a `Must` rule is invalid; consumers 
may treat it as if it had no `refresh-state`.
+
+##### Consumer options

Review Comment:
   Consumer:  Evaluating Refresh State
   
   to match with **Producer: Recording Refresh State**



##########
format/view-spec.md:
##########
@@ -160,7 +178,120 @@ Each entry in `version-log` is a struct with the 
following fields:
 | _required_  | `timestamp-ms` | Timestamp when the view's 
`current-version-id` was updated (ms from epoch) |
 | _required_  | `version-id`   | ID that `current-version-id` was set to |
 
-## Appendix A: An Example
+#### Storage Table Identifier
+
+The table identifier for the storage table that stores the precomputed results.
+
+| Requirement | Field name     | Description |
+|-------------|----------------|-------------|
+| _required_  | `namespace`    | A list of strings for namespace levels |
+| _required_  | `name`         | A string specifying the name of the table |
+
+### Storage table metadata
+
+This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+
+| Requirement | Field name      | Description |
+|-------------|-----------------|-------------|
+| _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
+
+#### Freshness
+
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
+
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
+
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
+
+##### Producer flexibility
+
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
+
+When writing the refresh state, producers:
+
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.
+- **May** leave `source-states` empty (e.g., when sources are non-Iceberg or 
freshness is determined by a mechanism outside this spec).
+
+A snapshot whose refresh state violates a `Must` rule is invalid; consumers 
may treat it as if it had no `refresh-state`.

Review Comment:
   Agree here too.



##########
format/view-spec.md:
##########
@@ -190,92 +190,93 @@ The table identifier for the storage table that stores 
the precomputed results.
 ### Storage table metadata
 
 This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
-The property "refresh-state" is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
 
 | Requirement | Field name      | Description |
 |-------------|-----------------|-------------|
 | _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
 
 #### Freshness
 
-A materialized view is "fresh" when the storage table adequately represents 
the result of the view query at the current state of its dependencies.
-Since different systems define freshness differently, it is left to the 
consumer to evaluate freshness based on its own policy.
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).

Review Comment:
   Instead of "intermediate" MV, how about "upstream" MVs?   This way can also 
talk about "downstream" MVs when discussing refresh propagation in future 
iterations of this spec.
   
   Also, we agreed to remove the term "dependencies" from this section and only 
talk about dependencies in context of "refresh state" definition later in the 
spec. 



##########
format/view-spec.md:
##########
@@ -190,92 +190,93 @@ The table identifier for the storage table that stores 
the precomputed results.
 ### Storage table metadata
 
 This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
-The property "refresh-state" is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
 
 | Requirement | Field name      | Description |
 |-------------|-----------------|-------------|
 | _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
 
 #### Freshness
 
-A materialized view is "fresh" when the storage table adequately represents 
the result of the view query at the current state of its dependencies.
-Since different systems define freshness differently, it is left to the 
consumer to evaluate freshness based on its own policy.
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
 
-**Consumer behavior:**
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
 
-When evaluating freshness, consumers:
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
 
-- May apply time-based freshness policies, such as allowing a staleness window 
based on `refresh-start-timestamp-ms`.
-- May compare the `source-states` list against the states loaded from the 
catalog to verify the producer's freshness interpretation.
-- May parse the view definition to implement more sophisticated policies.
-- When a materialized view is considered stale, can fail, refresh inline, or 
treat the materialized view as a logical view.
-- Should not consume the storage table as it is when the materialized view 
doesn't meet the freshness criteria.
+##### Producer flexibility
 
-**Producer behavior:**
-
-Producers should provide the necessary information in the [refresh 
state](#refresh-state) such that consumers can verify the logical equivalence 
of the precomputed data with the query definition.
-Different producers may have different freshness interpretations, based on how 
much of the refresh state's dependency graph should be evaluated.
-Some producers expect the entire dependency graph to be evaluated and 
therefore include source MV dependencies. Other producers may only expect 
dependencies in the MV's SQL to be evaluated and therefore do not include 
dependencies of source MVs.
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
 
 When writing the refresh state, producers:
 
-- Should provide a sufficient list of source states such that consumers can 
determine freshness according to the producer's intent. If the producers intent 
is such that it doesn't rely on the source-states to determine freshness, it 
may provide an empty list.
-- If the source state cannot be determined for all objects (for example, for 
non-Iceberg tables or non-deterministic functions) may leave the source states 
list empty.
-- If a stored object is reachable through multiple paths in the dependency 
graph (diamond dependency pattern), all distinct source states have to be 
included in the list.
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.
+- **May** leave `source-states` empty (e.g., when sources are non-Iceberg or 
freshness is determined by a mechanism outside this spec).
+
+A snapshot whose refresh state violates a `Must` rule is invalid; consumers 
may treat it as if it had no `refresh-state`.
+
+##### Consumer options
+
+Consumers may use any combination of the following to assess the storage table:
+
+- **Recency policy.** Accept the storage table when 
`refresh-start-timestamp-ms` falls within a staleness window. A recency policy 
bounds data age but does not establish freshness.
+- **Trust the recorded `source-states`.** Compare each entry against the 
current catalog state — `snapshot-id` for tables, `version-id` for views, 
optionally recursive verification for intermediate materialized views recorded 
by their storage tables. Also confirm that the recorded `view-version-id` 
equals the materialized view's current `view-version-id`.
+- **Verify by parsing the view query.** Derive the dependency set from the SQL 
and confirm every dependency is covered by `source-states` and matches the 
current state. Treat any uncovered dependency as undetermined.
+
+If a consumer's assessment passes, it reads from the storage table; otherwise 
it evaluates the view query in place of the storage table.
 
 #### Refresh state
 
-The refresh state record captures the dependencies in the materialized view's 
dependency graph.
-These dependencies include source Iceberg tables, views, and materialized 
views.
+The refresh state record captures the dependencies in the materialized view's 
dependency graph. Each dependency is recorded in `source-states` as either a 
`table` entry (a base table or an intermediate materialized view's storage 
table) or a `view` entry.
 
 The refresh state has the following fields:
 
-| Requirement | Field name     | Description |
-|-------------|----------------|-------------|
-| _required_  | `view-version-id`         | The `version-id` of the 
materialized view when the refresh operation was performed  |
-| _required_  | `source-states`        | A list of [source 
states](#source-state) records |
+| Requirement | Field name                   | Description |
+|-------------|------------------------------|-------------|
+| _required_  | `view-version-id`            | The `version-id` of the 
materialized view when the refresh operation was performed |
+| _required_  | `source-states`              | A list of [source 
state](#source-state) records |
 | _required_  | `refresh-start-timestamp-ms` | A timestamp of when the refresh 
operation was started |
 
 #### Source state
 
-Source state records capture the state of objects referenced by a materialized 
view including objects referenced by source materialized views.
-Each record has a `type` field that determines its form:
+Source state records capture the state of objects referenced by a materialized 
view. Each record has a `type` field that determines its form:
 
 | Type    | Description |
 |---------|-------------|
-| `table` | An Iceberg table, including storage tables of source materialized 
views |
-| `view`  | An Iceberg view, including source materialized views |
+| `table` | An Iceberg table — either a base table in the dependency graph, or 
the storage table of an intermediate materialized view |
+| `view`  | An Iceberg view in the dependency graph |
 
-Source materialized views are represented by two source state entries: one for 
the view itself and one for its storage table.
+An intermediate materialized view must be recorded as a single `table` entry 
referencing its storage table; recording it as a `view` entry is not permitted. 
The intermediate materialized view's own dependencies are reached recursively 
through its `refresh-state`.

Review Comment:
   I agree with Igor and Steven here too.



##########
format/view-spec.md:
##########
@@ -190,92 +190,93 @@ The table identifier for the storage table that stores 
the precomputed results.
 ### Storage table metadata
 
 This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
-The property "refresh-state" is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
 
 | Requirement | Field name      | Description |
 |-------------|-----------------|-------------|
 | _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
 
 #### Freshness
 
-A materialized view is "fresh" when the storage table adequately represents 
the result of the view query at the current state of its dependencies.
-Since different systems define freshness differently, it is left to the 
consumer to evaluate freshness based on its own policy.
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
 
-**Consumer behavior:**
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
 
-When evaluating freshness, consumers:
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
 
-- May apply time-based freshness policies, such as allowing a staleness window 
based on `refresh-start-timestamp-ms`.
-- May compare the `source-states` list against the states loaded from the 
catalog to verify the producer's freshness interpretation.
-- May parse the view definition to implement more sophisticated policies.
-- When a materialized view is considered stale, can fail, refresh inline, or 
treat the materialized view as a logical view.
-- Should not consume the storage table as it is when the materialized view 
doesn't meet the freshness criteria.
+##### Producer flexibility
 
-**Producer behavior:**
-
-Producers should provide the necessary information in the [refresh 
state](#refresh-state) such that consumers can verify the logical equivalence 
of the precomputed data with the query definition.
-Different producers may have different freshness interpretations, based on how 
much of the refresh state's dependency graph should be evaluated.
-Some producers expect the entire dependency graph to be evaluated and 
therefore include source MV dependencies. Other producers may only expect 
dependencies in the MV's SQL to be evaluated and therefore do not include 
dependencies of source MVs.
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
 
 When writing the refresh state, producers:
 
-- Should provide a sufficient list of source states such that consumers can 
determine freshness according to the producer's intent. If the producers intent 
is such that it doesn't rely on the source-states to determine freshness, it 
may provide an empty list.
-- If the source state cannot be determined for all objects (for example, for 
non-Iceberg tables or non-deterministic functions) may leave the source states 
list empty.
-- If a stored object is reachable through multiple paths in the dependency 
graph (diamond dependency pattern), all distinct source states have to be 
included in the list.
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.

Review Comment:
   Agree too..  "Should" is better.



##########
format/view-spec.md:
##########
@@ -322,3 +453,142 @@ 
s3://bucket/warehouse/default.db/event_agg/metadata/00002-(uuid).metadata.json
   } ]
 }
 ```
+
+### Materialized View Example
+
+Imagine the following operation, which creates a materialized view that 
precomputes daily event counts:
+
+```sql
+USE prod.default
+```
+```sql
+CREATE MATERIALIZED VIEW event_agg_mv (
+    event_count COMMENT 'Count of events',
+    event_date)
+COMMENT 'Precomputed daily event counts'
+AS
+SELECT
+    COUNT(1), CAST(event_ts AS DATE)
+FROM events
+GROUP BY 2
+```
+
+The materialized view metadata JSON file looks as follows:
+
+```
+s3://bucket/warehouse/default.db/event_agg_mv/metadata/00001-(uuid).metadata.json
+```
+```json
+{
+  "view-uuid": "b2a12651-3038-4a72-8a31-5027ab84da35",
+  "format-version" : 1,
+  "location" : "s3://bucket/warehouse/default.db/event_agg_mv",
+  "current-version-id" : 1,
+  "properties" : {
+    "comment" : "Precomputed daily event counts"
+  },
+  "versions" : [ {
+    "version-id" : 1,
+    "timestamp-ms" : 1573518431292,
+    "schema-id" : 1,
+    "default-catalog" : "prod",
+    "default-namespace" : [ "default" ],
+    "summary" : {
+      "engine-name" : "Spark",
+      "engine-version" : "3.4.1"
+    },
+    "representations" : [ {
+      "type" : "sql",
+      "sql" : "SELECT\n    COUNT(1), CAST(event_ts AS DATE)\nFROM 
events\nGROUP BY 2",
+      "dialect" : "spark"
+    } ],
+    "storage-table" : {
+      "namespace" : [ "default" ],
+      "name" : "event_agg_mv__storage"
+    }
+  } ],
+  "schemas": [ {
+    "schema-id": 1,
+    "type" : "struct",
+    "fields" : [ {
+      "id" : 1,
+      "name" : "event_count",
+      "required" : false,
+      "type" : "int",
+      "doc" : "Count of events"
+    }, {
+      "id" : 2,
+      "name" : "event_date",
+      "required" : false,
+      "type" : "date"
+    } ]
+  } ],
+  "version-log" : [ {
+    "timestamp-ms" : 1573518431292,
+    "version-id" : 1
+  } ]
+}
+```
+
+After a refresh operation, the storage table's snapshot summary contains the 
`refresh-state` property.
+The following is an example of the `refresh-state` JSON value stored in the 
snapshot summary of the storage table:
+
+```json
+{
+  "view-version-id" : 1,
+  "refresh-start-timestamp-ms" : 1573518435000,
+  "source-states" : [ {
+    "type" : "table",
+    "namespace" : [ "default" ],
+    "name" : "events",
+    "uuid" : "d4a10b5c-1e8a-4b72-9d67-3f4a8c9e1b2d",
+    "snapshot-id" : 6148331192489823102
+  } ]
+}
+```
+
+## Appendix B: What counts as a dependency
+
+The dependencies of a materialized view are determined by parsing the view 
query:
+
+- **Base Iceberg tables** in the dependency graph are recorded by 
`snapshot-id`.
+- **Iceberg views** in the dependency graph are recorded by `version-id`. A 
view's own dependencies are transitively dependencies of the materialized view 
and appear as additional entries in `source-states`.
+- **Intermediate materialized views** in the dependency graph are treated as 
their storage tables and recorded by the storage table's `snapshot-id`. Their 
own freshness is established recursively from their `refresh-state`.
+
+### Example
+
+The query under examination:
+
+- `A` (the materialized view being refreshed): `SELECT ... FROM B JOIN C ON 
...`
+- `B` (regular view): `SELECT ... FROM E JOIN D ON ...`
+- `C` (materialized view): `SELECT ... FROM F JOIN G ON ...`
+- `D` (materialized view): `SELECT ... FROM H WHERE ...`
+- `E`, `F`, `G`, `H`: base Iceberg tables
+
+`A`'s dependencies are `B`, `C`, and `D`. `B` is a regular view; its own 
dependencies (`E` and `D`) are transitively dependencies of `A`. `C` and `D` 
are materialized views; they appear in `A`'s `source-states` as their storage 
tables.

Review Comment:
   I agree with Steven.   D is not a direct dependency for A.  A only directly 
depends on B and C.  



##########
format/view-spec.md:
##########
@@ -160,7 +178,120 @@ Each entry in `version-log` is a struct with the 
following fields:
 | _required_  | `timestamp-ms` | Timestamp when the view's 
`current-version-id` was updated (ms from epoch) |
 | _required_  | `version-id`   | ID that `current-version-id` was set to |
 
-## Appendix A: An Example
+#### Storage Table Identifier
+
+The table identifier for the storage table that stores the precomputed results.
+
+| Requirement | Field name     | Description |
+|-------------|----------------|-------------|
+| _required_  | `namespace`    | A list of strings for namespace levels |
+| _required_  | `name`         | A string specifying the name of the table |
+
+### Storage table metadata
+
+This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+
+| Requirement | Field name      | Description |
+|-------------|-----------------|-------------|
+| _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
+
+#### Freshness
+
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
+
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
+
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
+
+##### Producer flexibility
+
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
+
+When writing the refresh state, producers:
+
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.
+- **May** leave `source-states` empty (e.g., when sources are non-Iceberg or 
freshness is determined by a mechanism outside this spec).
+
+A snapshot whose refresh state violates a `Must` rule is invalid; consumers 
may treat it as if it had no `refresh-state`.
+
+##### Consumer options
+
+Consumers may use any combination of the following to assess the storage table:
+
+- **Recency policy.** Accept the storage table when 
`refresh-start-timestamp-ms` falls within a staleness window. A recency policy 
bounds data age but does not establish freshness.
+- **Trust the recorded `source-states`.** Compare each entry against the 
current catalog state — `snapshot-id` for tables, `version-id` for views, 
optionally recursive verification for intermediate materialized views recorded 
by their storage tables. Also confirm that the recorded `view-version-id` 
equals the materialized view's current `view-version-id`.
+- **Verify by parsing the view query.** Derive the dependency set from the SQL 
and confirm every dependency is covered by `source-states` and matches the 
current state. Treat any uncovered dependency as undetermined.
+
+If a consumer's assessment passes, it reads from the storage table; otherwise 
it evaluates the view query in place of the storage table.

Review Comment:
   I agree... let's not prescribe the "otherwise" part.



##########
format/view-spec.md:
##########
@@ -160,7 +178,120 @@ Each entry in `version-log` is a struct with the 
following fields:
 | _required_  | `timestamp-ms` | Timestamp when the view's 
`current-version-id` was updated (ms from epoch) |
 | _required_  | `version-id`   | ID that `current-version-id` was set to |
 
-## Appendix A: An Example
+#### Storage Table Identifier
+
+The table identifier for the storage table that stores the precomputed results.
+
+| Requirement | Field name     | Description |
+|-------------|----------------|-------------|
+| _required_  | `namespace`    | A list of strings for namespace levels |
+| _required_  | `name`         | A string specifying the name of the table |
+
+### Storage table metadata
+
+This section describes additional metadata for the storage table that 
supplements the regular table metadata and is required for materialized views.
+The `refresh-state` property is set on the [snapshot 
summary](https://iceberg.apache.org/spec/#snapshots) property of a storage 
table snapshot to provide information about the state of the precomputed data.
+
+| Requirement | Field name      | Description |
+|-------------|-----------------|-------------|
+| _optional_  | `refresh-state` | A [refresh state](#refresh-state) record 
stored as a JSON-encoded string |
+
+#### Freshness
+
+A materialized view is **fresh** when the storage table represents the result 
of the current view query (at the materialized view's current 
`view-version-id`) over the current state of its dependencies. Dependencies are 
determined by parsing the SQL: base Iceberg tables, Iceberg views (whose own 
dependencies are transitively dependencies of the materialized view), and 
intermediate materialized views (treated as their storage tables, with their 
own freshness established recursively from their `refresh-state`).
+
+A change to the materialized view's definition produces a new 
`view-version-id`; any storage-table snapshot recorded at a prior 
`view-version-id` is not fresh under the current definition.
+
+The `refresh-state` summary on each storage-table snapshot records dependency 
state observed at refresh time. Producers populate it; consumers use it to 
assess freshness without re-executing the query. The spec does not mandate what 
producers record or how consumers assess. See [Appendix 
B](#appendix-b-what-counts-as-a-dependency) for what counts as a dependency.
+
+##### Producer flexibility
+
+Producers may selectively choose a subset of their dependencies to record — 
for example, skipping non-Iceberg sources or recording an empty list.
+
+When writing the refresh state, producers:
+
+- **Must** record `view-version-id` and `refresh-start-timestamp-ms`.
+- **Must** include all distinct source states for the inputs they chose to 
track.
+- **May** leave `source-states` empty (e.g., when sources are non-Iceberg or 
freshness is determined by a mechanism outside this spec).
+
+A snapshot whose refresh state violates a `Must` rule is invalid; consumers 
may treat it as if it had no `refresh-state`.
+
+##### Consumer options
+
+Consumers may use any combination of the following to assess the storage table:
+
+- **Recency policy.** Accept the storage table when 
`refresh-start-timestamp-ms` falls within a staleness window. A recency policy 
bounds data age but does not establish freshness.

Review Comment:
   I like "Recency Policy".  "Refresh Staleness" feels less clear to me.



-- 
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]

Reply via email to