@mastra/clickhouse 1.27.0 → 1.28.0-alpha.1

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
@@ -759,6 +759,7 @@ var ClickhouseDB = class extends MastraBase {
759
759
  const rawUpdatedAt = filteredRecord.updatedAt || /* @__PURE__ */ new Date();
760
760
  if ("createdAt" in filteredRecord || (await this.getTableColumns(tableName)).has("createdAt")) filteredRecord.createdAt = rawCreatedAt instanceof Date ? rawCreatedAt.toISOString() : rawCreatedAt;
761
761
  if ("updatedAt" in filteredRecord || (await this.getTableColumns(tableName)).has("updatedAt")) filteredRecord.updatedAt = rawUpdatedAt instanceof Date ? rawUpdatedAt.toISOString() : rawUpdatedAt;
762
+ for (const [key, value] of Object.entries(filteredRecord)) if (TABLE_SCHEMAS[tableName]?.[key]?.type === "timestamp" && typeof value === "number") filteredRecord[key] = new Date(value).toISOString();
762
763
  await this.client.insert({
763
764
  table: tableName,
764
765
  values: [filteredRecord],
@@ -2271,12 +2272,12 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
2271
2272
  }
2272
2273
  });
2273
2274
  const data = { ...updates };
2274
- if (data.endedAt instanceof Date) data.endedAt = data.endedAt.getTime();
2275
- if (data.startedAt instanceof Date) data.startedAt = data.startedAt.getTime();
2275
+ if (data.endedAt instanceof Date) data.endedAt = data.endedAt.toISOString();
2276
+ if (data.startedAt instanceof Date) data.startedAt = data.startedAt.toISOString();
2276
2277
  const updated = {
2277
2278
  ...existing,
2278
2279
  ...data,
2279
- updatedAt: Date.now()
2280
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2280
2281
  };
