@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.
@@ -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.22.1"
6
+ version: "1.23.0-alpha.1"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.22.1",
2
+ "version": "1.23.0-alpha.1",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -6580,6 +6580,10 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6580
6580
  function durationMsSql(startedAt, endedAt) {
6581
6581
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6582
6582
  }
6583
+ /** Top-level metadata string value on a trace root (alias `r`); `key` is a bound parameter placeholder. */
6584
+ function traceMetadataValueSql(key) {
6585
+ return `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
6586
+ }
6583
6587
  const TRACE_FIELDS = {
6584
6588
  traceId: {
6585
6589
  sql: "r.traceId",
@@ -6838,9 +6842,8 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
6838
6842
  if (predicate.type === "not") return `NOT (${compileScalarPredicate(predicate.arg, registry, parameters, allowMetadata)})`;
6839
6843
  const field = isMetadataField(predicate.field) ? (() => {
6840
6844
  if (!allowMetadata) throw new Error(`Unsupported trusted trace-query field: ${predicate.field}`);
6841
- const key = parameters.add(predicate.field.slice(9), "String");
6842
6845
  return {
6843
- sql: `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`,
6846
+ sql: traceMetadataValueSql(parameters.add(predicate.field.slice(9), "String")),
6844
6847
  parameterType: "String"
6845
6848
  };
6846
6849
  })() : fieldDefinition(registry, predicate.field);
@@ -6933,7 +6936,7 @@ function compileTenantScope(scope, parameters) {
6933
6936
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
6934
6937
  return sql;
6935
6938
  }
6936
- function compileClickHouseTraceScope(selection, relationCollections, parameters, scope) {
6939
+ function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = []) {
6937
6940
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
6938
6941
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
6939
6942
  const tenant = compileTenantScope(scope, parameters);
@@ -6943,9 +6946,9 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
6943
6946
  FROM ${TABLE_TRACE_ROOTS}
6944
6947
  WHERE traceId IN (
6945
6948
  SELECT traceId
6946
- FROM ${TABLE_TRACE_ROOTS}
6949
+ FROM ${TABLE_TRACE_ROOTS} r
6947
6950
  WHERE startedAt >= ${from}
6948
- AND startedAt < ${to}${tenant}
6951
+ AND startedAt < ${to}${tenant}${seedConjuncts.map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("")}
6949
6952
  )
6950
6953
  ORDER BY dedupeKey
6951
6954
  LIMIT 1 BY dedupeKey
@@ -7038,16 +7041,25 @@ function parseDeltaWatermark(value) {
7038
7041
  throw new _mastra_core_storage.TraceQueryCursorError("TRACE_QUERY_CURSOR_MALFORMED");
7039
7042
  }
7040
7043
  }
7041
- function compileClickHouseTraceQuery(plan, deltaHead) {
7042
- const parameters = new ParameterBuilder();
7043
- const ctes = compileClickHouseTraceScope(plan, collectRelationCollections(plan.where), parameters, plan.scope);
7044
- const predicate = plan.where ? compilePredicate(plan.where, parameters) : "1";
7044
+ /**
7045
+ * Builds the CTE chain ending in `candidates`: the completed, current trace roots in the
7046
+ * selection's time range and tenant scope that match its `where` predicate. Trace queries and
7047
+ * trace aggregates both select from this CTE so they always see the same population.
7048
+ */
7049
+ function compileClickHouseTraceCandidates(selection, columns, parameters) {
7050
+ const rootConjuncts = selection.where ? (selection.where.type === "boolean" && selection.where.operator === "and" ? selection.where.args : [selection.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : [];
7051
+ const ctes = compileClickHouseTraceScope(selection, collectRelationCollections(selection.where), parameters, selection.scope, rootConjuncts);
7052
+ const predicate = selection.where ? compilePredicate(selection.where, parameters) : "1";
7045
7053
  ctes.push(`candidates AS (
7046
- SELECT ${TRACE_SELECT}
7054
+ SELECT ${columns}
7047
7055
  FROM root_scope r
7048
7056
  WHERE ${predicate}
7049
7057
  )`);
7050
- const candidates = `WITH ${ctes.join(",\n")}`;
7058
+ return ctes;
7059
+ }
7060
+ function compileClickHouseTraceQuery(plan, deltaHead) {
7061
+ const parameters = new ParameterBuilder();
7062
+ const candidates = `WITH ${compileClickHouseTraceCandidates(plan, TRACE_SELECT, parameters).join(",\n")}`;
7051
7063
  if (plan.result === "groups") return {
7052
7064
  query: `${candidates}
7053
7065
  SELECT threadId
@@ -7223,10 +7235,8 @@ function compileClickHouseTraceQueryValues(plan) {
7223
7235
  const parameters = new ParameterBuilder();
7224
7236
  const ctes = compileClickHouseTraceScope(plan, discoveryCollections(plan.predicateScope), parameters, plan.scope);
7225
7237
  let field;
7226
- if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) {
7227
- const key = parameters.add(plan.path.slice(9), "String");
7228
- field = `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
7229
- } else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7238
+ if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) field = traceMetadataValueSql(parameters.add(plan.path.slice(9), "String"));
7239
+ else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7230
7240
  else field = fieldDefinition(discoveryRegistry(plan.predicateScope), plan.path).sql;
7231
7241
  const search = plan.search ? `AND positionCaseInsensitiveUTF8(value, ${parameters.add(plan.search, "String")}) > 0` : "";
7232
7242
  const limit = parameters.add(plan.limit + 1, "UInt64");
@@ -7433,6 +7443,380 @@ async function queryThreads(client, plan, timeoutMs) {
7433
7443
  }) : null }
7434
7444
  });
7435
7445
  }
7446
+ /** Compile a span-row filter with exactly the same rules as trace span predicates. */
7447
+ function compileSpanQueryPredicate(predicate) {
7448
+ const parameters = new ParameterBuilder();
7449
+ return {
7450
+ sql: compileScalarPredicate(predicate, SPAN_FIELDS, parameters),
7451
+ params: parameters.params
7452
+ };
7453
+ }
7454
+ //#endregion
7455
+ //#region src/storage/domains/observability/v-next/span-query.ts
7456
+ const identityFields = [
7457
+ "organizationId",
7458
+ "resourceId",
7459
+ "traceId",
7460
+ "spanId"
7461
+ ];
7462
+ const identities = identityFields.join(", ");
7463
+ const identitySetColumns = "isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''), traceId, spanId";
7464
+ const identitySetKey = `tuple(${identitySetColumns})`;
7465
+ const displayFields = [
7466
+ "parentSpanId",
7467
+ "name",
7468
+ "spanType",
7469
+ "entityType",
7470
+ "entityId",
7471
+ "entityName",
7472
+ "startedAt",
7473
+ "endedAt",
7474
+ "model",
7475
+ "provider",
7476
+ "durationMs",
7477
+ "status"
7478
+ ];
7479
+ var Parameters = class {
7480
+ values = {};
7481
+ add(value, timestamp = false) {
7482
+ const name = `span_${Object.keys(this.values).length}`;
7483
+ this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
7484
+ return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
7485
+ }
7486
+ };
7487
+ function identityValues(row) {
7488
+ return [
7489
+ row.organizationId === null ? 1 : 0,
7490
+ row.organizationId ?? "",
7491
+ row.resourceId === null ? 1 : 0,
7492
+ row.resourceId ?? "",
7493
+ row.traceId,
7494
+ row.spanId
7495
+ ];
7496
+ }
7497
+ function afterCursor(plan, parameters) {
7498
+ if (!plan.cursor) return "1";
7499
+ const cursor = plan.cursor;
7500
+ const timestamp = parameters.add(cursor.sortValue, true);
7501
+ const field = `s.${plan.orderBy.field}`;
7502
+ const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
7503
+ const equal = [`${field} = ${timestamp}`];
7504
+ for (const key of identityFields) {
7505
+ const value = cursor[key];
7506
+ if (value === null) equal.push(`isNull(s.${key})`);
7507
+ else {
7508
+ const parameter = parameters.add(value);
7509
+ terms.push(`(${equal.join(" AND ")} AND (s.${key} > ${parameter} OR isNull(s.${key})))`);
7510
+ equal.push(`s.${key} = ${parameter}`);
7511
+ }
7512
+ }
7513
+ return `(${terms.join(" OR ")})`;
7514
+ }
7515
+ function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
7516
+ if (!predicate) return fields;
7517
+ if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
7518
+ else if (predicate.type === "not") predicateFields(predicate.arg, fields);
7519
+ else fields.add(predicate.field);
7520
+ return fields;
7521
+ }
7522
+ function fieldExpression(field) {
7523
+ if (field === "model" || field === "provider") return `if(JSONType(e.attributes, '${field}') = 'String', JSONExtractString(e.attributes, '${field}'), NULL)`;
7524
+ if (field === "durationMs") return "dateDiff('millisecond', e.startedAt, e.endedAt)";
7525
+ if (field === "status") return "if(isNull(e.error), 'success', 'error')";
7526
+ if (field === "error") return "if(isNull(e.error), NULL, 'error')";
7527
+ return `e.${field}`;
7528
+ }
7529
+ function compileClickHouseSpanQuery(plan) {
7530
+ const parameters = new Parameters();
7531
+ const from = parameters.add(plan.timeRange.from, true);
7532
+ const to = parameters.add(plan.timeRange.to, true);
7533
+ const scope = [];
7534
+ if (plan.scope) {
7535
+ scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
7536
+ if (plan.scope.resourceId !== void 0) scope.push(`resourceId = ${parameters.add(plan.scope.resourceId)}`);
7537
+ }
7538
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where) : {
7539
+ sql: "1",
7540
+ params: {}
7541
+ };
7542
+ const selectionFields = [.../* @__PURE__ */ new Set([
7543
+ "startedAt",
7544
+ "endedAt",
7545
+ ...predicateFields(plan.where)
7546
+ ])].filter((field) => !new Set(identityFields).has(field));
7547
+ const expressions = selectionFields.map(fieldExpression);
7548
+ const after = afterCursor(plan, parameters);
7549
+ return {
7550
+ query: `WITH candidate_ids AS (
7551
+ SELECT ${identitySetKey} AS identity FROM ${TABLE_SPAN_EVENTS}
7552
+ PREWHERE endedAt >= ${from}
7553
+ WHERE startedAt >= ${from} AND startedAt < ${to} ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
7554
+ ), candidates AS (
7555
+ SELECT ${identities}, ${selectionFields.map((field, index) => `${expressions[index]} AS ${field}`).join(", ")}
7556
+ FROM ${TABLE_SPAN_EVENTS} e
7557
+ PREWHERE e.endedAt >= ${from}
7558
+ WHERE ${identitySetKey} IN (SELECT identity FROM candidate_ids) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
7559
+ ORDER BY ${identities}, e.endedAt DESC
7560
+ LIMIT 1 BY ${identities}
7561
+ )
7562
+ SELECT ${[
7563
+ ...identityFields,
7564
+ "startedAt",
7565
+ "endedAt"
7566
+ ].map((field) => `s.${field}`).join(", ")}
7567
+ FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
7568
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
7569
+ LIMIT ${parameters.add(plan.limit + 1)}`,
7570
+ query_params: {
7571
+ ...parameters.values,
7572
+ ...predicate.params
7573
+ }
7574
+ };
7575
+ }
7576
+ function iso(value) {
7577
+ return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
7578
+ }
7579
+ async function querySpans(client, plan, timeoutMs) {
7580
+ const deadline = performance.now() + _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
7581
+ const run = async (compiled) => {
7582
+ const remaining = Math.floor(deadline - performance.now());
7583
+ if (remaining <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
7584
+ return await runWithClickHouseTraceQueryTimeout(client, {
7585
+ timeoutMs: remaining,
7586
+ memoryLimitBytes: 512 * 1024 * 1024
7587
+ }, compiled);
7588
+ };
7589
+ const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
7590
+ ...row,
7591
+ startedAt: iso(row.startedAt),
7592
+ endedAt: iso(row.endedAt)
7593
+ }));
7594
+ const page = selected.slice(0, plan.limit);
7595
+ if (!page.length) return {
7596
+ spans: [],
7597
+ page: { next: null }
7598
+ };
7599
+ const payloadWhere = `tuple(${identitySetColumns}, endedAt) IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC')))}`;
7600
+ const payloads = await run({
7601
+ query: `SELECT ${identities}, ${displayFields.map((field, index) => `data.${index + 1} AS ${field}`).join(", ")},
7602
+ data.${displayFields.length + 1} AS inputPreview, data.${displayFields.length + 2} AS outputPreview
7603
+ FROM (SELECT ${identities}, any(tuple(${displayFields.map(fieldExpression).join(", ")}, substringUTF8(input, 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}), substringUTF8(output, 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}))) AS data
7604
+ FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
7605
+ query_params: { page: page.map((row) => new _clickhouse_client.TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")])) }
7606
+ });
7607
+ const metricWhere = `${identitySetKey} IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String))}`;
7608
+ const metrics = await run({
7609
+ query: `SELECT ${identities}, data.1 AS name, data.2 AS estimatedCost,
7610
+ data.3 AS costUnit, data.4 AS allocation, data.5 AS costError
7611
+ FROM (SELECT ${identities}, metricId, argMax(tuple(name, estimatedCost, costUnit,
7612
+ JSONExtractString(costMetadata, 'allocation'), JSONHas(costMetadata, 'error')), timestamp) AS data
7613
+ FROM ${TABLE_METRIC_EVENTS} WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
7614
+ GROUP BY ${identities}, metricId)
7615
+ LIMIT ${_mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS + 1}`,
7616
+ query_params: { page: page.map((row) => new _clickhouse_client.TupleParam(identityValues(row))) }
7617
+ });
7618
+ if (metrics.length > _mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS) throw new _mastra_core_storage.TraceQueryResourceLimitError();
7619
+ const byIdentity = new Map(payloads.map((row) => [_mastra_core_storage.spanQueryIdentityKey(row), row]));
7620
+ const hydrated = page.flatMap((identity) => {
7621
+ const row = byIdentity.get(_mastra_core_storage.spanQueryIdentityKey(identity));
7622
+ if (!row) return [];
7623
+ const { inputPreview: _input, outputPreview: _output, ...metadata } = row;
7624
+ return [{
7625
+ ...metadata,
7626
+ ...identity,
7627
+ durationMs: Number(row.durationMs)
7628
+ }];
7629
+ });
7630
+ const result = _mastra_core_storage.buildSpanQueryResponse(plan, hydrated, payloads, metrics);
7631
+ const last = page.at(-1);
7632
+ result.page.next = selected.length > plan.limit ? _mastra_core_storage.encodeSpanQueryCursor(plan, {
7633
+ organizationId: last.organizationId,
7634
+ resourceId: last.resourceId,
7635
+ traceId: last.traceId,
7636
+ spanId: last.spanId,
7637
+ sortValue: last[plan.orderBy.field]
7638
+ }) : null;
7639
+ return result;
7640
+ }
7641
+ //#endregion
7642
+ //#region src/storage/domains/observability/v-next/trace-aggregate.ts
7643
+ /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
7644
+ const DIMENSION_SQL = {
7645
+ entityType: "r.entityType",
7646
+ entityName: "r.entityName",
7647
+ environment: "r.environment",
7648
+ status: TRACE_STATUS_SQL,
7649
+ serviceName: "r.serviceName",
7650
+ executionSource: "r.executionSource",
7651
+ threadId: "r.threadId",
7652
+ resourceId: "r.resourceId",
7653
+ userId: "r.userId",
7654
+ sessionId: "r.sessionId",
7655
+ organizationId: "r.organizationId",
7656
+ experimentId: "r.experimentId"
7657
+ };
7658
+ const PERCENTILES = {
7659
+ "duration.p50": .5,
7660
+ "duration.p90": .9,
7661
+ "duration.p95": .95,
7662
+ "duration.p99": .99
7663
+ };
7664
+ const COMPARISON_SQL = {
7665
+ eq: "=",
7666
+ ne: "!=",
7667
+ lt: "<",
7668
+ lte: "<=",
7669
+ gt: ">",
7670
+ gte: ">="
7671
+ };
7672
+ /**
7673
+ * Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
7674
+ *
7675
+ * Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
7676
+ * countDistinct inputs, bucket). Without `interval`, groups are filtered, ordered, and limited in
7677
+ * one grouped select. With `interval`, one `GROUPING SETS` pass computes both the whole-window
7678
+ * group rows and the per-bucket rows: group rows are ranked (having → orderBy), and each bucket
7679
+ * row inherits its group's rank. ClickHouse re-executes a CTE at every reference, so this keeps
7680
+ * the candidate scan to a single pass instead of joining bucket rows back to a ranked CTE.
7681
+ */
7682
+ function compileClickHouseTraceAggregate(plan) {
7683
+ const parameters = new ParameterBuilder();
7684
+ const ctes = compileClickHouseTraceCandidates(plan, "*", parameters);
7685
+ const dimensionSql = (field) => {
7686
+ if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
7687
+ const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
7688
+ if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
7689
+ return sql;
7690
+ };
7691
+ const distinctSql = (field) => field === "traceId" ? "r.traceId" : dimensionSql(field);
7692
+ const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
7693
+ const distinctColumns = /* @__PURE__ */ new Map();
7694
+ const factColumns = plan.dimensions.map((dimension, index) => `CAST(${dimensionSql(dimension)} AS Nullable(String)) AS d${index}`);
7695
+ for (const measure of plan.measures) {
7696
+ if (measure.type !== "countDistinct" || distinctColumns.has(measure.field)) continue;
7697
+ const column = `cd${distinctColumns.size}`;
7698
+ distinctColumns.set(measure.field, column);
7699
+ factColumns.push(`CAST(${distinctSql(measure.field)} AS Nullable(String)) AS ${column}`);
7700
+ }
7701
+ factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `isNotNull(r.error) AS isError`, `cityHash64(r.traceId) AS traceSeed`);
7702
+ if (plan.interval !== void 0) {
7703
+ const intervalMs = _mastra_core_storage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
7704
+ if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
7705
+ factColumns.push(`fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(r.startedAt), ${intervalMs}) * ${intervalMs}, 'UTC') AS bucket`);
7706
+ }
7707
+ ctes.push(`facts AS (
7708
+ SELECT ${factColumns.join(",\n ")}
7709
+ FROM candidates r
7710
+ )`);
7711
+ const measureSql = (name) => {
7712
+ if (name === "count") return "toFloat64(count())";
7713
+ if (name === "errorCount") return "toFloat64(countIf(isError))";
7714
+ if (name === "errorRate") return "countIf(isError) / count()";
7715
+ if (name === "duration.avg") return "avg(durationMs)";
7716
+ if (name === "duration.min") return "min(durationMs)";
7717
+ if (name === "duration.max") return "max(durationMs)";
7718
+ const percentile = PERCENTILES[name];
7719
+ if (percentile !== void 0) return `quantileDeterministic(${percentile})(durationMs, traceSeed)`;
7720
+ const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
7721
+ const distinctColumn = measure && distinctColumns.get(measure.field);
7722
+ if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
7723
+ return `toFloat64(uniqExact(${distinctColumn}))`;
7724
+ };
7725
+ const compileHaving = (predicate) => {
7726
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileHaving(arg)})`).join(predicate.operator === "and" ? " AND " : " OR ");
7727
+ if (predicate.type === "not") return `NOT (${compileHaving(predicate.arg)})`;
7728
+ const measure = measureSql(predicate.measure);
7729
+ if (predicate.type === "membership") {
7730
+ if (predicate.values.length === 0) return predicate.operator === "in" ? "0" : "1";
7731
+ const list = predicate.values.map((value) => parameters.add(value, "Float64")).join(", ");
7732
+ return `${measure} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${list})`;
7733
+ }
7734
+ const operator = COMPARISON_SQL[predicate.operator];
7735
+ if (operator === void 0) throw new Error(`Unsupported trusted trace-aggregate operator: ${predicate.operator}`);
7736
+ return `${measure} ${operator} ${parameters.add(predicate.value, "Float64")}`;
7737
+ };
7738
+ const havingSql = ["count() > 0", ...plan.having ? [`(${compileHaving(plan.having)})`] : []].join(" AND ");
7739
+ const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
7740
+ let orderTarget;
7741
+ if (plan.orderBy.target === "measure") orderTarget = measureSql(plan.orderBy.measure);
7742
+ else {
7743
+ const index = plan.dimensions.indexOf(plan.orderBy.dimension);
7744
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate order dimension: ${plan.orderBy.dimension}`);
7745
+ orderTarget = dimensionColumns[index];
7746
+ }
7747
+ const tiebreakSql = dimensionColumns.map((column) => `${column} ASC NULLS LAST`);
7748
+ const measureColumns = plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`);
7749
+ const measureNames = plan.measures.map((_, index) => `m${index}`);
7750
+ if (plan.interval === void 0) {
7751
+ const limit = parameters.add(plan.limit + 1, "UInt64");
7752
+ return {
7753
+ query: `WITH ${ctes.join(",\n")}
7754
+ SELECT ${[...dimensionColumns, ...measureColumns].join(", ")}
7755
+ FROM facts
7756
+ ${dimensionColumns.length > 0 ? `GROUP BY ${dimensionColumns.join(", ")}\n` : ""}HAVING ${havingSql}
7757
+ ORDER BY ${[`${orderTarget} ${direction} NULLS LAST`, ...tiebreakSql].join(", ")}
7758
+ LIMIT ${limit}`,
7759
+ query_params: parameters.params
7760
+ };
7761
+ }
7762
+ const maxRank = parameters.add(plan.limit, "UInt64");
7763
+ const groupOrderSql = [`__order ${direction} NULLS LAST`, ...tiebreakSql].join(", ");
7764
+ const groupPartition = dimensionColumns.length > 0 ? `PARTITION BY ${dimensionColumns.join(", ")}` : "";
7765
+ ctes.push(`grouped AS (
7766
+ SELECT ${[
7767
+ ...dimensionColumns,
7768
+ "bucket",
7769
+ "grouping(bucket) = 1 AS __isGroup",
7770
+ ...measureColumns,
7771
+ `(${havingSql}) AS __keep`,
7772
+ `${orderTarget} AS __order`
7773
+ ].join(", ")}
7774
+ FROM facts
7775
+ GROUP BY GROUPING SETS ((${[...dimensionColumns, "bucket"].join(", ")}), (${dimensionColumns.join(", ")}))
7776
+ )`, `ranked AS (
7777
+ SELECT *,
7778
+ if(__isGroup AND __keep, row_number() OVER (PARTITION BY __isGroup, __keep ORDER BY ${groupOrderSql}), 0) AS __groupRank,
7779
+ countIf(__isGroup AND __keep) OVER () AS __survivingGroups
7780
+ FROM grouped
7781
+ )`, `expanded AS (
7782
+ SELECT *, maxIf(__groupRank, __isGroup) OVER (${groupPartition}) AS __rank
7783
+ FROM ranked
7784
+ )`);
7785
+ return {
7786
+ query: `WITH ${ctes.join(",\n")}
7787
+ SELECT ${[
7788
+ ...dimensionColumns,
7789
+ "bucket",
7790
+ ...measureNames,
7791
+ `toUInt8(__survivingGroups > ${maxRank}) AS truncated`
7792
+ ].join(", ")}
7793
+ FROM expanded
7794
+ WHERE NOT __isGroup AND __rank BETWEEN 1 AND ${maxRank}
7795
+ ORDER BY __rank ASC, bucket ASC`,
7796
+ query_params: parameters.params
7797
+ };
7798
+ }
7799
+ async function aggregateTraces(client, plan, timeoutMs) {
7800
+ const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
7801
+ let truncated;
7802
+ let resultRows;
7803
+ if (plan.interval === void 0) {
7804
+ truncated = result.length > plan.limit;
7805
+ resultRows = result.slice(0, plan.limit);
7806
+ } else {
7807
+ truncated = Number(result[0]?.truncated ?? 0) === 1;
7808
+ resultRows = result;
7809
+ }
7810
+ return {
7811
+ rows: resultRows.map((row) => {
7812
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
7813
+ if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
7814
+ if (plan.interval !== void 0) shaped.bucket = new Date(row.bucket).toISOString();
7815
+ return shaped;
7816
+ }),
7817
+ truncated
7818
+ };
7819
+ }
7436
7820
  //#endregion
