@mastra/clickhouse 1.22.1 → 1.23.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
@@ -1,4 +1,4 @@
1
- import { createClient } from "@clickhouse/client";
1
+ import { TupleParam, createClient } from "@clickhouse/client";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
3
  import * as coreStorage from "@mastra/core/storage";
4
4
  import { BRANCH_SPAN_TYPES, EntityType, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, ObservabilityStorage, SCORERS_SCHEMA, SPAN_SCHEMA, ScoresStorage, TABLE_AGENT_VERSIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_CURSORS, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_TRACES, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowsStorage, buildInputPreview, calculatePagination, computeTraceStatus, createStorageErrorId, getDefaultValue, getSqlType, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, normalizePerPage, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
@@ -6557,6 +6557,10 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6557
6557
  function durationMsSql(startedAt, endedAt) {
6558
6558
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6559
6559
  }
6560
+ /** Top-level metadata string value on a trace root (alias `r`); `key` is a bound parameter placeholder. */
6561
+ function traceMetadataValueSql(key) {
6562
+ return `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
6563
+ }
6560
6564
  const TRACE_FIELDS = {
6561
6565
  traceId: {
6562
6566
  sql: "r.traceId",
@@ -6815,9 +6819,8 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
6815
6819
  if (predicate.type === "not") return `NOT (${compileScalarPredicate(predicate.arg, registry, parameters, allowMetadata)})`;
6816
6820
  const field = isMetadataField(predicate.field) ? (() => {
6817
6821
  if (!allowMetadata) throw new Error(`Unsupported trusted trace-query field: ${predicate.field}`);
6818
- const key = parameters.add(predicate.field.slice(9), "String");
6819
6822
  return {
6820
- sql: `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`,
6823
+ sql: traceMetadataValueSql(parameters.add(predicate.field.slice(9), "String")),
6821
6824
  parameterType: "String"
6822
6825
  };
6823
6826
  })() : fieldDefinition(registry, predicate.field);
@@ -6910,7 +6913,7 @@ function compileTenantScope(scope, parameters) {
6910
6913
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
6911
6914
  return sql;
6912
6915
  }
6913
- function compileClickHouseTraceScope(selection, relationCollections, parameters, scope) {
6916
+ function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = []) {
6914
6917
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
6915
6918
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
6916
6919
  const tenant = compileTenantScope(scope, parameters);
@@ -6920,9 +6923,9 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
6920
6923
  FROM ${TABLE_TRACE_ROOTS}
6921
6924
  WHERE traceId IN (
6922
6925
  SELECT traceId
6923
- FROM ${TABLE_TRACE_ROOTS}
6926
+ FROM ${TABLE_TRACE_ROOTS} r
6924
6927
  WHERE startedAt >= ${from}
6925
- AND startedAt < ${to}${tenant}
6928
+ AND startedAt < ${to}${tenant}${seedConjuncts.map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("")}
6926
6929
  )
6927
6930
  ORDER BY dedupeKey
6928
6931
  LIMIT 1 BY dedupeKey
@@ -7015,16 +7018,25 @@ function parseDeltaWatermark(value) {
7015
7018
  throw new coreStorage.TraceQueryCursorError("TRACE_QUERY_CURSOR_MALFORMED");
7016
7019
  }
7017
7020
  }
7018
- function compileClickHouseTraceQuery(plan, deltaHead) {
7019
- const parameters = new ParameterBuilder();
7020
- const ctes = compileClickHouseTraceScope(plan, collectRelationCollections(plan.where), parameters, plan.scope);
7021
- const predicate = plan.where ? compilePredicate(plan.where, parameters) : "1";
7021
+ /**
7022
+ * Builds the CTE chain ending in `candidates`: the completed, current trace roots in the
7023
+ * selection's time range and tenant scope that match its `where` predicate. Trace queries and
7024
+ * trace aggregates both select from this CTE so they always see the same population.
7025
+ */
7026
+ function compileClickHouseTraceCandidates(selection, columns, parameters) {
7027
+ const rootConjuncts = selection.where ? (selection.where.type === "boolean" && selection.where.operator === "and" ? selection.where.args : [selection.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : [];
7028
+ const ctes = compileClickHouseTraceScope(selection, collectRelationCollections(selection.where), parameters, selection.scope, rootConjuncts);
7029
+ const predicate = selection.where ? compilePredicate(selection.where, parameters) : "1";
7022
7030
  ctes.push(`candidates AS (
7023
- SELECT ${TRACE_SELECT}
7031
+ SELECT ${columns}
7024
7032
  FROM root_scope r
7025
7033
  WHERE ${predicate}
7026
7034
  )`);
7027
- const candidates = `WITH ${ctes.join(",\n")}`;
7035
+ return ctes;
7036
+ }
7037
+ function compileClickHouseTraceQuery(plan, deltaHead) {
7038
+ const parameters = new ParameterBuilder();
7039
+ const candidates = `WITH ${compileClickHouseTraceCandidates(plan, TRACE_SELECT, parameters).join(",\n")}`;
7028
7040
  if (plan.result === "groups") return {
7029
7041
  query: `${candidates}
7030
7042
  SELECT threadId
@@ -7200,10 +7212,8 @@ function compileClickHouseTraceQueryValues(plan) {
7200
7212
  const parameters = new ParameterBuilder();
7201
7213
  const ctes = compileClickHouseTraceScope(plan, discoveryCollections(plan.predicateScope), parameters, plan.scope);
7202
7214
  let field;
7203
- if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) {
7204
- const key = parameters.add(plan.path.slice(9), "String");
7205
- field = `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
7206
- } else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7215
+ if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) field = traceMetadataValueSql(parameters.add(plan.path.slice(9), "String"));
7216
+ else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7207
7217
  else field = fieldDefinition(discoveryRegistry(plan.predicateScope), plan.path).sql;
7208
7218
  const search = plan.search ? `AND positionCaseInsensitiveUTF8(value, ${parameters.add(plan.search, "String")}) > 0` : "";
7209
7219
  const limit = parameters.add(plan.limit + 1, "UInt64");
@@ -7410,6 +7420,380 @@ async function queryThreads(client, plan, timeoutMs) {
7410
7420
  }) : null }
7411
7421
  });
7412
7422
  }
7423
+ /** Compile a span-row filter with exactly the same rules as trace span predicates. */
7424
+ function compileSpanQueryPredicate(predicate) {
7425
+ const parameters = new ParameterBuilder();
7426
+ return {
7427
+ sql: compileScalarPredicate(predicate, SPAN_FIELDS, parameters),
7428
+ params: parameters.params
7429
+ };
7430
+ }
7431
+ //#endregion
7432
+ //#region src/storage/domains/observability/v-next/span-query.ts
7433
+ const identityFields = [
7434
+ "organizationId",
7435
+ "resourceId",
7436
+ "traceId",
7437
+ "spanId"
7438
+ ];
7439
+ const identities = identityFields.join(", ");
7440
+ const identitySetColumns = "isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''), traceId, spanId";
7441
+ const identitySetKey = `tuple(${identitySetColumns})`;
7442
+ const displayFields = [
7443
+ "parentSpanId",
7444
+ "name",
7445
+ "spanType",
7446
+ "entityType",
7447
+ "entityId",
7448
+ "entityName",
7449
+ "startedAt",
7450
+ "endedAt",
7451
+ "model",
7452
+ "provider",
7453
+ "durationMs",
7454
+ "status"
7455
+ ];
7456
+ var Parameters = class {
7457
+ values = {};
7458
+ add(value, timestamp = false) {
7459
+ const name = `span_${Object.keys(this.values).length}`;
7460
+ this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
7461
+ return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
7462
+ }
7463
+ };
7464
+ function identityValues(row) {
7465
+ return [
7466
+ row.organizationId === null ? 1 : 0,
7467
+ row.organizationId ?? "",
7468
+ row.resourceId === null ? 1 : 0,
7469
+ row.resourceId ?? "",
7470
+ row.traceId,
7471
+ row.spanId
7472
+ ];
7473
+ }
7474
+ function afterCursor(plan, parameters) {
7475
+ if (!plan.cursor) return "1";
7476
+ const cursor = plan.cursor;
7477
+ const timestamp = parameters.add(cursor.sortValue, true);
7478
+ const field = `s.${plan.orderBy.field}`;
7479
+ const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
7480
+ const equal = [`${field} = ${timestamp}`];
7481
+ for (const key of identityFields) {
7482
+ const value = cursor[key];
7483
+ if (value === null) equal.push(`isNull(s.${key})`);
7484
+ else {
7485
+ const parameter = parameters.add(value);
7486
+ terms.push(`(${equal.join(" AND ")} AND (s.${key} > ${parameter} OR isNull(s.${key})))`);
7487
+ equal.push(`s.${key} = ${parameter}`);
7488
+ }
7489
+ }
7490
+ return `(${terms.join(" OR ")})`;
7491
+ }
7492
+ function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
7493
+ if (!predicate) return fields;
7494
+ if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
7495
+ else if (predicate.type === "not") predicateFields(predicate.arg, fields);
7496
+ else fields.add(predicate.field);
7497
+ return fields;
7498
+ }
7499
+ function fieldExpression(field) {
7500
+ if (field === "model" || field === "provider") return `if(JSONType(e.attributes, '${field}') = 'String', JSONExtractString(e.attributes, '${field}'), NULL)`;
7501
+ if (field === "durationMs") return "dateDiff('millisecond', e.startedAt, e.endedAt)";
7502
+ if (field === "status") return "if(isNull(e.error), 'success', 'error')";
7503
+ if (field === "error") return "if(isNull(e.error), NULL, 'error')";
7504
+ return `e.${field}`;
7505
+ }
7506
+ function compileClickHouseSpanQuery(plan) {
7507
+ const parameters = new Parameters();
7508
+ const from = parameters.add(plan.timeRange.from, true);
7509
+ const to = parameters.add(plan.timeRange.to, true);
7510
+ const scope = [];
7511
+ if (plan.scope) {
7512
+ scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
7513
+ if (plan.scope.resourceId !== void 0) scope.push(`resourceId = ${parameters.add(plan.scope.resourceId)}`);
7514
+ }
7515
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where) : {
7516
+ sql: "1",
7517
+ params: {}
7518
+ };
7519
+ const selectionFields = [.../* @__PURE__ */ new Set([
7520
+ "startedAt",
7521
+ "endedAt",
7522
+ ...predicateFields(plan.where)
7523
+ ])].filter((field) => !new Set(identityFields).has(field));
7524
+ const expressions = selectionFields.map(fieldExpression);
7525
+ const after = afterCursor(plan, parameters);
7526
+ return {
7527
+ query: `WITH candidate_ids AS (
7528
+ SELECT ${identitySetKey} AS identity FROM ${TABLE_SPAN_EVENTS}
7529
+ PREWHERE endedAt >= ${from}
7530
+ WHERE startedAt >= ${from} AND startedAt < ${to} ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
7531
+ ), candidates AS (
7532
+ SELECT ${identities}, ${selectionFields.map((field, index) => `${expressions[index]} AS ${field}`).join(", ")}
7533
+ FROM ${TABLE_SPAN_EVENTS} e
7534
+ PREWHERE e.endedAt >= ${from}
7535
+ WHERE ${identitySetKey} IN (SELECT identity FROM candidate_ids) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
7536
+ ORDER BY ${identities}, e.endedAt DESC
7537
+ LIMIT 1 BY ${identities}
7538
+ )
7539
+ SELECT ${[
7540
+ ...identityFields,
7541
+ "startedAt",
7542
+ "endedAt"
7543
+ ].map((field) => `s.${field}`).join(", ")}
7544
+ FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
7545
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
7546
+ LIMIT ${parameters.add(plan.limit + 1)}`,
7547
+ query_params: {
7548
+ ...parameters.values,
7549
+ ...predicate.params
7550
+ }
7551
+ };
7552
+ }
7553
+ function iso(value) {
7554
+ return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
7555
+ }
7556
+ async function querySpans(client, plan, timeoutMs) {
7557
+ const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
7558
+ const run = async (compiled) => {
7559
+ const remaining = Math.floor(deadline - performance.now());
7560
+ if (remaining <= 0) throw new coreStorage.TraceQueryExecutionError();
7561
+ return await runWithClickHouseTraceQueryTimeout(client, {
7562
+ timeoutMs: remaining,
7563
+ memoryLimitBytes: 512 * 1024 * 1024
7564
+ }, compiled);
7565
+ };
7566
+ const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
7567
+ ...row,
7568
+ startedAt: iso(row.startedAt),
7569
+ endedAt: iso(row.endedAt)
7570
+ }));
7571
+ const page = selected.slice(0, plan.limit);
7572
+ if (!page.length) return {
7573
+ spans: [],
7574
+ page: { next: null }
7575
+ };
7576
+ const payloadWhere = `tuple(${identitySetColumns}, endedAt) IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC')))}`;
7577
+ const payloads = await run({
7578
+ query: `SELECT ${identities}, ${displayFields.map((field, index) => `data.${index + 1} AS ${field}`).join(", ")},
7579
+ data.${displayFields.length + 1} AS inputPreview, data.${displayFields.length + 2} AS outputPreview
7580
+ FROM (SELECT ${identities}, any(tuple(${displayFields.map(fieldExpression).join(", ")}, substringUTF8(input, 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}), substringUTF8(output, 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}))) AS data
7581
+ FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
7582
+ query_params: { page: page.map((row) => new TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")])) }
7583
+ });
7584
+ const metricWhere = `${identitySetKey} IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String))}`;
7585
+ const metrics = await run({
7586
+ query: `SELECT ${identities}, data.1 AS name, data.2 AS estimatedCost,
7587
+ data.3 AS costUnit, data.4 AS allocation, data.5 AS costError
7588
+ FROM (SELECT ${identities}, metricId, argMax(tuple(name, estimatedCost, costUnit,
7589
+ JSONExtractString(costMetadata, 'allocation'), JSONHas(costMetadata, 'error')), timestamp) AS data
7590
+ FROM ${TABLE_METRIC_EVENTS} WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
7591
+ GROUP BY ${identities}, metricId)
7592
+ LIMIT ${coreStorage.SPAN_QUERY_MAX_COST_METRICS + 1}`,
7593
+ query_params: { page: page.map((row) => new TupleParam(identityValues(row))) }
7594
+ });
7595
+ if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
7596
+ const byIdentity = new Map(payloads.map((row) => [coreStorage.spanQueryIdentityKey(row), row]));
7597
+ const hydrated = page.flatMap((identity) => {
7598
+ const row = byIdentity.get(coreStorage.spanQueryIdentityKey(identity));
7599
+ if (!row) return [];
7600
+ const { inputPreview: _input, outputPreview: _output, ...metadata } = row;
7601
+ return [{
7602
+ ...metadata,
7603
+ ...identity,
7604
+ durationMs: Number(row.durationMs)
7605
+ }];
7606
+ });
7607
+ const result = coreStorage.buildSpanQueryResponse(plan, hydrated, payloads, metrics);
7608
+ const last = page.at(-1);
7609
+ result.page.next = selected.length > plan.limit ? coreStorage.encodeSpanQueryCursor(plan, {
7610
+ organizationId: last.organizationId,
7611
+ resourceId: last.resourceId,
7612
+ traceId: last.traceId,
7613
+ spanId: last.spanId,
7614
+ sortValue: last[plan.orderBy.field]
7615
+ }) : null;
7616
+ return result;
7617
+ }
7618
+ //#endregion
7619
+ //#region src/storage/domains/observability/v-next/trace-aggregate.ts
7620
+ /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
7621
+ const DIMENSION_SQL = {
7622
+ entityType: "r.entityType",
7623
+ entityName: "r.entityName",
7624
+ environment: "r.environment",
7625
+ status: TRACE_STATUS_SQL,
7626
+ serviceName: "r.serviceName",
7627
+ executionSource: "r.executionSource",
7628
+ threadId: "r.threadId",
7629
+ resourceId: "r.resourceId",
7630
+ userId: "r.userId",
7631
+ sessionId: "r.sessionId",
7632
+ organizationId: "r.organizationId",
7633
+ experimentId: "r.experimentId"
7634
+ };
7635
+ const PERCENTILES = {
7636
+ "duration.p50": .5,
7637
+ "duration.p90": .9,
7638
+ "duration.p95": .95,
7639
+ "duration.p99": .99
7640
+ };
7641
+ const COMPARISON_SQL = {
7642
+ eq: "=",
7643
+ ne: "!=",
7644
+ lt: "<",
7645
+ lte: "<=",
7646
+ gt: ">",
7647
+ gte: ">="
7648
+ };
7649
+ /**
7650
+ * Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
7651
+ *
7652
+ * Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
7653
+ * countDistinct inputs, bucket). Without `interval`, groups are filtered, ordered, and limited in
7654
+ * one grouped select. With `interval`, one `GROUPING SETS` pass computes both the whole-window
7655
+ * group rows and the per-bucket rows: group rows are ranked (having → orderBy), and each bucket
7656
+ * row inherits its group's rank. ClickHouse re-executes a CTE at every reference, so this keeps
7657
+ * the candidate scan to a single pass instead of joining bucket rows back to a ranked CTE.
7658
+ */
7659
+ function compileClickHouseTraceAggregate(plan) {
7660
+ const parameters = new ParameterBuilder();
7661
+ const ctes = compileClickHouseTraceCandidates(plan, "*", parameters);
7662
+ const dimensionSql = (field) => {
7663
+ if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
7664
+ const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
7665
+ if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
7666
+ return sql;
7667
+ };
7668
+ const distinctSql = (field) => field === "traceId" ? "r.traceId" : dimensionSql(field);
7669
+ const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
7670
+ const distinctColumns = /* @__PURE__ */ new Map();
7671
+ const factColumns = plan.dimensions.map((dimension, index) => `CAST(${dimensionSql(dimension)} AS Nullable(String)) AS d${index}`);
7672
+ for (const measure of plan.measures) {
7673
+ if (measure.type !== "countDistinct" || distinctColumns.has(measure.field)) continue;
7674
+ const column = `cd${distinctColumns.size}`;
7675
+ distinctColumns.set(measure.field, column);
7676
+ factColumns.push(`CAST(${distinctSql(measure.field)} AS Nullable(String)) AS ${column}`);
7677
+ }
7678
+ factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `isNotNull(r.error) AS isError`, `cityHash64(r.traceId) AS traceSeed`);
7679
+ if (plan.interval !== void 0) {
7680
+ const intervalMs = coreStorage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
7681
+ if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
7682
+ factColumns.push(`fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(r.startedAt), ${intervalMs}) * ${intervalMs}, 'UTC') AS bucket`);
7683
+ }
7684
+ ctes.push(`facts AS (
7685
+ SELECT ${factColumns.join(",\n ")}
7686
+ FROM candidates r
7687
+ )`);
7688
+ const measureSql = (name) => {
7689
+ if (name === "count") return "toFloat64(count())";
7690
+ if (name === "errorCount") return "toFloat64(countIf(isError))";
7691
+ if (name === "errorRate") return "countIf(isError) / count()";
7692
+ if (name === "duration.avg") return "avg(durationMs)";
7693
+ if (name === "duration.min") return "min(durationMs)";
7694
+ if (name === "duration.max") return "max(durationMs)";
7695
+ const percentile = PERCENTILES[name];
7696
+ if (percentile !== void 0) return `quantileDeterministic(${percentile})(durationMs, traceSeed)`;
7697
+ const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
7698
+ const distinctColumn = measure && distinctColumns.get(measure.field);
7699
+ if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
7700
+ return `toFloat64(uniqExact(${distinctColumn}))`;
7701
+ };
7702
+ const compileHaving = (predicate) => {
7703
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileHaving(arg)})`).join(predicate.operator === "and" ? " AND " : " OR ");
7704
+ if (predicate.type === "not") return `NOT (${compileHaving(predicate.arg)})`;
7705
+ const measure = measureSql(predicate.measure);
7706
+ if (predicate.type === "membership") {
7707
+ if (predicate.values.length === 0) return predicate.operator === "in" ? "0" : "1";
7708
+ const list = predicate.values.map((value) => parameters.add(value, "Float64")).join(", ");
7709
+ return `${measure} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${list})`;
7710
+ }
7711
+ const operator = COMPARISON_SQL[predicate.operator];
7712
+ if (operator === void 0) throw new Error(`Unsupported trusted trace-aggregate operator: ${predicate.operator}`);
7713
+ return `${measure} ${operator} ${parameters.add(predicate.value, "Float64")}`;
7714
+ };
7715
+ const havingSql = ["count() > 0", ...plan.having ? [`(${compileHaving(plan.having)})`] : []].join(" AND ");
7716
+ const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
7717
+ let orderTarget;
7718
+ if (plan.orderBy.target === "measure") orderTarget = measureSql(plan.orderBy.measure);
7719
+ else {
7720
+ const index = plan.dimensions.indexOf(plan.orderBy.dimension);
7721
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate order dimension: ${plan.orderBy.dimension}`);
7722
+ orderTarget = dimensionColumns[index];
7723
+ }
7724
+ const tiebreakSql = dimensionColumns.map((column) => `${column} ASC NULLS LAST`);
7725
+ const measureColumns = plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`);
7726
+ const measureNames = plan.measures.map((_, index) => `m${index}`);
7727
+ if (plan.interval === void 0) {
7728
+ const limit = parameters.add(plan.limit + 1, "UInt64");
7729
+ return {
7730
+ query: `WITH ${ctes.join(",\n")}
7731
+ SELECT ${[...dimensionColumns, ...measureColumns].join(", ")}
7732
+ FROM facts
7733
+ ${dimensionColumns.length > 0 ? `GROUP BY ${dimensionColumns.join(", ")}\n` : ""}HAVING ${havingSql}
7734
+ ORDER BY ${[`${orderTarget} ${direction} NULLS LAST`, ...tiebreakSql].join(", ")}
7735
+ LIMIT ${limit}`,
7736
+ query_params: parameters.params
7737
+ };
7738
+ }
7739
+ const maxRank = parameters.add(plan.limit, "UInt64");
7740
+ const groupOrderSql = [`__order ${direction} NULLS LAST`, ...tiebreakSql].join(", ");
7741
+ const groupPartition = dimensionColumns.length > 0 ? `PARTITION BY ${dimensionColumns.join(", ")}` : "";
7742
+ ctes.push(`grouped AS (
7743
+ SELECT ${[
7744
+ ...dimensionColumns,
7745
+ "bucket",
7746
+ "grouping(bucket) = 1 AS __isGroup",
7747
+ ...measureColumns,
7748
+ `(${havingSql}) AS __keep`,
7749
+ `${orderTarget} AS __order`
7750
+ ].join(", ")}
7751
+ FROM facts
7752
+ GROUP BY GROUPING SETS ((${[...dimensionColumns, "bucket"].join(", ")}), (${dimensionColumns.join(", ")}))
7753
+ )`, `ranked AS (
7754
+ SELECT *,
7755
+ if(__isGroup AND __keep, row_number() OVER (PARTITION BY __isGroup, __keep ORDER BY ${groupOrderSql}), 0) AS __groupRank,
7756
+ countIf(__isGroup AND __keep) OVER () AS __survivingGroups
7757
+ FROM grouped
7758
+ )`, `expanded AS (
7759
+ SELECT *, maxIf(__groupRank, __isGroup) OVER (${groupPartition}) AS __rank
7760
+ FROM ranked
7761
+ )`);
7762
+ return {
7763
+ query: `WITH ${ctes.join(",\n")}
7764
+ SELECT ${[
7765
+ ...dimensionColumns,
7766
+ "bucket",
7767
+ ...measureNames,
7768
+ `toUInt8(__survivingGroups > ${maxRank}) AS truncated`
7769
+ ].join(", ")}
7770
+ FROM expanded
7771
+ WHERE NOT __isGroup AND __rank BETWEEN 1 AND ${maxRank}
7772
+ ORDER BY __rank ASC, bucket ASC`,
7773
+ query_params: parameters.params
7774
+ };
7775
+ }
7776
+ async function aggregateTraces(client, plan, timeoutMs) {
7777
+ const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
7778
+ let truncated;
7779
+ let resultRows;
7780
+ if (plan.interval === void 0) {
7781
+ truncated = result.length > plan.limit;
7782
+ resultRows = result.slice(0, plan.limit);
7783
+ } else {
7784
+ truncated = Number(result[0]?.truncated ?? 0) === 1;
7785
+ resultRows = result;
7786
+ }
7787
+ return {
7788
+ rows: resultRows.map((row) => {
7789
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
7790
+ if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
7791
+ if (plan.interval !== void 0) shaped.bucket = new Date(row.bucket).toISOString();
7792
+ return shaped;
7793
+ }),
7794
+ truncated
7795
+ };
7796
+ }
7413
7797
  //#endregion
