From 6ff229b6cc9f36752c7a84da11d6da98e8409ece Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Wed, 9 Sep 2026 09:53:23 +0200 Subject: [PATCH] Drop the timeseries tableEngine field, which the api is removing No read has returned tableEngine since the api marked it @JsonIgnore: which ClickHouse engine backs a series is an internal storage decision, not part of the entity. The field was kept anyway because create still accepted it, so it was write-only in practice and always None on the way back. The api is now removing it outright, which takes that last reason with it - and since request bodies are read strictly, a body still naming it would be a 400. Gone from TimeSeries and both its constructors, from the PyO3 constructor and getter, from the stub, and from the tests that pinned it as always-None. Also from the node serialization test's foreign/own field lists, so a timeseries no longer claims a field it does not have. Safe to land in either order relative to the api: nothing in this SDK ever set the field (there is no setter, and skip_serializing_if kept it off the wire), and responses stay lenient, so a response that still carries it deserializes fine. src/resources/tests.rs keeps its reference to the @JsonIgnore change in d70b57ad - that is a record of what the parity test found, still accurate. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: jgjesdal --- AGENTS.md | 2 +- .../python/intellistream_datahub_sdk/__init__.pyi | 4 ---- datahub_python_bindings/src/timeseries/construction.rs | 1 - datahub_python_bindings/src/timeseries/general.rs | 8 -------- python_tests/test_polymorphic_nodes.py | 3 --- src/nodes.rs | 9 +-------- src/timeseries/mod.rs | 10 ---------- 7 files changed, 2 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ddf6adf..195f4fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ The DataHub REST API this SDK targets is a separate Spring Boot project; the HTT This crate is a thin async HTTP SDK around a DataHub-style REST API. Entry point is `create_api_service()` in `src/lib.rs`, which returns an `Arc` built with `Arc::new_cyclic` so each subservice holds a `Weak` back-reference. Subservices are fields on `ApiService`: -- `time_series` (`src/timeseries/`) — `TimeSeries` + datapoint ingestion/retrieval. Neither `TimeSeries` nor `TimeSeriesUpdate` has **`securityCategories`**: it was stored, writable and returned, but nothing ever read it — no part in access control (dataset grants are Keycloak organization groups), no query filtering on it, and the backend silently dropped any id that did not already exist, so the field never round-tripped. It has been removed server-side along with its join table, and the api reads request bodies strictly, so sending it is now a 400. Files keep their own `securityCategories` (`INode` in `src/generic.rs`) — separate entity, separate question. `ListFieldU64` went with it: it was the only field of that type, so the Python wrapper class is gone too (`ListFieldStr` and `ListFieldIdCollection` remain). **`tableEngine` is stale in the same way but still present**: the api marks it `@JsonIgnore`, so no response carries it, flat or graph — it is still accepted on create, so the SDK keeps the field as write-only rather than removing it, and it always reads back `None`. +- `time_series` (`src/timeseries/`) — `TimeSeries` + datapoint ingestion/retrieval. Neither `TimeSeries` nor `TimeSeriesUpdate` has **`securityCategories`**: it was stored, writable and returned, but nothing ever read it — no part in access control (dataset grants are Keycloak organization groups), no query filtering on it, and the backend silently dropped any id that did not already exist, so the field never round-tripped. It has been removed server-side along with its join table, and the api reads request bodies strictly, so sending it is now a 400. Files keep their own `securityCategories` (`INode` in `src/generic.rs`) — separate entity, separate question. `ListFieldU64` went with it: it was the only field of that type, so the Python wrapper class is gone too (`ListFieldStr` and `ListFieldIdCollection` remain). **`tableEngine`** went the same way. No read had returned it since the api marked it `@JsonIgnore` — which ClickHouse engine backs a series is an internal storage decision — and it has now been removed from the api entirely, so the write side that made it worth keeping as a write-only field is gone too. - `units` (`src/unit/`) - `events` (`src/events/`) — event CRUD, filter/search, plus the vocabulary endpoints (`list_types`/`search_types` and the same pair for sub-types, statuses and sources, over `EventDimension`). Those answer "what values does this tenant actually use" for the four categorical fields and back filter dropdowns; they read small server-side dimension tables rather than scanning events, so they are cheap but *eventually consistent* with the events. Note the route asymmetry the SDK hides: `/events/list/{plural}` but `/events/search/{singular}`. `EventUpdate` has **no `event_time`**: an event's time is immutable after creation — the events table is partitioned by it, so ClickHouse refuses the mutation outright, and the api used to validate the field, echo the new value back with a 200 and then fail to apply it. It has been dropped from the update form, so sending it is now a 400. Record a corrected time as a new event. - `resources` (`src/resources/`) — the generic node service. Its reads span **every** node type and answer with [`Node`](#the-polymorphic-node-type) rather than one flat shape; relationship edges live in `src/relations/` (`EdgeProxy`, `RelForm`, `RelatedNode`) diff --git a/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi b/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi index accc707..fb6fc05 100644 --- a/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi +++ b/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi @@ -359,10 +359,6 @@ class TimeSeries: @labels.setter def labels(self, value: list[str] | None) -> None: ... @property - def table_engine(self) -> str | None: - """The ClickHouse table engine. On a series reached through `neighbors()` this is the - API's default rather than data — re-read the series by id for the real value.""" - @property def id(self) -> int | None: ... @property def external_id(self) -> str: ... diff --git a/datahub_python_bindings/src/timeseries/construction.rs b/datahub_python_bindings/src/timeseries/construction.rs index f676282..e05a11d 100644 --- a/datahub_python_bindings/src/timeseries/construction.rs +++ b/datahub_python_bindings/src/timeseries/construction.rs @@ -82,7 +82,6 @@ impl PyTimeSeries { .map(|r| r.into_iter().map(RelatedNode::from).collect()) .unwrap_or_default(), labels: None, - table_engine: None, }; Ok(PyTimeSeries { inner, diff --git a/datahub_python_bindings/src/timeseries/general.rs b/datahub_python_bindings/src/timeseries/general.rs index aeddb19..0d4e10e 100644 --- a/datahub_python_bindings/src/timeseries/general.rs +++ b/datahub_python_bindings/src/timeseries/general.rs @@ -97,14 +97,6 @@ impl PyTimeSeries { pub fn set_labels(&mut self, value: Option>) { self.inner.labels = value; } - /// The ClickHouse table engine backing this series. Server-assigned. - /// - /// On a series reached through `neighbors()` this is the api's DTO default rather than data — - /// the graph does not store the column. Re-read the series by id for the real value. - #[getter] - pub fn table_engine(&self) -> Option<&str> { - self.inner.table_engine.as_deref() - } /// Always `"timeseries"`. Present on every node class so data-driven code can dispatch /// without an `isinstance` ladder. #[getter] diff --git a/python_tests/test_polymorphic_nodes.py b/python_tests/test_polymorphic_nodes.py index 42d9c49..bf4089f 100644 --- a/python_tests/test_polymorphic_nodes.py +++ b/python_tests/test_polymorphic_nodes.py @@ -145,7 +145,6 @@ def test_a_timeseries_read_through_resources_carries_its_timeseries_fields( assert isinstance(node, TimeSeries) assert node.unit == "bar" assert node.value_type == "float" - # No table_engine: the api marks it @JsonIgnore, so no read returns it. finally: try: sync_client.timeseries.delete([ext]) @@ -209,8 +208,6 @@ def reached(): assert isinstance(graph_ts, TimeSeries) assert graph_ts.unit == "bar", "the graph carries the unit column" assert graph_ts.value_type == "float" - # table_engine is the one field no read returns: the api marks it @JsonIgnore. - assert graph_ts.table_engine is None finally: try: sync_client.timeseries.delete([ts_ext]) diff --git a/src/nodes.rs b/src/nodes.rs index 2c78aef..e15cfb6 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -819,8 +819,6 @@ mod tests { assert_eq!(ts.unit.as_deref(), Some("deg C")); assert_eq!(ts.unit_external_id.as_deref(), Some("deg_c")); assert_eq!(ts.value_type.as_deref(), Some("float")); - // No `table_engine`: the api marks it @JsonIgnore, so no read carries it. - assert_eq!(ts.table_engine, None); assert_eq!(ts.data_set_id, Some(21)); assert_eq!(ts.labels.as_deref(), Some(&["TIMESERIES".to_string()][..])); } @@ -864,7 +862,6 @@ mod tests { let ts = node.into_time_series().expect("timeseries"); assert_eq!(ts.value_type, None, "not told, rather than a wrong default"); assert_eq!(ts.unit, None); - assert_eq!(ts.table_engine, None); } #[test] @@ -913,7 +910,6 @@ mod tests { let foreign = [ "unit", "unitExternalId", - "tableEngine", "valueType", "policies", "connectedDataSets", @@ -926,10 +922,7 @@ mod tests { ]; let own: HashMap<&str, &[&str]> = HashMap::from([ ("ASSET", &["geoLocation", "isRoot"][..]), - ( - "TIMESERIES", - &["unit", "unitExternalId", "tableEngine", "valueType"][..], - ), + ("TIMESERIES", &["unit", "unitExternalId", "valueType"][..]), ("FUNCTION", &[][..]), ("DATASET", &["policies", "connectedDataSets"][..]), ("POLICY", &["type", "value", "deactivated", "templateId"][..]), diff --git a/src/timeseries/mod.rs b/src/timeseries/mod.rs index 1ca5bfe..d71fe66 100644 --- a/src/timeseries/mod.rs +++ b/src/timeseries/mod.rs @@ -619,14 +619,6 @@ pub struct TimeSeries { /// heterogeneous `/resources` result — see [`crate::nodes::Node`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub labels: Option>, - /// ClickHouse table engine backing this series (`MERGETREE` by default). - /// - /// Stale: no read returns this. Which ClickHouse engine backs a series is an internal - /// storage decision, so the api marks it `@JsonIgnore` and omits it from every response — - /// flat and graph alike. It is still *accepted* on create, and the field is kept rather - /// than removed, so it is write-only in practice and always `None` on the way back. - #[serde(rename = "tableEngine", default, skip_serializing_if = "Option::is_none")] - pub table_engine: Option, } impl TimeSeries { @@ -646,7 +638,6 @@ impl TimeSeries { last_updated_time: None, related_resources: vec![], labels: None, - table_engine: None, } } pub fn from_dict(dict: HashMap) -> Self { @@ -667,7 +658,6 @@ impl TimeSeries { last_updated_time: None, related_resources: vec![], labels: None, - table_engine: None, } }