[default values] Tables: apply server-supplied per-column defaults at read time#645
[default values] Tables: apply server-supplied per-column defaults at read time#645cbb330 wants to merge 1 commit into
Conversation
c6641c7 to
bc2d69c
Compare
9b5e906 to
14c82c3
Compare
7c276f0 to
fdfaf48
Compare
d4ac8fd to
7df59f3
Compare
| } | ||
| // TODO(read-bridge): overlay columnDefaults onto raw.schemas() via withSchemaOverlay; future V3 | ||
| // features bridged from config are applied here too. | ||
| return raw; |
There was a problem hiding this comment.
This method is currently a no-op. The apply() method currently always returns raw unchanged, and I couldn’t find a call site where this method is invoked during table metadata refresh/load. The server stamps the read-bridge config into LoadTableResponse, but the client never consumes it, so the feature isn’t active end-to-end. Do we plan to extend this further in the future PRs as a follow-up?
There was a problem hiding this comment.
Yes, by design this #645 is the O.S. substrate and the overlay transform marked TODO, and defaults only arrive once a ColumnDefaultsSource is supplied in closed/source since it relies on linkedin schema object "avro.schema.literal"
| int fieldId = Integer.parseInt(entry.getKey().substring(COLUMN_DEFAULT_PREFIX.length())); | ||
| byFieldId.put(fieldId, MAPPER.readTree(entry.getValue())); | ||
| } catch (RuntimeException | JsonProcessingException e) { | ||
| log.warn( |
There was a problem hiding this comment.
Minor: It would be useful to log the COLUMN_DEFAULT_PREFIX to aid debugging
| try { | ||
| int fieldId = Integer.parseInt(entry.getKey().substring(COLUMN_DEFAULT_PREFIX.length())); | ||
| byFieldId.put(fieldId, MAPPER.readTree(entry.getValue())); | ||
| } catch (RuntimeException | JsonProcessingException e) { |
There was a problem hiding this comment.
Is it safe to silently not consider the (key, value) pairs that fails with exception and chose the other values. Are we expecting a fixed set of configuration key as json in this case? If so, would be reasonable to set default if the values for config-key are malformed?
shanthoosh
left a comment
There was a problem hiding this comment.
Looks good overall. Have few minor clarification questions/comments.
7df59f3 to
747c7ef
Compare
… GetTableResponseBody and capture it client-side (#644) ## Summary Adds a generic, server-stamped, per-table client **`config`** map to `GetTableResponseBody` and captures it in `OpenHouseTableOperations` so subclasses can read it. It follows the **Iceberg REST `LoadTableResponse.config` convention** — a `Map<String,String>` of client-side behavior overrides the server controls at runtime, without re-rolling the slow-to-upgrade Java client fleet. **No behavior change**: the map is null/empty until a server stamps it, and the base client has nothing to act on. ## Changes - [x] Client-facing API Changes - [x] Internal API Changes - [ ] Bug Fixes - [x] New Features - [x] Tests **Client-facing API Changes** - READ_ONLY, nullable `config` field (`Map<String,String>`) on `GetTableResponseBody`, modeled on the Iceberg REST load-table `config`. Namespaced keys (e.g. `openhouse.read-bridge`); clients ignore keys they do not understand. - Why a response field: `doRefresh` already fetches a live `GetTableResponseBody` on every table load (and on commit responses) but discarded all but `getTableLocation()` — it is the natural, already-present, server-controlled, zero-staleness delivery channel. The generated client sets `FAIL_ON_UNKNOWN_PROPERTIES = false`, so a new response field cannot break older clients (and unknown config keys are simply carried). Same additive, nullable, READ_ONLY pattern as `sortOrder` etc. **Internal API Changes** - `OpenHouseTableOperations.doRefresh` keeps the full response and stashes `config` in an `AtomicReference`, exposed to subclasses via the new `protected currentConfig()`. READ_ONLY + side-channel: never sent back on writes. - `TablesMapper` (the table DTO serializer into gettableresponsebody) ignores `config` (`@Mapping(target = "config", ignore = true)`) — it is stamped separately, not sourced from `TableDto`. **New Features** - A flat string map keyed by namespaced keys means new features become new `config` entries rather than an API/schema change or client regen. ## Testing Done - [x] Added new tests. Compile/codegen verified (`:services:tables`, `:client:tableclient`, `:integrations:java:iceberg-1.2:openhouse-java-runtime`); the client regenerates with `getConfig()` returning `Map<String,String>`. `OpenHouseTableOperationsTest` covers: `currentConfig()` null before refresh; `doRefresh` captures/clears `config`; the REST-style string map deserializes from a response and tolerates unknown fields. ## Stack **#644 (channel)** → #645 (read-bridge mechanism) → [li-openhouse #2166](https://github.com/linkedin-multiproduct/li-openhouse/pull/2166) (li column-default source from avro.schema.literal) This is the substrate PR — intentionally behaviorless on its own. ## Next steps (this PR) - [ ] Review + merge first — it is the base of the stack; #645 and #2166 depend on it. This PR delivers only the channel; it stays inert until two further pieces exist, both **required**, and both delivered by the next PR in the stack: - **A server stamp** to populate `config` (it is null until something stamps it) — required, delivered by #645 via `ReadBridgeConfigResolver` + the `ColumnDefaultsSource` seam. - **A client consumer** that reads `currentConfig()` and acts on it — required, delivered by #645's read-bridge metadata overlay, driven by the column defaults that #2166 supplies. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
747c7ef to
112cdab
Compare
Adds the open-source read-bridge feature on top of the per-table `config` channel (linkedin#644): - ColumnDefaultsSource: the single open-source/closed-source seam (field-id -> Iceberg single-value JSON). Open-source default is a no-op lambda in ApiConfig; a deployment overrides this bean (e.g. li-openhouse, from avro.schema.literal). - ReadBridgeConfigResolver: server-side encoder that stamps each default as a flat namespaced config entry (openhouse.read-bridge.column-default.<fieldId> = single-value JSON) — no envelope/POJO; the config map carries the structure. - ReadBridge (client): decodes those entries and applies the overlay at metadata-load time (the column-default transform is a marked TODO, and the place further V3 features get backported). Keeps loadMetadata to one call. Behaviorless until a ColumnDefaultsSource is supplied; fail-closed throughout. Co-authored-by: Cursor <cursoragent@cursor.com>
112cdab to
2f2fccb
Compare
Summary
Adds the open-source read-bridge feature on top of the per-table
configchannel (#644): the OH server stamps per-column initial-defaults ontoGetTableResponseBody.config, and the Java client overlays them at metadata-load time so a column added after data exists reads its declared default instead of NULL (a v2→v3 read-time bridge).The whole feature is open source; it exposes exactly one deployment seam:
ColumnDefaultsSource(interface) — the single open-source/closed-source line:field-id -> Iceberg single-value JSON. The open-source default is a no-op lambda inApiConfig; a deployment overrides this bean (li-openhouse derives values fromavro.schema.literal).ReadBridgeConfigResolver(server) — stamps each default as a flat, namespaced config entry:openhouse.read-bridge.column-default.<fieldId> = <single-value-json>. No envelope/POJO — the RESTconfigstring map carries the structure directly.ReadBridge(client) — decodes those entries and applies the read-time overlay inOpenHouseTableOperations.loadMetadata(thewithInitialDefault/withSchemaOverlaytransform is a marked TODO).loadMetadatastays one delegating call.Scope (engines)
Stack
#644 (config channel) → #645 (read-bridge mechanism) → li-openhouse #2166 (li column-default source from avro.schema.literal)
Stacked on #644 — review/merge after #644.
Next steps (this PR)
ReadBridge.apply—withInitialDefault/withSchemaOverlay(currently a TODO returning raw); pure + fail-closed.Testing Done
:services:tables+ iceberg-1.2 runtime compile (JDK 17).ReadBridgeConfigResolverTest(server): each default round-trips as a flatopenhouse.read-bridge.column-default.<fieldId>entry; empty when the source supplies nothing.ReadBridgeTest(client): decodes the flat entries by field-id and skips bad field-ids / unparseable values.OpenHouseTableOperationsTestconfig capture/deserialization pass. Repo-widespotlessCheckgreen.