@mastra/clickhouse 1.26.0-alpha.4 → 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
@@ -6901,6 +6901,10 @@ const TRACE_FIELDS = {
6901
6901
  sql: "r.timeToFirstChunkMs",
6902
6902
  parameterType: "Float64"
6903
6903
  },
6904
+ entityId: {
6905
+ sql: "r.entityId",
6906
+ parameterType: "String"
6907
+ },
6904
6908
  entityName: {
6905
6909
  sql: "r.entityName",
6906
6910
  parameterType: "String"
@@ -7299,6 +7303,28 @@ function compileTenantScope(scope, parameters) {
7299
7303
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
7300
7304
  return sql;
7301
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`;
7302
7328
  function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = [], options = {}) {
7303
7329
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
7304
7330
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
@@ -7322,7 +7348,15 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7322
7348
  SELECT *
7323
7349
  FROM ${TABLE_TRACE_ROOTS} r
7324
7350
  WHERE startedAt >= ${from}
7325
- 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
7326
7360
  )`] : [`current_roots AS (
7327
7361
  SELECT *
7328
7362
  FROM ${TABLE_TRACE_ROOTS}
@@ -7332,7 +7366,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7332
7366
  WHERE startedAt >= ${from}
7333
7367
  AND startedAt < ${to}${tenant}${seedFilter}
7334
7368
  )${tenant}
7335
- ORDER BY traceId, dedupeKey
7369
+ ORDER BY traceId, startedAt, spanId, ${ROOT_VERSION_ORDER}
7336
7370
  LIMIT 1 BY traceId
7337
7371
  )`, `root_scope AS (
7338
7372
  SELECT *
@@ -7462,11 +7496,7 @@ function traceOrder(plan) {
7462
7496
  direction: plan.orderBy.direction === "asc" ? "ASC" : "DESC"
7463
7497
  };
7464
7498
  }
7465
- /**
7466
- * Keyset cursor on a trace's sort value. Applied after each trace is reduced to one sort value:
7467
- * filtering root rows first would let a trace with several roots (or unmerged copies with
7468
- * different `endedAt`) come back on a later page under a root that sorts after the cursor.
7469
- */
7499
+ /** Keyset cursor on a trace's sort value (the current root's), with traceId breaking ties. */
7470
7500
  function keysetCursorCondition(plan, order, parameters) {
7471
7501
  if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7472
7502
  if (plan.cursor.sortValue === null) return `(isNull(sortValue) AND traceId > ${parameters.add(plan.cursor.traceId, "String")})`;
@@ -7477,9 +7507,8 @@ function keysetCursorCondition(plan, order, parameters) {
7477
7507
  }
7478
7508
  /**
7479
7509
  * First statement of keyset and page mode: the page's traces (after the keyset cursor, `limit + 1`
7480
- * for keyset mode, `perPage` at the page offset for page mode) as root keys, and the
7481
- * number of matching traces on every row. Per trace, the root row that sorts first gives the sort
7482
- * 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.
7483
7512
  */
7484
7513
  function compileClickHouseTraceQueryKeys(plan) {
7485
7514
  if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
@@ -7487,19 +7516,12 @@ function compileClickHouseTraceQueryKeys(plan) {
7487
7516
  const order = traceOrder(plan);
7488
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);
7489
7518
  const cursor = keysetCursorCondition(plan, order, parameters);
7490
- const pick = order.direction === "DESC" ? "max" : "min";
7491
- const rootOrder = order.field === "timeToFirstChunkMs" ? `ifNull(${order.field}, ${pick === "max" ? "-inf" : "inf"})` : order.field;
7492
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")}`;
7493
7520
  return {
7494
- query: `WITH ${ctes.join(",\n")},
7495
- per_trace AS (
7496
- SELECT traceId, ${pick}(${order.field}) AS sortValue,
7497
- ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${rootOrder}) AS root
7498
- FROM candidates
7499
- GROUP BY traceId${cursor ? `\n HAVING ${cursor}` : ""}
7500
- )
7501
- SELECT traceId, root.1 AS dedupeKey, sortValue, root.2 AS rootStartedAt, count() OVER () AS __total
7502
- FROM per_trace
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}` : ""}
7503
7525
  ORDER BY sortValue ${order.direction} NULLS LAST, traceId ASC
7504
7526
  ${limit}`,
7505
7527
  query_params: parameters.params,
@@ -7507,27 +7529,28 @@ ${limit}`,
7507
7529
  };
7508
7530
  }
