@mastra/pg 1.22.0-alpha.3 → 1.22.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
@@ -5021,7 +5021,7 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
5021
5021
  }
5022
5022
  });
5023
5023
  }
5024
- async updateTask(taskId, update) {
5024
+ async updateTask(taskId, update, options) {
5025
5025
  const setClauses = [];
5026
5026
  const params = [];
5027
5027
  let paramIdx = 1;
@@ -5063,10 +5063,15 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
5063
5063
  const val = update.completedAt?.toISOString() ?? null;
5064
5064
  params.push(val, val);
5065
5065
  }
5066
- if (setClauses.length === 0) return;
5066
+ if (setClauses.length === 0) return false;
5067
5067
  const table = getTableName$4(getSchemaName$4(this.#schema));
5068
5068
  params.push(taskId);
5069
- await this.#db.client.none(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE "id" = $${paramIdx}`, params);
5069
+ let where = `"id" = $${paramIdx++}`;
5070
+ if (options?.expectedStatus) {
5071
+ where += ` AND "status" = $${paramIdx}`;
5072
+ params.push(options.expectedStatus);
5073
+ }
5074
+ return ((await this.#db.client.query(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE ${where}`, params)).rowCount ?? 0) > 0;
5070
5075
  }
5071
5076
  async getTask(taskId) {
5072
5077
  const table = getTableName$4(getSchemaName$4(this.#schema));
@@ -6711,6 +6716,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
6711
6716
  "tags",
6712
6717
  "comment",
6713
6718
  "toolMockReport",
6719
+ "metadata",
6714
6720
  "organizationId",
6715
6721
  "projectId",
6716
6722
  "attempt"
@@ -6907,6 +6913,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
6907
6913
  input: safelyParseJSON(row.input),
6908
6914
  output: row.output ? safelyParseJSON(row.output) : null,
6909
6915
  groundTruth: row.groundTruth ? safelyParseJSON(row.groundTruth) : null,
6916
+ metadata: row.metadata ? safelyParseJSON(row.metadata) : null,
6910
6917
  error: row.error ? safelyParseJSON(row.error) : null,
6911
6918
  startedAt: ensureDate(row.startedAtZ || row.startedAt),
6912
6919
  completedAt: ensureDate(row.completedAtZ || row.completedAt),
@@ -7215,6 +7222,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7215
7222
  input: input.input,
7216
7223
  output: input.output ?? null,
7217
7224
  groundTruth: input.groundTruth ?? null,
7225
+ metadata: input.metadata ?? null,
7218
7226
  error: input.error ?? null,
7219
7227
  startedAt: input.startedAt.toISOString(),
7220
7228
  completedAt: input.completedAt.toISOString(),
@@ -7237,6 +7245,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7237
7245
  input: input.input,
7238
7246
  output: input.output ?? null,
7239
7247
  groundTruth: input.groundTruth ?? null,
7248
+ metadata: input.metadata ?? null,
7240
7249
  error: input.error ?? null,
7241
7250
  startedAt: input.startedAt,
7242
7251
  completedAt: input.completedAt,
@@ -7273,9 +7282,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7273
7282
  const id = crypto.randomUUID();
7274
7283
  return t.one(`INSERT INTO ${tableName} (
7275
7284
  "id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
7276
- "input", "output", "groundTruth", "error", "startedAt", "completedAt",
7285
+ "input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
7277
7286
  "retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
7278
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
7287
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
7279
7288
  RETURNING *`, [
7280
7289
  id,
7281
7290
  input.experimentId,
@@ -7286,6 +7295,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7286
7295
  JSON.stringify(input.input),
7287
7296
  input.output != null ? JSON.stringify(input.output) : null,
7288
7297
  input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
7298
+ input.metadata != null ? JSON.stringify(input.metadata) : null,
7289
7299
  input.error != null ? JSON.stringify(input.error) : null,
7290
7300
  input.startedAt.toISOString(),
7291
7301
  input.completedAt.toISOString(),
@@ -7300,9 +7310,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7300
7310
  }
7301
7311
  return t.one(`UPDATE ${tableName} SET
7302
7312
  "itemDatasetVersion" = $2, "organizationId" = $3, "projectId" = $4,
7303
- "input" = $5, "output" = $6, "groundTruth" = $7, "error" = $8,
7304
- "startedAt" = $9, "completedAt" = $10, "retryCount" = $11, "attempt" = $12,
7305
- "traceId" = $13, "status" = $14, "tags" = $15, "toolMockReport" = $16
7313
+ "input" = $5, "output" = $6, "groundTruth" = $7, "metadata" = $8, "error" = $9,
7314
+ "startedAt" = $10, "completedAt" = $11, "retryCount" = $12, "attempt" = $13,
7315
+ "traceId" = $14, "status" = $15, "tags" = $16, "toolMockReport" = $17
7306
7316
  WHERE "id" = $1 RETURNING *`, [
7307
7317
  existing.id,
7308
7318
  input.itemDatasetVersion ?? null,
@@ -7311,6 +7321,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7311
7321
  JSON.stringify(input.input),
7312
7322
  input.output != null ? JSON.stringify(input.output) : null,
7313
7323
  input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
7324
+ input.metadata != null ? JSON.stringify(input.metadata) : null,
7314
7325
  input.error != null ? JSON.stringify(input.error) : null,
7315
7326
  input.startedAt.toISOString(),
7316
7327
  input.completedAt.toISOString(),
@@ -14417,6 +14428,7 @@ function allIndexDDL(schema) {
14417
14428
  //#endregion
14418
14429
  //#region src/storage/domains/observability/v-next/discovery.ts
14419
14430
  const DEFAULT_TTL_SECONDS = 300;
14431
+ const DEFAULT_LOOKBACK_SECONDS = 720 * 60 * 60;
14420
14432
  /** All signal tables that contain a column. Used by cross-signal discovery. */
14421
14433
  const SIGNAL_TABLES_WITH_CONTEXT = [
14422
14434
  TABLE_SPAN_EVENTS,
@@ -14481,22 +14493,71 @@ function startOrJoinRefresh(dedupeKey, cacheKey, refresh, upsert, logger) {
14481
14493
  async function readWithRefresh(client, schema, cacheKey, refresh, ttlSeconds, logger) {
14482
14494
  const table = qualifiedTable(schema, TABLE_DISCOVERY);
14483
14495
  const row = await client.oneOrNone(`SELECT "values", "refreshedAt" FROM ${table} WHERE "cacheKey" = $1`, [cacheKey]);
14484
- const refreshedAtMs = row ? new Date(row.refreshedAt).getTime() : 0;
14485
- if (!(!row || Date.now() - refreshedAtMs > ttlSeconds * 1e3)) return row.values;
14486
- const refreshing = startOrJoinRefresh(`${schema}:${cacheKey}`, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
14496
+ const dedupeKey = `${schema}:${cacheKey}`;
14497
+ const startRefresh = () => startOrJoinRefresh(dedupeKey, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
14487
14498
  if (ttlSeconds <= 0) try {
14488
- return await refreshing;
14499
+ return await startRefresh();
14489
14500
  } catch {
14490
14501
  return row?.values ?? [];
14491
14502
  }
14503
+ const refreshedAtMs = row ? new Date(row.refreshedAt).getTime() : 0;
14504
+ if (!(!row || Date.now() - refreshedAtMs > ttlSeconds * 1e3)) return row.values;
14492
14505
  if (!row) try {
14493
- return await refreshing;
14506
+ return await startRefresh();
14494
14507
  } catch {
14495
14508
  return [];
14496
14509
  }
14497
- refreshing.catch(() => {});
14510
+ const previousRefreshedAt = row.refreshedAt;
14511
+ claimRefresh(client, schema, cacheKey, ttlSeconds).then((claimedAt) => {
14512
+ if (!claimedAt) return;
14513
+ return startRefresh().then(() => {}, () => releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt));
14514
+ }).catch(() => {});
14498
14515
  return row.values;
14499
14516
  }
14517
+ /**
14518
+ * Try to become the process that refreshes `cacheKey`.
14519
+ *
14520
+ * The conditional UPDATE re-checks staleness in the database and stamps
14521
+ * `refreshedAt` in the same statement, so exactly one caller can transition a
14522
+ * given cache row out of the stale window — losers see zero rows updated and
14523
+ * skip their refresh. This is the cross-process counterpart to
14524
+ * `inFlightRefreshes`: N frontends going stale together run one refresh
14525
+ * instead of N.
14526
+ *
14527
+ * It reuses the cache table rather than an advisory lock because `DbClient`
14528
+ * is pool-backed, so a transaction-scoped lock would mean threading explicit
14529
+ * transaction handling through the discovery read path.
14530
+ *
14531
+ * The stamp is provisional in both directions: a completed refresh overwrites
14532
+ * it via `upsertCache`, and a failed one restores it via `releaseClaim`.
14533
+ *
14534
+ * Returns the exact provisional stamp written by the claim (or `null` when
14535
+ * the claim was lost) so `releaseClaim` can restore the row only while it
14536
+ * still holds this claim's stamp.
14537
+ */
14538
+ async function claimRefresh(client, schema, cacheKey, ttlSeconds) {
14539
+ const table = qualifiedTable(schema, TABLE_DISCOVERY);
14540
+ return (await client.oneOrNone(`UPDATE ${table} SET "refreshedAt" = NOW()
14541
+ WHERE "cacheKey" = $1 AND "refreshedAt" <= NOW() - ($2 || ' seconds')::interval
14542
+ RETURNING "refreshedAt"::text AS "refreshedAt"`, [cacheKey, Math.floor(ttlSeconds)]))?.refreshedAt ?? null;
14543
+ }
14544
+ /**
14545
+ * Hand back a claim whose refresh failed by restoring the timestamp the
14546
+ * claiming reader observed. Guarded on the row still holding this claim's
14547
+ * exact provisional stamp so a refresh that completed in the meantime
14548
+ * (another process, or a later claim) isn't dragged backwards into staleness.
14549
+ */
14550
+ async function releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt) {
14551
+ const table = qualifiedTable(schema, TABLE_DISCOVERY);
14552
+ try {
14553
+ await client.query(`UPDATE ${table} SET "refreshedAt" = $2
14554
+ WHERE "cacheKey" = $1 AND "refreshedAt" = $3::timestamptz`, [
14555
+ cacheKey,
14556
+ previousRefreshedAt,
14557
+ claimedAt
14558
+ ]);
14559
+ } catch {}
14560
+ }
14500
14561
  async function upsertCache(client, schema, cacheKey, values) {
14501
14562
  const table = qualifiedTable(schema, TABLE_DISCOVERY);
14502
14563
  await client.query(`INSERT INTO ${table} ("cacheKey", "refreshedAt", "values")
@@ -14505,61 +14566,95 @@ async function upsertCache(client, schema, cacheKey, values) {
14505
14566
  "refreshedAt" = EXCLUDED."refreshedAt",
14506
14567
  "values" = EXCLUDED."values"`, [cacheKey, JSON.stringify(values)]);
14507
14568
  }
14508
- async function distinctAcrossTables(client, schema, column, tables, filterSql = "", filterParams = []) {
14569
+ /**
14570
+ * Time predicate that bounds a discovery refresh to recent events.
14571
+ *
14572
+ * Each signal table is range-partitioned on its own time column
14573
+ * (`SIGNAL_TIME_COLUMN`: `endedAt` for spans, `timestamp` for everything
14574
+ * else), so the predicate has to be built per table rather than shared across
14575
+ * a UNION. Bounding on that exact column is what lets the planner prune
14576
+ * partitions/chunks instead of scanning all history.
14577
+ *
14578
+ * The interval is interpolated rather than bound to a parameter on purpose:
14579
+ * the UNION branches in `distinctAcrossTables` and `getTags` all reference the
14580
+ * same positional placeholders, so adding a per-branch parameter would break
14581
+ * `$N` numbering. Safety comes from coercing to a finite integer here — the
14582
+ * value originates from `DiscoveryConfig`, never from user input.
14583
+ */
14584
+ function lookbackPredicate(table, lookbackSeconds) {
14585
+ if (!Number.isFinite(lookbackSeconds) || lookbackSeconds <= 0) return "";
14586
+ const seconds = Math.floor(lookbackSeconds);
14587
+ const timeColumn = SIGNAL_TIME_COLUMN[table];
14588
+ if (!timeColumn) return "";
14589
+ return `AND "${timeColumn}" >= NOW() - INTERVAL '${seconds} seconds'`;
14590
+ }
14591
+ async function distinctAcrossTables(client, schema, column, tables, lookbackSeconds, filterSql = "", filterParams = []) {
14509
14592
  const safeColumn = parseSqlIdentifier(column, "column name");
14510
- const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql}`).join(" UNION ");
14593
+ const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql} ${lookbackPredicate(t, lookbackSeconds)}`).join(" UNION ");
14511
14594
  return (await client.manyOrNone(`SELECT v FROM (${unions}) sub ORDER BY v`, filterParams)).map((r) => r.v);
14512
14595
  }
14513
14596
  async function getEntityTypes(client, schema, _args, config) {
14514
- return { entityTypes: await readWithRefresh(client, schema, "entity_types", () => distinctAcrossTables(client, schema, "entityType", ENTITY_DISCOVERY_TABLES), config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger) };
14597
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14598
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14599
+ return { entityTypes: await readWithRefresh(client, schema, "entity_types", () => distinctAcrossTables(client, schema, "entityType", ENTITY_DISCOVERY_TABLES, lookback), ttl, config.logger) };
14515
14600
  }
14516
14601
  async function getEntityNames(client, schema, args, config) {
14517
14602
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14603
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14518
14604
  const cacheKey = args.entityType ? `entity_names:${args.entityType}` : "entity_names";
14519
14605
  const filterSql = args.entityType ? `AND "entityType" = $1` : "";
14520
14606
  const filterParams = args.entityType ? [args.entityType] : [];
14521
- return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, filterSql, filterParams), ttl, config.logger) };
14607
+ return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, lookback, filterSql, filterParams), ttl, config.logger) };
14522
14608
  }
14523
14609
  async function getServiceNames(client, schema, _args, config) {
14524
- return { serviceNames: await readWithRefresh(client, schema, "service_names", () => distinctAcrossTables(client, schema, "serviceName", SIGNAL_TABLES_WITH_CONTEXT), config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger) };
14610
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14611
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14612
+ return { serviceNames: await readWithRefresh(client, schema, "service_names", () => distinctAcrossTables(client, schema, "serviceName", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
14525
14613
  }
14526
14614
  async function getEnvironments(client, schema, _args, config) {
14527
- return { environments: await readWithRefresh(client, schema, "environments", () => distinctAcrossTables(client, schema, "environment", SIGNAL_TABLES_WITH_CONTEXT), config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger) };
14615
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14616
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14617
+ return { environments: await readWithRefresh(client, schema, "environments", () => distinctAcrossTables(client, schema, "environment", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
14528
14618
  }
14529
14619
  async function getTags(client, schema, args, config) {
14530
14620
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14621
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14531
14622
  const cacheKey = args.entityType ? `tags:${args.entityType}` : "tags";
14532
14623
  const refresh = async () => {
14533
14624
  const filter = args.entityType ? `AND "entityType" = $1` : "";
14534
14625
  const params = args.entityType ? [args.entityType] : [];
14535
- const unions = ENTITY_DISCOVERY_TABLES.map((t) => `SELECT DISTINCT UNNEST("tags") AS v FROM ${qualifiedTable(schema, t)} WHERE array_length("tags", 1) > 0 ${filter}`).join(" UNION ");
14626
+ const unions = ENTITY_DISCOVERY_TABLES.map((t) => `SELECT DISTINCT UNNEST("tags") AS v FROM ${qualifiedTable(schema, t)} WHERE array_length("tags", 1) > 0 ${filter} ${lookbackPredicate(t, lookback)}`).join(" UNION ");
14536
14627
  return (await client.manyOrNone(`SELECT v FROM (${unions}) sub WHERE v IS NOT NULL AND v <> '' ORDER BY v`, params)).map((r) => r.v);
14537
14628
  };
14538
14629
  return { tags: await readWithRefresh(client, schema, cacheKey, refresh, ttl, config.logger) };
14539
14630
  }
14540
14631
  async function getMetricNames(client, schema, args, config) {
14632
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14633
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14541
14634
  let filtered = await readWithRefresh(client, schema, "metric_names", async () => {
14542
14635
  return (await client.manyOrNone(`SELECT DISTINCT "name" AS v FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
14543
- WHERE "name" IS NOT NULL AND "name" <> '' ORDER BY "name"`)).map((r) => r.v);
14544
- }, config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger);
14636
+ WHERE "name" IS NOT NULL AND "name" <> '' ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY "name"`)).map((r) => r.v);
14637
+ }, ttl, config.logger);
14545
14638
  if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
14546
14639
  if (args.limit) filtered = filtered.slice(0, args.limit);
14547
14640
  return { names: filtered };
14548
14641
  }
14549
14642
  async function getMetricLabelKeys(client, schema, args, config) {
14550
14643
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14644
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14551
14645
  return { keys: await readWithRefresh(client, schema, `metric_label_keys:${args.metricName}`, async () => {
14552
14646
  return (await client.manyOrNone(`SELECT DISTINCT k AS v
14553
14647
  FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}, jsonb_object_keys("labels") k
14554
- WHERE "name" = $1 ORDER BY k`, [args.metricName])).map((r) => r.v);
14648
+ WHERE "name" = $1 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY k`, [args.metricName])).map((r) => r.v);
14555
14649
  }, ttl, config.logger) };
14556
14650
  }
14557
14651
  async function getMetricLabelValues(client, schema, args, config) {
14558
14652
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14653
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14559
14654
  let filtered = await readWithRefresh(client, schema, `metric_label_values:${args.metricName}:${args.labelKey}`, async () => {
14560
14655
  return (await client.manyOrNone(`SELECT DISTINCT "labels" ->> $2 AS v
14561
14656
  FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
14562
- WHERE "name" = $1 AND "labels" ? $2
14657
+ WHERE "name" = $1 AND "labels" ? $2 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)}
14563
14658
  ORDER BY v`, [args.metricName, args.labelKey])).map((r) => r.v).filter((v) => v != null && v !== "");
14564
14659
  }, ttl, config.logger);
14565
14660
  if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
@@ -21907,7 +22002,7 @@ var PgFactoryStorageOps = class {
21907
22002
  row[name] = Boolean(value);
21908
22003
  break;
21909
22004
  case "json":
21910
- row[name] = typeof value === "string" ? JSON.parse(value) : value;
22005
+ row[name] = value;
21911
22006
  break;
21912
22007
  case "bigint":
21913
22008
  case "integer":