@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.
@@ -3,7 +3,7 @@ name: mastra-pg
3
3
  description: Documentation for @mastra/pg. Use when working with @mastra/pg APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/pg"
6
- version: "1.30.0-alpha.1"
6
+ version: "1.30.0-alpha.3"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.30.0-alpha.1",
2
+ "version": "1.30.0-alpha.3",
3
3
  "package": "@mastra/pg",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -18280,6 +18280,166 @@ async function queryThreads(client, schema, plan, timeoutMs) {
18280
18280
  }) : null }
18281
18281
  });
18282
18282
  }
18283
+ /** Compile a span-row filter with exactly the same rules as trace span predicates. */
18284
+ function compileSpanQueryPredicate(predicate, offset) {
18285
+ return compileScalarPredicate(predicate, SPAN_FIELDS, offset);
18286
+ }
18287
+ //#endregion
18288
+ //#region src/storage/domains/observability/v-next/span-query.ts
18289
+ const identityFields = [
18290
+ "organizationId",
18291
+ "resourceId",
18292
+ "traceId",
18293
+ "spanId"
18294
+ ];
18295
+ const fields = [
18296
+ ...identityFields,
18297
+ "parentSpanId",
18298
+ "name",
18299
+ "spanType",
18300
+ "entityType",
18301
+ "entityId",
18302
+ "entityName",
18303
+ "entityVersionId",
18304
+ "parentEntityVersionId",
18305
+ "rootEntityVersionId",
18306
+ "runId",
18307
+ "sessionId",
18308
+ "userId"
18309
+ ];
18310
+ const identities = identityFields.map((field) => `"${field}"`).join(", ");
18311
+ const identityOrder = identityFields.map((field) => `"${field}" COLLATE "C" ASC NULLS LAST`).join(", ");
18312
+ function selectedKeys(rows, values) {
18313
+ return rows.map((row) => `(${identityFields.map((field) => {
18314
+ values.push(row[field]);
18315
+ return `"${field}" ${field === "traceId" || field === "spanId" ? "=" : "IS NOT DISTINCT FROM"} $${values.length}::text`;
18316
+ }).join(" AND ")})`).join(" OR ");
18317
+ }
18318
+ function afterCursor(plan, values) {
18319
+ if (!plan.cursor) return "TRUE";
18320
+ const cursor = plan.cursor;
18321
+ values.push(cursor.sortValue);
18322
+ const timestamp = `$${values.length}::timestamptz`;
18323
+ const field = `s."${plan.orderBy.field}"`;
18324
+ const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
18325
+ const equal = [`${field} = ${timestamp}`];
18326
+ for (const key of identityFields) {
18327
+ const ref = `s."${key}" COLLATE "C"`;
18328
+ const value = cursor[key];
18329
+ if (value === null) equal.push(`${ref} IS NULL`);
18330
+ else {
18331
+ values.push(value);
18332
+ const parameter = `$${values.length}::text COLLATE "C"`;
18333
+ terms.push(`(${equal.join(" AND ")} AND (${ref} > ${parameter} OR ${ref} IS NULL))`);
18334
+ equal.push(`${ref} = ${parameter}`);
18335
+ }
18336
+ }
18337
+ return `(${terms.join(" OR ")})`;
18338
+ }
18339
+ function compilePostgresSpanQuery(schema, plan) {
18340
+ const values = [plan.timeRange.from, plan.timeRange.to];
18341
+ const scope = [];
18342
+ if (plan.scope) {
18343
+ values.push(plan.scope.organizationId);
18344
+ scope.push(`"organizationId" = $${values.length}`);
18345
+ if (plan.scope.resourceId !== void 0) {
18346
+ values.push(plan.scope.resourceId);
18347
+ scope.push(`"resourceId" = $${values.length}`);
18348
+ }
18349
+ }
18350
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where, values.length + 1) : {
18351
+ sql: "TRUE",
18352
+ values: []
18353
+ };
18354
+ values.push(...predicate.values);
18355
+ const after = afterCursor(plan, values);
18356
+ values.push(plan.limit + 1);
18357
+ const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
18358
+ return {
18359
+ text: `WITH candidate_ids AS MATERIALIZED (
18360
+ SELECT DISTINCT ${identities} FROM ${table}
18361
+ WHERE "startedAt" >= $1 AND "startedAt" < $2 AND "endedAt" >= $1 AND NOT "isPending"
18362
+ ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
18363
+ ), current_spans AS MATERIALIZED (
18364
+ SELECT DISTINCT ON (${identities})
18365
+ ${fields.map((field) => `"${field}"`).join(", ")}, "startedAt", "endedAt", "cursorId"::text AS version,
18366
+ CASE WHEN jsonb_typeof(attributes -> 'model') = 'string' THEN attributes ->> 'model' END AS model,
18367
+ CASE WHEN jsonb_typeof(attributes -> 'provider') = 'string' THEN attributes ->> 'provider' END AS provider,
18368
+ (EXTRACT(EPOCH FROM ("endedAt" - "startedAt")) * 1000)::float8 AS "durationMs",
18369
+ CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
18370
+ error IS NOT NULL AS "hasError"
18371
+ FROM ${table} e
18372
+ WHERE NOT "isPending" AND "endedAt" >= $1 AND (
18373
+ "organizationId" IS NULL, COALESCE("organizationId", ''), "resourceId" IS NULL, COALESCE("resourceId", ''), "traceId", "spanId"
18374
+ ) IN (
18375
+ SELECT "organizationId" IS NULL, COALESCE("organizationId", ''), "resourceId" IS NULL, COALESCE("resourceId", ''), "traceId", "spanId" FROM candidate_ids
18376
+ ) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
18377
+ ORDER BY ${identities}, "endedAt" DESC, "cursorId" DESC
18378
+ ), candidates AS (
18379
+ SELECT *, CASE WHEN "hasError" THEN 'error' ELSE NULL END AS error FROM current_spans
18380
+ )
18381
+ SELECT ${[
18382
+ ...identityFields,
18383
+ "parentSpanId",
18384
+ "name",
18385
+ "spanType",
18386
+ "entityType",
18387
+ "entityId",
18388
+ "entityName",
18389
+ "model",
18390
+ "provider",
18391
+ "startedAt",
18392
+ "endedAt",
18393
+ "durationMs",
18394
+ "status",
18395
+ "version"
18396
+ ].map((field) => `s."${field}"`).join(", ")}
18397
+ FROM candidates s WHERE s."startedAt" >= $1 AND s."startedAt" < $2 AND (${predicate.sql}) AND ${after}
18398
+ ORDER BY s."${plan.orderBy.field}" ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityOrder}
18399
+ LIMIT $${values.length}`,
18400
+ values
18401
+ };
18402
+ }
18403
+ async function querySpans(client, schema, plan, timeoutMs) {
18404
+ const deadline = performance.now() + _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
18405
+ return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
18406
+ const query = compilePostgresSpanQuery(schema, plan);
18407
+ const rows = await tx.any(query.text, query.values);
18408
+ const selected = rows.map(({ version: _version, startedAt, endedAt, ...row }) => ({
18409
+ ...row,
18410
+ startedAt: startedAt.toISOString(),
18411
+ endedAt: endedAt.toISOString()
18412
+ }));
18413
+ const page = rows.slice(0, plan.limit);
18414
+ if (!page.length) return _mastra_core_storage.buildSpanQueryResponse(plan, selected, [], []);
18415
+ const remaining = async () => {
18416
+ const ms = Math.floor(deadline - performance.now());
18417
+ if (ms <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
18418
+ await tx.query(`SELECT set_config('statement_timeout', $1, true)`, [`${ms}ms`]);
18419
+ };
18420
+ await remaining();
18421
+ const payloadValues = [];
18422
+ const payloadWhere = page.map((row) => {
18423
+ const key = selectedKeys([row], payloadValues);
18424
+ payloadValues.push(row.version);
18425
+ return `(${key} AND "cursorId" = $${payloadValues.length}::bigint)`;
18426
+ }).join(" OR ");
18427
+ const payloads = await tx.any(`SELECT ${identities},
18428
+ left(input::text, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS "inputPreview", left(output::text, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS "outputPreview"
18429
+ FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues);
18430
+ await remaining();
18431
+ const metricValues = [];
18432
+ const metricWhere = selectedKeys(page, metricValues);
18433
+ const metrics = await tx.any(`SELECT ${identities}, name, "estimatedCost", "costUnit",
18434
+ "costMetadata" ->> 'allocation' AS allocation, COALESCE("costMetadata" ? 'error', false) AS "costError"
18435
+ FROM (SELECT DISTINCT ON (${identities}, "metricId") ${identities}, name, "estimatedCost", "costUnit", "costMetadata" FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
18436
+ WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
18437
+ ORDER BY ${identities}, "metricId", "cursorId" DESC) m
18438
+ LIMIT ${_mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS + 1}`, metricValues);
18439
+ if (metrics.length > _mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS) throw new _mastra_core_storage.TraceQueryResourceLimitError();
18440
+ return _mastra_core_storage.buildSpanQueryResponse(plan, selected, payloads, metrics);
18441
+ }, { repeatableRead: true });
18442
+ }
18283
18443
  //#endregion
18284
18444
  //#region src/storage/domains/observability/v-next/trace-aggregate.ts
18285
18445
  /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
@@ -18348,13 +18508,32 @@ function compilePostgresTraceAggregate(schema, plan) {
18348
18508
  if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
18349
18509
  factColumns.push(`to_timestamp(floor(EXTRACT(EPOCH FROM r."startedAt") * 1000 / ${intervalMs}) * ${intervalMs} / 1000.0) AS "bucket"`);
18350
18510
  }
18511
+ const measureKinds = new Set(plan.measures.map((measure) => measureRule(measure.name)?.kind));
18512
+ const hasCost = measureKinds.has("cost");
18513
+ let usageJoin = "";
18514
+ if (hasCost || measureKinds.has("tokens")) {
18515
+ ctes.push(...compileUsageCtes(schema, plan, param));
18516
+ usageJoin = "\n LEFT JOIN usage u ON u.\"traceId\" = r.\"traceId\"";
18517
+ USAGE_METRIC_NAMES.forEach((_, index) => factColumns.push(`CASE WHEN u."traceId" IS NOT NULL THEN COALESCE(u."t${index}", 0) END AS "t${index}"`));
18518
+ 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"`);
18519
+ }
18351
18520
  ctes.push(`facts AS (
18352
18521
  SELECT ${factColumns.join(",\n ")}
18353
- FROM candidates r
18522
+ FROM candidates r${usageJoin}
18354
18523
  )`);
18524
+ const mixedUnitsSql = (alias) => `MIN(${alias}"unitMin" COLLATE "C") <> MAX(${alias}"unitMax" COLLATE "C")`;
18355
18525
  const measureSql = (name, alias = "") => {
18356
18526
  const column = (name) => `${alias}"${name}"`;
18357
18527
  if (name === "count") return "COUNT(*)::float8";
18528
+ const rule = measureRule(name);
18529
+ if (rule?.kind === "tokens" && rule.metricNames) {
18530
+ const value = rule.metricNames.map((metricName) => column(`t${usageMetricIndex(metricName)}`)).join(" + ");
18531
+ return `(${rule.statistic === "avg" ? "AVG" : "SUM"}(${value}))::float8`;
18532
+ }
18533
+ if (rule?.kind === "cost") {
18534
+ const aggregate = rule.statistic === "avg" ? "AVG" : "SUM";
18535
+ return `(CASE WHEN ${mixedUnitsSql(alias)} THEN NULL ELSE ${aggregate}(${column("traceCost")}) END)::float8`;
18536
+ }
18358
18537
  if (name === "errorCount") return `(COUNT(*) FILTER (WHERE ${column("isError")}))::float8`;
18359
18538
  if (name === "errorRate") return `(COUNT(*) FILTER (WHERE ${column("isError")}))::float8 / COUNT(*)`;
18360
18539
  if (name === "duration.avg") return `AVG(${column("durationMs")})`;
@@ -18392,8 +18571,17 @@ function compilePostgresTraceAggregate(schema, plan) {
18392
18571
  const orderSql = [`${orderTarget} ${direction} NULLS LAST`, ...dimensionColumns.map((column) => `${column} COLLATE "C" ASC NULLS LAST`)].join(", ");
18393
18572
  const groupSql = dimensionColumns.length > 0 ? dimensionColumns.join(", ") : "()";
18394
18573
  const limitParameter = param(plan.limit + 1);
18574
+ const costColumns = (alias) => {
18575
+ if (!hasCost) return [];
18576
+ const mixedUnit = param(_mastra_core_storage.TRACE_AGGREGATE_MIXED_COST_UNIT);
18577
+ 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"`];
18578
+ };
18395
18579
  if (plan.interval === void 0) {
18396
- const projected = [...dimensionColumns, ...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS "m${index}"`)];
18580
+ const projected = [
18581
+ ...dimensionColumns,
18582
+ ...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS "m${index}"`),
18583
+ ...costColumns("")
18584
+ ];
18397
18585
  return {
18398
18586
  text: `WITH ${ctes.join(",\n")}
18399
18587
  SELECT ${projected.join(", ")}
@@ -18419,6 +18607,7 @@ LIMIT ${limitParameter}`,
18419
18607
  ...dimensionColumns.map((column) => `f.${column}`),
18420
18608
  "f.\"bucket\"",
18421
18609
  ...plan.measures.map((measure, index) => `${measureSql(measure.name, "f.")} AS "m${index}"`),
18610
+ ...costColumns("f."),
18422
18611
  `(SELECT COUNT(*) FROM ranked) > ${maxRankParameter} AS "truncated"`
18423
18612
  ];
18424
18613
  return {
@@ -18436,6 +18625,59 @@ ORDER BY g."rank" ASC, f."bucket" ASC`,
18436
18625
  values
18437
18626
  };
18438
18627
  }
18628
+ const USAGE_METRIC_NAMES = _mastra_core_storage.TRACE_AGGREGATE_USAGE_METRIC_NAMES;
18629
+ function measureRule(name) {
18630
+ return _mastra_core_storage.isTraceAggregateCanonicalMeasure(name) ? _mastra_core_storage.TRACE_AGGREGATE_MEASURE_REGISTRY[name] : void 0;
18631
+ }
18632
+ function usageMetricIndex(name) {
18633
+ const index = USAGE_METRIC_NAMES.indexOf(name);
18634
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate token metric: ${name}`);
18635
+ return index;
18636
+ }
18637
+ /**
18638
+ * Builds `usage`: one row per candidate trace that has at least one token metric row, holding
18639
+ * per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and the trace's
18640
+ * pricing state. Metric rows are matched by traceId, usage metric name, tenant scope, and
18641
+ * `timestamp >= from` only: a trace that starts in the window can emit metrics after `to`, and
18642
+ * the candidates semi-join already bounds the scan.
18643
+ *
18644
+ * Rows are deduplicated on `metricId` alone. The primary key is (`metricId`, `timestamp`)
18645
+ * because the table is partitioned by timestamp, so retried copies with different timestamps
18646
+ * are both stored.
18647
+ */
18648
+ function compileUsageCtes(schema, plan, param) {
18649
+ const scopeConditions = plan.scope ? [`m."organizationId" = ${param(plan.scope.organizationId)}`, ...plan.scope.resourceId === void 0 ? [] : [`m."resourceId" = ${param(plan.scope.resourceId)}`]] : [];
18650
+ const rowConditions = [
18651
+ `m."traceId" IN (SELECT "traceId" FROM candidates)`,
18652
+ `m."name" = ANY(${param(USAGE_METRIC_NAMES)}::text[])`,
18653
+ `m."timestamp" >= ${param(plan.timeRange.from)}::timestamptz`,
18654
+ ...scopeConditions
18655
+ ];
18656
+ const costRow = `"name" = ANY(${param(_mastra_core_storage.TRACE_AGGREGATE_COST_METRIC_NAMES)}::text[])`;
18657
+ const priced = `${costRow} AND "estimatedCost" IS NOT NULL AND "costUnit" IS NOT NULL AND NOT "hasError"`;
18658
+ const failed = `${costRow} AND ("hasError" OR ("estimatedCost" IS NOT NULL AND "costUnit" IS NULL))`;
18659
+ const tokenColumns = USAGE_METRIC_NAMES.map((name, index) => `SUM("value") FILTER (WHERE "name" = ${param(name)}) AS "t${index}"`);
18660
+ return [`usage_rows AS (
18661
+ SELECT DISTINCT ON (m."metricId") m."traceId", m."name", m."value", m."estimatedCost", m."costUnit",
18662
+ COALESCE(jsonb_typeof(m."costMetadata" -> 'error'), 'null') <> 'null' AS "hasError"
18663
+ FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)} m
18664
+ WHERE ${rowConditions.join("\n AND ")}
18665
+ ORDER BY m."metricId", m."timestamp"
18666
+ )`, `usage AS (
18667
+ SELECT "traceId",
18668
+ ${tokenColumns.join(",\n ")},
18669
+ SUM("estimatedCost") FILTER (WHERE ${priced}) AS "cost",
18670
+ bool_or(${priced}) AS "priced",
18671
+ bool_or(${failed}) AS "pricingFailure",
18672
+ MIN("costUnit" COLLATE "C") FILTER (WHERE ${priced}) AS "unitMin",
18673
+ MAX("costUnit" COLLATE "C") FILTER (WHERE ${priced}) AS "unitMax"
18674
+ FROM usage_rows
18675
+ GROUP BY "traceId"
18676
+ )`];
18677
+ }
18678
+ function nullableNumber(value) {
18679
+ return value === null || value === void 0 ? null : Number(value);
18680
+ }
18439
18681
  function toIsoTimestamp(value) {
18440
18682
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
18441
18683
  }
@@ -18453,7 +18695,11 @@ async function aggregateTraces(client, schema, plan, timeoutMs) {
18453
18695
  }
18454
18696
  return {
18455
18697
  rows: resultRows.map((row) => {
18456
- const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
18698
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, nullableNumber(row[`m${index}`])])) };
18699
+ if ("costCoverage" in row) shaped.cost = {
18700
+ coverage: nullableNumber(row.costCoverage),
18701
+ unit: row.costUnit ?? null
18702
+ };
18457
18703
  if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
18458
18704
  if (plan.interval !== void 0) shaped.bucket = toIsoTimestamp(row.bucket);
18459
18705
  return shaped;
@@ -19175,6 +19421,7 @@ async function dangerouslyClearTracing(client, schema) {
19175
19421
  * application database — observability writes will degrade app performance.
19176
19422
  * Use it through `MastraCompositeStore` with a dedicated Postgres connection.
19177
19423
  */
19424
+ const spanQueryFeatures = typeof _mastra_core_storage.planSpanQuery === "function" ? ["span-query"] : [];
19178
19425
  function wrapError(op, error, details) {
19179
19426
  if (error instanceof _mastra_core_error.MastraError || error instanceof _mastra_core_storage.TraceQueryCursorError || error instanceof _mastra_core_storage.TraceQueryExecutionError || error instanceof _mastra_core_storage.TraceQueryResourceLimitError) throw error;
19180
19427
  throw new _mastra_core_error.MastraError({
@@ -19364,6 +19611,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19364
19611
  "metric-discovery",
19365
19612
  "trace-query",
19366
19613
  "trace-aggregate",
19614
+ ...spanQueryFeatures,
19367
19615
  "trace-query-root-duration",
19368
19616
  "trace-query-discovery",
19369
19617
  "thread-query",
@@ -19383,6 +19631,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19383
19631
  "delta-polling",
19384
19632
  "trace-query",
19385
19633
  "trace-aggregate",
19634
+ ...spanQueryFeatures,
19386
19635
  "trace-query-root-duration",
19387
19636
  "trace-query-discovery",
19388
19637
  "thread-query",
@@ -19431,6 +19680,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
19431
19680
  async listTraces(args) {
19432
19681
  return this.#run("LIST_TRACES", () => listTraces(this.#readClient, this.#schema, args));
19433
19682
  }
19683
+ async querySpans(plan) {
19684
+ return this.#run("QUERY_SPANS", () => querySpans(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19685
+ }
19434
19686
  async queryTraces(plan) {
19435
19687
  return this.#run("QUERY_TRACES", () => queryTraces(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
19436
19688
  }