@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.
package/dist/index.js CHANGED
@@ -6846,7 +6846,9 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6846
6846
  * Core before 1.68 has no field registry at all.
6847
6847
  */
6848
6848
  const timeToFirstChunkSupported = "timeToFirstChunkMs" in (coreStorage.TRACE_QUERY_FIELD_REGISTRY?.trace ?? {});
6849
- const timeToFirstChunkFeatures = timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [];
6849
+ /** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
6850
+ const timeToFirstChunkSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
6851
+ const timeToFirstChunkFeatures = timeToFirstChunkSupported ? timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk", "trace-query-time-to-first-chunk-sort"] : ["trace-query-time-to-first-chunk"] : [];
6850
6852
  function durationMsSql(startedAt, endedAt) {
6851
6853
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6852
6854
  }
@@ -6899,6 +6901,10 @@ const TRACE_FIELDS = {
6899
6901
  sql: "r.timeToFirstChunkMs",
6900
6902
  parameterType: "Float64"
6901
6903
  },
6904
+ entityId: {
6905
+ sql: "r.entityId",
6906
+ parameterType: "String"
6907
+ },
6902
6908
  entityName: {
6903
6909
  sql: "r.entityName",
6904
6910
  parameterType: "String"
@@ -7189,7 +7195,7 @@ function fieldDefinition(registry, field) {
7189
7195
  return definition;
7190
7196
  }
7191
7197
  function resolveOrderField(field) {
7192
- if (field === "startedAt" || field === "endedAt") return field;
7198
+ if (field === "startedAt" || field === "endedAt" || field === "timeToFirstChunkMs") return field;
7193
7199
  throw new Error(`Unsupported trusted trace-query field: ${field}`);
7194
7200
  }
7195
7201
  function isMetadataField(field) {
@@ -7297,6 +7303,28 @@ function compileTenantScope(scope, parameters) {
7297
7303
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
7298
7304
  return sql;
7299
7305
  }
7306
+ /**
7307
+ * Order of the versions of one root span, current version first. `mastra_trace_roots` has no
7308
+ * version column, so until a background merge runs, every export of a root is its own row with the
7309
+ * same sort key, and merges never meet versions that ended on different days (it is partitioned by
7310
+ * `endedAt`). The version that ended last is current, as a resumed or completed root is exported
7311
+ * again with a later `endedAt`; a pending copy stores `endedAt = startedAt` and can lack `threadId`,
7312
+ * so a copy with a thread wins an equal-time tie. Remaining ties are broken by insertion order, last
7313
+ * insert first: the part's first block number, then the row's offset in the part. Tied versions
7314
+ * share a partition and sort key, so a merge meets them and keeps the last insert too, and reads
7315
+ * pick the same version before and after it. Both are virtual columns, so the order reads nothing
7316
+ * beyond `endedAt` and `threadId`.
7317
+ *
7318
+ * Merges don't follow this order across `endedAt`: within one day's partition a merge keeps the last
7319
+ * insert, not the version that ended last. If an outdated export of a root is redelivered after its
7320
+ * current version, on the same day, reads pick the current version until a merge, and the outdated
7321
+ * one afterwards (last write wins, as in the PG and DuckDB stores). Making merges keep the version
7322
+ * that ended last needs `ReplacingMergeTree(endedAt)`, a schema change.
7323
+ *
7324
+ * Every root collapse uses this order: trace rows, aggregates, thread rows and the discovery reads.
7325
+ */
7326
+ const ROOT_VERSION_ORDER = `endedAt DESC, isNotNull(threadId) DESC, toUInt64(splitByChar('_', _part)[2]) DESC,
7327
+ _part_offset DESC`;
7300
7328
  function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = [], options = {}) {
7301
7329
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
7302
7330
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
@@ -7320,7 +7348,15 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7320
7348
  SELECT *
7321
7349
  FROM ${TABLE_TRACE_ROOTS} r
7322
7350
  WHERE startedAt >= ${from}
7323
- AND startedAt < ${to}${tenant}${seedFilter}${bound}
7351
+ AND startedAt < ${to}${tenant}${bound}${seedFilter ? `
7352
+ AND traceId IN (
7353
+ SELECT traceId
7354
+ FROM ${TABLE_TRACE_ROOTS} r
7355
+ WHERE startedAt >= ${from}
7356
+ AND startedAt < ${to}${tenant}${seedFilter}
7357
+ )` : ""}
7358
+ ORDER BY startedAt, traceId, spanId, ${ROOT_VERSION_ORDER}
7359
+ LIMIT 1 BY traceId
7324
7360
  )`] : [`current_roots AS (
7325
7361
  SELECT *
7326
7362
  FROM ${TABLE_TRACE_ROOTS}
@@ -7330,7 +7366,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7330
7366
  WHERE startedAt >= ${from}
7331
7367
  AND startedAt < ${to}${tenant}${seedFilter}
7332
7368
  )${tenant}