7509
7531
  /**
7510
- * Second statement of keyset and page mode: the rows of the keyed roots, with `metadata`
7511
- * and the stored previews, read by sort-key tuple. Unmerged copies of a root key are redeliveries of one
7512
- * span; the relation-free `where` conjuncts are re-applied so a copy that doesn't match isn't
7513
- * 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.
7514
7542
  */
7515
7543
  function compileClickHouseTraceRowsByKey(plan, keys) {
7516
7544
  const parameters = new ParameterBuilder();
7517
7545
  const order = traceOrder(plan);
7518
7546
  const ts = (value) => parameters.add(value, "DateTime64(3, 'UTC')");
7519
- const byEnd = order.field === "endedAt";
7520
- const tuples = keys.map((key) => {
7521
- const started = ts(key.rootStartedAt);
7522
- const id = `${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}`;
7523
- return byEnd ? `(${started}, ${id}, ${ts(String(key.sortValue))})` : `(${started}, ${id})`;
7524
- });
7547
+ const tuples = keys.map((key) => `(${ts(key.rootStartedAt)}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}, ${ts(key.rootEndedAt)})`);
7525
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("");
7526
7549
  return {
7527
7550
  query: `SELECT ${TRACE_SELECT}
7528
7551
  FROM ${TABLE_TRACE_ROOTS} r
7529
- WHERE (r.startedAt, r.traceId, r.dedupeKey${byEnd ? ", r.endedAt" : ""}) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7530
- ORDER BY ${order.field} ${order.direction} NULLS LAST, 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}
7531
7554
  LIMIT 1 BY traceId`,
7532
7555
  query_params: parameters.params
7533
7556
  };
@@ -7546,9 +7569,7 @@ FROM candidates`,
7546
7569
  /**
7547
7570
  * The list statement of groups and delta mode (rows without `metadata` and the previews). Keyset
7548
7571
  * and page mode list with compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey.
7549
- * A trace with several candidate rows (several roots, or unmerged copies of one) is listed in delta
7550
- * mode under the row that sorts first in the list order, as in the keys statement; ties go to the
7551
- * 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.
7552
7573
  */
7553
7574
  function compileClickHouseTraceQuery(plan, deltaHead) {
7554
7575
  const parameters = new ParameterBuilder();
@@ -7569,7 +7590,6 @@ LIMIT ${limit}`,
7569
7590
  };
7570
7591
  }
7571
7592
  if (plan.paginationMode !== "delta") throw new Error("Keyset and page trace queries use compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey");
7572
- const order = traceOrder(plan);
7573
7593
  const watermark = coreStorage.getTraceQueryDeltaWatermark(plan, "clickhouse");
7574
7594
  const after = watermark ? parseDeltaWatermark(watermark) : {
7575
7595
  cursorId: "0",
@@ -7588,8 +7608,7 @@ LIMIT ${limit}`,
7588
7608
  SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
7589
7609
  FROM candidates c
7590
7610
  INNER JOIN delta_candidates d ON c.traceId = d.traceId
7591
- ORDER BY d.latestCursorId ASC, c.traceId ASC, c.${order.field} ${order.direction}, c.dedupeKey ASC, c.endedAt DESC
7592
- LIMIT 1 BY c.traceId
7611
+ ORDER BY d.latestCursorId ASC, c.traceId ASC
7593
7612
  LIMIT ${limit}`,
7594
7613
  query_params: parameters.params,
7595
7614
  sharedSnapshot: true
@@ -7633,6 +7652,7 @@ function compileClickHouseTraceRootPayloads(keys, scope, columns = "payload") {
7633
7652
  query: `SELECT traceId, spanId AS rootSpanId, dedupeKey, organizationId, resourceId, ${columns === "payload" ? `metadataRaw AS metadata, ${PREVIEW_COLUMNS.join(", ")}` : "input, output, error"}
7634
7653
  FROM ${TABLE_TRACE_ROOTS}
7635
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}
7636
7656
  LIMIT 1 BY traceId, dedupeKey, organizationId, resourceId`,
7637
7657
  query_params: parameters.params
7638
7658
  };
@@ -7929,7 +7949,8 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7929
7949
  traceId: String(key.traceId),
7930
7950
  dedupeKey: String(key.dedupeKey),
7931
7951
  sortValue: sortValue(key.sortValue),
7932
- rootStartedAt: asIsoTimestamp(key.rootStartedAt)
7952
+ rootStartedAt: asIsoTimestamp(key.rootStartedAt),
7953
+ rootEndedAt: asIsoTimestamp(key.rootEndedAt)
7933
7954
  })))) : [];