7437
7821
  //#region src/storage/domains/observability/v-next/trace-roots.ts
7438
7822
  /**
@@ -8245,6 +8629,7 @@ function buildBranchCursor(row) {
8245
8629
  }
8246
8630
  //#endregion
8247
8631
  //#region src/storage/domains/observability/v-next/index.ts
8632
+ const spanQueryFeatures = typeof _mastra_core_storage.planSpanQuery === "function" ? ["span-query"] : [];
8248
8633
  function buildSignalMigrationRequiredMessage(args) {
8249
8634
  const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
8250
8635
  return `
@@ -8676,12 +9061,14 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8676
9061
  "tag-discovery",
8677
9062
  "metric-discovery",
8678
9063
  "trace-query",
9064
+ ...spanQueryFeatures,
8679
9065
  "trace-query-root-duration",
8680
9066
  "trace-query-discovery",
8681
9067
  "thread-query",
8682
9068
  "trace-query-tenant-scope",
8683
9069
  "feedback",
8684
- "trace-query-context-ids"
9070
+ "trace-query-context-ids",
9071
+ "trace-aggregate"
8685
9072
  ];
8686
9073
  return [
8687
9074
  "metrics",
@@ -8694,12 +9081,14 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8694
9081
  "metric-discovery",
8695
9082
  "delta-polling",
8696
9083
  "trace-query",
9084
+ ...spanQueryFeatures,
8697
9085
  "trace-query-root-duration",
8698
9086
  "trace-query-discovery",
8699
9087
  "thread-query",
8700
9088
  "trace-query-tenant-scope",
8701
9089
  "feedback",
8702
- "trace-query-context-ids"
9090
+ "trace-query-context-ids",
9091
+ "trace-aggregate"
8703
9092
  ];
8704
9093
  }
8705
9094
  async createSpan(args) {
@@ -8814,6 +9203,18 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8814
9203
  }, error);
8815
9204
  }
8816
9205
  }
9206
+ async querySpans(plan) {
9207
+ try {
9208
+ return await querySpans(this.#client, plan, this.#traceQueryTimeoutMs);
9209
+ } catch (error) {
9210
+ if (error instanceof _mastra_core_error.MastraError || error instanceof _mastra_core_storage.TraceQueryExecutionError || error instanceof _mastra_core_storage.TraceQueryCursorError || error instanceof _mastra_core_storage.TraceQueryResourceLimitError) throw error;
9211
+ throw new _mastra_core_error.MastraError({
9212
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "QUERY_SPANS", "FAILED"),
9213
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
9214
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY
9215
+ }, error);
9216
+ }
9217
+ }
8817
9218
  async queryTraces(plan) {
8818
9219
  try {
8819
9220
  return await queryTraces(this.#client, plan, this.#traceQueryTimeoutMs, this.#deltaCursorStrategy);
@@ -8826,6 +9227,18 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8826
9227
  }, error);
8827
9228
  }
8828
9229
  }
9230
+ async aggregateTraces(plan) {
9231
+ try {
9232
+ return await aggregateTraces(this.#client, plan, this.#traceQueryTimeoutMs);
9233
+ } catch (error) {
9234
+ if (error instanceof _mastra_core_error.MastraError || error instanceof _mastra_core_storage.TraceQueryExecutionError || error instanceof _mastra_core_storage.TraceQueryResourceLimitError) throw error;
9235
+ throw new _mastra_core_error.MastraError({
9236
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "AGGREGATE_TRACES", "FAILED"),
9237
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
9238
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY
9239
+ }, error);
9240
+ }
9241
+ }
8829
9242
  async getTraceQueryObservedFields(plan) {
8830
9243
  try {
8831
9244
  return await getTraceQueryObservedFields(this.#client, plan, this.#traceQueryDiscoveryLimits);