From e5550a3b9b8c197f004e54a6f4dc52f29396c100 Mon Sep 17 00:00:00 2001 From: Meredith Heller Date: Thu, 24 Sep 2026 12:08:52 -0700 Subject: [PATCH 1/5] ci: add ClickHouse 26.8 to the version test matrix Adds `clickhouse/clickhouse-server:26.8.1.2041` (26.8 LTS, mirrored in getsentry/image-mirror#33) to the `clickhouse-versions` test matrix. It's the official image rather than an Altinity build, so it lives under a different mirror repo. `matrix.version` now carries `:` after the shared `ghcr.io/getsentry/image-mirror-` prefix. The default `CLICKHOUSE_IMAGE` for all other jobs stays on 25.8.16.10001. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55a37f8c5..4d2659550 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -490,8 +490,9 @@ jobs: matrix: version: [ - "25.8.16.10001.altinitystable", - "26.3.33.10001.altinitystable", + "altinity-clickhouse-server:25.8.16.10001.altinitystable", + "altinity-clickhouse-server:26.3.33.10001.altinitystable", + "clickhouse-clickhouse-server:26.8.1.2041", ] test_type: [snuba-test, snuba-test-migrations] @@ -523,7 +524,7 @@ jobs: docker load --input /tmp/docker-deps/deps.tar fi images=( - "ghcr.io/getsentry/image-mirror-altinity-clickhouse-server:${{ matrix.version }}" + "ghcr.io/getsentry/image-mirror-${{ matrix.version }}" "ghcr.io/getsentry/image-mirror-confluentinc-cp-kafka:6.2.0" "ghcr.io/getsentry/image-mirror-confluentinc-cp-zookeeper:6.2.0" "ghcr.io/getsentry/docker-redis-cluster:7.0.10" @@ -540,7 +541,7 @@ jobs: - name: Docker Snuba Test other ClickHouse versions run: | - export CLICKHOUSE_IMAGE=ghcr.io/getsentry/image-mirror-altinity-clickhouse-server:${{matrix.version}} + export CLICKHOUSE_IMAGE=ghcr.io/getsentry/image-mirror-${{matrix.version}} SNUBA_IMAGE=ghcr.io/getsentry/snuba-ci:${{ github.event.pull_request.head.sha || github.sha }} SNUBA_SETTINGS=test docker compose -f docker-compose.gcb.yml run --rm ${{ matrix.test_type }} image-smoke-test: From dd80a9c1d22c7d2d5ad5fe49042ed9793bf31072 Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Tue, 29 Sep 2026 16:33:10 -0700 Subject: [PATCH 2/5] ci: add ClickHouse 26.8.10.6 to the version test matrix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d2659550..0dfa3d5cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -493,6 +493,7 @@ jobs: "altinity-clickhouse-server:25.8.16.10001.altinitystable", "altinity-clickhouse-server:26.3.33.10001.altinitystable", "clickhouse-clickhouse-server:26.8.1.2041", + "clickhouse-clickhouse-server:26.8.10.6", ] test_type: [snuba-test, snuba-test-migrations] From 6edca89d3811387f5117eb0de8e25256a6f6262a Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Tue, 29 Sep 2026 16:34:12 -0700 Subject: [PATCH 3/5] ci: drop ClickHouse 26.8.1.2041 in favor of 26.8.10.6 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dfa3d5cc..67197d0eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -492,7 +492,6 @@ jobs: [ "altinity-clickhouse-server:25.8.16.10001.altinitystable", "altinity-clickhouse-server:26.3.33.10001.altinitystable", - "clickhouse-clickhouse-server:26.8.1.2041", "clickhouse-clickhouse-server:26.8.10.6", ] test_type: [snuba-test, snuba-test-migrations] From 95b1f6357f23f6bfe3b6f2d5058b47ea8841a28d Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Wed, 30 Sep 2026 12:12:13 -0700 Subject: [PATCH 4/5] fix(processors): Send DateTime64 values as quoted seconds strings ClickHouse 26.8 reads an unquoted JSON number for a DateTime64 column as seconds instead of raw ticks (ClickHouse#108091). We sent millisecond/microsecond tick counts as bare integers for errors.timestamp_ms, search_issues.timestamp_ms, functions start/end_timestamp and profile_chunks start/end_timestamp, which 26.8 would store as garbage or reject. Send a quoted "." string instead. It is parsed identically by 25.8, 26.3 and 26.8+. --- rust_snuba/src/processors/errors.rs | 5 ++- rust_snuba/src/processors/functions.rs | 3 ++ rust_snuba/src/processors/profile_chunks.rs | 12 +++-- ...ents__1__error-with-null-threads.json.snap | 2 +- ...__error-with-null-values-threads.json.snap | 2 +- ...or-events__1__error-with-threads.json.snap | 2 +- ...rorsProcessor-events__1__errors1.json.snap | 2 +- ...ocessor-events__1__null-tag-keys.json.snap | 2 +- ...Processor-events__1__null-values.json.snap | 2 +- ...ocessor-events__1__sdk-info-java.json.snap | 2 +- ...nts__1__weird-transaction-source.json.snap | 2 +- ...r-snuba-profile-chunks__1__valid.json.snap | 4 +- rust_snuba/src/processors/utils.rs | 45 ++++++++++++++++++- .../processors/search_issues_processor.py | 10 +++-- tests/datasets/test_errors_processor.py | 3 +- .../datasets/test_search_issues_processor.py | 3 +- 16 files changed, 80 insertions(+), 21 deletions(-) diff --git a/rust_snuba/src/processors/errors.rs b/rust_snuba/src/processors/errors.rs index d048b9feb..41e8a0263 100644 --- a/rust_snuba/src/processors/errors.rs +++ b/rust_snuba/src/processors/errors.rs @@ -15,7 +15,9 @@ use uuid::Uuid; use sentry_arroyo::backends::kafka::types::KafkaPayload; use crate::config::ProcessorConfig; -use crate::processors::utils::{enforce_standard_retention, StringToIntDatetime64}; +use crate::processors::utils::{ + enforce_standard_retention, serialize_datetime64_ms, StringToIntDatetime64, +}; use crate::types::{ InsertBatch, InsertOrReplacement, KafkaMessageMetadata, ReplacementData, RowData, }; @@ -414,6 +416,7 @@ struct ErrorRow { #[serde(rename = "flags.value")] flags_value: Vec, timestamp: u32, + #[serde(serialize_with = "serialize_datetime64_ms")] timestamp_ms: u64, title: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rust_snuba/src/processors/functions.rs b/rust_snuba/src/processors/functions.rs index a8a4eb3c7..ec0fac417 100644 --- a/rust_snuba/src/processors/functions.rs +++ b/rust_snuba/src/processors/functions.rs @@ -7,6 +7,7 @@ use uuid::Uuid; use sentry_arroyo::backends::kafka::types::KafkaPayload; +use crate::processors::utils::serialize_opt_datetime64_us; use crate::types::{InsertBatch, KafkaMessageMetadata}; pub fn process_message( @@ -103,7 +104,9 @@ struct Function<'a> { release: Option<&'a str>, retention_days: u32, timestamp: u64, + #[serde(serialize_with = "serialize_opt_datetime64_us")] start_timestamp: Option, + #[serde(serialize_with = "serialize_opt_datetime64_us")] end_timestamp: Option, transaction_name: &'a str, thread_id: String, diff --git a/rust_snuba/src/processors/profile_chunks.rs b/rust_snuba/src/processors/profile_chunks.rs index d79d654bf..18caa01e2 100644 --- a/rust_snuba/src/processors/profile_chunks.rs +++ b/rust_snuba/src/processors/profile_chunks.rs @@ -6,7 +6,7 @@ use sentry_arroyo::backends::kafka::types::KafkaPayload; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::processors::utils::enforce_standard_retention; +use crate::processors::utils::{enforce_standard_retention, serialize_datetime64_us}; use crate::types::{InsertBatch, KafkaMessageMetadata}; pub fn process_message( @@ -43,9 +43,15 @@ struct Chunk { project_id: u64, profiler_id: Uuid, chunk_id: Uuid, - #[serde(rename(serialize = "start_timestamp"))] + #[serde( + rename(serialize = "start_timestamp"), + serialize_with = "serialize_datetime64_us" + )] start_timestamp_micro: u64, - #[serde(rename(serialize = "end_timestamp"))] + #[serde( + rename(serialize = "end_timestamp"), + serialize_with = "serialize_datetime64_us" + )] end_timestamp_micro: u64, environment: Option, retention_days: Option, diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-threads.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-threads.json.snap index 1189662f7..31a909ef1 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-threads.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-threads.json.snap @@ -332,7 +332,7 @@ expression: snapshot_payload "id:b7478c1e-a9ba-4e5a-bc65-364142284f89" ], "timestamp": 1677512412, - "timestamp_ms": 1677512412223, + "timestamp_ms": "1677512412.223", "title": "Exception: Some exception.", "trace_id": "1da660e8-8c01-430b-85fe-68537ed5838b", "transaction_name": "MainActivity.capture_exception", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-values-threads.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-values-threads.json.snap index 5b54c48a1..4f6ad469d 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-values-threads.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-null-values-threads.json.snap @@ -332,7 +332,7 @@ expression: snapshot_payload "id:b7478c1e-a9ba-4e5a-bc65-364142284f89" ], "timestamp": 1677512412, - "timestamp_ms": 1677512412223, + "timestamp_ms": "1677512412.223", "title": "Exception: Some exception.", "trace_id": "1da660e8-8c01-430b-85fe-68537ed5838b", "transaction_name": "MainActivity.capture_exception", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-threads.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-threads.json.snap index 97b19736b..37451b841 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-threads.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__error-with-threads.json.snap @@ -333,7 +333,7 @@ expression: snapshot_payload "id:b7478c1e-a9ba-4e5a-bc65-364142284f89" ], "timestamp": 1677512412, - "timestamp_ms": 1677512412223, + "timestamp_ms": "1677512412.223", "title": "Exception: Some exception.", "trace_id": "1da660e8-8c01-430b-85fe-68537ed5838b", "transaction_name": "MainActivity.capture_exception", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__errors1.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__errors1.json.snap index d81918790..48f0dc7f6 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__errors1.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__errors1.json.snap @@ -53,7 +53,7 @@ expression: snapshot_payload "tags.key": [], "tags.value": [], "timestamp": 1677512412, - "timestamp_ms": 1677512412223, + "timestamp_ms": "1677512412.223", "title": "", "transaction_name": "", "type": "error", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-tag-keys.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-tag-keys.json.snap index 73ecd1ad9..e9122cd86 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-tag-keys.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-tag-keys.json.snap @@ -53,7 +53,7 @@ expression: snapshot_payload "tags.key": [], "tags.value": [], "timestamp": 1678209849, - "timestamp_ms": 1678209849987, + "timestamp_ms": "1678209849.987", "title": "", "transaction_name": "", "type": "error", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-values.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-values.json.snap index 329c138d5..51e227acf 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-values.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__null-values.json.snap @@ -53,7 +53,7 @@ expression: snapshot_payload "tags.key": [], "tags.value": [], "timestamp": 1677512412, - "timestamp_ms": 1677512412223, + "timestamp_ms": "1677512412.223", "title": "", "transaction_name": "", "type": "error", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__sdk-info-java.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__sdk-info-java.json.snap index 74829cc7a..39fd6ba0a 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__sdk-info-java.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__sdk-info-java.json.snap @@ -57,7 +57,7 @@ expression: snapshot_payload "tags.key": [], "tags.value": [], "timestamp": 1679315796, - "timestamp_ms": 1679315796051, + "timestamp_ms": "1679315796.051", "title": "redacted", "transaction_name": "", "type": "error", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__weird-transaction-source.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__weird-transaction-source.json.snap index e7894c85e..28dbf35fd 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__weird-transaction-source.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@events-ErrorsProcessor-events__1__weird-transaction-source.json.snap @@ -53,7 +53,7 @@ expression: snapshot_payload "tags.key": [], "tags.value": [], "timestamp": 1678728889, - "timestamp_ms": 1678728889328, + "timestamp_ms": "1678728889.328", "title": "", "transaction_name": "", "type": "error", diff --git a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@snuba-profile-chunks-ProfileChunksProcessor-snuba-profile-chunks__1__valid.json.snap b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@snuba-profile-chunks-ProfileChunksProcessor-snuba-profile-chunks__1__valid.json.snap index 64faf2c8d..a293be5a2 100644 --- a/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@snuba-profile-chunks-ProfileChunksProcessor-snuba-profile-chunks__1__valid.json.snap +++ b/rust_snuba/src/processors/snapshots/rust_snuba__processors__tests__schemas@snuba-profile-chunks-ProfileChunksProcessor-snuba-profile-chunks__1__valid.json.snap @@ -6,13 +6,13 @@ expression: snapshot_payload [ { "chunk_id": "0432a0a4-c25f-4697-bf9f-0a2fcbe6a814", - "end_timestamp": 1710805689123456, + "end_timestamp": "1710805689.123456", "environment": null, "offset": 1, "partition": 0, "profiler_id": "4d229f1d-3807-421b-a62a-5f8bc295d836", "project_id": 1, "retention_days": 30, - "start_timestamp": 1710805688123456 + "start_timestamp": "1710805688.123456" } ] diff --git a/rust_snuba/src/processors/utils.rs b/rust_snuba/src/processors/utils.rs index 3dbc42308..394255ad0 100644 --- a/rust_snuba/src/processors/utils.rs +++ b/rust_snuba/src/processors/utils.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use schemars::JsonSchema; use sentry_options::options; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; // Equivalent to "%Y-%m-%dT%H:%M:%S.%fZ" in python // Notice the differennce of .%fZ vs %.fZ, this comes from a difference in how rust's chrono handles the format @@ -117,6 +117,41 @@ pub struct StringToIntDatetime( pub u32, ); +/// Formats a DateTime64 tick count (at `precision` fractional digits) as a +/// quoted `"."` string for JSONEachRow inserts. +/// +/// Since ClickHouse 26.8, an unquoted JSON number fed to a DateTime64 column +/// is read as seconds instead of raw ticks +/// (https://github.com/ClickHouse/ClickHouse/pull/108091). A quoted decimal +/// seconds string is parsed identically by old and new versions. +pub fn format_datetime64(ticks: u64, precision: u32) -> String { + let scale = 10u64.pow(precision); + format!( + "{}.{:0width$}", + ticks / scale, + ticks % scale, + width = precision as usize + ) +} + +pub fn serialize_datetime64_ms(ticks: &u64, s: S) -> Result { + s.serialize_str(&format_datetime64(*ticks, 3)) +} + +pub fn serialize_datetime64_us(ticks: &u64, s: S) -> Result { + s.serialize_str(&format_datetime64(*ticks, 6)) +} + +pub fn serialize_opt_datetime64_us( + ticks: &Option, + s: S, +) -> Result { + match ticks { + Some(t) => serialize_datetime64_us(t, s), + None => s.serialize_none(), + } +} + #[derive(Debug, Deserialize, JsonSchema, Default, Serialize)] pub struct StringToIntDatetime64( #[serde(deserialize_with = "ensure_valid_datetime_64")] @@ -159,6 +194,14 @@ mod tests { assert_eq!(enforce_standard_retention(Some(100)), 90); } + #[test] + fn test_format_datetime64() { + assert_eq!(format_datetime64(1677512412223, 3), "1677512412.223"); + assert_eq!(format_datetime64(1677512412005, 3), "1677512412.005"); + assert_eq!(format_datetime64(1710805688000012, 6), "1710805688.000012"); + assert_eq!(format_datetime64(0, 3), "0.000"); + } + #[test] fn test_downsampled_defaults() { init_options(); diff --git a/snuba/datasets/processors/search_issues_processor.py b/snuba/datasets/processors/search_issues_processor.py index d8cd6e709..3882b6b8c 100644 --- a/snuba/datasets/processors/search_issues_processor.py +++ b/snuba/datasets/processors/search_issues_processor.py @@ -219,11 +219,13 @@ def _process_timestamp_ms( self, event_data: IssueEventData, processed: MutableMapping[str, Any] ) -> None: client_timestamp = processed["client_timestamp"] - # NOTE: we do this conversion because the JSONRowEncoder will strip out milliseconds out - # of datetime objects specifically. To work around that, we convert the datetime to a - # timestamp in milliseconds + # NOTE: the JSONRowEncoder strips milliseconds from datetime objects, so we send a + # quoted "." string instead. It must be quoted: since ClickHouse 26.8 + # an unquoted number for a DateTime64 column is read as seconds, not raw ticks + # (https://github.com/ClickHouse/ClickHouse/pull/108091). client_timestamp = client_timestamp.replace(tzinfo=UTC) - processed["timestamp_ms"] = int(client_timestamp.timestamp() * 1000) + ms = int(client_timestamp.timestamp() * 1000) + processed["timestamp_ms"] = f"{ms // 1000}.{ms % 1000:03d}" def process_insert_v1( self, event: SearchIssueEvent, metadata: KafkaMessageMetadata diff --git a/tests/datasets/test_errors_processor.py b/tests/datasets/test_errors_processor.py index 47ed8c56a..2b2834fc5 100644 --- a/tests/datasets/test_errors_processor.py +++ b/tests/datasets/test_errors_processor.py @@ -332,7 +332,8 @@ def build_result(self, meta: KafkaMessageMetadata) -> MutableMapping[str, Any]: "partition": meta.partition, "offset": meta.offset, "message_timestamp": int(self.timestamp.replace(tzinfo=UTC).timestamp()), - "timestamp_ms": int(self.timestamp.replace(tzinfo=UTC).timestamp() * 1000), + "timestamp_ms": f"{int(self.timestamp.replace(tzinfo=UTC).timestamp())}" + f".{self.timestamp.microsecond // 1000:03d}", "retention_days": 58, "deleted": 0, "group_id": self.group_id, diff --git a/tests/datasets/test_search_issues_processor.py b/tests/datasets/test_search_issues_processor.py index 823572f6b..82f3705a8 100644 --- a/tests/datasets/test_search_issues_processor.py +++ b/tests/datasets/test_search_issues_processor.py @@ -122,7 +122,8 @@ def test_extract_timestamp_ms(self, message_base): self.assert_required_columns(processed) insert_row = processed.rows[0] client_timestamp_utc = insert_row["client_timestamp"].replace(tzinfo=UTC) - assert insert_row["timestamp_ms"] == int(client_timestamp_utc.timestamp() * 1000) + ms = int(client_timestamp_utc.timestamp() * 1000) + assert insert_row["timestamp_ms"] == f"{ms // 1000}.{ms % 1000:03d}" def test_extract_user(self, message_base): message_with_user = message_base From 61f4315df6963cac64383dc3cc970ea3c99a7a35 Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Wed, 30 Sep 2026 12:12:18 -0700 Subject: [PATCH 5/5] test: Make tests pass on ClickHouse 26.8 - copy_tables: 26.8 wraps single-expression PARTITION BY keys in parentheses in SHOW CREATE TABLE output; normalize before comparing. - load_retriever: treat NaN aggregate results like NULL (fall back to -1) so NaN isn't cached or emitted as a gauge. - extrapolation test: order the grouped query explicitly instead of relying on ClickHouse's row order. --- .../web/rpc/storage_routing/load_retriever.py | 5 ++++- tests/admin/clickhouse/test_copy_tables.py | 21 +++++++++++++++++-- ...endpoint_trace_item_table_extrapolation.py | 6 ++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/snuba/web/rpc/storage_routing/load_retriever.py b/snuba/web/rpc/storage_routing/load_retriever.py index f5e8baf5b..e9af18700 100644 --- a/snuba/web/rpc/storage_routing/load_retriever.py +++ b/snuba/web/rpc/storage_routing/load_retriever.py @@ -2,6 +2,7 @@ import inspect import json +import math from collections.abc import Callable from dataclasses import dataclass, fields from functools import wraps @@ -36,11 +37,13 @@ def to_dict(self) -> dict[str, float]: @classmethod def from_dict(cls, load_info_dict: dict[str, float | int | None]) -> LoadInfo: + # Missing and NaN values fall back to the -1 default. ClickHouse 26.8+ can + # return NaN instead of NULL for an aggregate with no matching metric rows. return cls( **{ f.name: float(v) for f in fields(cls) - if (v := load_info_dict.get(f.name)) is not None + if (v := load_info_dict.get(f.name)) is not None and not math.isnan(v) } ) diff --git a/tests/admin/clickhouse/test_copy_tables.py b/tests/admin/clickhouse/test_copy_tables.py index 8fd11273b..1a4b088de 100644 --- a/tests/admin/clickhouse/test_copy_tables.py +++ b/tests/admin/clickhouse/test_copy_tables.py @@ -1,4 +1,5 @@ import os +import re from unittest.mock import MagicMock, patch import pytest @@ -24,6 +25,16 @@ from snuba.migrations.groups import MigrationGroup from snuba.state.sentry_options import SNUBA_OPTIONS_NAMESPACE + +def _normalize_statement(statement: str) -> str: + """ + ClickHouse 26.8+ wraps single-expression PARTITION BY keys in parentheses + in SHOW CREATE TABLE output, e.g. `PARTITION BY (toStartOfMonth(timestamp))`. + Strip them so expectations hold across versions. + """ + return re.sub(r"^PARTITION BY \((.*)\)$", r"PARTITION BY \1", statement, flags=re.MULTILINE) + + OUTCOMES_DAILY_TABLE_NO_CLUSTER = """ CREATE TABLE IF NOT EXISTS {db}.outcomes_daily_local_v2 ( @@ -144,7 +155,10 @@ def test_get_table_statements( ) ts = table_statements[0] assert ts.is_mergetree == is_mergetree - assert ts.statement == statement.format(db=database_name, engine=engine).strip() + assert ( + _normalize_statement(ts.statement) + == statement.format(db=database_name, engine=engine).strip() + ) @pytest.mark.redis_db @@ -181,7 +195,10 @@ def test_get_table_statement_without_cluster() -> None: ) ts = table_statements[0] assert ts.is_mergetree - assert ts.statement == statement.format(db=database_name, engine=engine).strip() + assert ( + _normalize_statement(ts.statement) + == statement.format(db=database_name, engine=engine).strip() + ) @pytest.mark.redis_db diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py index f5393de0d..681ce8ba4 100644 --- a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py +++ b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py @@ -1056,6 +1056,12 @@ def test_formula_reliability_with_group_by(self) -> None: group_by=[ AttributeKey(type=AttributeKey.TYPE_STRING, name="myattr"), ], + order_by=[ + TraceItemTableRequest.OrderBy( + column=Column(key=AttributeKey(type=AttributeKey.TYPE_STRING, name="myattr")), + descending=True, + ), + ], limit=10, ) response = EndpointTraceItemTable().execute(message)