7333
- ORDER BY traceId, dedupeKey
7369
+ ORDER BY traceId, startedAt, spanId, ${ROOT_VERSION_ORDER}
7334
7370
  LIMIT 1 BY traceId
7335
7371
  )`, `root_scope AS (
7336
7372
  SELECT *
@@ -7460,69 +7496,61 @@ function traceOrder(plan) {
7460
7496
  direction: plan.orderBy.direction === "asc" ? "ASC" : "DESC"
7461
7497
  };
7462
7498
  }
7463
- /**
7464
- * Keyset cursor on a trace's sort value. Applied after each trace is reduced to one sort value:
7465
- * filtering root rows first would let a trace with several roots (or unmerged copies with
7466
- * different `endedAt`) come back on a later page under a root that sorts after the cursor.
7467
- */
7499
+ /** Keyset cursor on a trace's sort value (the current root's), with traceId breaking ties. */
7468
7500
  function keysetCursorCondition(plan, order, parameters) {
7469
7501
  if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7502
+ if (plan.cursor.sortValue === null) return `(isNull(sortValue) AND traceId > ${parameters.add(plan.cursor.traceId, "String")})`;
7503
+ const numeric = order.field === "timeToFirstChunkMs";
7470
7504
  const comparison = order.direction === "ASC" ? ">" : "<";
7471
- const sortValue = parameters.add(plan.cursor.sortValue, "DateTime64(3, 'UTC')");
7472
- return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")}))`;
7505
+ const sortValue = parameters.add(plan.cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
7506
+ return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")})${numeric ? " OR isNull(sortValue)" : ""})`;
7473
7507
  }
7474
7508
  /**
7475
7509
  * First statement of keyset and page mode: the page's traces (after the keyset cursor, `limit + 1`
7476
- * for keyset mode, `perPage` at the page offset for page mode) as root keys, and the
7477
- * number of matching traces on every row. Per trace, the root row that sorts first gives the sort
7478
- * value and the key, so the key always names a row with that sort value. No blob columns are read.
7510
+ * for keyset mode, `perPage` at the page offset for page mode) as keys of their current roots, and
7511
+ * the number of matching traces on every row. No blob columns are read.
7479
7512
  */
7480
7513
  function compileClickHouseTraceQueryKeys(plan) {
7481
7514
  if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
7482
7515
  const parameters = new ParameterBuilder();
7483
7516
  const order = traceOrder(plan);
7484
- const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt", parameters, QUERY_TRACES_SCOPE);
7517
+ 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);
7485
7518
  const cursor = keysetCursorCondition(plan, order, parameters);
7486
- const pick = order.direction === "DESC" ? "max" : "min";
7487
7519
  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")}`;
7488
7520
  return {
7489
- query: `WITH ${ctes.join(",\n")},
7490
- per_trace AS (
7491
- SELECT traceId, ${pick}(${order.field}) AS sortValue,
7492
- ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${order.field}) AS root
7493
- FROM candidates
7494
- GROUP BY traceId${cursor ? `\n HAVING ${cursor}` : ""}
7495
- )
7496
- SELECT traceId, root.1 AS dedupeKey, sortValue, root.2 AS rootStartedAt, count() OVER () AS __total
7497
- FROM per_trace
7498
- ORDER BY sortValue ${order.direction}, traceId ASC
7521
+ query: `WITH ${ctes.join(",\n")}
7522
+ SELECT traceId, dedupeKey, ${order.field} AS sortValue, startedAt AS rootStartedAt, endedAt AS rootEndedAt,
7523
+ count() OVER () AS __total
7524
+ FROM candidates${cursor ? `\nWHERE ${cursor}` : ""}
7525
+ ORDER BY sortValue ${order.direction} NULLS LAST, traceId ASC
7499
7526
  ${limit}`,
7500
7527
  query_params: parameters.params,
7501
7528
  sharedSnapshot: true
7502
7529
  };
7503
7530
  }
7504
7531
  /**
7505
- * Second statement of keyset and page mode: the rows of the keyed roots, with `metadata`
7506
- * and the stored previews, read by sort-key tuple. Unmerged copies of a root key are redeliveries of one
7507
- * span; the relation-free `where` conjuncts are re-applied so a copy that doesn't match isn't
7508
- * returned, and the copy that sorts first is kept, as in the keys statement.
7532
+ * Second statement of keyset and page mode: the rows of the keyed roots, with `metadata` and the
7533
+ * stored previews, read by sort key and `endedAt`, so only the keyed version's rows match. Versions
7534
+ * that end at the same instant are ordered as in the keys statement (ROOT_VERSION_ORDER). The
7535
+ * relation-free `where` conjuncts are re-applied, so a version written between the two statements
7536
+ * that doesn't match isn't returned.
7537
+ *
7538
+ * Trade-off of matching on `endedAt`: if a newer version is written between the two statements and
7539
+ * a same-day merge then drops the keyed version, the trace has no row and drops off the page. Without
7540
+ * `endedAt` the lookup would find the replacement, but could also return a version other than the
7541
+ * keyed one whenever both are unmerged. The window is the gap between the two statements.
7509
7542
  */
7510
7543
  function compileClickHouseTraceRowsByKey(plan, keys) {
7511
7544
  const parameters = new ParameterBuilder();
7512
7545
  const order = traceOrder(plan);
7513
7546
  const ts = (value) => parameters.add(value, "DateTime64(3, 'UTC')");
7514
- const byEnd = order.field === "endedAt";
7515
- const tuples = keys.map((key) => {
7516
- const started = ts(byEnd ? key.rootStartedAt : key.sortValue);
7517
- const id = `${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}`;
7518
- return byEnd ? `(${started}, ${id}, ${ts(key.sortValue)})` : `(${started}, ${id})`;
7519
- });
7547
+ const tuples = keys.map((key) => `(${ts(key.rootStartedAt)}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}, ${ts(key.rootEndedAt)})`);
7520
7548
  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("");
7521
7549
  return {
7522
7550
  query: `SELECT ${TRACE_SELECT}
7523
7551
  FROM ${TABLE_TRACE_ROOTS} r
7524
- WHERE (r.startedAt, r.traceId, r.dedupeKey${byEnd ? ", r.endedAt" : ""}) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7525
- ORDER BY ${order.field} ${order.direction}, traceId ASC
7552
+ WHERE (r.startedAt, r.traceId, r.dedupeKey, r.endedAt) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7553
+ ORDER BY ${order.field} ${order.direction} NULLS LAST, traceId ASC, ${ROOT_VERSION_ORDER}
7526
7554
  LIMIT 1 BY traceId`,
7527
7555
  query_params: parameters.params
7528
7556
  };
@@ -7541,9 +7569,7 @@ FROM candidates`,
7541
7569
  /**
7542
7570
  * The list statement of groups and delta mode (rows without `metadata` and the previews). Keyset
7543
7571
  * and page mode list with compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey.
7544
- * A trace with several candidate rows (several roots, or unmerged copies of one) is listed in delta
7545
- * mode under the row that sorts first in the list order, as in the keys statement; ties go to the
7546
- * lowest dedupeKey, then the latest endedAt, so every poll picks the same row.
7572
+ * Candidates hold one current root per trace, as in the keys statement.
7547
7573
  */
7548
7574
  function compileClickHouseTraceQuery(plan, deltaHead) {
7549
7575
  const parameters = new ParameterBuilder();
@@ -7564,7 +7590,6 @@ LIMIT ${limit}`,
7564
7590
  };
7565
7591
  }
7566
7592
  if (plan.paginationMode !== "delta") throw new Error("Keyset and page trace queries use compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey");
7567
- const order = traceOrder(plan);
7568
7593
  const watermark = coreStorage.getTraceQueryDeltaWatermark(plan, "clickhouse");
7569
7594
  const after = watermark ? parseDeltaWatermark(watermark) : {
7570
7595
  cursorId: "0",
@@ -7583,8 +7608,7 @@ LIMIT ${limit}`,
7583
7608
  SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
7584
7609
  FROM candidates c
7585
7610
  INNER JOIN delta_candidates d ON c.traceId = d.traceId
7586
- ORDER BY d.latestCursorId ASC, c.traceId ASC, c.${order.field} ${order.direction}, c.dedupeKey ASC, c.endedAt DESC
7587
- LIMIT 1 BY c.traceId
7611
+ ORDER BY d.latestCursorId ASC, c.traceId ASC
7588
7612
  LIMIT ${limit}`,
7589
7613
  query_params: parameters.params,
7590
7614
  sharedSnapshot: true
@@ -7628,6 +7652,7 @@ function compileClickHouseTraceRootPayloads(keys, scope, columns = "payload") {
7628
7652
  query: `SELECT traceId, spanId AS rootSpanId, dedupeKey, organizationId, resourceId, ${columns === "payload" ? `metadataRaw AS metadata, ${PREVIEW_COLUMNS.join(", ")}` : "input, output, error"}
7629
7653
  FROM ${TABLE_TRACE_ROOTS}
7630
7654
  WHERE (startedAt, traceId, dedupeKey, endedAt, ifNull(organizationId, ''), isNull(organizationId), ifNull(resourceId, ''), isNull(resourceId)) IN (${tuples.join(", ")})${compileTenantScope(scope, parameters)}
7655
+ ORDER BY ${ROOT_VERSION_ORDER}
7631
7656
  LIMIT 1 BY traceId, dedupeKey, organizationId, resourceId`,
7632
7657
  query_params: parameters.params
7633
7658
  };
@@ -7913,6 +7938,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7913
7938
  });
7914
7939
  }
7915
7940
  const keys = await run(compileClickHouseTraceQueryKeys(plan));
7941
+ const sortValue = (value) => plan.orderBy.field !== "timeToFirstChunkMs" ? asIsoTimestamp(value) : value == null ? null : Number(value);
7916
7942
  let total = Number(keys[0]?.__total ?? 0);
7917
7943
  if (plan.paginationMode === "page" && keys.length === 0 && plan.page > 0) {
7918
7944
  const [stats] = await run(compileClickHouseTraceQueryTotal(plan));
@@ -7922,8 +7948,9 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7922
7948
  const rows = visible.length > 0 ? await run(compileClickHouseTraceRowsByKey(plan, visible.map((key) => ({
7923
7949
  traceId: String(key.traceId),
7924
7950
  dedupeKey: String(key.dedupeKey),
7925
- sortValue: asIsoTimestamp(key.sortValue),
7926
- rootStartedAt: asIsoTimestamp(key.rootStartedAt)
7951
+ sortValue: sortValue(key.sortValue),
7952
+ rootStartedAt: asIsoTimestamp(key.rootStartedAt),
7953
+ rootEndedAt: asIsoTimestamp(key.rootEndedAt)
7927
7954
  })))) : [];
7928
7955
  if (plan.paginationMode === "page") return coreStorage.traceQueryResponseSchema.parse({
7929
7956
  traces: await toTraceRecords(rows),
@@ -7940,7 +7967,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7940
7967
  traces: await toTraceRecords(rows),
7941
7968
  page: { next: keys.length > plan.limit && lastKey ? coreStorage.encodeTraceQueryCursor(plan, {
7942
7969
  result: "traces",
7943
- sortValue: asIsoTimestamp(lastKey.sortValue),
7970
+ sortValue: sortValue(lastKey.sortValue),
7944
7971
  traceId: String(lastKey.traceId)
7945
7972
  }) : null }
7946
7973
  });
@@ -8020,10 +8047,10 @@ const displayFields = [
8020
8047
  ];
8021
8048
  var Parameters = class {
8022
8049
  values = {};
8023
- add(value, timestamp = false) {
8050
+ add(value, type = typeof value === "number" ? "UInt64" : "String") {
8024
8051
  const name = `span_${Object.keys(this.values).length}`;
8025
- this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
8026
- return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
8052
+ this.values[name] = type === "DateTime64(3, 'UTC')" ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
8053
+ return `{${name}:${type}}`;
8027
8054
  }
8028
8055
  };
8029
8056
  function identityValues(row) {
@@ -8039,10 +8066,17 @@ function identityValues(row) {
8039
8066
  function afterCursor$1(plan, parameters) {
8040
8067
  if (!plan.cursor) return "1";
8041
8068
  const cursor = plan.cursor;
8042
- const timestamp = parameters.add(cursor.sortValue, true);
8043
8069
  const field = `s.${plan.orderBy.field}`;
8044
- const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
8045
- const equal = [`${field} = ${timestamp}`];
8070
+ const terms = [];
8071
+ const equal = [];
8072
+ if (cursor.sortValue === null) equal.push(`isNull(${field})`);
8073
+ else {
8074
+ const numeric = plan.orderBy.field === "timeToFirstChunkMs";
8075
+ const value = parameters.add(cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
8076
+ terms.push(`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${value}`);
8077
+ if (numeric) terms.push(`isNull(${field})`);
8078
+ equal.push(`${field} = ${value}`);
8079
+ }
8046
8080
  for (const key of identityFields) {
8047
8081
  const value = cursor[key];
8048
8082
  if (value === null) equal.push(`isNull(s.${key})`);
@@ -8070,8 +8104,8 @@ function fieldExpression(field) {
8070
8104
  }
8071
8105
  function compileClickHouseSpanQuery(plan) {
8072
8106
  const parameters = new Parameters();
8073
- const from = parameters.add(plan.timeRange.from, true);
8074
- const to = parameters.add(plan.timeRange.to, true);
8107
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8108
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8075
8109
  const scope = [];
8076
8110
  if (plan.scope) {
8077
8111
  scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
@@ -8084,6 +8118,7 @@ function compileClickHouseSpanQuery(plan) {
8084
8118
  const selectionFields = [.../* @__PURE__ */ new Set([
8085
8119
  "startedAt",
8086
8120
  "endedAt",
8121
+ plan.orderBy.field,
8087
8122
  ...predicateFields$1(plan.where)
8088
8123
  ])].filter((field) => !new Set(identityFields).has(field));
8089
8124
  const expressions = selectionFields.map(fieldExpression);
@@ -8097,13 +8132,14 @@ function compileClickHouseSpanQuery(plan) {
8097
8132
  ORDER BY ${identities}, e.endedAt DESC
8098
8133
  LIMIT 1 BY ${identities}
8099
8134
  )
8100
- SELECT ${[
8135
+ SELECT ${[.../* @__PURE__ */ new Set([
8101
8136
  ...identityFields,
8102
8137
  "startedAt",
8103
- "endedAt"
8104
- ].map((field) => `s.${field}`).join(", ")}
8138
+ "endedAt",
8139
+ plan.orderBy.field
8140
+ ])].map((field) => `s.${field}`).join(", ")}
8105
8141
  FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
8106
- ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8142
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"} NULLS LAST, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8107
8143
  LIMIT ${parameters.add(plan.limit + 1)}`,
8108
8144
  query_params: {
8109
8145
  ...parameters.values,
@@ -8195,7 +8231,7 @@ async function querySpans(client, plan, timeoutMs) {
8195
8231
  resourceId: last.resourceId,
8196
8232
  traceId: last.traceId,
8197
8233
  spanId: last.spanId,
8198
- sortValue: last[plan.orderBy.field]
8234
+ sortValue: last[plan.orderBy.field] ?? null
8199
8235
  }) : null;
8200
8236
  return result;
8201
8237
  }
@@ -8323,7 +8359,7 @@ function compileClickHouseThreadRowsQuery(plan, strategy = "full") {
8323
8359
  const sortedRoots = `roots AS (
8324
8360
  SELECT organizationId, traceId, ${rootDataFields.map((field) => `${rootExpression(field)} AS ${field}`).join(", ")}
8325
8361
  FROM ${TABLE_TRACE_ROOTS} ${scope.length || strategy === "candidates" ? `WHERE ${[...scope, ...strategy === "candidates" ? [candidateTracesFilter] : []].join(" AND ")}` : ""}
8326
- ORDER BY organizationId, traceId, startedAt ASC, spanId, endedAt DESC, isNotNull(threadId) DESC, dedupeKey
8362
+ ORDER BY organizationId, traceId, startedAt ASC, spanId, ${ROOT_VERSION_ORDER}
8327
8363
  LIMIT 1 BY organizationId, traceId
8328
8364
  )`;
8329
8365
  return {
@@ -8425,7 +8461,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8425
8461
  ${contextFields.join(", ")}, if(spanType = 'agent_run', timeToFirstChunkMs, NULL) AS timeToFirstChunkMs,
8426
8462
  if(isNull(error), 'success', 'error') AS status, ${PREVIEW_COLUMNS.join(", ")}
8427
8463
  FROM ${TABLE_TRACE_ROOTS} WHERE ${rootWhere}
8428
- ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8464
+ ORDER BY ${ROOT_VERSION_ORDER} LIMIT 1 BY organizationId, traceId, spanId`,
8429
8465
  query_params: { roots: keys(batch) }
8430
8466
  }));
8431
8467
  const byRoot = new Map(stored.map((row) => [rootKey(row), row]));
@@ -8433,7 +8469,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8433
8469
  for (const batch of batches(missing, 32)) {
8434
8470
  const payloads = await run({
8435
8471
  query: `SELECT organizationId, traceId, spanId, input, output, error FROM ${TABLE_TRACE_ROOTS}
8436
- WHERE ${rootWhere} ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8472
+ WHERE ${rootWhere} ORDER BY ${ROOT_VERSION_ORDER} LIMIT 1 BY organizationId, traceId, spanId`,
8437
8473
  query_params: { roots: keys(batch) }
8438
8474
  });
8439
8475
  for (const payload of payloads) {
@@ -8502,6 +8538,7 @@ const DIMENSION_SQL = {
8502
8538
  entityName: "r.entityName",
8503
8539
  name: "r.name",
8504
8540
  spanType: "r.spanType",
8541
+ entityId: "r.entityId",
8505
8542
  environment: "r.environment",
8506
8543
  status: TRACE_STATUS_SQL,
8507
8544
  serviceName: "r.serviceName",
@@ -8540,12 +8577,6 @@ const COMPARISON_SQL = {
8540
8577
  function compileClickHouseTraceAggregate(plan) {
8541
8578
  const parameters = new ParameterBuilder();
8542
8579
  const ctes = compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true });
8543
- ctes.push(`trace_roots AS (
8544
- SELECT *
8545
- FROM candidates
8546
- ORDER BY traceId, dedupeKey
8547
- LIMIT 1 BY traceId
8548
- )`);
8549
8580
  const dimensionSql = (field) => {
8550
8581
  if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8551
8582
  const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
@@ -8579,7 +8610,7 @@ function compileClickHouseTraceAggregate(plan) {
8579
8610
  }
8580
8611
  ctes.push(`facts AS (
8581
8612
  SELECT ${factColumns.join(",\n ")}
8582
- FROM trace_roots r${usageJoin}
8613
+ FROM candidates r${usageJoin}
8583
8614
  )`);
8584
8615
  const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
8585
8616
  const measureSql = (name) => {
@@ -10085,6 +10116,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10085
10116
  "trace-query-context-ids",
10086
10117
  ...timeToFirstChunkFeatures,
10087
10118
  "trace-query-root-span-fields",
10119
+ "trace-query-root-entity-id",
10088
10120
  "trace-aggregate",
10089
10121
  ...spanQueryFeatures,
10090
10122
  ...threadQueryFeatures
@@ -10108,6 +10140,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10108
10140
  "trace-query-context-ids",
10109
10141
  ...timeToFirstChunkFeatures,
10110
10142
  "trace-query-root-span-fields",
10143
+ "trace-query-root-entity-id",
10111
10144
  "trace-aggregate",
10112
10145
  ...spanQueryFeatures,
10113
10146
  ...threadQueryFeatures