@mastra/pg 1.30.0-alpha.1 → 1.30.0-alpha.3

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
@@ -18256,6 +18256,166 @@ async function queryThreads(client, schema, plan, timeoutMs) {
18256
18256
  }) : null }
18257
18257
  });
18258
18258
  }
18259
+ /** Compile a span-row filter with exactly the same rules as trace span predicates. */
18260
+ function compileSpanQueryPredicate(predicate, offset) {
18261
+ return compileScalarPredicate(predicate, SPAN_FIELDS, offset);
18262
+ }
18263
+ //#endregion
18264
+ //#region src/storage/domains/observability/v-next/span-query.ts
18265
+ const identityFields = [
18266
+ "organizationId",
18267
+ "resourceId",
18268
+ "traceId",
18269
+ "spanId"
18270
+ ];
18271
+ const fields = [
18272
+ ...identityFields,
18273
+ "parentSpanId",
18274
+ "name",
18275
+ "spanType",
18276
+ "entityType",
18277
+ "entityId",
18278
+ "entityName",
18279
+ "entityVersionId",
18280
+ "parentEntityVersionId",
18281
+ "rootEntityVersionId",
18282
+ "runId",
18283
+ "sessionId",
18284
+ "userId"
18285
+ ];
18286
+ const identities = identityFields.map((field) => `"${field}"`).join(", ");
18287
+ const identityOrder = identityFields.map((field) => `"${field}" COLLATE "C" ASC NULLS LAST`).join(", ");
18288
+ function selectedKeys(rows, values) {
18289
+ return rows.map((row) => `(${identityFields.map((field) => {
18290
+ values.push(row[field]);
18291
+ return `"${field}" ${field === "traceId" || field === "spanId" ? "=" : "IS NOT DISTINCT FROM"} $${values.length}::text`;
18292
+ }).join(" AND ")})`).join(" OR ");
18293
+ }
18294
+ function afterCursor(plan, values) {
18295
+ if (!plan.cursor) return "TRUE";
18296
+ const cursor = plan.cursor;
18297
+ values.push(cursor.sortValue);
18298
+ const timestamp = `$${values.length}::timestamptz`;
18299
+ const field = `s."${plan.orderBy.field}"`;
18300
+ const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
18301
+ const equal = [`${field} = ${timestamp}`];
18302
+ for (const key of identityFields) {
18303
+ const ref = `s."${key}" COLLATE "C"`;
18304
+ const value = cursor[key];
18305
+ if (value === null) equal.push(`${ref} IS NULL`);
18306
+ else {
18307
+ values.push(value);
18308
+ const parameter = `$${values.length}::text COLLATE "C"`;
18309
+ terms.push(`(${equal.join(" AND ")} AND (${ref} > ${parameter} OR ${ref} IS NULL))`);
18310
+ equal.push(`${ref} = ${parameter}`);
18311
+ }
18312
+ }
18313
+ return `(${terms.join(" OR ")})`;
18314
+ }
18315
+ function compilePostgresSpanQuery(schema, plan) {
18316
+ const values = [plan.timeRange.from, plan.timeRange.to];
18317
+ const scope = [];
18318
+ if (plan.scope) {
18319
+ values.push(plan.scope.organizationId);
18320
+ scope.push(`"organizationId" = $${values.length}`);
18321
+ if (plan.scope.resourceId !== void 0) {
18322
+ values.push(plan.scope.resourceId);
18323
+ scope.push(`"resourceId" = $${values.length}`);
18324
+ }
18325
+ }
18326
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where, values.length + 1) : {
18327
+ sql: "TRUE",
18328
+ values: []
18329
+ };
18330
+ values.push(...predicate.values);
18331
+ const after = afterCursor(plan, values);
18332
+ values.push(plan.limit + 1);
18333
+ const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
18334
+ return {
18335
+ text: `WITH candidate_ids AS MATERIALIZED (
18336
+ SELECT DISTINCT ${identities} FROM ${table}
18337
+ WHERE "startedAt" >= $1 AND "startedAt" < $2 AND "endedAt" >= $1 AND NOT "isPending"
18338
+ ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
18339
+ ), current_spans AS MATERIALIZED (
18340
+ SELECT DISTINCT ON (${identities})
18341
+ ${fields.map((field) => `"${field}"`).join(", ")}, "startedAt", "endedAt", "cursorId"::text AS version,
18342
+ CASE WHEN jsonb_typeof(attributes -> 'model') = 'string' THEN attributes ->> 'model' END AS model,
18343
+ CASE WHEN jsonb_typeof(attributes -> 'provider') = 'string' THEN attributes ->> 'provider' END AS provider,
18344
+ (EXTRACT(EPOCH FROM ("endedAt" - "startedAt")) * 1000)::float8 AS "durationMs",
18345
+ CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
18346
+ error IS NOT NULL AS "hasError"
18347
+ FROM ${table} e
18348
+ WHERE NOT "isPending" AND "endedAt" >= $1 AND (
18349
+ "organizationId" IS NULL, COALESCE("organizationId", ''), "resourceId" IS NULL, COALESCE("resourceId", ''), "traceId", "spanId"
18350
+ ) IN (
18351
+ SELECT "organizationId" IS NULL, COALESCE("organizationId", ''), "resourceId" IS NULL, COALESCE("resourceId", ''), "traceId", "spanId" FROM candidate_ids
18352
+ ) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
18353
+ ORDER BY ${identities}, "endedAt" DESC, "cursorId" DESC
18354
+ ), candidates AS (
18355
+ SELECT *, CASE WHEN "hasError" THEN 'error' ELSE NULL END AS error FROM current_spans
18356
+ )
18357
+ SELECT ${[
18358
+ ...identityFields,
18359
+ "parentSpanId",
18360
+ "name",
18361
+ "spanType",
18362
+ "entityType",
18363
+ "entityId",
18364
+ "entityName",
18365
+ "model",
18366
+ "provider",
18367
+ "startedAt",
18368
+ "endedAt",
18369
+ "durationMs",
18370
+ "status",
18371
+ "version"
18372
+ ].map((field) => `s."${field}"`).join(", ")}
18373
+ FROM candidates s WHERE s."startedAt" >= $1 AND s."startedAt" < $2 AND (${predicate.sql}) AND ${after}
18374
+ ORDER BY s."${plan.orderBy.field}" ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityOrder}
18375
+ LIMIT $${values.length}`,
18376
+ values
18377
+ };
18378
+ }
18379
+ async function querySpans(client, schema, plan, timeoutMs) {
18380
+ const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
18381
+ return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
18382
+ const query = compilePostgresSpanQuery(schema, plan);
18383
+ const rows = await tx.any(query.text, query.values);
18384
+ const selected = rows.map(({ version: _version, startedAt, endedAt, ...row }) => ({
18385
+ ...row,
18386
+ startedAt: startedAt.toISOString(),
18387
+ endedAt: endedAt.toISOString()
18388
+ }));
18389
+ const page = rows.slice(0, plan.limit);
18390
+ if (!page.length) return coreStorage.buildSpanQueryResponse(plan, selected, [], []);
18391
+ const remaining = async () => {
18392
+ const ms = Math.floor(deadline - performance.now());
18393
+ if (ms <= 0) throw new coreStorage.TraceQueryExecutionError();
18394
+ await tx.query(`SELECT set_config('statement_timeout', $1, true)`, [`${ms}ms`]);
18395
+ };
18396
+ await remaining();
18397
+ const payloadValues = [];
18398
+ const payloadWhere = page.map((row) => {
18399
+ const key = selectedKeys([row], payloadValues);
18400
+ payloadValues.push(row.version);
18401
+ return `(${key} AND "cursorId" = $${payloadValues.length}::bigint)`;
18402
+ }).join(" OR ");
18403
+ const payloads = await tx.any(`SELECT ${identities},
18404
+ left(input::text, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS "inputPreview", left(output::text, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS "outputPreview"
18405
+ FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues);
18406
+ await remaining();
18407
+ const metricValues = [];
18408
+ const metricWhere = selectedKeys(page, metricValues);
18409
+ const metrics = await tx.any(`SELECT ${identities}, name, "estimatedCost", "costUnit",
18410
+ "costMetadata" ->> 'allocation' AS allocation, COALESCE("costMetadata" ? 'error', false) AS "costError"
18411
+ FROM (SELECT DISTINCT ON (${identities}, "metricId") ${identities}, name, "estimatedCost", "costUnit", "costMetadata" FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
18412
+ WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
18413
+ ORDER BY ${identities}, "metricId", "cursorId" DESC) m
18414
+ LIMIT ${coreStorage.SPAN_QUERY_MAX_COST_METRICS + 1}`, metricValues);
18415
+ if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
18416
+ return coreStorage.buildSpanQueryResponse(plan, selected, payloads, metrics);
18417
+ }, { repeatableRead: true });
18418
+ }
18259
18419
  //#endregion
18260
18420
  //#region src/storage/domains/observability/v-next/trace-aggregate.ts
18261
18421
  /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
@@ -18324,13 +18484,32 @@ function compilePostgresTraceAggregate(schema, plan) {
18324
18484
  if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
18325
18485
  factColumns.push(`to_timestamp(floor(EXTRACT(EPOCH FROM r."startedAt") * 1000 / ${intervalMs}) * ${intervalMs} / 1000.0) AS "bucket"`);
18326
18486
  }
18487
+ const measureKinds = new Set(plan.measures.map((measure) => measureRule(measure.name)?.kind));
18488
+ const hasCost = measureKinds.has("cost");
18489
+ let usageJoin = "";
18490
+ if (hasCost || measureKinds.has("tokens")) {
18491
+ ctes.push(...compileUsageCtes(schema, plan, param));
18492
+ usageJoin = "\n LEFT JOIN usage u ON u.\"traceId\" = r.\"traceId\"";
18493
+ USAGE_METRIC_NAMES.forEach((_, index) => factColumns.push(`CASE WHEN u."traceId" IS NOT NULL THEN COALESCE(u."t${index}", 0) END AS "t${index}"`));
18494
+ factColumns.push(`u."traceId" IS NOT NULL AS "usageBearing"`, `CASE WHEN u."priced" THEN u."cost" END AS "traceCost"`, `COALESCE(u."priced" AND NOT u."pricingFailure", FALSE) AS "covered"`, `u."unitMin"`, `u."unitMax"`);
18495
+ }
18327
18496
  ctes.push(`facts AS (
18328
18497
  SELECT ${factColumns.join(",\n ")}
18329
- FROM candidates r
18498
+ FROM candidates r${usageJoin}
18330
18499
  )`);
18500
+ const mixedUnitsSql = (alias) => `MIN(${alias}"unitMin" COLLATE "C") <> MAX(${alias}"unitMax" COLLATE "C")`;
18331
18501
  const measureSql = (name, alias = "") => {
18332
18502
  const column = (name) => `${alias}"${name}"`;
18333
18503
  if (name === "count") return "COUNT(*)::float8";
18504
+ const rule = measureRule(name);
18505
+ if (rule?.kind === "tokens" && rule.metricNames) {
18506
+ const value = rule.metricNames.map((metricName) => column(`t${usageMetricIndex(metricName)}`)).join(" + ");
18507
+ return `(${rule.statistic === "avg" ? "AVG" : "SUM"}(${value}))::float8`;
18508
+ }
18509
+ if (rule?.kind === "cost") {
18510
+ const aggregate = rule.statistic === "avg" ? "AVG" : "SUM";
18511
+ return `(CASE WHEN ${mixedUnitsSql(alias)} THEN NULL ELSE ${aggregate}(${column("traceCost")}) END)::float8`;
18512
+ }
18334
18513
  if (name === "errorCount") return `(COUNT(*) FILTER (WHERE ${column("isError")}))::float8`;
18335
18514
  if (name === "errorRate") return `(COUNT(*) FILTER (WHERE ${column("isError")}))::float8 / COUNT(*)`;
18336
18515
  if (name === "duration.avg") return `AVG(${column("durationMs")})`;
@@ -18368,8 +18547,17 @@ function compilePostgresTraceAggregate(schema, plan) {
18368
18547
  const orderSql = [`${orderTarget} ${direction} NULLS LAST`, ...dimensionColumns.map((column) => `${column} COLLATE "C" ASC NULLS LAST`)].join(", ");
18369
18548
  const groupSql = dimensionColumns.length > 0 ? dimensionColumns.join(", ") : "()";
18370
18549
  const limitParameter = param(plan.limit + 1);
18550
+ const costColumns = (alias) => {
18551
+ if (!hasCost) return [];
18552
+ const mixedUnit = param(coreStorage.TRACE_AGGREGATE_MIXED_COST_UNIT);
18553
+ return [`(COUNT(*) FILTER (WHERE ${alias}"covered"))::float8 / NULLIF(COUNT(*) FILTER (WHERE ${alias}"usageBearing"), 0) AS "costCoverage"`, `CASE WHEN ${mixedUnitsSql(alias)} THEN ${mixedUnit}::text ELSE MIN(${alias}"unitMin" COLLATE "C") END AS "costUnit"`];
18554
+ };
18371
18555
  if (plan.interval === void 0) {
18372
- const projected = [...dimensionColumns, ...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS "m${index}"`)];
18556
+ const projected = [
18557
+ ...dimensionColumns,
18558
+ ...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS "m${index}"`),
18559
+ ...costColumns("")
18560
+ ];
18373
18561
  return {
18374
18562
  text: `WITH ${ctes.join(",\n")}
18375
18563
  SELECT ${projected.join(", ")}
@@ -18395,6 +18583,7 @@ LIMIT ${limitParameter}`,
18395
18583
  ...dimensionColumns.map((column) => `f.${column}`),
18396
18584
  "f.\"bucket\"",
18397
18585
  ...plan.measures.map((measure, index) => `${measureSql(measure.name, "f.")} AS "m${index}"`),
18586
+ ...costColumns("f."),
18398
18587
  `(SELECT COUNT(*) FROM ranked) > ${maxRankParameter} AS "truncated"`
18399
18588
  ];
18400
18589
  return {
@@ -18412,6 +18601,59 @@ ORDER BY g."rank" ASC, f."bucket" ASC`,
18412
18601
  values
18413
18602
  };