7934
7955
  if (plan.paginationMode === "page") return coreStorage.traceQueryResponseSchema.parse({
7935
7956
  traces: await toTraceRecords(rows),
@@ -8338,7 +8359,7 @@ function compileClickHouseThreadRowsQuery(plan, strategy = "full") {
8338
8359
  const sortedRoots = `roots AS (
8339
8360
  SELECT organizationId, traceId, ${rootDataFields.map((field) => `${rootExpression(field)} AS ${field}`).join(", ")}
8340
8361
  FROM ${TABLE_TRACE_ROOTS} ${scope.length || strategy === "candidates" ? `WHERE ${[...scope, ...strategy === "candidates" ? [candidateTracesFilter] : []].join(" AND ")}` : ""}
8341
- ORDER BY organizationId, traceId, startedAt ASC, spanId, endedAt DESC, isNotNull(threadId) DESC, dedupeKey
8362
+ ORDER BY organizationId, traceId, startedAt ASC, spanId, ${ROOT_VERSION_ORDER}
8342
8363
  LIMIT 1 BY organizationId, traceId
8343
8364
  )`;
8344
8365
  return {
@@ -8440,7 +8461,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8440
8461
  ${contextFields.join(", ")}, if(spanType = 'agent_run', timeToFirstChunkMs, NULL) AS timeToFirstChunkMs,
8441
8462
  if(isNull(error), 'success', 'error') AS status, ${PREVIEW_COLUMNS.join(", ")}
8442
8463
  FROM ${TABLE_TRACE_ROOTS} WHERE ${rootWhere}
8443
- ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8464
+ ORDER BY ${ROOT_VERSION_ORDER} LIMIT 1 BY organizationId, traceId, spanId`,
8444
8465
  query_params: { roots: keys(batch) }
8445
8466
  }));
8446
8467
  const byRoot = new Map(stored.map((row) => [rootKey(row), row]));
@@ -8448,7 +8469,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8448
8469
  for (const batch of batches(missing, 32)) {
8449
8470
  const payloads = await run({
8450
8471
  query: `SELECT organizationId, traceId, spanId, input, output, error FROM ${TABLE_TRACE_ROOTS}
8451
- 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`,
8452
8473
  query_params: { roots: keys(batch) }
8453
8474
  });
8454
8475
  for (const payload of payloads) {
@@ -8517,6 +8538,7 @@ const DIMENSION_SQL = {
8517
8538
  entityName: "r.entityName",
8518
8539
  name: "r.name",
8519
8540
  spanType: "r.spanType",
8541
+ entityId: "r.entityId",
8520
8542
  environment: "r.environment",
8521
8543
  status: TRACE_STATUS_SQL,
8522
8544
  serviceName: "r.serviceName",
@@ -8555,12 +8577,6 @@ const COMPARISON_SQL = {
8555
8577
  function compileClickHouseTraceAggregate(plan) {
8556
8578
  const parameters = new ParameterBuilder();
8557
8579
  const ctes = compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true });
8558
- ctes.push(`trace_roots AS (
8559
- SELECT *
8560
- FROM candidates
8561
- ORDER BY traceId, dedupeKey
8562
- LIMIT 1 BY traceId
8563
- )`);
8564
8580
  const dimensionSql = (field) => {
8565
8581
  if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8566
8582
  const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
@@ -8594,7 +8610,7 @@ function compileClickHouseTraceAggregate(plan) {
8594
8610
  }
8595
8611
  ctes.push(`facts AS (
8596
8612
  SELECT ${factColumns.join(",\n ")}
8597
- FROM trace_roots r${usageJoin}
8613
+ FROM candidates r${usageJoin}
8598
8614
  )`);
8599
8615
  const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
8600
8616
  const measureSql = (name) => {
@@ -10100,6 +10116,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10100
10116
  "trace-query-context-ids",
10101
10117
  ...timeToFirstChunkFeatures,
10102
10118
  "trace-query-root-span-fields",
10119
+ "trace-query-root-entity-id",
10103
10120
  "trace-aggregate",
10104
10121
  ...spanQueryFeatures,
10105
10122
  ...threadQueryFeatures
@@ -10123,6 +10140,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10123
10140
  "trace-query-context-ids",
10124
10141
  ...timeToFirstChunkFeatures,
10125
10142
  "trace-query-root-span-fields",
10143
+ "trace-query-root-entity-id",
10126
10144
  "trace-aggregate",
10127
10145
  ...spanQueryFeatures,
10128
10146
  ...threadQueryFeatures