@mastra/clickhouse 1.26.0-alpha.3 → 1.26.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ name: mastra-clickhouse
3
3
  description: Documentation for @mastra/clickhouse. Use when working with @mastra/clickhouse APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/clickhouse"
6
- version: "1.26.0-alpha.3"
6
+ version: "1.26.0-alpha.5"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.26.0-alpha.3",
2
+ "version": "1.26.0-alpha.5",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -290,7 +290,7 @@ Don't set `replication` on ClickHouse Cloud. Cloud rewrites `MergeTree` to `Shar
290
290
 
291
291
  ### Observability domain options
292
292
 
293
- `ObservabilityStorageClickhouse` and `ObservabilityStorageClickhouseVNext` accept the same connection options as `ClickhouseStore` (`url`, `username`, `password`, or a pre-configured `client`). `ObservabilityStorageClickhouseVNext` also accepts `retention`, the per-signal TTL in days described in [ClickHouse native TTL](https://mastra.ai/reference/storage/retention), and `traceQuery`, the execution limits for [trace queries](https://mastra.ai/reference/observability/tracing/trace-query).
293
+ `ObservabilityStorageClickhouse` and `ObservabilityStorageClickhouseVNext` accept the same connection options as `ClickhouseStore` (`url`, `username`, `password`, or a pre-configured `client`). `ObservabilityStorageClickhouseVNext` also accepts `retention`, the per-signal TTL in days described in [ClickHouse native TTL](https://mastra.ai/reference/storage/retention), and `traceQuery`, the execution limits for [trace queries](https://mastra.ai/reference/observability/tracing/advanced-queries).
294
294
 
295
295
  ## Hosting options
296
296
 
package/dist/index.cjs CHANGED
@@ -6869,7 +6869,9 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6869
6869
  * Core before 1.68 has no field registry at all.
6870
6870
  */
6871
6871
  const timeToFirstChunkSupported = "timeToFirstChunkMs" in (_mastra_core_storage.TRACE_QUERY_FIELD_REGISTRY?.trace ?? {});
6872
- const timeToFirstChunkFeatures = timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [];
6872
+ /** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
6873
+ const timeToFirstChunkSortSupported = _mastra_core_storage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
6874
+ const timeToFirstChunkFeatures = timeToFirstChunkSupported ? timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk", "trace-query-time-to-first-chunk-sort"] : ["trace-query-time-to-first-chunk"] : [];
6873
6875
  function durationMsSql(startedAt, endedAt) {
6874
6876
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6875
6877
  }
@@ -6922,6 +6924,10 @@ const TRACE_FIELDS = {
6922
6924
  sql: "r.timeToFirstChunkMs",
6923
6925
  parameterType: "Float64"
6924
6926
  },
6927
+ entityId: {
6928
+ sql: "r.entityId",
6929
+ parameterType: "String"
6930
+ },
6925
6931
  entityName: {
6926
6932
  sql: "r.entityName",
6927
6933
  parameterType: "String"
@@ -7212,7 +7218,7 @@ function fieldDefinition(registry, field) {
7212
7218
  return definition;
7213
7219
  }
7214
7220
  function resolveOrderField(field) {
7215
- if (field === "startedAt" || field === "endedAt") return field;
7221
+ if (field === "startedAt" || field === "endedAt" || field === "timeToFirstChunkMs") return field;
7216
7222
  throw new Error(`Unsupported trusted trace-query field: ${field}`);
7217
7223
  }
7218
7224
  function isMetadataField(field) {
@@ -7320,6 +7326,28 @@ function compileTenantScope(scope, parameters) {
7320
7326
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
7321
7327
  return sql;
7322
7328
  }
7329
+ /**
7330
+ * Order of the versions of one root span, current version first. `mastra_trace_roots` has no
7331
+ * version column, so until a background merge runs, every export of a root is its own row with the
7332
+ * same sort key, and merges never meet versions that ended on different days (it is partitioned by
7333
+ * `endedAt`). The version that ended last is current, as a resumed or completed root is exported
7334
+ * again with a later `endedAt`; a pending copy stores `endedAt = startedAt` and can lack `threadId`,
7335
+ * so a copy with a thread wins an equal-time tie. Remaining ties are broken by insertion order, last
7336
+ * insert first: the part's first block number, then the row's offset in the part. Tied versions
7337
+ * share a partition and sort key, so a merge meets them and keeps the last insert too, and reads
7338
+ * pick the same version before and after it. Both are virtual columns, so the order reads nothing
7339
+ * beyond `endedAt` and `threadId`.
7340
+ *
7341
+ * Merges don't follow this order across `endedAt`: within one day's partition a merge keeps the last
7342
+ * insert, not the version that ended last. If an outdated export of a root is redelivered after its
7343
+ * current version, on the same day, reads pick the current version until a merge, and the outdated
7344
+ * one afterwards (last write wins, as in the PG and DuckDB stores). Making merges keep the version
7345
+ * that ended last needs `ReplacingMergeTree(endedAt)`, a schema change.
7346
+ *
7347
+ * Every root collapse uses this order: trace rows, aggregates, thread rows and the discovery reads.
7348
+ */
7349
+ const ROOT_VERSION_ORDER = `endedAt DESC, isNotNull(threadId) DESC, toUInt64(splitByChar('_', _part)[2]) DESC,
7350
+ _part_offset DESC`;
7323
7351
  function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = [], options = {}) {
7324
7352
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
7325
7353
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
@@ -7343,7 +7371,15 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7343
7371
  SELECT *
7344
7372
  FROM ${TABLE_TRACE_ROOTS} r
7345
7373
  WHERE startedAt >= ${from}
7346
- AND startedAt < ${to}${tenant}${seedFilter}${bound}
7374
+ AND startedAt < ${to}${tenant}${bound}${seedFilter ? `
7375
+ AND traceId IN (
7376
+ SELECT traceId
7377
+ FROM ${TABLE_TRACE_ROOTS} r
7378
+ WHERE startedAt >= ${from}
7379
+ AND startedAt < ${to}${tenant}${seedFilter}
7380
+ )` : ""}
7381
+ ORDER BY startedAt, traceId, spanId, ${ROOT_VERSION_ORDER}
7382
+ LIMIT 1 BY traceId
7347
7383
  )`] : [`current_roots AS (
7348
7384
  SELECT *
7349
7385
  FROM ${TABLE_TRACE_ROOTS}
@@ -7353,7 +7389,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7353
7389
  WHERE startedAt >= ${from}
7354
7390
  AND startedAt < ${to}${tenant}${seedFilter}
7355
7391
  )${tenant}
7356
- ORDER BY traceId, dedupeKey
7392
+ ORDER BY traceId, startedAt, spanId, ${ROOT_VERSION_ORDER}
7357
7393
  LIMIT 1 BY traceId
7358
7394
  )`, `root_scope AS (
7359
7395
  SELECT *
@@ -7483,69 +7519,61 @@ function traceOrder(plan) {
7483
7519
  direction: plan.orderBy.direction === "asc" ? "ASC" : "DESC"
7484
7520
  };
7485
7521
  }