18414
18603
  }
18604
+ const USAGE_METRIC_NAMES = coreStorage.TRACE_AGGREGATE_USAGE_METRIC_NAMES;
18605
+ function measureRule(name) {
18606
+ return coreStorage.isTraceAggregateCanonicalMeasure(name) ? coreStorage.TRACE_AGGREGATE_MEASURE_REGISTRY[name] : void 0;
18607
+ }
18608
+ function usageMetricIndex(name) {
18609
+ const index = USAGE_METRIC_NAMES.indexOf(name);
18610
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate token metric: ${name}`);
18611
+ return index;
18612
+ }
18613
+ /**
18614
+ * Builds `usage`: one row per candidate trace that has at least one token metric row, holding
18615
+ * per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and the trace's
18616
+ * pricing state. Metric rows are matched by traceId, usage metric name, tenant scope, and
18617
+ * `timestamp >= from` only: a trace that starts in the window can emit metrics after `to`, and
18618
+ * the candidates semi-join already bounds the scan.
18619
+ *
18620
+ * Rows are deduplicated on `metricId` alone. The primary key is (`metricId`, `timestamp`)
18621
+ * because the table is partitioned by timestamp, so retried copies with different timestamps
18622
+ * are both stored.
18623
+ */
18624
+ function compileUsageCtes(schema, plan, param) {
18625
+ const scopeConditions = plan.scope ? [`m."organizationId" = ${param(plan.scope.organizationId)}`, ...plan.scope.resourceId === void 0 ? [] : [`m."resourceId" = ${param(plan.scope.resourceId)}`]] : [];
18626
+ const rowConditions = [
18627
+ `m."traceId" IN (SELECT "traceId" FROM candidates)`,
18628
+ `m."name" = ANY(${param(USAGE_METRIC_NAMES)}::text[])`,
18629
+ `m."timestamp" >= ${param(plan.timeRange.from)}::timestamptz`,
18630
+ ...scopeConditions
18631
+ ];
18632
+ const costRow = `"name" = ANY(${param(coreStorage.TRACE_AGGREGATE_COST_METRIC_NAMES)}::text[])`;
18633
+ const priced = `${costRow} AND "estimatedCost" IS NOT NULL AND "costUnit" IS NOT NULL AND NOT "hasError"`;
18634
+ const failed = `${costRow} AND ("hasError" OR ("estimatedCost" IS NOT NULL AND "costUnit" IS NULL))`;
18635
+ const tokenColumns = USAGE_METRIC_NAMES.map((name, index) => `SUM("value") FILTER (WHERE "name" = ${param(name)}) AS "t${index}"`);
18636
+ return [`usage_rows AS (
18637
+ SELECT DISTINCT ON (m."metricId") m."traceId", m."name", m."value", m."estimatedCost", m."costUnit",
18638
+ COALESCE(jsonb_typeof(m."costMetadata" -> 'error'), 'null') <> 'null' AS "hasError"
18639
+ FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)} m
18640
+ WHERE ${rowConditions.join("\n AND ")}
18641
+ ORDER BY m."metricId", m."timestamp"
18642
+ )`, `usage AS (
18643
+ SELECT "traceId",
18644
+ ${tokenColumns.join(",\n ")},
18645
+ SUM("estimatedCost") FILTER (WHERE ${priced}) AS "cost",
18646
+ bool_or(${priced}) AS "priced",
18647
+ bool_or(${failed}) AS "pricingFailure",
18648
+ MIN("costUnit" COLLATE "C") FILTER (WHERE ${priced}) AS "unitMin",
18649
+ MAX("costUnit" COLLATE "C") FILTER (WHERE ${priced}) AS "unitMax"
18650
+ FROM usage_rows
18651
+ GROUP BY "traceId"
18652
+ )`];
18653
+ }
18654
+ function nullableNumber(value) {
18655
+ return value === null || value === void 0 ? null : Number(value);
18656
+ }
18415
18657
  function toIsoTimestamp(value) {
18416
18658
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
18417
18659
  }
@@ -18429,7 +18671,11 @@ async function aggregateTraces(client, schema, plan, timeoutMs) {
18429
18671
  }
18430
18672
  return {
18431
18673
  rows: resultRows.map((row) => {
18432
- const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
18674
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, nullableNumber(row[`m${index}`])])) };
18675
+ if ("costCoverage" in row) shaped.cost = {
18676
+ coverage: nullableNumber(row.costCoverage),
18677
+ unit: row.costUnit ?? null
18678
+ };
18433
18679
  if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
18434
18680
  if (plan.interval !== void 0) shaped.bucket = toIsoTimestamp(row.bucket);
18435
18681
  return shaped;
@@ -19151,6 +19397,7 @@ async function dangerouslyClearTracing(client, schema) {
19151
19397
  * application database — observability writes will degrade app performance.
19152
19398
  * Use it through `MastraCompositeStore` with a dedicated Postgres connection.
19153
19399
  */
19400
+ const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? ["span-query"] : [];
19154
19401
  function wrapError(op, error, details) {
19155
19402
  if (error instanceof MastraError || error instanceof coreStorage.TraceQueryCursorError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
19156
19403
  throw new MastraError({
@@ -19340,6 +19587,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19340
19587
  "metric-discovery",
19341
19588
  "trace-query",
19342
19589
  "trace-aggregate",
19590
+ ...spanQueryFeatures,
19343
19591
  "trace-query-root-duration",
19344
19592
  "trace-query-discovery",
19345
19593
  "thread-query",
@@ -19359,6 +19607,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19359
19607
  "delta-polling",
19360
19608
  "trace-query",
19361
19609
  "trace-aggregate",
19610
+ ...spanQueryFeatures,
19362
19611
  "trace-query-root-duration",
19363
19612
  "trace-query-discovery",
19364
19613
  "thread-query",
@@ -19407,6 +19656,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19407
19656
  async listTraces(args) {
19408
19657
  return this.#run("LIST_TRACES", () => listTraces(this.#readClient, this.#schema, args));
19409
19658
  }
19659
+ async querySpans(plan) {
19660
+ return this.#run("QUERY_SPANS", () => querySpans(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19661
+ }
19410
19662
  async queryTraces(plan) {
19411
19663
  return this.#run("QUERY_TRACES", () => queryTraces(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19412
19664
  }