@mastra/pg 1.28.1 → 1.30.0-alpha.0

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
@@ -10455,6 +10455,7 @@ function dedupeMessagesForSave(messages) {
10455
10455
  var MemoryPG = class MemoryPG extends MemoryStorage {
10456
10456
  supportsPartialThreadUpdate = true;
10457
10457
  supportsObservationalMemory = true;
10458
+ supportsObservationalMemoryHistorySearch = true;
10458
10459
  /**
10459
10460
  * Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
10460
10461
  * on the timezone-aware `createdAtZ` mirror column (kept in sync by triggers),
@@ -12061,6 +12062,10 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
12061
12062
  const conditions = [`"lookupKey" = $1`];
12062
12063
  const params = [lookupKey];
12063
12064
  let paramIndex = 2;
12065
+ if (options?.recordId !== void 0) {
12066
+ conditions.push(`id = $${paramIndex++}`);
12067
+ params.push(options.recordId);
12068
+ }
12064
12069
  if (options?.from) {
12065
12070
  conditions.push(`"createdAtZ" >= $${paramIndex}`);
12066
12071
  params.push(options.from.toISOString());
@@ -12071,8 +12076,27 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
12071
12076
  params.push(options.to.toISOString());
12072
12077
  paramIndex++;
12073
12078
  }
12079
+ if (options?.groupId !== void 0) {
12080
+ conditions.push(`(strpos("activeObservations", $${paramIndex}) > 0 OR EXISTS (
12081
+ SELECT 1 FROM jsonb_array_elements(
12082
+ CASE WHEN jsonb_typeof("bufferedObservationChunks") = 'array' THEN "bufferedObservationChunks" ELSE '[]'::jsonb END
12083
+ ) AS chunk
12084
+ WHERE strpos(chunk->>'observations', $${paramIndex}) > 0
12085
+ ))`);
12086
+ paramIndex++;
12087
+ params.push(`<observation-group id="${options.groupId}"`);
12088
+ }
12089
+ if (options?.beforeGeneration !== void 0) {
12090
+ conditions.push(`"generationCount" < $${paramIndex++}`);
12091
+ params.push(options.beforeGeneration);
12092
+ }
12093
+ if (options?.afterGeneration !== void 0) {
12094
+ conditions.push(`"generationCount" > $${paramIndex++}`);
12095
+ params.push(options.afterGeneration);
12096
+ }
12097
+ const order = options?.sortDirection === "ASC" ? `"generationCount" ASC, "createdAt" ASC, id ASC` : OM_GENERATION_ORDER;
12074
12098
  params.push(limit);
12075
- let sql = `SELECT * FROM ${tableName} WHERE ${conditions.join(" AND ")} ORDER BY ${OM_GENERATION_ORDER} LIMIT $${paramIndex}`;
12099
+ let sql = `SELECT * FROM ${tableName} WHERE ${conditions.join(" AND ")} ORDER BY ${order} LIMIT $${paramIndex}`;
12076
12100
  paramIndex++;
12077
12101
  if (options?.offset != null) {
12078
12102
  params.push(options.offset);
@@ -17460,6 +17484,16 @@ function fieldSql(registry, field) {
17460
17484
  if (sql === void 0) throw new Error(`Unsupported trusted trace-query field: ${field}`);
17461
17485
  return sql;
17462
17486
  }
17487
+ /**
17488
+ * Value of trace-root `metadata.<key>` (alias `r`) as filtering, discovery, and aggregate grouping
17489
+ * all see it: the normalized search value, falling back to the trimmed raw string.
17490
+ */
17491
+ function traceMetadataValueSql(keyParameter) {
17492
+ return `COALESCE(
17493
+ CASE WHEN jsonb_typeof(r."metadataSearch" -> ${keyParameter}) = 'string' THEN r."metadataSearch" ->> ${keyParameter} END,
17494
+ CASE WHEN jsonb_typeof(r."metadataRaw" -> ${keyParameter}) = 'string' THEN NULLIF(btrim(r."metadataRaw" ->> ${keyParameter}), '') END
17495
+ )`;
17496
+ }
17463
17497
  function isMetadataField(field) {
17464
17498
  return field.startsWith("metadata.");
17465
17499
  }
@@ -17489,11 +17523,7 @@ function compileScalarPredicate(predicate, registry, parameterOffset, allowMetad
17489
17523
  let fieldValues = [];
17490
17524
  if (isMetadataField(predicate.field)) {
17491
17525
  if (!allowMetadata) throw new Error(`Unsupported trusted trace-query field: ${predicate.field}`);
17492
- const keyParameter = `$${parameterOffset++}`;
17493
- field = `COALESCE(
17494
- CASE WHEN jsonb_typeof(r."metadataSearch" -> ${keyParameter}) = 'string' THEN r."metadataSearch" ->> ${keyParameter} END,
17495
- CASE WHEN jsonb_typeof(r."metadataRaw" -> ${keyParameter}) = 'string' THEN NULLIF(btrim(r."metadataRaw" ->> ${keyParameter}), '') END
17496
- )`;
17526
+ field = traceMetadataValueSql(`$${parameterOffset++}`);
17497
17527
  fieldValues = [predicate.field.slice(9)];
17498
17528
  } else field = fieldSql(registry, predicate.field);
17499
17529
  if (predicate.type === "presence") return {
@@ -17613,6 +17643,54 @@ function collectThreadRelationCollections(predicate, collections) {
17613
17643
  else collectThreadRelationCollections(predicate.arg, collections);
17614
17644
  return collections;
17615
17645
  }
17646
+ const ROOT_PUSHDOWN_FIELDS = /* @__PURE__ */ new Set([
17647
+ "traceId",
17648
+ "threadId",
17649
+ "resourceId",
17650
+ "runId",
17651
+ "sessionId",
17652
+ "userId",
17653
+ "organizationId",
17654
+ "startedAt",
17655
+ "endedAt",
17656
+ "entityName",
17657
+ "entityType",
17658
+ "environment"
17659
+ ]);
17660
+ function isRootPushdownPredicate(predicate) {
17661
+ if (predicate.type === "relation") return false;
17662
+ if (predicate.type === "boolean") return predicate.args.every(isRootPushdownPredicate);
17663
+ if (predicate.type === "not") return isRootPushdownPredicate(predicate.arg);
17664
+ return ROOT_PUSHDOWN_FIELDS.has(predicate.field);
17665
+ }
17666
+ /**
17667
+ * Splits `where` into top-level AND conjuncts that can filter `root_scope` directly and the rest,
17668
+ * which is applied in `candidates`. The latest-root check compares against the unfiltered span
17669
+ * table, so filtering a root earlier cannot change which root is current, but it stops long windows
17670
+ * from materializing every root (and running the latest-root check on it) before filtering.
17671
+ */
17672
+ function splitRootPredicate(predicate) {
17673
+ if (!predicate) return {
17674
+ root: [],
17675
+ rest: []
17676
+ };
17677
+ const conjuncts = predicate.type === "boolean" && predicate.operator === "and" ? predicate.args : [predicate];
17678
+ const root = [];
17679
+ const rest = [];
17680
+ for (const conjunct of conjuncts) (isRootPushdownPredicate(conjunct) ? root : rest).push(conjunct);
17681
+ return {
17682
+ root,
17683
+ rest
17684
+ };
17685
+ }
17686
+ function compileConjunction(predicates, values) {
17687
+ if (predicates.length === 0) return "TRUE";
17688
+ return predicates.map((predicate) => {
17689
+ const compiled = compilePredicate(predicate, values.length + 1);
17690
+ values.push(...compiled.values);
17691
+ return `(${compiled.sql})`;
17692
+ }).join(" AND ");
17693
+ }
17616
17694
  function compilePredicate(predicate, parameterOffset) {
17617
17695
  if (predicate.type === "relation") {
17618
17696
  const compiled = predicate.collection === "feedback" ? compileFeedbackScalarPredicate(predicate.predicate, parameterOffset) : compileScalarPredicate(predicate.predicate, predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS, parameterOffset);
@@ -17677,7 +17755,7 @@ function compileThreadPredicate(predicate, parameterOffset) {
17677
17755
  values: compiled.values
17678
17756
  };
17679
17757
  }
17680
- function compilePostgresTraceScope(schema, selection, relationCollections, scope, deltaWindow) {
17758
+ function compilePostgresTraceScope(schema, selection, relationCollections, scope, deltaWindow, rootPredicates = []) {
17681
17759
  const spanTable = qualifiedTable(schema, TABLE_SPAN_EVENTS);
17682
17760
  const scoreTable = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17683
17761
  const feedbackTable = qualifiedTable(schema, TABLE_FEEDBACK_EVENTS);
@@ -17697,10 +17775,7 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
17697
17775
  }
17698
17776
  }
17699
17777
  const scopeSql = (alias) => scopeConditions.map((condition) => `\n AND ${alias}.${condition}`).join("");
17700
- const ctes = [`root_scope AS MATERIALIZED (
17701
- SELECT *
17702
- FROM ${spanTable} r
17703
- WHERE ${[
17778
+ const rootConditions = [
17704
17779
  `r."parentSpanId" IS NULL`,
17705
17780
  latestRootPredicate$1(spanTable),
17706
17781
  `NOT r."isPending"`,
@@ -17709,7 +17784,12 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
17709
17784
  `r."startedAt" < $2`,
17710
17785
  ...deltaConditions,
17711
17786
  ...scopeConditions.map((condition) => `r.${condition}`)
17712
- ].join("\n AND ")}
17787
+ ];
17788
+ if (rootPredicates.length > 0) rootConditions.push(compileConjunction(rootPredicates, values));
17789
+ const ctes = [`root_scope AS MATERIALIZED (
17790
+ SELECT *
17791
+ FROM ${spanTable} r
17792
+ WHERE ${rootConditions.join("\n AND ")}
17713
17793
  )`];
17714
17794
  if (relationCollections.has("spans")) ctes.push(`current_spans AS MATERIALIZED (
17715
17795
  SELECT
@@ -17782,8 +17862,27 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
17782
17862
  values
17783
17863
  };
17784
17864
  }
17865
+ /**
17866
+ * Builds the CTE chain ending in `candidates`: the completed, current trace roots in the
17867
+ * selection's time range and tenant scope that match its `where` predicate. Trace queries and
17868
+ * trace aggregates both select from this CTE so they always see the same population.
17869
+ */
17870
+ function compilePostgresTraceCandidates(schema, selection, columns, deltaWindow) {
17871
+ const relationCollections = collectRelationCollections(selection.where);
17872
+ const { root, rest } = splitRootPredicate(selection.where);
17873
+ const { ctes, values } = compilePostgresTraceScope(schema, selection, relationCollections, selection.scope, deltaWindow, root);
17874
+ const predicateSql = compileConjunction(rest, values);
17875
+ ctes.push(`candidates AS (
17876
+ SELECT ${columns}
17877
+ FROM root_scope r
17878
+ WHERE ${predicateSql}
17879
+ )`);
17880
+ return {
17881
+ ctes,
17882
+ values
17883
+ };
17884
+ }
17785
17885
  function compilePostgresTraceQuery(schema, plan, mode = "data", safeHorizon) {
17786
- const relationCollections = collectRelationCollections(plan.where);
17787
17886
  let deltaWindow;
17788
17887
  if (plan.paginationMode === "delta") {
17789
17888
  const watermark = coreStorage.getTraceQueryDeltaWatermark(plan, "pg");
@@ -17793,18 +17892,7 @@ function compilePostgresTraceQuery(schema, plan, mode = "data", safeHorizon) {
17793
17892
  safeHorizon
17794
17893
  };
17795
17894
  }
17796
- const { ctes, values } = compilePostgresTraceScope(schema, plan, relationCollections, plan.scope, deltaWindow);
17797
- let predicateSql = "TRUE";
17798
- if (plan.where) {
17799
- const predicate = compilePredicate(plan.where, values.length + 1);
17800
- predicateSql = predicate.sql;
17801
- values.push(...predicate.values);
17802
- }
17803
- ctes.push(`candidates AS (
17804
- SELECT ${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.\"xactId\", r.\"cursorId\"" : ""}
17805
- FROM root_scope r
17806
- WHERE ${predicateSql}
17807
- )`);
17895
+ const { ctes, values } = compilePostgresTraceCandidates(schema, plan, `${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.\"xactId\", r.\"cursorId\"" : ""}`, deltaWindow);
17808
17896
  const candidates = `WITH ${ctes.join(",\n")}`;
17809
17897
  if (plan.paginationMode === "page" && mode === "count") return {
17810
17898
  text: `${candidates}
@@ -17957,11 +18045,7 @@ function compilePostgresTraceQueryValues(schema, plan) {
17957
18045
  let extracted;
17958
18046
  if (plan.predicateScope === "trace" && plan.path === "tags") extracted = `SELECT value FROM (SELECT DISTINCT r."traceId", UNNEST(r."tags") AS value FROM root_scope r) t`;
17959
18047
  else if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) {
17960
- const keyParameter = `$${values.length + 1}`;
17961
- extracted = `SELECT COALESCE(
17962
- CASE WHEN jsonb_typeof(r."metadataSearch" -> ${keyParameter}) = 'string' THEN r."metadataSearch" ->> ${keyParameter} END,
17963
- CASE WHEN jsonb_typeof(r."metadataRaw" -> ${keyParameter}) = 'string' THEN NULLIF(btrim(r."metadataRaw" ->> ${keyParameter}), '') END
17964
- )::text AS value FROM root_scope r`;
18048
+ extracted = `SELECT ${traceMetadataValueSql(`$${values.length + 1}`)}::text AS value FROM root_scope r`;
17965
18049
  values.push(plan.path.slice(9));
17966
18050
  } else extracted = `SELECT ${fieldSql(discoveryRegistry(plan.predicateScope), plan.path)}::text AS value FROM ${discoverySource(plan.predicateScope)}`;
17967
18051
  const searchParameter = values.length + 1;
@@ -18173,6 +18257,187 @@ async function queryThreads(client, schema, plan, timeoutMs) {
18173
18257
  });
18174
18258
  }
18175
18259
  //#endregion
18260
+ //#region src/storage/domains/observability/v-next/trace-aggregate.ts
18261
+ /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
18262
+ const DIMENSION_SQL = {
18263
+ entityType: "r.\"entityType\"",
18264
+ entityName: "r.\"entityName\"",
18265
+ environment: "r.\"environment\"",
18266
+ status: TRACE_STATUS_SQL,
18267
+ serviceName: "r.\"serviceName\"",
18268
+ executionSource: "r.\"executionSource\"",
18269
+ threadId: "r.\"threadId\"",
18270
+ resourceId: "r.\"resourceId\"",
18271
+ userId: "r.\"userId\"",
18272
+ sessionId: "r.\"sessionId\"",
18273
+ organizationId: "r.\"organizationId\"",
18274
+ experimentId: "r.\"experimentId\""
18275
+ };
18276
+ const PERCENTILES = {
18277
+ "duration.p50": .5,
18278
+ "duration.p90": .9,
18279
+ "duration.p95": .95,
18280
+ "duration.p99": .99
18281
+ };
18282
+ const COMPARISON_SQL = {
18283
+ eq: "=",
18284
+ ne: "<>",
18285
+ lt: "<",
18286
+ lte: "<=",
18287
+ gt: ">",
18288
+ gte: ">="
18289
+ };
18290
+ /**
18291
+ * Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
18292
+ *
18293
+ * Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
18294
+ * countDistinct inputs, bucket) so grouping, measures, and joins only reference plain columns.
18295
+ * Without `interval`, groups are filtered, ordered, and limited in one grouped select. With
18296
+ * `interval`, a `ranked` CTE ranks groups on whole-window measures, then only the top `limit`
18297
+ * groups are expanded into per-bucket rows.
18298
+ */
18299
+ function compilePostgresTraceAggregate(schema, plan) {
18300
+ const { ctes, values } = compilePostgresTraceCandidates(schema, plan, "r.*");
18301
+ const param = (value) => {
18302
+ values.push(value);
18303
+ return `$${values.length}`;
18304
+ };
18305
+ const dimensionSql = (field) => {
18306
+ if (field.startsWith("metadata.")) return traceMetadataValueSql(param(field.slice(9)));
18307
+ const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
18308
+ if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
18309
+ return sql;
18310
+ };
18311
+ const distinctSql = (field) => field === "traceId" ? "r.\"traceId\"" : dimensionSql(field);
18312
+ const dimensionColumns = plan.dimensions.map((_, index) => `"d${index}"`);
18313
+ const distinctColumns = /* @__PURE__ */ new Map();
18314
+ const factColumns = plan.dimensions.map((dimension, index) => `(${dimensionSql(dimension)})::text AS "d${index}"`);
18315
+ for (const measure of plan.measures) {
18316
+ if (measure.type !== "countDistinct" || distinctColumns.has(measure.field)) continue;
18317
+ const column = `"cd${distinctColumns.size}"`;
18318
+ distinctColumns.set(measure.field, column);
18319
+ factColumns.push(`(${distinctSql(measure.field)})::text AS ${column}`);
18320
+ }
18321
+ factColumns.push(`(${durationMsSql("r.\"startedAt\"", "r.\"endedAt\"")})::float8 AS "durationMs"`, `r."error" IS NOT NULL AS "isError"`);
18322
+ if (plan.interval !== void 0) {
18323
+ const intervalMs = coreStorage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
18324
+ if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
18325
+ factColumns.push(`to_timestamp(floor(EXTRACT(EPOCH FROM r."startedAt") * 1000 / ${intervalMs}) * ${intervalMs} / 1000.0) AS "bucket"`);
18326
+ }
18327
+ ctes.push(`facts AS (
18328
+ SELECT ${factColumns.join(",\n ")}
18329
+ FROM candidates r
18330
+ )`);
18331
+ const measureSql = (name, alias = "") => {
18332
+ const column = (name) => `${alias}"${name}"`;
18333
+ if (name === "count") return "COUNT(*)::float8";
18334
+ if (name === "errorCount") return `(COUNT(*) FILTER (WHERE ${column("isError")}))::float8`;
18335
+ if (name === "errorRate") return `(COUNT(*) FILTER (WHERE ${column("isError")}))::float8 / COUNT(*)`;
18336
+ if (name === "duration.avg") return `AVG(${column("durationMs")})`;
18337
+ if (name === "duration.min") return `MIN(${column("durationMs")})`;
18338
+ if (name === "duration.max") return `MAX(${column("durationMs")})`;
18339
+ const percentile = PERCENTILES[name];
18340
+ if (percentile !== void 0) return `percentile_cont(${percentile}) WITHIN GROUP (ORDER BY ${column("durationMs")})`;
18341
+ const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
18342
+ const distinctColumn = measure && distinctColumns.get(measure.field);
18343
+ if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
18344
+ return `(COUNT(DISTINCT ${alias}${distinctColumn}))::float8`;
18345
+ };
18346
+ const compileHaving = (predicate) => {
18347
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileHaving(arg)})`).join(predicate.operator === "and" ? " AND " : " OR ");
18348
+ if (predicate.type === "not") return `NOT (${compileHaving(predicate.arg)})`;
18349
+ const measure = measureSql(predicate.measure);
18350
+ if (predicate.type === "membership") {
18351
+ if (predicate.values.length === 0) return predicate.operator === "in" ? "FALSE" : "TRUE";
18352
+ const list = predicate.values.map((value) => `${param(value)}::float8`).join(", ");
18353
+ return `${measure} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${list})`;
18354
+ }
18355
+ const operator = COMPARISON_SQL[predicate.operator];
18356
+ if (operator === void 0) throw new Error(`Unsupported trusted trace-aggregate operator: ${predicate.operator}`);
18357
+ return `${measure} ${operator} ${param(predicate.value)}::float8`;
18358
+ };
18359
+ const havingSql = ["COUNT(*) > 0", ...plan.having ? [`(${compileHaving(plan.having)})`] : []].join(" AND ");
18360
+ const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
18361
+ let orderTarget;
18362
+ if (plan.orderBy.target === "measure") orderTarget = measureSql(plan.orderBy.measure);
18363
+ else {
18364
+ const index = plan.dimensions.indexOf(plan.orderBy.dimension);
18365
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate order dimension: ${plan.orderBy.dimension}`);
18366
+ orderTarget = `${dimensionColumns[index]} COLLATE "C"`;
18367
+ }
18368
+ const orderSql = [`${orderTarget} ${direction} NULLS LAST`, ...dimensionColumns.map((column) => `${column} COLLATE "C" ASC NULLS LAST`)].join(", ");
18369
+ const groupSql = dimensionColumns.length > 0 ? dimensionColumns.join(", ") : "()";
18370
+ const limitParameter = param(plan.limit + 1);
18371
+ if (plan.interval === void 0) {
18372
+ const projected = [...dimensionColumns, ...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS "m${index}"`)];
18373
+ return {
18374
+ text: `WITH ${ctes.join(",\n")}
18375
+ SELECT ${projected.join(", ")}
18376
+ FROM facts
18377
+ GROUP BY ${groupSql}
18378
+ HAVING ${havingSql}
18379
+ ORDER BY ${orderSql}
18380
+ LIMIT ${limitParameter}`,
18381
+ values
18382
+ };
18383
+ }
18384
+ const maxRankParameter = param(plan.limit);
18385
+ ctes.push(`ranked AS (
18386
+ SELECT ${[...dimensionColumns, `row_number() OVER (ORDER BY ${orderSql}) AS "rank"`].join(", ")}
18387
+ FROM facts
18388
+ GROUP BY ${groupSql}
18389
+ HAVING ${havingSql}
18390
+ ORDER BY ${orderSql}
18391
+ LIMIT ${limitParameter}
18392
+ )`);
18393
+ const joinSql = dimensionColumns.length > 0 ? dimensionColumns.map((column) => `COALESCE(f.${column}, '') = COALESCE(g.${column}, '') AND (f.${column} IS NULL) = (g.${column} IS NULL)`).join(" AND ") : "TRUE";
18394
+ const projected = [
18395
+ ...dimensionColumns.map((column) => `f.${column}`),
18396
+ "f.\"bucket\"",
18397
+ ...plan.measures.map((measure, index) => `${measureSql(measure.name, "f.")} AS "m${index}"`),
18398
+ `(SELECT COUNT(*) FROM ranked) > ${maxRankParameter} AS "truncated"`
18399
+ ];
18400
+ return {
18401
+ text: `WITH ${ctes.join(",\n")}
18402
+ SELECT ${projected.join(", ")}
18403
+ FROM facts f
18404
+ JOIN ranked g ON ${joinSql}
18405
+ WHERE g."rank" <= ${maxRankParameter}
18406
+ GROUP BY ${[
18407
+ "g.\"rank\"",
18408
+ ...dimensionColumns.map((column) => `f.${column}`),
18409
+ "f.\"bucket\""
18410
+ ].join(", ")}
18411
+ ORDER BY g."rank" ASC, f."bucket" ASC`,
18412
+ values
18413
+ };
18414
+ }
18415
+ function toIsoTimestamp(value) {
18416
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
18417
+ }
18418
+ async function aggregateTraces(client, schema, plan, timeoutMs) {
18419
+ const compiled = compilePostgresTraceAggregate(schema, plan);
18420
+ const result = await runWithPostgresTraceQueryTimeout(client, timeoutMs, (transaction) => transaction.any(compiled.text, compiled.values));
18421
+ let truncated;
18422
+ let resultRows;
18423
+ if (plan.interval === void 0) {
18424
+ truncated = result.length > plan.limit;
18425
+ resultRows = result.slice(0, plan.limit);
18426
+ } else {
18427
+ truncated = result[0]?.truncated === true;
18428
+ resultRows = result;
18429
+ }
18430
+ return {
18431
+ rows: resultRows.map((row) => {
18432
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
18433
+ if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
18434
+ if (plan.interval !== void 0) shaped.bucket = toIsoTimestamp(row.bucket);
18435
+ return shaped;
18436
+ }),
18437
+ truncated
18438
+ };
18439
+ }
18440
+ //#endregion
18176
18441
  //#region src/storage/domains/observability/v-next/traces.ts
18177
18442
  /**
18178
18443
  * Trace / branch list reads for the v-next Postgres observability domain.
@@ -19074,6 +19339,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19074
19339
  "tag-discovery",
19075
19340
  "metric-discovery",
19076
19341
  "trace-query",
19342
+ "trace-aggregate",
19077
19343
  "trace-query-root-duration",
19078
19344
  "trace-query-discovery",
19079
19345
  "thread-query",
@@ -19092,6 +19358,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19092
19358
  "metric-discovery",
19093
19359
  "delta-polling",
19094
19360
  "trace-query",
19361
+ "trace-aggregate",
19095
19362
  "trace-query-root-duration",
19096
19363
  "trace-query-discovery",
19097
19364
  "thread-query",
@@ -19143,6 +19410,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19143
19410
  async queryTraces(plan) {
19144
19411
  return this.#run("QUERY_TRACES", () => queryTraces(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19145
19412
  }
19413
+ async aggregateTraces(plan) {
19414
+ return this.#run("AGGREGATE_TRACES", () => aggregateTraces(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19415
+ }
19146
19416
  async getTraceQueryObservedFields(plan) {
19147
19417
  return this.#run("GET_TRACE_QUERY_OBSERVED_FIELDS", () => getTraceQueryObservedFields(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19148
19418
  }