7486
- /**
7487
- * Keyset cursor on a trace's sort value. Applied after each trace is reduced to one sort value:
7488
- * filtering root rows first would let a trace with several roots (or unmerged copies with
7489
- * different `endedAt`) come back on a later page under a root that sorts after the cursor.
7490
- */
7522
+ /** Keyset cursor on a trace's sort value (the current root's), with traceId breaking ties. */
7491
7523
  function keysetCursorCondition(plan, order, parameters) {
7492
7524
  if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7525
+ if (plan.cursor.sortValue === null) return `(isNull(sortValue) AND traceId > ${parameters.add(plan.cursor.traceId, "String")})`;
7526
+ const numeric = order.field === "timeToFirstChunkMs";
7493
7527
  const comparison = order.direction === "ASC" ? ">" : "<";
7494
- const sortValue = parameters.add(plan.cursor.sortValue, "DateTime64(3, 'UTC')");
7495
- return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")}))`;
7528
+ const sortValue = parameters.add(plan.cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
7529
+ return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")})${numeric ? " OR isNull(sortValue)" : ""})`;
7496
7530
  }
7497
7531
  /**
7498
7532
  * First statement of keyset and page mode: the page's traces (after the keyset cursor, `limit + 1`
7499
- * for keyset mode, `perPage` at the page offset for page mode) as root keys, and the
7500
- * number of matching traces on every row. Per trace, the root row that sorts first gives the sort
7501
- * value and the key, so the key always names a row with that sort value. No blob columns are read.
7533
+ * for keyset mode, `perPage` at the page offset for page mode) as keys of their current roots, and
7534
+ * the number of matching traces on every row. No blob columns are read.
7502
7535
  */
7503
7536
  function compileClickHouseTraceQueryKeys(plan) {
7504
7537
  if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
7505
7538
  const parameters = new ParameterBuilder();
7506
7539
  const order = traceOrder(plan);
7507
- const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt", parameters, QUERY_TRACES_SCOPE);
7540
+ const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt, r.timeToFirstChunkMs AS timeToFirstChunkMs", parameters, QUERY_TRACES_SCOPE);
7508
7541
  const cursor = keysetCursorCondition(plan, order, parameters);
7509
- const pick = order.direction === "DESC" ? "max" : "min";
7510
7542
  const limit = plan.paginationMode === "page" ? `LIMIT ${parameters.add(plan.perPage, "UInt64")} OFFSET ${parameters.add(plan.page * plan.perPage, "UInt64")}` : `LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`;
7511
7543
  return {
7512
- query: `WITH ${ctes.join(",\n")},
7513
- per_trace AS (
7514
- SELECT traceId, ${pick}(${order.field}) AS sortValue,
7515
- ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${order.field}) AS root
7516
- FROM candidates
7517
- GROUP BY traceId${cursor ? `\n HAVING ${cursor}` : ""}
7518
- )
7519
- SELECT traceId, root.1 AS dedupeKey, sortValue, root.2 AS rootStartedAt, count() OVER () AS __total
7520
- FROM per_trace
7521
- ORDER BY sortValue ${order.direction}, traceId ASC
7544
+ query: `WITH ${ctes.join(",\n")}
7545
+ SELECT traceId, dedupeKey, ${order.field} AS sortValue, startedAt AS rootStartedAt, endedAt AS rootEndedAt,
7546
+ count() OVER () AS __total
7547
+ FROM candidates${cursor ? `\nWHERE ${cursor}` : ""}
7548
+ ORDER BY sortValue ${order.direction} NULLS LAST, traceId ASC
7522
7549
  ${limit}`,
7523
7550
  query_params: parameters.params,
7524
7551
  sharedSnapshot: true
7525
7552
  };
7526
7553
  }
7527
7554
  /**
7528
- * Second statement of keyset and page mode: the rows of the keyed roots, with `metadata`
7529
- * and the stored previews, read by sort-key tuple. Unmerged copies of a root key are redeliveries of one
7530
- * span; the relation-free `where` conjuncts are re-applied so a copy that doesn't match isn't
7531
- * returned, and the copy that sorts first is kept, as in the keys statement.
7555
+ * Second statement of keyset and page mode: the rows of the keyed roots, with `metadata` and the
7556
+ * stored previews, read by sort key and `endedAt`, so only the keyed version's rows match. Versions
7557
+ * that end at the same instant are ordered as in the keys statement (ROOT_VERSION_ORDER). The
7558
+ * relation-free `where` conjuncts are re-applied, so a version written between the two statements
7559
+ * that doesn't match isn't returned.
7560
+ *
7561
+ * Trade-off of matching on `endedAt`: if a newer version is written between the two statements and
7562
+ * a same-day merge then drops the keyed version, the trace has no row and drops off the page. Without
7563
+ * `endedAt` the lookup would find the replacement, but could also return a version other than the
7564
+ * keyed one whenever both are unmerged. The window is the gap between the two statements.
7532
7565
  */
7533
7566
  function compileClickHouseTraceRowsByKey(plan, keys) {
7534
7567
  const parameters = new ParameterBuilder();
7535
7568
  const order = traceOrder(plan);
7536
7569
  const ts = (value) => parameters.add(value, "DateTime64(3, 'UTC')");
7537
- const byEnd = order.field === "endedAt";
7538
- const tuples = keys.map((key) => {
7539
- const started = ts(byEnd ? key.rootStartedAt : key.sortValue);
7540
- const id = `${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}`;
7541
- return byEnd ? `(${started}, ${id}, ${ts(key.sortValue)})` : `(${started}, ${id})`;
7542
- });
7570
+ const tuples = keys.map((key) => `(${ts(key.rootStartedAt)}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}, ${ts(key.rootEndedAt)})`);
7543
7571
  const predicate = (plan.where ? (plan.where.type === "boolean" && plan.where.operator === "and" ? plan.where.args : [plan.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : []).map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("");
7544
7572
  return {
7545
7573
  query: `SELECT ${TRACE_SELECT}
7546
7574
  FROM ${TABLE_TRACE_ROOTS} r
7547
- WHERE (r.startedAt, r.traceId, r.dedupeKey${byEnd ? ", r.endedAt" : ""}) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7548
- ORDER BY ${order.field} ${order.direction}, traceId ASC
7575
+ WHERE (r.startedAt, r.traceId, r.dedupeKey, r.endedAt) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7576
+ ORDER BY ${order.field} ${order.direction} NULLS LAST, traceId ASC, ${ROOT_VERSION_ORDER}
7549
7577
  LIMIT 1 BY traceId`,
7550
7578
  query_params: parameters.params
7551
7579
  };
@@ -7564,9 +7592,7 @@ FROM candidates`,
7564
7592
  /**
7565
7593
  * The list statement of groups and delta mode (rows without `metadata` and the previews). Keyset
7566
7594
  * and page mode list with compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey.
7567
- * A trace with several candidate rows (several roots, or unmerged copies of one) is listed in delta
7568
- * mode under the row that sorts first in the list order, as in the keys statement; ties go to the
7569
- * lowest dedupeKey, then the latest endedAt, so every poll picks the same row.
7595
+ * Candidates hold one current root per trace, as in the keys statement.
7570
7596
  */
7571
7597
  function compileClickHouseTraceQuery(plan, deltaHead) {
7572
7598
  const parameters = new ParameterBuilder();
@@ -7587,7 +7613,6 @@ LIMIT ${limit}`,
7587
7613
  };
7588
7614
  }
7589
7615
  if (plan.paginationMode !== "delta") throw new Error("Keyset and page trace queries use compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey");
7590
- const order = traceOrder(plan);
7591
7616
  const watermark = _mastra_core_storage.getTraceQueryDeltaWatermark(plan, "clickhouse");
7592
7617
  const after = watermark ? parseDeltaWatermark(watermark) : {
7593
7618
  cursorId: "0",
@@ -7606,8 +7631,7 @@ LIMIT ${limit}`,
7606
7631
  SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
7607
7632
  FROM candidates c
7608
7633
  INNER JOIN delta_candidates d ON c.traceId = d.traceId
7609
- ORDER BY d.latestCursorId ASC, c.traceId ASC, c.${order.field} ${order.direction}, c.dedupeKey ASC, c.endedAt DESC
7610
- LIMIT 1 BY c.traceId
7634
+ ORDER BY d.latestCursorId ASC, c.traceId ASC
7611
7635
  LIMIT ${limit}`,
7612
7636
  query_params: parameters.params,
7613
7637
  sharedSnapshot: true
@@ -7651,6 +7675,7 @@ function compileClickHouseTraceRootPayloads(keys, scope, columns = "payload") {
7651
7675
  query: `SELECT traceId, spanId AS rootSpanId, dedupeKey, organizationId, resourceId, ${columns === "payload" ? `metadataRaw AS metadata, ${PREVIEW_COLUMNS.join(", ")}` : "input, output, error"}
7652
7676
  FROM ${TABLE_TRACE_ROOTS}
7653
7677
  WHERE (startedAt, traceId, dedupeKey, endedAt, ifNull(organizationId, ''), isNull(organizationId), ifNull(resourceId, ''), isNull(resourceId)) IN (${tuples.join(", ")})${compileTenantScope(scope, parameters)}
7678
+ ORDER BY ${ROOT_VERSION_ORDER}
7654
7679
  LIMIT 1 BY traceId, dedupeKey, organizationId, resourceId`,
7655
7680
  query_params: parameters.params
7656
7681
  };
@@ -7936,6 +7961,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7936
7961
  });
7937
7962
  }
7938
7963
  const keys = await run(compileClickHouseTraceQueryKeys(plan));
7964
+ const sortValue = (value) => plan.orderBy.field !== "timeToFirstChunkMs" ? asIsoTimestamp(value) : value == null ? null : Number(value);
7939
7965
  let total = Number(keys[0]?.__total ?? 0);
7940
7966
  if (plan.paginationMode === "page" && keys.length === 0 && plan.page > 0) {
7941
7967
  const [stats] = await run(compileClickHouseTraceQueryTotal(plan));
@@ -7945,8 +7971,9 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7945
7971
  const rows = visible.length > 0 ? await run(compileClickHouseTraceRowsByKey(plan, visible.map((key) => ({
7946
7972
  traceId: String(key.traceId),
7947
7973
  dedupeKey: String(key.dedupeKey),
7948
- sortValue: asIsoTimestamp(key.sortValue),
7949
- rootStartedAt: asIsoTimestamp(key.rootStartedAt)
7974
+ sortValue: sortValue(key.sortValue),
7975
+ rootStartedAt: asIsoTimestamp(key.rootStartedAt),
7976
+ rootEndedAt: asIsoTimestamp(key.rootEndedAt)
7950
7977
  })))) : [];
7951
7978
  if (plan.paginationMode === "page") return _mastra_core_storage.traceQueryResponseSchema.parse({
7952
7979
  traces: await toTraceRecords(rows),
@@ -7963,7 +7990,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7963
7990
  traces: await toTraceRecords(rows),
7964
7991
  page: { next: keys.length > plan.limit && lastKey ? _mastra_core_storage.encodeTraceQueryCursor(plan, {
7965
7992
  result: "traces",
7966
- sortValue: asIsoTimestamp(lastKey.sortValue),
7993
+ sortValue: sortValue(lastKey.sortValue),
7967
7994
  traceId: String(lastKey.traceId)
7968
7995
  }) : null }
7969
7996
  });
@@ -8043,10 +8070,10 @@ const displayFields = [
8043
8070
  ];
8044
8071
  var Parameters = class {
8045
8072
  values = {};
8046
- add(value, timestamp = false) {
8073
+ add(value, type = typeof value === "number" ? "UInt64" : "String") {
8047
8074
  const name = `span_${Object.keys(this.values).length}`;
8048
- this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
8049
- return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
8075
+ this.values[name] = type === "DateTime64(3, 'UTC')" ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
8076
+ return `{${name}:${type}}`;
8050
8077
  }
8051
8078
  };
8052
8079
  function identityValues(row) {
@@ -8062,10 +8089,17 @@ function identityValues(row) {
8062
8089
  function afterCursor$1(plan, parameters) {
8063
8090
  if (!plan.cursor) return "1";
8064
8091
  const cursor = plan.cursor;
8065
- const timestamp = parameters.add(cursor.sortValue, true);
8066
8092
  const field = `s.${plan.orderBy.field}`;
8067
- const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
8068
- const equal = [`${field} = ${timestamp}`];
8093
+ const terms = [];
8094
+ const equal = [];
8095
+ if (cursor.sortValue === null) equal.push(`isNull(${field})`);
8096
+ else {
8097
+ const numeric = plan.orderBy.field === "timeToFirstChunkMs";
8098
+ const value = parameters.add(cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
8099
+ terms.push(`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${value}`);
8100
+ if (numeric) terms.push(`isNull(${field})`);
8101
+ equal.push(`${field} = ${value}`);
8102
+ }
8069
8103
  for (const key of identityFields) {
8070
8104
  const value = cursor[key];
8071
8105
  if (value === null) equal.push(`isNull(s.${key})`);
@@ -8093,8 +8127,8 @@ function fieldExpression(field) {
8093
8127
  }
8094
8128
  function compileClickHouseSpanQuery(plan) {
8095
8129
  const parameters = new Parameters();
8096
- const from = parameters.add(plan.timeRange.from, true);
8097
- const to = parameters.add(plan.timeRange.to, true);
8130
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8131
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8098
8132
  const scope = [];
8099
8133
  if (plan.scope) {
8100
8134
  scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
@@ -8107,6 +8141,7 @@ function compileClickHouseSpanQuery(plan) {
8107
8141
  const selectionFields = [.../* @__PURE__ */ new Set([
8108
8142
  "startedAt",
8109
8143
  "endedAt",
8144
+ plan.orderBy.field,
8110
8145
  ...predicateFields$1(plan.where)
8111
8146
  ])].filter((field) => !new Set(identityFields).has(field));
8112
8147
  const expressions = selectionFields.map(fieldExpression);
@@ -8120,13 +8155,14 @@ function compileClickHouseSpanQuery(plan) {
8120
8155
  ORDER BY ${identities}, e.endedAt DESC
8121
8156
  LIMIT 1 BY ${identities}
8122
8157
  )
8123
- SELECT ${[
8158
+ SELECT ${[.../* @__PURE__ */ new Set([
8124
8159
  ...identityFields,
8125
8160
  "startedAt",
8126
- "endedAt"
8127
- ].map((field) => `s.${field}`).join(", ")}
8161
+ "endedAt",
8162
+ plan.orderBy.field
8163
+ ])].map((field) => `s.${field}`).join(", ")}
8128
8164
  FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
8129
- ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8165
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"} NULLS LAST, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8130
8166
  LIMIT ${parameters.add(plan.limit + 1)}`,
8131
8167
  query_params: {
8132
8168
  ...parameters.values,
@@ -8218,7 +8254,7 @@ async function querySpans(client, plan, timeoutMs) {
8218
8254
  resourceId: last.resourceId,
8219
8255
  traceId: last.traceId,
8220
8256
  spanId: last.spanId,
8221
- sortValue: last[plan.orderBy.field]
8257
+ sortValue: last[plan.orderBy.field] ?? null
8222
8258
  }) : null;
8223
8259
  return result;
8224
8260
  }
@@ -8346,7 +8382,7 @@ function compileClickHouseThreadRowsQuery(plan, strategy = "full") {
8346
8382
  const sortedRoots = `roots AS (
8347
8383
  SELECT organizationId, traceId, ${rootDataFields.map((field) => `${rootExpression(field)} AS ${field}`).join(", ")}
8348
8384
  FROM ${TABLE_TRACE_ROOTS} ${scope.length || strategy === "candidates" ? `WHERE ${[...scope, ...strategy === "candidates" ? [candidateTracesFilter] : []].join(" AND ")}` : ""}
8349
- ORDER BY organizationId, traceId, startedAt ASC, spanId, endedAt DESC, isNotNull(threadId) DESC, dedupeKey
8385
+ ORDER BY organizationId, traceId, startedAt ASC, spanId, ${ROOT_VERSION_ORDER}
8350
8386
  LIMIT 1 BY organizationId, traceId
8351
8387
  )`;
8352
8388
  return {
@@ -8448,7 +8484,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8448
8484
  ${contextFields.join(", ")}, if(spanType = 'agent_run', timeToFirstChunkMs, NULL) AS timeToFirstChunkMs,
8449
8485
  if(isNull(error), 'success', 'error') AS status, ${PREVIEW_COLUMNS.join(", ")}
8450
8486
  FROM ${TABLE_TRACE_ROOTS} WHERE ${rootWhere}
8451
- ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8487
+ ORDER BY ${ROOT_VERSION_ORDER} LIMIT 1 BY organizationId, traceId, spanId`,
8452
8488
  query_params: { roots: keys(batch) }
8453
8489
  }));
8454
8490
  const byRoot = new Map(stored.map((row) => [rootKey(row), row]));
@@ -8456,7 +8492,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8456
8492
  for (const batch of batches(missing, 32)) {
8457
8493
  const payloads = await run({
8458
8494
  query: `SELECT organizationId, traceId, spanId, input, output, error FROM ${TABLE_TRACE_ROOTS}
8459
- WHERE ${rootWhere} ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8495
+ WHERE ${rootWhere} ORDER BY ${ROOT_VERSION_ORDER} LIMIT 1 BY organizationId, traceId, spanId`,
8460
8496
  query_params: { roots: keys(batch) }
8461
8497
  });
8462
8498
  for (const payload of payloads) {
@@ -8525,6 +8561,7 @@ const DIMENSION_SQL = {
8525
8561
  entityName: "r.entityName",
8526
8562
  name: "r.name",
8527
8563
  spanType: "r.spanType",
8564
+ entityId: "r.entityId",
8528
8565
  environment: "r.environment",
8529
8566
  status: TRACE_STATUS_SQL,
8530
8567
  serviceName: "r.serviceName",
@@ -8563,12 +8600,6 @@ const COMPARISON_SQL = {
8563
8600
  function compileClickHouseTraceAggregate(plan) {
8564
8601
  const parameters = new ParameterBuilder();
8565
8602
  const ctes = compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true });
8566
- ctes.push(`trace_roots AS (
8567
- SELECT *
8568
- FROM candidates
8569
- ORDER BY traceId, dedupeKey
8570
- LIMIT 1 BY traceId
8571
- )`);
8572
8603
  const dimensionSql = (field) => {
8573
8604
  if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8574
8605
  const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
@@ -8602,7 +8633,7 @@ function compileClickHouseTraceAggregate(plan) {
8602
8633
  }
8603
8634
  ctes.push(`facts AS (
8604
8635
  SELECT ${factColumns.join(",\n ")}
8605
- FROM trace_roots r${usageJoin}
8636
+ FROM candidates r${usageJoin}
8606
8637
  )`);
8607
8638
  const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
8608
8639
  const measureSql = (name) => {
@@ -10108,6 +10139,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
10108
10139
  "trace-query-context-ids",
10109
10140
  ...timeToFirstChunkFeatures,
10110
10141
  "trace-query-root-span-fields",
10142
+ "trace-query-root-entity-id",
10111
10143
  "trace-aggregate",
10112
10144
  ...spanQueryFeatures,
10113
10145
  ...threadQueryFeatures
@@ -10131,6 +10163,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
10131
10163
  "trace-query-context-ids",
10132
10164
  ...timeToFirstChunkFeatures,
10133
10165
  "trace-query-root-span-fields",
10166
+ "trace-query-root-entity-id",
10134
10167
  "trace-aggregate",
10135
10168
  ...spanQueryFeatures,
10136
10169
  ...threadQueryFeatures