2281
2282
  await this.client.insert({
2282
2283
  table: TABLE_SPANS,
@@ -2514,7 +2515,7 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
2514
2515
  }
2515
2516
  async batchUpdateSpans(args) {
2516
2517
  try {
2517
- const now = Date.now();
2518
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2518
2519
  if (args.records.length === 0) return;
2519
2520
  const keyParams = {};
2520
2521
  const keyTuples = args.records.map((record, i) => {
@@ -2547,8 +2548,8 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
2547
2548
  const existing = current.get(key);
2548
2549
  if (!existing) continue;
2549
2550
  const updates = { ...record.updates };
2550
- if (updates.startedAt instanceof Date) updates.startedAt = updates.startedAt.getTime();
2551
- if (updates.endedAt instanceof Date) updates.endedAt = updates.endedAt.getTime();
2551
+ if (updates.startedAt instanceof Date) updates.startedAt = updates.startedAt.toISOString();
2552
+ if (updates.endedAt instanceof Date) updates.endedAt = updates.endedAt.toISOString();
2552
2553
  current.set(key, {
2553
2554
  ...existing,
2554
2555
  ...updates,
@@ -6906,7 +6907,17 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6906
6907
  const timeToFirstChunkSupported = "timeToFirstChunkMs" in (coreStorage.TRACE_QUERY_FIELD_REGISTRY?.trace ?? {});
6907
6908
  /** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
6908
6909
  const timeToFirstChunkSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
6909
- const timeToFirstChunkFeatures = timeToFirstChunkSupported ? timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk", "trace-query-time-to-first-chunk-sort"] : ["trace-query-time-to-first-chunk"] : [];
6910
+ /** Same rule for time to first token: returned and sorted only when core knows the field. */
6911
+ const timeToFirstTokenSupported = "timeToFirstTokenMs" in (coreStorage.TRACE_QUERY_FIELD_REGISTRY?.trace ?? {});
6912
+ const timeToFirstTokenSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstTokenMs") === true;
6913
+ const timingFeatures = [
6914
+ ...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [],
6915
+ ...timeToFirstChunkSupported && timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : [],
6916
+ ...timeToFirstTokenSupported ? ["trace-query-time-to-first-token"] : [],
6917
+ ...timeToFirstTokenSupported && timeToFirstTokenSortSupported ? ["trace-query-time-to-first-token-sort"] : []
6918
+ ];
6919
+ /** Order fields holding numbers that can be empty; the rest are timestamps. */
6920
+ const NUMERIC_ORDER_FIELDS = /* @__PURE__ */ new Set(["timeToFirstChunkMs", "timeToFirstTokenMs"]);
6910
6921
  function durationMsSql(startedAt, endedAt) {
6911
6922
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6912
6923
  }
@@ -6959,6 +6970,10 @@ const TRACE_FIELDS = {
6959
6970
  sql: "r.timeToFirstChunkMs",
6960
6971
  parameterType: "Float64"
6961
6972
  },
6973
+ timeToFirstTokenMs: {
6974
+ sql: "r.timeToFirstTokenMs",
6975
+ parameterType: "Float64"
6976
+ },
6962
6977
  entityId: {
6963
6978
  sql: "r.entityId",
6964
6979
  parameterType: "String"
@@ -7025,6 +7040,10 @@ const SPAN_FIELDS = {
7025
7040
  sql: "s.timeToFirstChunkMs",
7026
7041
  parameterType: "Float64"
7027
7042
  },
7043
+ timeToFirstTokenMs: {
7044
+ sql: "s.timeToFirstTokenMs",
7045
+ parameterType: "Float64"
7046
+ },
7028
7047
  status: {
7029
7048
  sql: "s.status",
7030
7049
  parameterType: "String"
@@ -7237,6 +7256,7 @@ const TRACE_SELECT = `
7237
7256
  r.entityType AS entityType,
7238
7257
  r.environment AS environment,
7239
7258
  r.timeToFirstChunkMs AS timeToFirstChunkMs,
7259
+ r.timeToFirstTokenMs AS timeToFirstTokenMs,
7240
7260
  ${TRACE_STATUS_SQL} AS status`;
7241
7261
  var ParameterBuilder = class {
7242
7262
  params = {};
@@ -7253,7 +7273,7 @@ function fieldDefinition(registry, field) {
7253
7273
  return definition;
7254
7274
  }
7255
7275
  function resolveOrderField(field) {
7256
- if (field === "startedAt" || field === "endedAt" || field === "timeToFirstChunkMs") return field;
7276
+ if (field === "startedAt" || field === "endedAt" || NUMERIC_ORDER_FIELDS.has(field)) return field;
7257
7277
  throw new Error(`Unsupported trusted trace-query field: ${field}`);
7258
7278
  }
7259
7279
  function isMetadataField(field) {
@@ -7443,6 +7463,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7443
7463
  endedAt,
7444
7464
  ${durationMsSql("startedAt", "endedAt")} AS durationMs,
7445
7465
  timeToFirstChunkMs,
7466
+ timeToFirstTokenMs,
7446
7467
  if(isNotNull(error), 'error', 'success') AS status,
7447
7468
  error,
7448
7469
  entityType,
@@ -7558,7 +7579,7 @@ function traceOrder(plan) {
7558
7579
  function keysetCursorCondition(plan, order, parameters) {
7559
7580
  if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7560
7581
  if (plan.cursor.sortValue === null) return `(isNull(sortValue) AND traceId > ${parameters.add(plan.cursor.traceId, "String")})`;
7561
- const numeric = order.field === "timeToFirstChunkMs";
7582
+ const numeric = NUMERIC_ORDER_FIELDS.has(order.field);
7562
7583
  const comparison = order.direction === "ASC" ? ">" : "<";
7563
7584
  const sortValue = parameters.add(plan.cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
7564
7585
  return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")})${numeric ? " OR isNull(sortValue)" : ""})`;
@@ -7572,7 +7593,7 @@ function compileClickHouseTraceQueryKeys(plan) {
7572
7593
  if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
7573
7594
  const parameters = new ParameterBuilder();
7574
7595
  const order = traceOrder(plan);
7575
- 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);
7596
+ const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt, r.timeToFirstChunkMs AS timeToFirstChunkMs, r.timeToFirstTokenMs AS timeToFirstTokenMs", parameters, QUERY_TRACES_SCOPE);
7576
7597
  const cursor = keysetCursorCondition(plan, order, parameters);
7577
7598
  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")}`;
7578
7599
  return {
@@ -7973,7 +7994,8 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7973
7994
  entityType: row.entityType == null ? null : String(row.entityType),
7974
7995
  environment: row.environment == null ? null : String(row.environment),
7975
7996
  status: row.status,
7976
- ...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) }
7997
+ ...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) },
7998
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: row.timeToFirstTokenMs == null ? null : Number(row.timeToFirstTokenMs) }
7977
7999
  });
7978
8000
  if (plan.paginationMode === "delta") {
7979
8001
  const rows = await run(compileClickHouseTraceQuery(plan, deltaHead));
@@ -7996,7 +8018,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7996
8018
  });
7997
8019
  }
7998
8020
  const keys = await run(compileClickHouseTraceQueryKeys(plan));
7999
- const sortValue = (value) => plan.orderBy.field !== "timeToFirstChunkMs" ? asIsoTimestamp(value) : value == null ? null : Number(value);
8021
+ const sortValue = (value) => !NUMERIC_ORDER_FIELDS.has(plan.orderBy.field) ? asIsoTimestamp(value) : value == null ? null : Number(value);
8000
8022
  let total = Number(keys[0]?.__total ?? 0);
8001
8023
  if (plan.paginationMode === "page" && keys.length === 0 && plan.page > 0) {
8002
8024
  const [stats] = await run(compileClickHouseTraceQueryTotal(plan));
@@ -8099,6 +8121,7 @@ const displayFields = [
8099
8121
  "provider",
8100
8122
  "durationMs",
8101
8123
  "timeToFirstChunkMs",
8124
+ "timeToFirstTokenMs",
8102
8125
  "status"
8103
8126
  ];
8104
8127
  var Parameters = class {
@@ -8127,7 +8150,7 @@ function afterCursor$1(plan, parameters) {
8127
8150
  const equal = [];
8128
8151
  if (cursor.sortValue === null) equal.push(`isNull(${field})`);
8129
8152
  else {
8130
- const numeric = plan.orderBy.field === "timeToFirstChunkMs";
8153
+ const numeric = plan.orderBy.field === "timeToFirstChunkMs" || plan.orderBy.field === "timeToFirstTokenMs";
8131
8154
  const value = parameters.add(cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
8132
8155
  terms.push(`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${value}`);
8133
8156
  if (numeric) terms.push(`isNull(${field})`);
@@ -8223,6 +8246,7 @@ const TRACE_BRANCHES_SPAN_FIELD_COLUMNS = {
8223
8246
  "startedAt",
8224
8247
  "endedAt",
8225
8248
  "timeToFirstChunkMs",
8249
+ "timeToFirstTokenMs",
8226
8250
  "threadId",
8227
8251
  "runId",
8228
8252
  "sessionId",
@@ -8383,12 +8407,13 @@ async function querySpans(client, plan, timeoutMs) {
8383
8407
  const hydrated = page.flatMap((identity) => {
8384
8408
  const row = byIdentity.get(coreStorage.spanQueryIdentityKey(identity));
8385
8409
  if (!row) return [];
8386
- const { inputPreview: _input, outputPreview: _output, errorPreview: _error, timeToFirstChunkMs, ...metadata } = row;
8410
+ const { inputPreview: _input, outputPreview: _output, errorPreview: _error, timeToFirstChunkMs, timeToFirstTokenMs, ...metadata } = row;
8387
8411
  return [{
8388
8412
  ...metadata,
8389
8413
  ...identity,
8390
8414
  durationMs: Number(row.durationMs),
8391
- ...timeToFirstChunkSupported && { timeToFirstChunkMs: timeToFirstChunkMs == null ? null : Number(timeToFirstChunkMs) }
8415
+ ...timeToFirstChunkSupported && { timeToFirstChunkMs: timeToFirstChunkMs == null ? null : Number(timeToFirstChunkMs) },
8416
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: timeToFirstTokenMs == null ? null : Number(timeToFirstTokenMs) }
8392
8417
  }];
8393
8418
  });
8394
8419
  let metrics = [];
@@ -8511,7 +8536,11 @@ function compileClickHouseThreadRowsQuery(plan, strategy = "full") {
8511
8536
  const after = afterCursor(plan, parameters);
8512
8537
  const qualify = plan.timeRange.mode === "traces" ? "s.inWindow > 0" : `s.${plan.timeRange.field} >= ${from} AND s.${plan.timeRange.field} < ${to}`;
8513
8538
  const filtered = predicateFields(plan.where);
8514
- const firstContext = [...contextFields, "timeToFirstChunkMs"].filter((field) => field === "resourceId" || filtered.has(field));
8539
+ const firstContext = [
8540
+ ...contextFields,
8541
+ "timeToFirstChunkMs",
8542
+ "timeToFirstTokenMs"
8543
+ ].filter((field) => field === "resourceId" || filtered.has(field));
8515
8544
  const firstFields = [
8516
8545
  "startedAt",
8517
8546
  "traceId",
@@ -8535,7 +8564,7 @@ function compileClickHouseThreadRowsQuery(plan, strategy = "full") {
8535
8564
  ...firstContext,
8536
8565
  ...filtered.has("status") ? ["status"] : []
8537
8566
  ];
8538
- const rootExpression = (field) => field === "timeToFirstChunkMs" ? "if(spanType = 'agent_run', timeToFirstChunkMs, NULL)" : field === "status" ? "if(isNull(error), 'success', 'error')" : field;
8567
+ const rootExpression = (field) => field === "timeToFirstChunkMs" || field === "timeToFirstTokenMs" ? `if(spanType = 'agent_run', ${field}, NULL)` : field === "status" ? "if(isNull(error), 'success', 'error')" : field;
8539
8568
  const sortedRoots = `roots AS (
8540
8569
  SELECT organizationId, traceId, ${rootDataFields.map((field) => `${rootExpression(field)} AS ${field}`).join(", ")}
8541
8570
  FROM ${TABLE_TRACE_ROOTS} ${scope.length || strategy === "candidates" ? `WHERE ${[...scope, ...strategy === "candidates" ? [candidateTracesFilter] : []].join(" AND ")}` : ""}
@@ -8639,6 +8668,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8639
8668
  for (const batch of batches([...roots.values()])) stored.push(...await run({
8640
8669
  query: `SELECT organizationId, traceId, spanId, startedAt, endedAt,
8641
8670
  ${contextFields.join(", ")}, if(spanType = 'agent_run', timeToFirstChunkMs, NULL) AS timeToFirstChunkMs,
8671
+ if(spanType = 'agent_run', timeToFirstTokenMs, NULL) AS timeToFirstTokenMs,
8642
8672
  if(isNull(error), 'success', 'error') AS status, ${PREVIEW_COLUMNS.join(", ")}
8643
8673
  FROM ${TABLE_TRACE_ROOTS} WHERE ${rootWhere}
8644
8674
  ORDER BY ${ROOT_VERSION_ORDER} LIMIT 1 BY organizationId, traceId, spanId`,
@@ -8672,6 +8702,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8672
8702
  endedAt: iso(row.endedAt),
8673
8703
  traceCount: Number(row.traceCount),
8674
8704
  timeToFirstChunkMs: first.timeToFirstChunkMs,
8705
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: first.timeToFirstTokenMs },
8675
8706
  inputPreview: resolveStoredPreviews(first).inputPreview,
8676
8707
  outputPreview: resolveStoredPreviews(last).outputPreview,
8677
8708
  errorPreview: resolveStoredPreviews(last).errorPreview
@@ -10328,7 +10359,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10328
10359
  "trace-query-tenant-scope",
10329
10360
  "feedback",
10330
10361
  "trace-query-context-ids",
10331
- ...timeToFirstChunkFeatures,
10362
+ ...timingFeatures,
10332
10363
  "trace-query-root-span-fields",
10333
10364
  "trace-query-root-entity-id",
10334
10365
  "trace-aggregate",
@@ -10353,7 +10384,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10353
10384
  "trace-query-tenant-scope",
10354
10385
  "feedback",
10355
10386
  "trace-query-context-ids",
10356
- ...timeToFirstChunkFeatures,
10387
+ ...timingFeatures,
10357
10388
  "trace-query-root-span-fields",
10358
10389
  "trace-query-root-entity-id",
10359
10390
  "trace-aggregate",