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

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.4"
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.4",
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
  }
@@ -7212,7 +7214,7 @@ function fieldDefinition(registry, field) {
7212
7214
  return definition;
7213
7215
  }
7214
7216
  function resolveOrderField(field) {
7215
- if (field === "startedAt" || field === "endedAt") return field;
7217
+ if (field === "startedAt" || field === "endedAt" || field === "timeToFirstChunkMs") return field;
7216
7218
  throw new Error(`Unsupported trusted trace-query field: ${field}`);
7217
7219
  }
7218
7220
  function isMetadataField(field) {
@@ -7490,9 +7492,11 @@ function traceOrder(plan) {
7490
7492
  */
7491
7493
  function keysetCursorCondition(plan, order, parameters) {
7492
7494
  if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7495
+ if (plan.cursor.sortValue === null) return `(isNull(sortValue) AND traceId > ${parameters.add(plan.cursor.traceId, "String")})`;
7496
+ const numeric = order.field === "timeToFirstChunkMs";
7493
7497
  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")}))`;
7498
+ const sortValue = parameters.add(plan.cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
7499
+ return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")})${numeric ? " OR isNull(sortValue)" : ""})`;
7496
7500
  }
7497
7501
  /**
7498
7502
  * First statement of keyset and page mode: the page's traces (after the keyset cursor, `limit + 1`
@@ -7504,21 +7508,22 @@ function compileClickHouseTraceQueryKeys(plan) {
7504
7508
  if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
7505
7509
  const parameters = new ParameterBuilder();
7506
7510
  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);
7511
+ 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
7512
  const cursor = keysetCursorCondition(plan, order, parameters);
7509
7513
  const pick = order.direction === "DESC" ? "max" : "min";
7514
+ const rootOrder = order.field === "timeToFirstChunkMs" ? `ifNull(${order.field}, ${pick === "max" ? "-inf" : "inf"})` : order.field;
7510
7515
  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
7516
  return {
7512
7517
  query: `WITH ${ctes.join(",\n")},
7513
7518
  per_trace AS (
7514
7519
  SELECT traceId, ${pick}(${order.field}) AS sortValue,
7515
- ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${order.field}) AS root
7520
+ ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${rootOrder}) AS root
7516
7521
  FROM candidates
7517
7522
  GROUP BY traceId${cursor ? `\n HAVING ${cursor}` : ""}
7518
7523
  )
7519
7524
  SELECT traceId, root.1 AS dedupeKey, sortValue, root.2 AS rootStartedAt, count() OVER () AS __total
7520
7525
  FROM per_trace
7521
- ORDER BY sortValue ${order.direction}, traceId ASC
7526
+ ORDER BY sortValue ${order.direction} NULLS LAST, traceId ASC
7522
7527
  ${limit}`,
7523
7528
  query_params: parameters.params,
7524
7529
  sharedSnapshot: true
@@ -7536,16 +7541,16 @@ function compileClickHouseTraceRowsByKey(plan, keys) {
7536
7541
  const ts = (value) => parameters.add(value, "DateTime64(3, 'UTC')");
7537
7542
  const byEnd = order.field === "endedAt";
7538
7543
  const tuples = keys.map((key) => {
7539
- const started = ts(byEnd ? key.rootStartedAt : key.sortValue);
7544
+ const started = ts(key.rootStartedAt);
7540
7545
  const id = `${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}`;
7541
- return byEnd ? `(${started}, ${id}, ${ts(key.sortValue)})` : `(${started}, ${id})`;
7546
+ return byEnd ? `(${started}, ${id}, ${ts(String(key.sortValue))})` : `(${started}, ${id})`;
7542
7547
  });
7543
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("");
7544
7549
  return {
7545
7550
  query: `SELECT ${TRACE_SELECT}
7546
7551
  FROM ${TABLE_TRACE_ROOTS} r
7547
7552
  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
7553
+ ORDER BY ${order.field} ${order.direction} NULLS LAST, traceId ASC
7549
7554
  LIMIT 1 BY traceId`,
7550
7555
  query_params: parameters.params
7551
7556
  };
@@ -7936,6 +7941,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7936
7941
  });
7937
7942
  }
7938
7943
  const keys = await run(compileClickHouseTraceQueryKeys(plan));
7944
+ const sortValue = (value) => plan.orderBy.field !== "timeToFirstChunkMs" ? asIsoTimestamp(value) : value == null ? null : Number(value);
7939
7945
  let total = Number(keys[0]?.__total ?? 0);
7940
7946
  if (plan.paginationMode === "page" && keys.length === 0 && plan.page > 0) {
7941
7947
  const [stats] = await run(compileClickHouseTraceQueryTotal(plan));
@@ -7945,7 +7951,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7945
7951
  const rows = visible.length > 0 ? await run(compileClickHouseTraceRowsByKey(plan, visible.map((key) => ({
7946
7952
  traceId: String(key.traceId),
7947
7953
  dedupeKey: String(key.dedupeKey),
7948
- sortValue: asIsoTimestamp(key.sortValue),
7954
+ sortValue: sortValue(key.sortValue),
7949
7955
  rootStartedAt: asIsoTimestamp(key.rootStartedAt)
7950
7956
  })))) : [];
7951
7957
  if (plan.paginationMode === "page") return _mastra_core_storage.traceQueryResponseSchema.parse({
@@ -7963,7 +7969,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7963
7969
  traces: await toTraceRecords(rows),
7964
7970
  page: { next: keys.length > plan.limit && lastKey ? _mastra_core_storage.encodeTraceQueryCursor(plan, {
7965
7971
  result: "traces",
7966
- sortValue: asIsoTimestamp(lastKey.sortValue),
7972
+ sortValue: sortValue(lastKey.sortValue),
7967
7973
  traceId: String(lastKey.traceId)
7968
7974
  }) : null }
7969
7975
  });
@@ -8043,10 +8049,10 @@ const displayFields = [
8043
8049
  ];
8044
8050
  var Parameters = class {
8045
8051
  values = {};
8046
- add(value, timestamp = false) {
8052
+ add(value, type = typeof value === "number" ? "UInt64" : "String") {
8047
8053
  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"}}`;
8054
+ this.values[name] = type === "DateTime64(3, 'UTC')" ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
8055
+ return `{${name}:${type}}`;
8050
8056
  }
8051
8057
  };
8052
8058
  function identityValues(row) {
@@ -8062,10 +8068,17 @@ function identityValues(row) {
8062
8068
  function afterCursor$1(plan, parameters) {
8063
8069
  if (!plan.cursor) return "1";
8064
8070
  const cursor = plan.cursor;
8065
- const timestamp = parameters.add(cursor.sortValue, true);
8066
8071
  const field = `s.${plan.orderBy.field}`;
8067
- const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
8068
- const equal = [`${field} = ${timestamp}`];
8072
+ const terms = [];
8073
+ const equal = [];
8074
+ if (cursor.sortValue === null) equal.push(`isNull(${field})`);
8075
+ else {
8076
+ const numeric = plan.orderBy.field === "timeToFirstChunkMs";
8077
+ const value = parameters.add(cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
8078
+ terms.push(`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${value}`);
8079
+ if (numeric) terms.push(`isNull(${field})`);
8080
+ equal.push(`${field} = ${value}`);
8081
+ }
8069
8082
  for (const key of identityFields) {
8070
8083
  const value = cursor[key];
8071
8084
  if (value === null) equal.push(`isNull(s.${key})`);
@@ -8093,8 +8106,8 @@ function fieldExpression(field) {
8093
8106
  }
8094
8107
  function compileClickHouseSpanQuery(plan) {
8095
8108
  const parameters = new Parameters();
8096
- const from = parameters.add(plan.timeRange.from, true);
8097
- const to = parameters.add(plan.timeRange.to, true);
8109
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8110
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8098
8111
  const scope = [];
8099
8112
  if (plan.scope) {
8100
8113
  scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
@@ -8107,6 +8120,7 @@ function compileClickHouseSpanQuery(plan) {
8107
8120
  const selectionFields = [.../* @__PURE__ */ new Set([
8108
8121
  "startedAt",
8109
8122
  "endedAt",
8123
+ plan.orderBy.field,
8110
8124
  ...predicateFields$1(plan.where)
8111
8125
  ])].filter((field) => !new Set(identityFields).has(field));
8112
8126
  const expressions = selectionFields.map(fieldExpression);
@@ -8120,13 +8134,14 @@ function compileClickHouseSpanQuery(plan) {
8120
8134
  ORDER BY ${identities}, e.endedAt DESC
8121
8135
  LIMIT 1 BY ${identities}
8122
8136
  )
8123
- SELECT ${[
8137
+ SELECT ${[.../* @__PURE__ */ new Set([
8124
8138
  ...identityFields,
8125
8139
  "startedAt",
8126
- "endedAt"
8127
- ].map((field) => `s.${field}`).join(", ")}
8140
+ "endedAt",
8141
+ plan.orderBy.field
8142
+ ])].map((field) => `s.${field}`).join(", ")}
8128
8143
  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(", ")}
8144
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"} NULLS LAST, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8130
8145
  LIMIT ${parameters.add(plan.limit + 1)}`,
8131
8146
  query_params: {
8132
8147
  ...parameters.values,
@@ -8218,7 +8233,7 @@ async function querySpans(client, plan, timeoutMs) {
8218
8233
  resourceId: last.resourceId,
8219
8234
  traceId: last.traceId,
8220
8235
  spanId: last.spanId,
8221
- sortValue: last[plan.orderBy.field]
8236
+ sortValue: last[plan.orderBy.field] ?? null
8222
8237
  }) : null;
8223
8238
  return result;
8224
8239
  }