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, } }