7414
7798
  //#region src/storage/domains/observability/v-next/trace-roots.ts
7415
7799
  /**
@@ -8222,6 +8606,7 @@ function buildBranchCursor(row) {
8222
8606
  }
8223
8607
  //#endregion
8224
8608
  //#region src/storage/domains/observability/v-next/index.ts
8609
+ const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? ["span-query"] : [];
8225
8610
  function buildSignalMigrationRequiredMessage(args) {
8226
8611
  const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
8227
8612
  return `
@@ -8653,12 +9038,14 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
8653
9038
  "tag-discovery",
8654
9039
  "metric-discovery",
8655
9040
  "trace-query",
9041
+ ...spanQueryFeatures,
8656
9042
  "trace-query-root-duration",
8657
9043
  "trace-query-discovery",
8658
9044
  "thread-query",
8659
9045
  "trace-query-tenant-scope",
8660
9046
  "feedback",
8661
- "trace-query-context-ids"
9047
+ "trace-query-context-ids",
9048
+ "trace-aggregate"
8662
9049
  ];
8663
9050
  return [
8664
9051
  "metrics",
@@ -8671,12 +9058,14 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
8671
9058
  "metric-discovery",
8672
9059
  "delta-polling",
8673
9060
  "trace-query",
9061
+ ...spanQueryFeatures,
8674
9062
  "trace-query-root-duration",
8675
9063
  "trace-query-discovery",
8676
9064
  "thread-query",
8677
9065
  "trace-query-tenant-scope",
8678
9066
  "feedback",
8679
- "trace-query-context-ids"
9067
+ "trace-query-context-ids",
9068
+ "trace-aggregate"
8680
9069
  ];
8681
9070
  }
8682
9071
  async createSpan(args) {
@@ -8791,6 +9180,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
8791
9180
  }, error);
8792
9181
  }
8793
9182
  }
9183
+ async querySpans(plan) {
9184
+ try {
9185
+ return await querySpans(this.#client, plan, this.#traceQueryTimeoutMs);
9186
+ } catch (error) {
9187
+ if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryCursorError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
9188
+ throw new MastraError({
9189
+ id: createStorageErrorId("CLICKHOUSE", "QUERY_SPANS", "FAILED"),
9190
+ domain: ErrorDomain.STORAGE,
9191
+ category: ErrorCategory.THIRD_PARTY
9192
+ }, error);
9193
+ }
9194
+ }
8794
9195
  async queryTraces(plan) {
8795
9196
  try {
8796
9197
  return await queryTraces(this.#client, plan, this.#traceQueryTimeoutMs, this.#deltaCursorStrategy);
@@ -8803,6 +9204,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
8803
9204
  }, error);
8804
9205
  }
8805
9206
  }
9207
+ async aggregateTraces(plan) {
9208
+ try {
9209
+ return await aggregateTraces(this.#client, plan, this.#traceQueryTimeoutMs);
9210
+ } catch (error) {
9211
+ if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
9212
+ throw new MastraError({
9213
+ id: createStorageErrorId("CLICKHOUSE", "AGGREGATE_TRACES", "FAILED"),
9214
+ domain: ErrorDomain.STORAGE,
9215
+ category: ErrorCategory.THIRD_PARTY
9216
+ }, error);
9217
+ }
9218
+ }
8806
9219
  async getTraceQueryObservedFields(plan) {
8807
9220
  try {
8808
9221
  return await getTraceQueryObservedFields(this.#client, plan, this.#traceQueryDiscoveryLimits);