@mastra/pg 1.14.3 → 1.15.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.
- package/CHANGELOG.md +181 -0
- package/dist/docs/SKILL.md +2 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-memory-semantic-recall.md +8 -0
- package/dist/docs/references/docs-memory-storage.md +6 -0
- package/dist/docs/references/docs-memory-working-memory.md +2 -0
- package/dist/docs/references/docs-rag-overview.md +2 -0
- package/dist/docs/references/docs-rag-retrieval.md +2 -0
- package/dist/docs/references/docs-rag-vector-databases.md +2 -0
- package/dist/docs/references/reference-memory-memory-class.md +3 -0
- package/dist/docs/references/reference-processors-message-history-processor.md +3 -0
- package/dist/docs/references/reference-processors-semantic-recall-processor.md +3 -0
- package/dist/docs/references/reference-processors-working-memory-processor.md +3 -0
- package/dist/docs/references/reference-rag-metadata-filters.md +2 -0
- package/dist/docs/references/reference-storage-composite.md +2 -0
- package/dist/docs/references/reference-storage-dynamodb.md +2 -0
- package/dist/docs/references/reference-storage-postgresql.md +2 -0
- package/dist/docs/references/reference-storage-retention.md +180 -0
- package/dist/docs/references/reference-tools-vector-query-tool.md +2 -0
- package/dist/docs/references/reference-vectors-pg.md +2 -0
- package/dist/index.cjs +932 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +933 -54
- package/dist/index.js.map +1 -1
- package/dist/shared/config.d.ts +8 -1
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/storage/db/index.d.ts +51 -0
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/domains/background-tasks/index.d.ts +19 -1
- package/dist/storage/domains/background-tasks/index.d.ts.map +1 -1
- package/dist/storage/domains/datasets/index.d.ts +6 -7
- package/dist/storage/domains/datasets/index.d.ts.map +1 -1
- package/dist/storage/domains/experiments/index.d.ts +39 -5
- package/dist/storage/domains/experiments/index.d.ts.map +1 -1
- package/dist/storage/domains/memory/index.d.ts +39 -1
- package/dist/storage/domains/memory/index.d.ts.map +1 -1
- package/dist/storage/domains/notifications/index.d.ts +18 -1
- package/dist/storage/domains/notifications/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/index.d.ts +22 -1
- package/dist/storage/domains/observability/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +17 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/retention.d.ts +64 -0
- package/dist/storage/domains/observability/v-next/retention.d.ts.map +1 -0
- package/dist/storage/domains/schedules/index.d.ts +23 -1
- package/dist/storage/domains/schedules/index.d.ts.map +1 -1
- package/dist/storage/domains/scorer-definitions/index.d.ts.map +1 -1
- package/dist/storage/domains/scores/index.d.ts +26 -5
- package/dist/storage/domains/scores/index.d.ts.map +1 -1
- package/dist/storage/domains/utils.d.ts +20 -1
- package/dist/storage/domains/utils.d.ts.map +1 -1
- package/dist/storage/domains/workflows/index.d.ts +19 -1
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/dist/storage/retention.d.ts +76 -0
- package/dist/storage/retention.d.ts.map +1 -0
- package/dist/storage/test-utils.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
|
|
2
|
-
import { BRANCH_SPAN_TYPES, createVectorErrorId, AgentsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, BackgroundTasksStorage, TABLE_BACKGROUND_TASKS, BlobStore, TABLE_SKILL_BLOBS, ChannelsStorage, TABLE_CHANNEL_INSTALLATIONS, TABLE_CHANNEL_CONFIG, DatasetsStorage, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, DATASETS_SCHEMA, TABLE_CONFIGS, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, ensureDate, safelyParseJSON, ExperimentsStorage, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, FavoritesStorage, TABLE_FAVORITES, MCPClientsStorage, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, MCPServersStorage, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, MemoryStorage, TABLE_THREADS,
|
|
2
|
+
import { BRANCH_SPAN_TYPES, createVectorErrorId, AgentsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, BackgroundTasksStorage, TABLE_BACKGROUND_TASKS, BlobStore, TABLE_SKILL_BLOBS, ChannelsStorage, TABLE_CHANNEL_INSTALLATIONS, TABLE_CHANNEL_CONFIG, DatasetsStorage, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, DATASETS_SCHEMA, TABLE_CONFIGS, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, ensureDate, safelyParseJSON, ExperimentsStorage, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, FavoritesStorage, TABLE_FAVORITES, MCPClientsStorage, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, MCPServersStorage, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, MemoryStorage, TABLE_THREADS, TABLE_RESOURCES, TABLE_MESSAGES, NotificationsStorage, TABLE_NOTIFICATIONS, ObservabilityStorage, TABLE_SPANS, listTracesArgsSchema, toTraceSpans, PromptBlocksStorage, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, SchedulesStorage, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, ScorerDefinitionsStorage, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, ScoresStorage, TABLE_SCORERS, SkillsStorage, TABLE_SKILLS, TABLE_SKILL_VERSIONS, ToolProviderConnectionsStorage, TABLE_TOOL_PROVIDER_CONNECTIONS, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, mergeWorkflowStepResult, WorkspacesStorage, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, MastraCompositeStore, TraceStatus, getDefaultValue, parseDuration, listBranchesArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listFeedbackArgsSchema, transformScoreRow as transformScoreRow$1, getSqlType, EntityType, METRIC_DISTINCT_COLUMNS } from '@mastra/core/storage';
|
|
3
3
|
import { parseSqlIdentifier, parseFieldKey } from '@mastra/core/utils';
|
|
4
4
|
import { MastraVector, validateTopK, validateUpsertInput } from '@mastra/core/vector';
|
|
5
5
|
import { Mutex } from 'async-mutex';
|
|
@@ -2517,6 +2517,11 @@ CREATE TRIGGER ${triggerName}
|
|
|
2517
2517
|
EXECUTE FUNCTION ${functionName}();`;
|
|
2518
2518
|
}
|
|
2519
2519
|
var schemaSetupRegistry = /* @__PURE__ */ new Map();
|
|
2520
|
+
function assertPositiveLimit(limit) {
|
|
2521
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
2522
|
+
throw new Error(`prune limit must be a positive integer; received ${limit}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2520
2525
|
var PgDB = class extends MastraBase {
|
|
2521
2526
|
client;
|
|
2522
2527
|
schemaName;
|
|
@@ -3553,6 +3558,96 @@ Note: This migration may take some time for large tables.
|
|
|
3553
3558
|
async deleteData({ tableName }) {
|
|
3554
3559
|
return this.clearTable({ tableName });
|
|
3555
3560
|
}
|
|
3561
|
+
// ---------------------------------------------------------------------------
|
|
3562
|
+
// Retention helpers (prune)
|
|
3563
|
+
// ---------------------------------------------------------------------------
|
|
3564
|
+
/**
|
|
3565
|
+
* Deletes up to `limit` rows from `tableName` whose `column` value is strictly
|
|
3566
|
+
* older than `cutoff`, in a single bounded statement. Returns the number of
|
|
3567
|
+
* rows deleted so the caller's batch loop can decide whether the table is
|
|
3568
|
+
* drained.
|
|
3569
|
+
*
|
|
3570
|
+
* PostgreSQL has no `DELETE ... LIMIT`, so this targets a bounded set of
|
|
3571
|
+
* physical rows via the `ctid` system column (PG's row identity, analogous to
|
|
3572
|
+
* SQLite's `rowid`). `cutoff` is bound as a parameter — a `Date`/ISO-8601
|
|
3573
|
+
* string compared against a `timestamptz` anchor column, or a `number`
|
|
3574
|
+
* compared against a `bigint` epoch-ms anchor column.
|
|
3575
|
+
*/
|
|
3576
|
+
async pruneBatch({
|
|
3577
|
+
tableName,
|
|
3578
|
+
column,
|
|
3579
|
+
cutoff,
|
|
3580
|
+
limit
|
|
3581
|
+
}) {
|
|
3582
|
+
assertPositiveLimit(limit);
|
|
3583
|
+
const fullTableName = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
|
|
3584
|
+
const parsedColumn = `"${parseSqlIdentifier(column, "column name")}"`;
|
|
3585
|
+
const sql = `
|
|
3586
|
+
DELETE FROM ${fullTableName}
|
|
3587
|
+
WHERE ctid IN (
|
|
3588
|
+
SELECT ctid FROM ${fullTableName}
|
|
3589
|
+
WHERE ${parsedColumn} < $1
|
|
3590
|
+
LIMIT $2
|
|
3591
|
+
)
|
|
3592
|
+
`;
|
|
3593
|
+
const result = await this.client.query(sql, [cutoff, limit]);
|
|
3594
|
+
return result.rowCount ?? 0;
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Deletes up to `limit` aged parent rows *and* their child rows together, in
|
|
3598
|
+
* a single transaction (used by whole-unit pruning such as experiments →
|
|
3599
|
+
* experiment_results). The aged parent IDs are selected first and both
|
|
3600
|
+
* deletes target that exact ID set, so a bound or abort between batches never
|
|
3601
|
+
* leaves a parent hollow (kept, but with its children gone) or children
|
|
3602
|
+
* orphaned.
|
|
3603
|
+
*/
|
|
3604
|
+
async pruneUnitsBatch({
|
|
3605
|
+
parentTable,
|
|
3606
|
+
parentKey,
|
|
3607
|
+
parentColumn,
|
|
3608
|
+
childTable,
|
|
3609
|
+
childForeignKey,
|
|
3610
|
+
cutoff,
|
|
3611
|
+
limit
|
|
3612
|
+
}) {
|
|
3613
|
+
assertPositiveLimit(limit);
|
|
3614
|
+
const schemaName = getSchemaName(this.schemaName);
|
|
3615
|
+
const fullChildTable = getTableName({ indexName: childTable, schemaName });
|
|
3616
|
+
const fullParentTable = getTableName({ indexName: parentTable, schemaName });
|
|
3617
|
+
const childFk = `"${parseSqlIdentifier(childForeignKey, "column name")}"`;
|
|
3618
|
+
const parentPk = `"${parseSqlIdentifier(parentKey, "column name")}"`;
|
|
3619
|
+
const parentCol = `"${parseSqlIdentifier(parentColumn, "column name")}"`;
|
|
3620
|
+
return this.client.tx(async (t) => {
|
|
3621
|
+
const rows = await t.manyOrNone(
|
|
3622
|
+
`SELECT ${parentPk} AS id FROM ${fullParentTable} WHERE ${parentCol} < $1 LIMIT $2 FOR UPDATE SKIP LOCKED`,
|
|
3623
|
+
[cutoff, limit]
|
|
3624
|
+
);
|
|
3625
|
+
const ids = rows.map((r) => r.id);
|
|
3626
|
+
if (ids.length === 0) return { parents: 0, children: 0 };
|
|
3627
|
+
const childResult = await t.query(`DELETE FROM ${fullChildTable} WHERE ${childFk} = ANY($1)`, [ids]);
|
|
3628
|
+
const parentResult = await t.query(`DELETE FROM ${fullParentTable} WHERE ${parentPk} = ANY($1)`, [ids]);
|
|
3629
|
+
return {
|
|
3630
|
+
parents: parentResult.rowCount ?? 0,
|
|
3631
|
+
children: childResult.rowCount ?? 0
|
|
3632
|
+
};
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* Creates a btree index on `column` for `tableName` if it does not already
|
|
3637
|
+
* exist, so age-based prune deletes stay fast. Delegates to {@link createIndex}
|
|
3638
|
+
* (which is a no-op when the index is present).
|
|
3639
|
+
*
|
|
3640
|
+
* The name is lowercased and truncated to Postgres' 63-byte identifier limit
|
|
3641
|
+
* (schema-prefixed names can exceed it), mirroring {@link buildConstraintName}.
|
|
3642
|
+
*/
|
|
3643
|
+
async ensureIndex({
|
|
3644
|
+
indexName,
|
|
3645
|
+
tableName,
|
|
3646
|
+
column
|
|
3647
|
+
}) {
|
|
3648
|
+
const name = buildConstraintName({ baseName: indexName });
|
|
3649
|
+
await this.createIndex({ name, table: tableName, columns: [column] });
|
|
3650
|
+
}
|
|
3556
3651
|
async executeUpdate(client, {
|
|
3557
3652
|
tableName,
|
|
3558
3653
|
keys,
|
|
@@ -3628,6 +3723,20 @@ function transformFromSqlRow({
|
|
|
3628
3723
|
});
|
|
3629
3724
|
return result;
|
|
3630
3725
|
}
|
|
3726
|
+
function tenancyWhere(filters, startIndex) {
|
|
3727
|
+
const conditions = [];
|
|
3728
|
+
const params = [];
|
|
3729
|
+
let idx = startIndex;
|
|
3730
|
+
if (filters?.organizationId !== void 0) {
|
|
3731
|
+
conditions.push(`"organizationId" = $${idx++}`);
|
|
3732
|
+
params.push(filters.organizationId);
|
|
3733
|
+
}
|
|
3734
|
+
if (filters?.projectId !== void 0) {
|
|
3735
|
+
conditions.push(`"projectId" = $${idx++}`);
|
|
3736
|
+
params.push(filters.projectId);
|
|
3737
|
+
}
|
|
3738
|
+
return { conditions, params, nextIndex: idx };
|
|
3739
|
+
}
|
|
3631
3740
|
|
|
3632
3741
|
// src/storage/domains/agents/index.ts
|
|
3633
3742
|
var AgentsPG = class _AgentsPG extends AgentsStorage {
|
|
@@ -4511,6 +4620,98 @@ var AgentsPG = class _AgentsPG extends AgentsStorage {
|
|
|
4511
4620
|
};
|
|
4512
4621
|
}
|
|
4513
4622
|
};
|
|
4623
|
+
var DEFAULT_BATCH_SIZE = 1e3;
|
|
4624
|
+
async function sleep(ms, signal) {
|
|
4625
|
+
if (ms <= 0 || signal?.aborted) return;
|
|
4626
|
+
await new Promise((resolve) => {
|
|
4627
|
+
const timer = setTimeout(() => {
|
|
4628
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4629
|
+
resolve();
|
|
4630
|
+
}, ms);
|
|
4631
|
+
const onAbort = () => {
|
|
4632
|
+
clearTimeout(timer);
|
|
4633
|
+
resolve();
|
|
4634
|
+
};
|
|
4635
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4636
|
+
});
|
|
4637
|
+
}
|
|
4638
|
+
function cutoffFor(policy, anchorType, now = Date.now()) {
|
|
4639
|
+
const cutoffMs = now - parseDuration(policy.maxAge);
|
|
4640
|
+
return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs);
|
|
4641
|
+
}
|
|
4642
|
+
async function runBatchedDelete({
|
|
4643
|
+
deleteBatch,
|
|
4644
|
+
batchSize,
|
|
4645
|
+
options
|
|
4646
|
+
}) {
|
|
4647
|
+
if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
|
|
4648
|
+
throw new Error(`retention batchSize must be a positive integer; received ${batchSize}`);
|
|
4649
|
+
}
|
|
4650
|
+
let deleted = 0;
|
|
4651
|
+
let batches = 0;
|
|
4652
|
+
while (true) {
|
|
4653
|
+
if (options?.signal?.aborted) return { deleted, done: false };
|
|
4654
|
+
if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return { deleted, done: false };
|
|
4655
|
+
let limit = batchSize;
|
|
4656
|
+
if (options?.maxRows !== void 0) {
|
|
4657
|
+
const remaining = options.maxRows - deleted;
|
|
4658
|
+
if (remaining <= 0) return { deleted, done: false };
|
|
4659
|
+
limit = Math.min(limit, remaining);
|
|
4660
|
+
}
|
|
4661
|
+
const affected = await deleteBatch(limit);
|
|
4662
|
+
deleted += affected;
|
|
4663
|
+
batches += 1;
|
|
4664
|
+
if (affected < limit) return { deleted, done: true };
|
|
4665
|
+
if (options?.pauseMs) {
|
|
4666
|
+
await sleep(options.pauseMs, options.signal);
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
async function runPrune({
|
|
4671
|
+
db,
|
|
4672
|
+
domain,
|
|
4673
|
+
targets,
|
|
4674
|
+
options
|
|
4675
|
+
}) {
|
|
4676
|
+
const results = [];
|
|
4677
|
+
const now = Date.now();
|
|
4678
|
+
for (const target of targets) {
|
|
4679
|
+
if (options?.signal?.aborted) {
|
|
4680
|
+
results.push({ domain, table: target.table, deleted: 0, done: false });
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
const cutoff = cutoffFor(target.policy, target.anchorType, now);
|
|
4684
|
+
const batchSize = target.policy.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
4685
|
+
const { deleted, done } = await runBatchedDelete({
|
|
4686
|
+
deleteBatch: (limit) => db.pruneBatch({ tableName: target.table, column: target.column, cutoff, limit }),
|
|
4687
|
+
batchSize,
|
|
4688
|
+
options
|
|
4689
|
+
});
|
|
4690
|
+
results.push({ domain, table: target.table, deleted, done });
|
|
4691
|
+
}
|
|
4692
|
+
return results;
|
|
4693
|
+
}
|
|
4694
|
+
function resolveTargets({
|
|
4695
|
+
policies,
|
|
4696
|
+
descriptor,
|
|
4697
|
+
order
|
|
4698
|
+
}) {
|
|
4699
|
+
const targets = [];
|
|
4700
|
+
for (const key of order) {
|
|
4701
|
+
const policy = policies[key];
|
|
4702
|
+
const entry = descriptor[key];
|
|
4703
|
+
if (!policy || !entry) continue;
|
|
4704
|
+
targets.push({
|
|
4705
|
+
table: entry.table,
|
|
4706
|
+
column: entry.column,
|
|
4707
|
+
anchorType: entry.anchorType ?? "timestamp",
|
|
4708
|
+
policy
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
return targets;
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
// src/storage/domains/background-tasks/index.ts
|
|
4514
4715
|
function getSchemaName3(schema) {
|
|
4515
4716
|
return schema ? `"${schema}"` : '"public"';
|
|
4516
4717
|
}
|
|
@@ -4565,6 +4766,14 @@ var BackgroundTasksPG = class _BackgroundTasksPG extends BackgroundTasksStorage
|
|
|
4565
4766
|
#skipDefaultIndexes;
|
|
4566
4767
|
#indexes;
|
|
4567
4768
|
static MANAGED_TABLES = [TABLE_BACKGROUND_TASKS];
|
|
4769
|
+
/**
|
|
4770
|
+
* Completed background tasks accumulate as dead weight. Anchored on the
|
|
4771
|
+
* timezone-aware `completedAtZ` mirror column: NULL (in-flight) rows are
|
|
4772
|
+
* excluded automatically since `completedAtZ < cutoff` is false for NULL.
|
|
4773
|
+
*/
|
|
4774
|
+
static retentionTables = {
|
|
4775
|
+
backgroundTasks: { table: TABLE_BACKGROUND_TASKS, column: "completedAtZ", indexed: true }
|
|
4776
|
+
};
|
|
4568
4777
|
constructor(config) {
|
|
4569
4778
|
super();
|
|
4570
4779
|
const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
|
|
@@ -4586,6 +4795,40 @@ var BackgroundTasksPG = class _BackgroundTasksPG extends BackgroundTasksStorage
|
|
|
4586
4795
|
await this.createDefaultIndexes();
|
|
4587
4796
|
await this.createCustomIndexes();
|
|
4588
4797
|
}
|
|
4798
|
+
/**
|
|
4799
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
4800
|
+
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
4801
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
4802
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
4803
|
+
* logged and pruning proceeds (correct, just slower).
|
|
4804
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
4805
|
+
* so its supporting index is not part of the default index set.
|
|
4806
|
+
*/
|
|
4807
|
+
async ensureRetentionIndexes(policies) {
|
|
4808
|
+
const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
4809
|
+
for (const [key, entry] of Object.entries(_BackgroundTasksPG.retentionTables)) {
|
|
4810
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
4811
|
+
try {
|
|
4812
|
+
await this.#db.ensureIndex({
|
|
4813
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
4814
|
+
tableName: entry.table,
|
|
4815
|
+
column: entry.column
|
|
4816
|
+
});
|
|
4817
|
+
} catch (error) {
|
|
4818
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
/** Delete completed tasks older than the `backgroundTasks` policy's `maxAge`, batched. */
|
|
4823
|
+
async prune(policies, options) {
|
|
4824
|
+
await this.ensureRetentionIndexes(policies);
|
|
4825
|
+
const targets = resolveTargets({
|
|
4826
|
+
policies,
|
|
4827
|
+
descriptor: _BackgroundTasksPG.retentionTables,
|
|
4828
|
+
order: ["backgroundTasks"]
|
|
4829
|
+
});
|
|
4830
|
+
return runPrune({ db: this.#db, domain: "backgroundTasks", targets, options });
|
|
4831
|
+
}
|
|
4589
4832
|
static getDefaultIndexDefs(schemaPrefix) {
|
|
4590
4833
|
return [
|
|
4591
4834
|
{
|
|
@@ -5349,8 +5592,8 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5349
5592
|
groundTruthSchema: input.groundTruthSchema ?? null,
|
|
5350
5593
|
requestContextSchema: input.requestContextSchema ?? null,
|
|
5351
5594
|
targetType: input.targetType ?? null,
|
|
5352
|
-
targetIds: input.targetIds
|
|
5353
|
-
scorerIds: input.scorerIds
|
|
5595
|
+
targetIds: input.targetIds ?? null,
|
|
5596
|
+
scorerIds: input.scorerIds ?? null,
|
|
5354
5597
|
organizationId: input.organizationId ?? null,
|
|
5355
5598
|
projectId: input.projectId ?? null,
|
|
5356
5599
|
candidateKey: input.candidateKey ?? null,
|
|
@@ -5390,10 +5633,15 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5390
5633
|
);
|
|
5391
5634
|
}
|
|
5392
5635
|
}
|
|
5393
|
-
async getDatasetById({
|
|
5636
|
+
async getDatasetById({
|
|
5637
|
+
id,
|
|
5638
|
+
filters
|
|
5639
|
+
}) {
|
|
5394
5640
|
try {
|
|
5395
5641
|
const tableName = getTableName2({ indexName: TABLE_DATASETS, schemaName: getSchemaName2(this.#schema) });
|
|
5396
|
-
const
|
|
5642
|
+
const { conditions, params } = tenancyWhere(filters, 2);
|
|
5643
|
+
const whereSql = ['"id" = $1', ...conditions].join(" AND ");
|
|
5644
|
+
const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
|
|
5397
5645
|
return result ? this.transformDatasetRow(result) : null;
|
|
5398
5646
|
} catch (error) {
|
|
5399
5647
|
throw new MastraError(
|
|
@@ -5408,7 +5656,7 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5408
5656
|
}
|
|
5409
5657
|
async _doUpdateDataset(args) {
|
|
5410
5658
|
try {
|
|
5411
|
-
const existing = await this.getDatasetById({ id: args.id });
|
|
5659
|
+
const existing = await this.getDatasetById({ id: args.id, filters: args.filters });
|
|
5412
5660
|
if (!existing) {
|
|
5413
5661
|
throw new MastraError({
|
|
5414
5662
|
id: createStorageErrorId("PG", "UPDATE_DATASET", "NOT_FOUND"),
|
|
@@ -5497,7 +5745,7 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5497
5745
|
);
|
|
5498
5746
|
}
|
|
5499
5747
|
}
|
|
5500
|
-
async deleteDataset({ id }) {
|
|
5748
|
+
async deleteDataset({ id, filters }) {
|
|
5501
5749
|
try {
|
|
5502
5750
|
const datasetsTable = getTableName2({ indexName: TABLE_DATASETS, schemaName: getSchemaName2(this.#schema) });
|
|
5503
5751
|
const itemsTable = getTableName2({ indexName: TABLE_DATASET_ITEMS, schemaName: getSchemaName2(this.#schema) });
|
|
@@ -5510,24 +5758,31 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5510
5758
|
indexName: TABLE_EXPERIMENT_RESULTS,
|
|
5511
5759
|
schemaName: getSchemaName2(this.#schema)
|
|
5512
5760
|
});
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
`DELETE FROM ${experimentResultsTable} WHERE "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $1)`,
|
|
5516
|
-
[id]
|
|
5517
|
-
);
|
|
5518
|
-
} catch {
|
|
5519
|
-
}
|
|
5520
|
-
try {
|
|
5521
|
-
await this.#db.client.none(
|
|
5522
|
-
`UPDATE ${experimentsTable} SET "datasetId" = NULL, "datasetVersion" = NULL WHERE "datasetId" = $1`,
|
|
5523
|
-
[id]
|
|
5524
|
-
);
|
|
5525
|
-
} catch {
|
|
5526
|
-
}
|
|
5761
|
+
const { conditions, params } = tenancyWhere(filters, 2);
|
|
5762
|
+
const scopedWhere = ['"id" = $1', ...conditions].join(" AND ");
|
|
5527
5763
|
await this.#db.client.tx(async (t) => {
|
|
5764
|
+
const locked = await t.oneOrNone(`SELECT "id" FROM ${datasetsTable} WHERE ${scopedWhere} FOR UPDATE`, [
|
|
5765
|
+
id,
|
|
5766
|
+
...params
|
|
5767
|
+
]);
|
|
5768
|
+
if (!locked) return;
|
|
5769
|
+
const experimentTablesExist = await t.oneOrNone(
|
|
5770
|
+
`SELECT to_regclass($1) IS NOT NULL AND to_regclass($2) IS NOT NULL AS exists`,
|
|
5771
|
+
[experimentResultsTable, experimentsTable]
|
|
5772
|
+
);
|
|
5773
|
+
if (experimentTablesExist?.exists) {
|
|
5774
|
+
await t.none(
|
|
5775
|
+
`DELETE FROM ${experimentResultsTable} WHERE "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $1)`,
|
|
5776
|
+
[id]
|
|
5777
|
+
);
|
|
5778
|
+
await t.none(
|
|
5779
|
+
`UPDATE ${experimentsTable} SET "datasetId" = NULL, "datasetVersion" = NULL WHERE "datasetId" = $1`,
|
|
5780
|
+
[id]
|
|
5781
|
+
);
|
|
5782
|
+
}
|
|
5528
5783
|
await t.none(`DELETE FROM ${versionsTable} WHERE "datasetId" = $1`, [id]);
|
|
5529
5784
|
await t.none(`DELETE FROM ${itemsTable} WHERE "datasetId" = $1`, [id]);
|
|
5530
|
-
await t.none(`DELETE FROM ${datasetsTable} WHERE
|
|
5785
|
+
await t.none(`DELETE FROM ${datasetsTable} WHERE ${scopedWhere}`, [id, ...params]);
|
|
5531
5786
|
});
|
|
5532
5787
|
} catch (error) {
|
|
5533
5788
|
if (error instanceof MastraError) throw error;
|
|
@@ -5549,7 +5804,7 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5549
5804
|
const queryParams = [];
|
|
5550
5805
|
let paramIndex = 1;
|
|
5551
5806
|
if (args.filters) {
|
|
5552
|
-
const { organizationId, projectId, candidateKey, candidateId } = args.filters;
|
|
5807
|
+
const { organizationId, projectId, candidateKey, candidateId, targetType, targetIds, name } = args.filters;
|
|
5553
5808
|
if (organizationId !== void 0) {
|
|
5554
5809
|
conditions.push(`"organizationId" = $${paramIndex++}`);
|
|
5555
5810
|
queryParams.push(organizationId);
|
|
@@ -5566,6 +5821,18 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
5566
5821
|
conditions.push(`"candidateId" = $${paramIndex++}`);
|
|
5567
5822
|
queryParams.push(candidateId);
|
|
5568
5823
|
}
|
|
5824
|
+
if (targetType !== void 0) {
|
|
5825
|
+
conditions.push(`"targetType" = $${paramIndex++}`);
|
|
5826
|
+
queryParams.push(targetType);
|
|
5827
|
+
}
|
|
5828
|
+
if (targetIds !== void 0 && targetIds.length > 0) {
|
|
5829
|
+
conditions.push(`"targetIds" ?| $${paramIndex++}::text[]`);
|
|
5830
|
+
queryParams.push(targetIds);
|
|
5831
|
+
}
|
|
5832
|
+
if (name !== void 0 && name.length > 0) {
|
|
5833
|
+
conditions.push(`"name" ILIKE $${paramIndex++}`);
|
|
5834
|
+
queryParams.push(`%${name}%`);
|
|
5835
|
+
}
|
|
5569
5836
|
}
|
|
5570
5837
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5571
5838
|
const countResult = await this.#db.client.one(
|
|
@@ -6223,12 +6490,24 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
|
|
|
6223
6490
|
await this.#db.clearTable({ tableName: TABLE_DATASETS });
|
|
6224
6491
|
}
|
|
6225
6492
|
};
|
|
6493
|
+
var DEFAULT_PRUNE_BATCH_SIZE = 1e3;
|
|
6226
6494
|
var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
|
|
6227
6495
|
#db;
|
|
6228
6496
|
#schema;
|
|
6229
6497
|
#skipDefaultIndexes;
|
|
6230
6498
|
#indexes;
|
|
6231
6499
|
static MANAGED_TABLES = [TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS];
|
|
6500
|
+
/**
|
|
6501
|
+
* Experiments prune as whole units: an aged experiment and its result rows go
|
|
6502
|
+
* together, mirroring `deleteExperiment`. Anchored on `completedAt` (not the
|
|
6503
|
+
* `completedAtZ` mirror, which carries a `DEFAULT NOW()` this domain never
|
|
6504
|
+
* overrides — it holds insert time even for running rows). `completedAt` is
|
|
6505
|
+
* written as a UTC ISO string and stays NULL while running, so
|
|
6506
|
+
* `completedAt < cutoff` is false for in-flight experiments.
|
|
6507
|
+
*/
|
|
6508
|
+
static retentionTables = {
|
|
6509
|
+
experiments: { table: TABLE_EXPERIMENTS, column: "completedAt", indexed: true }
|
|
6510
|
+
};
|
|
6232
6511
|
constructor(config) {
|
|
6233
6512
|
super();
|
|
6234
6513
|
const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
|
|
@@ -6267,6 +6546,75 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
|
|
|
6267
6546
|
await this.createDefaultIndexes();
|
|
6268
6547
|
await this.createCustomIndexes();
|
|
6269
6548
|
}
|
|
6549
|
+
/**
|
|
6550
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
6551
|
+
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
6552
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
6553
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
6554
|
+
* logged and pruning proceeds (correct, just slower).
|
|
6555
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
6556
|
+
* so its supporting index is not part of the default index set.
|
|
6557
|
+
*/
|
|
6558
|
+
async ensureRetentionIndexes(policies) {
|
|
6559
|
+
const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
6560
|
+
for (const [key, entry] of Object.entries(_ExperimentsPG.retentionTables)) {
|
|
6561
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
6562
|
+
try {
|
|
6563
|
+
await this.#db.ensureIndex({
|
|
6564
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
6565
|
+
tableName: entry.table,
|
|
6566
|
+
column: entry.column
|
|
6567
|
+
});
|
|
6568
|
+
} catch (error) {
|
|
6569
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
/**
|
|
6574
|
+
* Delete experiments whose `completedAt` is older than the policy's `maxAge`.
|
|
6575
|
+
*
|
|
6576
|
+
* Each batch selects up to `batchSize` aged experiments and deletes their
|
|
6577
|
+
* `experiment_results` rows and the experiment rows in one transaction —
|
|
6578
|
+
* mirroring `deleteExperiment` — so hitting `maxBatches`/`maxRows` or the
|
|
6579
|
+
* abort signal between batches never leaves a run hollow (parent kept,
|
|
6580
|
+
* results gone). NULL `completedAt` (still running) is excluded by the
|
|
6581
|
+
* `< cutoff` predicate. Bounds count whole experiments, not rows.
|
|
6582
|
+
*/
|
|
6583
|
+
async prune(policies, options) {
|
|
6584
|
+
const policy = policies["experiments"];
|
|
6585
|
+
if (!policy || options?.signal?.aborted) {
|
|
6586
|
+
return policy ? [
|
|
6587
|
+
{ domain: "experiments", table: TABLE_EXPERIMENT_RESULTS, deleted: 0, done: false },
|
|
6588
|
+
{ domain: "experiments", table: TABLE_EXPERIMENTS, deleted: 0, done: false }
|
|
6589
|
+
] : [];
|
|
6590
|
+
}
|
|
6591
|
+
await this.ensureRetentionIndexes(policies);
|
|
6592
|
+
const rawCutoff = cutoffFor(policy, "timestamp");
|
|
6593
|
+
const cutoff = rawCutoff instanceof Date ? rawCutoff.toISOString() : rawCutoff;
|
|
6594
|
+
const batchSize = policy.batchSize ?? DEFAULT_PRUNE_BATCH_SIZE;
|
|
6595
|
+
let childDeleted = 0;
|
|
6596
|
+
const parent = await runBatchedDelete({
|
|
6597
|
+
deleteBatch: async (limit) => {
|
|
6598
|
+
const { parents, children } = await this.#db.pruneUnitsBatch({
|
|
6599
|
+
parentTable: TABLE_EXPERIMENTS,
|
|
6600
|
+
parentKey: "id",
|
|
6601
|
+
parentColumn: "completedAt",
|
|
6602
|
+
childTable: TABLE_EXPERIMENT_RESULTS,
|
|
6603
|
+
childForeignKey: "experimentId",
|
|
6604
|
+
cutoff,
|
|
6605
|
+
limit
|
|
6606
|
+
});
|
|
6607
|
+
childDeleted += children;
|
|
6608
|
+
return parents;
|
|
6609
|
+
},
|
|
6610
|
+
batchSize,
|
|
6611
|
+
options
|
|
6612
|
+
});
|
|
6613
|
+
return [
|
|
6614
|
+
{ domain: "experiments", table: TABLE_EXPERIMENT_RESULTS, deleted: childDeleted, done: parent.done },
|
|
6615
|
+
{ domain: "experiments", table: TABLE_EXPERIMENTS, deleted: parent.deleted, done: parent.done }
|
|
6616
|
+
];
|
|
6617
|
+
}
|
|
6270
6618
|
getDefaultIndexDefinitions() {
|
|
6271
6619
|
return [
|
|
6272
6620
|
{ name: "idx_experiments_datasetid", table: TABLE_EXPERIMENTS, columns: ["datasetId"] },
|
|
@@ -6496,10 +6844,15 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
|
|
|
6496
6844
|
);
|
|
6497
6845
|
}
|
|
6498
6846
|
}
|
|
6499
|
-
async getExperimentById({
|
|
6847
|
+
async getExperimentById({
|
|
6848
|
+
id,
|
|
6849
|
+
filters
|
|
6850
|
+
}) {
|
|
6500
6851
|
try {
|
|
6501
6852
|
const tableName = getTableName2({ indexName: TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
|
|
6502
|
-
const
|
|
6853
|
+
const { conditions, params } = tenancyWhere(filters, 2);
|
|
6854
|
+
const whereSql = ['"id" = $1', ...conditions].join(" AND ");
|
|
6855
|
+
const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
|
|
6503
6856
|
return result ? this.transformExperimentRow(result) : null;
|
|
6504
6857
|
} catch (error) {
|
|
6505
6858
|
throw new MastraError(
|
|
@@ -6586,15 +6939,21 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
|
|
|
6586
6939
|
);
|
|
6587
6940
|
}
|
|
6588
6941
|
}
|
|
6589
|
-
async deleteExperiment({ id }) {
|
|
6942
|
+
async deleteExperiment({ id, filters }) {
|
|
6590
6943
|
try {
|
|
6591
6944
|
const resultsTable = getTableName2({
|
|
6592
6945
|
indexName: TABLE_EXPERIMENT_RESULTS,
|
|
6593
6946
|
schemaName: getSchemaName2(this.#schema)
|
|
6594
6947
|
});
|
|
6595
6948
|
const experimentsTable = getTableName2({ indexName: TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
|
|
6596
|
-
|
|
6597
|
-
|
|
6949
|
+
const { conditions, params } = tenancyWhere(filters, 2);
|
|
6950
|
+
const gateSql = `SELECT "id" FROM ${experimentsTable} WHERE ${['"id" = $1', ...conditions].join(" AND ")} FOR UPDATE`;
|
|
6951
|
+
await this.#db.client.tx(async (t) => {
|
|
6952
|
+
const parent = await t.oneOrNone(gateSql, [id, ...params]);
|
|
6953
|
+
if (!parent) return;
|
|
6954
|
+
await t.none(`DELETE FROM ${resultsTable} WHERE "experimentId" = $1`, [id]);
|
|
6955
|
+
await t.none(`DELETE FROM ${experimentsTable} WHERE "id" = $1`, [id]);
|
|
6956
|
+
});
|
|
6598
6957
|
} catch (error) {
|
|
6599
6958
|
throw new MastraError(
|
|
6600
6959
|
{
|
|
@@ -6724,10 +7083,15 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
|
|
|
6724
7083
|
);
|
|
6725
7084
|
}
|
|
6726
7085
|
}
|
|
6727
|
-
async getExperimentResultById({
|
|
7086
|
+
async getExperimentResultById({
|
|
7087
|
+
id,
|
|
7088
|
+
filters
|
|
7089
|
+
}) {
|
|
6728
7090
|
try {
|
|
6729
7091
|
const tableName = getTableName2({ indexName: TABLE_EXPERIMENT_RESULTS, schemaName: getSchemaName2(this.#schema) });
|
|
6730
|
-
const
|
|
7092
|
+
const { conditions, params } = tenancyWhere(filters, 2);
|
|
7093
|
+
const whereSql = ['"id" = $1', ...conditions].join(" AND ");
|
|
7094
|
+
const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
|
|
6731
7095
|
return result ? this.transformExperimentResultRow(result) : null;
|
|
6732
7096
|
} catch (error) {
|
|
6733
7097
|
throw new MastraError(
|
|
@@ -6802,9 +7166,23 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
|
|
|
6802
7166
|
);
|
|
6803
7167
|
}
|
|
6804
7168
|
}
|
|
6805
|
-
async deleteExperimentResults({
|
|
7169
|
+
async deleteExperimentResults({
|
|
7170
|
+
experimentId,
|
|
7171
|
+
filters
|
|
7172
|
+
}) {
|
|
6806
7173
|
try {
|
|
6807
7174
|
const tableName = getTableName2({ indexName: TABLE_EXPERIMENT_RESULTS, schemaName: getSchemaName2(this.#schema) });
|
|
7175
|
+
const experimentsTable = getTableName2({ indexName: TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
|
|
7176
|
+
if (filters?.organizationId !== void 0 || filters?.projectId !== void 0) {
|
|
7177
|
+
const { conditions, params } = tenancyWhere(filters, 2);
|
|
7178
|
+
const gateSql = `SELECT "id" FROM ${experimentsTable} WHERE ${['"id" = $1', ...conditions].join(" AND ")} FOR UPDATE`;
|
|
7179
|
+
await this.#db.client.tx(async (t) => {
|
|
7180
|
+
const parent = await t.oneOrNone(gateSql, [experimentId, ...params]);
|
|
7181
|
+
if (!parent) return;
|
|
7182
|
+
await t.none(`DELETE FROM ${tableName} WHERE "experimentId" = $1`, [experimentId]);
|
|
7183
|
+
});
|
|
7184
|
+
return;
|
|
7185
|
+
}
|
|
6808
7186
|
await this.#db.client.none(`DELETE FROM ${tableName} WHERE "experimentId" = $1`, [experimentId]);
|
|
6809
7187
|
} catch (error) {
|
|
6810
7188
|
throw new MastraError(
|
|
@@ -8435,6 +8813,18 @@ function dedupeMessagesForSave(messages) {
|
|
|
8435
8813
|
}
|
|
8436
8814
|
var MemoryPG = class _MemoryPG extends MemoryStorage {
|
|
8437
8815
|
supportsObservationalMemory = true;
|
|
8816
|
+
/**
|
|
8817
|
+
* Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
|
|
8818
|
+
* on the timezone-aware `createdAtZ` mirror column (kept in sync by triggers),
|
|
8819
|
+
* and are indexed for fast batched deletes. Cascade order is enforced in
|
|
8820
|
+
* `prune()` (children before threads), not here. Observational memory has no
|
|
8821
|
+
* timestamp anchor and is deliberately excluded.
|
|
8822
|
+
*/
|
|
8823
|
+
static retentionTables = {
|
|
8824
|
+
messages: { table: TABLE_MESSAGES, column: "createdAtZ", indexed: true },
|
|
8825
|
+
resources: { table: TABLE_RESOURCES, column: "createdAtZ", indexed: true },
|
|
8826
|
+
threads: { table: TABLE_THREADS, column: "createdAtZ", indexed: true }
|
|
8827
|
+
};
|
|
8438
8828
|
#db;
|
|
8439
8829
|
#schema;
|
|
8440
8830
|
#skipDefaultIndexes;
|
|
@@ -8480,6 +8870,30 @@ var MemoryPG = class _MemoryPG extends MemoryStorage {
|
|
|
8480
8870
|
await this.createDefaultIndexes();
|
|
8481
8871
|
await this.createCustomIndexes();
|
|
8482
8872
|
}
|
|
8873
|
+
/**
|
|
8874
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
8875
|
+
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
8876
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
8877
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
8878
|
+
* logged and pruning proceeds (correct, just slower).
|
|
8879
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
8880
|
+
* so its supporting index is not part of the default index set.
|
|
8881
|
+
*/
|
|
8882
|
+
async ensureRetentionIndexes(policies) {
|
|
8883
|
+
const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
8884
|
+
for (const [key, entry] of Object.entries(_MemoryPG.retentionTables)) {
|
|
8885
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
8886
|
+
try {
|
|
8887
|
+
await this.#db.ensureIndex({
|
|
8888
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
8889
|
+
tableName: entry.table,
|
|
8890
|
+
column: entry.column
|
|
8891
|
+
});
|
|
8892
|
+
} catch (error) {
|
|
8893
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
8894
|
+
}
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8483
8897
|
/**
|
|
8484
8898
|
* Returns default index definitions for the memory domain tables.
|
|
8485
8899
|
* @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
|
|
@@ -8580,6 +8994,76 @@ var MemoryPG = class _MemoryPG extends MemoryStorage {
|
|
|
8580
8994
|
await this.#db.clearTable({ tableName: TABLE_THREADS });
|
|
8581
8995
|
await this.#db.clearTable({ tableName: TABLE_RESOURCES });
|
|
8582
8996
|
}
|
|
8997
|
+
/**
|
|
8998
|
+
* Deletes rows older than the configured `maxAge` per table, in bounded,
|
|
8999
|
+
* batched, cancellable chunks. Tables are pruned children-first (messages and
|
|
9000
|
+
* resources before threads) since PostgreSQL has no FK cascade in this schema.
|
|
9001
|
+
* Unset tables are kept forever.
|
|
9002
|
+
*
|
|
9003
|
+
* When a `messages` policy is set, semantic-recall embeddings for pruned
|
|
9004
|
+
* messages are also swept from same-schema `memory_messages*` vector tables
|
|
9005
|
+
* (best-effort, mirroring `deleteThread`). Embeddings held in an external
|
|
9006
|
+
* vector store are out of reach and must be pruned by the operator.
|
|
9007
|
+
*/
|
|
9008
|
+
async prune(policies, options) {
|
|
9009
|
+
await this.ensureRetentionIndexes(policies);
|
|
9010
|
+
const targets = resolveTargets({
|
|
9011
|
+
policies,
|
|
9012
|
+
descriptor: _MemoryPG.retentionTables,
|
|
9013
|
+
order: ["messages", "resources", "threads"]
|
|
9014
|
+
});
|
|
9015
|
+
const results = await runPrune({ db: this.#db, domain: "memory", targets, options });
|
|
9016
|
+
if (policies["messages"]) {
|
|
9017
|
+
await this.pruneOrphanedVectorRows(policies["messages"], options);
|
|
9018
|
+
}
|
|
9019
|
+
return results;
|
|
9020
|
+
}
|
|
9021
|
+
/**
|
|
9022
|
+
* Best-effort sweep of semantic-recall vector rows whose source message no
|
|
9023
|
+
* longer exists (e.g. it was just pruned), so recall doesn't keep returning
|
|
9024
|
+
* embeddings that resolve to nothing. Only same-schema default vector tables
|
|
9025
|
+
* (`memory_messages*`) are covered — the same set `deleteThread` cleans up.
|
|
9026
|
+
* Failures are logged, never thrown: vector cleanup must not fail the prune.
|
|
9027
|
+
*/
|
|
9028
|
+
async pruneOrphanedVectorRows(policy, options) {
|
|
9029
|
+
try {
|
|
9030
|
+
const schemaName = this.#schema || "public";
|
|
9031
|
+
const vectorTables = await this.#db.client.manyOrNone(
|
|
9032
|
+
`
|
|
9033
|
+
SELECT tablename
|
|
9034
|
+
FROM pg_tables
|
|
9035
|
+
WHERE schemaname = $1
|
|
9036
|
+
AND (tablename = 'memory_messages' OR tablename LIKE 'memory_messages_%')
|
|
9037
|
+
`,
|
|
9038
|
+
[schemaName]
|
|
9039
|
+
);
|
|
9040
|
+
const messagesTable = getTableName4({ indexName: TABLE_MESSAGES, schemaName: getSchemaName4(this.#schema) });
|
|
9041
|
+
for (const { tablename } of vectorTables) {
|
|
9042
|
+
const vectorTableName = getTableName4({ indexName: tablename, schemaName: getSchemaName4(this.#schema) });
|
|
9043
|
+
await runBatchedDelete({
|
|
9044
|
+
deleteBatch: async (limit) => {
|
|
9045
|
+
const result = await this.#db.client.query(
|
|
9046
|
+
`
|
|
9047
|
+
DELETE FROM ${vectorTableName}
|
|
9048
|
+
WHERE ctid IN (
|
|
9049
|
+
SELECT v.ctid FROM ${vectorTableName} v
|
|
9050
|
+
WHERE v.metadata->>'message_id' IS NOT NULL
|
|
9051
|
+
AND NOT EXISTS (SELECT 1 FROM ${messagesTable} m WHERE m.id = v.metadata->>'message_id')
|
|
9052
|
+
LIMIT $1
|
|
9053
|
+
)
|
|
9054
|
+
`,
|
|
9055
|
+
[limit]
|
|
9056
|
+
);
|
|
9057
|
+
return result.rowCount ?? 0;
|
|
9058
|
+
},
|
|
9059
|
+
batchSize: policy.batchSize ?? 1e3,
|
|
9060
|
+
options
|
|
9061
|
+
});
|
|
9062
|
+
}
|
|
9063
|
+
} catch (error) {
|
|
9064
|
+
this.logger?.warn?.("Failed to sweep orphaned semantic-recall vector rows after prune:", error);
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
8583
9067
|
/**
|
|
8584
9068
|
* Normalizes message row from database by applying createdAtZ fallback
|
|
8585
9069
|
*/
|
|
@@ -10891,6 +11375,13 @@ var NotificationsPG = class _NotificationsPG extends NotificationsStorage {
|
|
|
10891
11375
|
#skipDefaultIndexes;
|
|
10892
11376
|
#indexes;
|
|
10893
11377
|
static MANAGED_TABLES = [TABLE_NOTIFICATIONS];
|
|
11378
|
+
/**
|
|
11379
|
+
* Notifications are an append-only event feed that grows unbounded. Single
|
|
11380
|
+
* table, anchored on the timezone-aware `createdAtZ` mirror column.
|
|
11381
|
+
*/
|
|
11382
|
+
static retentionTables = {
|
|
11383
|
+
notifications: { table: TABLE_NOTIFICATIONS, column: "createdAtZ", indexed: true }
|
|
11384
|
+
};
|
|
10894
11385
|
constructor(config) {
|
|
10895
11386
|
super();
|
|
10896
11387
|
const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
|
|
@@ -10907,6 +11398,40 @@ var NotificationsPG = class _NotificationsPG extends NotificationsStorage {
|
|
|
10907
11398
|
await this.createDefaultIndexes();
|
|
10908
11399
|
await this.createCustomIndexes();
|
|
10909
11400
|
}
|
|
11401
|
+
/**
|
|
11402
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
11403
|
+
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
11404
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
11405
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
11406
|
+
* logged and pruning proceeds (correct, just slower).
|
|
11407
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
11408
|
+
* so its supporting index is not part of the default index set.
|
|
11409
|
+
*/
|
|
11410
|
+
async ensureRetentionIndexes(policies) {
|
|
11411
|
+
const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
11412
|
+
for (const [key, entry] of Object.entries(_NotificationsPG.retentionTables)) {
|
|
11413
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
11414
|
+
try {
|
|
11415
|
+
await this.#db.ensureIndex({
|
|
11416
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
11417
|
+
tableName: entry.table,
|
|
11418
|
+
column: entry.column
|
|
11419
|
+
});
|
|
11420
|
+
} catch (error) {
|
|
11421
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
11422
|
+
}
|
|
11423
|
+
}
|
|
11424
|
+
}
|
|
11425
|
+
/** Delete notifications older than the `notifications` policy's `maxAge`, batched. */
|
|
11426
|
+
async prune(policies, options) {
|
|
11427
|
+
await this.ensureRetentionIndexes(policies);
|
|
11428
|
+
const targets = resolveTargets({
|
|
11429
|
+
policies,
|
|
11430
|
+
descriptor: _NotificationsPG.retentionTables,
|
|
11431
|
+
order: ["notifications"]
|
|
11432
|
+
});
|
|
11433
|
+
return runPrune({ db: this.#db, domain: "notifications", targets, options });
|
|
11434
|
+
}
|
|
10910
11435
|
static getDefaultIndexDefs(schemaPrefix) {
|
|
10911
11436
|
return [
|
|
10912
11437
|
{
|
|
@@ -11148,6 +11673,14 @@ var NotificationsPG = class _NotificationsPG extends NotificationsStorage {
|
|
|
11148
11673
|
}
|
|
11149
11674
|
};
|
|
11150
11675
|
var ObservabilityPG = class _ObservabilityPG extends ObservabilityStorage {
|
|
11676
|
+
/**
|
|
11677
|
+
* Retention-eligible tables. The observability domain has a single physical
|
|
11678
|
+
* table (`mastra_ai_spans`); spans anchor on the timezone-aware `startedAtZ`
|
|
11679
|
+
* mirror column and are indexed for fast batched deletes.
|
|
11680
|
+
*/
|
|
11681
|
+
static retentionTables = {
|
|
11682
|
+
spans: { table: TABLE_SPANS, column: "startedAtZ", indexed: true }
|
|
11683
|
+
};
|
|
11151
11684
|
#db;
|
|
11152
11685
|
#schema;
|
|
11153
11686
|
#skipDefaultIndexes;
|
|
@@ -11172,6 +11705,30 @@ var ObservabilityPG = class _ObservabilityPG extends ObservabilityStorage {
|
|
|
11172
11705
|
await this.createDefaultIndexes();
|
|
11173
11706
|
await this.createCustomIndexes();
|
|
11174
11707
|
}
|
|
11708
|
+
/**
|
|
11709
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
11710
|
+
* anchor column so age-based `prune()` deletes stay fast on large span tables.
|
|
11711
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
11712
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
11713
|
+
* logged and pruning proceeds (correct, just slower).
|
|
11714
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
11715
|
+
* so its supporting index is not part of the default index set.
|
|
11716
|
+
*/
|
|
11717
|
+
async ensureRetentionIndexes(policies) {
|
|
11718
|
+
const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
11719
|
+
for (const [key, entry] of Object.entries(_ObservabilityPG.retentionTables)) {
|
|
11720
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
11721
|
+
try {
|
|
11722
|
+
await this.#db.ensureIndex({
|
|
11723
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
11724
|
+
tableName: entry.table,
|
|
11725
|
+
column: entry.column
|
|
11726
|
+
});
|
|
11727
|
+
} catch (error) {
|
|
11728
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
11729
|
+
}
|
|
11730
|
+
}
|
|
11731
|
+
}
|
|
11175
11732
|
/**
|
|
11176
11733
|
* Returns default index definitions for the observability domain tables.
|
|
11177
11734
|
* @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
|
|
@@ -11316,6 +11873,19 @@ var ObservabilityPG = class _ObservabilityPG extends ObservabilityStorage {
|
|
|
11316
11873
|
async dangerouslyClearAll() {
|
|
11317
11874
|
await this.#db.clearTable({ tableName: TABLE_SPANS });
|
|
11318
11875
|
}
|
|
11876
|
+
/**
|
|
11877
|
+
* Deletes spans older than the configured `maxAge`, in bounded, batched,
|
|
11878
|
+
* cancellable chunks. Unset = keep forever.
|
|
11879
|
+
*/
|
|
11880
|
+
async prune(policies, options) {
|
|
11881
|
+
await this.ensureRetentionIndexes(policies);
|
|
11882
|
+
const targets = resolveTargets({
|
|
11883
|
+
policies,
|
|
11884
|
+
descriptor: _ObservabilityPG.retentionTables,
|
|
11885
|
+
order: ["spans"]
|
|
11886
|
+
});
|
|
11887
|
+
return runPrune({ db: this.#db, domain: "observability", targets, options });
|
|
11888
|
+
}
|
|
11319
11889
|
get tracingStrategy() {
|
|
11320
11890
|
return {
|
|
11321
11891
|
preferred: "batch-with-updates",
|
|
@@ -14012,6 +14582,78 @@ async function setupPartitioning(client, schema, options = {}) {
|
|
|
14012
14582
|
return "native";
|
|
14013
14583
|
}
|
|
14014
14584
|
}
|
|
14585
|
+
function retentionCutoff(policy, now = Date.now()) {
|
|
14586
|
+
return new Date(now - parseDuration(policy.maxAge));
|
|
14587
|
+
}
|
|
14588
|
+
async function listChildPartitions(client, schema, table) {
|
|
14589
|
+
const rows = await client.manyOrNone(
|
|
14590
|
+
`SELECT c.relname AS "name", pg_get_expr(c.relpartbound, c.oid) AS "bound"
|
|
14591
|
+
FROM pg_inherits i
|
|
14592
|
+
JOIN pg_class c ON c.oid = i.inhrelid
|
|
14593
|
+
JOIN pg_class p ON p.oid = i.inhparent
|
|
14594
|
+
JOIN pg_namespace n ON n.oid = p.relnamespace
|
|
14595
|
+
WHERE n.nspname = $1 AND p.relname = $2`,
|
|
14596
|
+
[schema, table]
|
|
14597
|
+
);
|
|
14598
|
+
const partitions = [];
|
|
14599
|
+
for (const row of rows ?? []) {
|
|
14600
|
+
const match = /TO \('([^']+)'\)/.exec(row.bound ?? "");
|
|
14601
|
+
if (!match?.[1]) continue;
|
|
14602
|
+
const upperBound = new Date(match[1]);
|
|
14603
|
+
if (Number.isNaN(upperBound.getTime())) continue;
|
|
14604
|
+
partitions.push({ name: row.name, upperBound });
|
|
14605
|
+
}
|
|
14606
|
+
return partitions.sort((a, b) => a.upperBound.getTime() - b.upperBound.getTime());
|
|
14607
|
+
}
|
|
14608
|
+
async function prunePartitionedTable({
|
|
14609
|
+
client,
|
|
14610
|
+
schema,
|
|
14611
|
+
table,
|
|
14612
|
+
cutoff,
|
|
14613
|
+
options
|
|
14614
|
+
}) {
|
|
14615
|
+
const partitions = await listChildPartitions(client, schema, table);
|
|
14616
|
+
const droppable = partitions.filter((p) => p.upperBound.getTime() <= cutoff.getTime());
|
|
14617
|
+
let deleted = 0;
|
|
14618
|
+
let batches = 0;
|
|
14619
|
+
for (const partition of droppable) {
|
|
14620
|
+
if (options?.signal?.aborted) return { deleted, done: false };
|
|
14621
|
+
if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return { deleted, done: false };
|
|
14622
|
+
if (options?.maxRows !== void 0 && deleted >= options.maxRows) return { deleted, done: false };
|
|
14623
|
+
const child = qualifiedName(schema, partition.name);
|
|
14624
|
+
const parent = qualifiedTable(schema, table);
|
|
14625
|
+
const row = await client.one(`SELECT count(*)::int AS "n" FROM ${child}`);
|
|
14626
|
+
await client.none(`ALTER TABLE ${parent} DETACH PARTITION ${child}`);
|
|
14627
|
+
await client.none(`DROP TABLE IF EXISTS ${child}`);
|
|
14628
|
+
deleted += row.n;
|
|
14629
|
+
batches += 1;
|
|
14630
|
+
}
|
|
14631
|
+
return { deleted, done: true };
|
|
14632
|
+
}
|
|
14633
|
+
async function pruneTimescaleTable({
|
|
14634
|
+
client,
|
|
14635
|
+
schema,
|
|
14636
|
+
table,
|
|
14637
|
+
cutoff,
|
|
14638
|
+
options
|
|
14639
|
+
}) {
|
|
14640
|
+
if (options?.signal?.aborted || options?.maxBatches === 0) return { deleted: 0, done: false };
|
|
14641
|
+
const tableExpr = qualifiedTable(schema, table);
|
|
14642
|
+
const chunks = await client.manyOrNone(
|
|
14643
|
+
`SELECT show_chunks($1::regclass, older_than => $2::timestamptz)::text AS "chunk"`,
|
|
14644
|
+
[tableExpr, cutoff.toISOString()]
|
|
14645
|
+
);
|
|
14646
|
+
let deleted = 0;
|
|
14647
|
+
for (const { chunk } of chunks ?? []) {
|
|
14648
|
+
const row = await client.one(`SELECT count(*)::int AS "n" FROM ${chunk}`);
|
|
14649
|
+
deleted += row.n;
|
|
14650
|
+
}
|
|
14651
|
+
await client.none(`SELECT drop_chunks($1::regclass, older_than => $2::timestamptz)`, [
|
|
14652
|
+
tableExpr,
|
|
14653
|
+
cutoff.toISOString()
|
|
14654
|
+
]);
|
|
14655
|
+
return { deleted, done: true };
|
|
14656
|
+
}
|
|
14015
14657
|
function applyScoreFilters(acc, filters) {
|
|
14016
14658
|
applyCommonFilters(acc, filters);
|
|
14017
14659
|
applySingleOrArrayFilter(acc, "scorerId", filters?.scorerId);
|
|
@@ -14758,7 +15400,7 @@ function wrapError(op, error, details) {
|
|
|
14758
15400
|
error
|
|
14759
15401
|
);
|
|
14760
15402
|
}
|
|
14761
|
-
var ObservabilityStoragePostgresVNext = class extends ObservabilityStorage {
|
|
15403
|
+
var ObservabilityStoragePostgresVNext = class _ObservabilityStoragePostgresVNext extends ObservabilityStorage {
|
|
14762
15404
|
#client;
|
|
14763
15405
|
#schema;
|
|
14764
15406
|
#partitioning;
|
|
@@ -14842,6 +15484,50 @@ var ObservabilityStoragePostgresVNext = class extends ObservabilityStorage {
|
|
|
14842
15484
|
get partitionMode() {
|
|
14843
15485
|
return this.#partitionMode;
|
|
14844
15486
|
}
|
|
15487
|
+
// -------------------------------------------------------------------------
|
|
15488
|
+
// Retention
|
|
15489
|
+
// -------------------------------------------------------------------------
|
|
15490
|
+
/**
|
|
15491
|
+
* All five signal tables are insert-only growth tables. The anchor column is
|
|
15492
|
+
* each table's partition / chunk key, so age-based expiry drops whole day
|
|
15493
|
+
* partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
|
|
15494
|
+
* reflects that expiry never scans — it acts on partition bounds.
|
|
15495
|
+
*/
|
|
15496
|
+
static retentionTables = {
|
|
15497
|
+
spans: { table: TABLE_SPAN_EVENTS, column: "endedAt", indexed: true },
|
|
15498
|
+
metrics: { table: TABLE_METRIC_EVENTS, column: "timestamp", indexed: true },
|
|
15499
|
+
logs: { table: TABLE_LOG_EVENTS, column: "timestamp", indexed: true },
|
|
15500
|
+
scores: { table: TABLE_SCORE_EVENTS, column: "timestamp", indexed: true },
|
|
15501
|
+
feedback: { table: TABLE_FEEDBACK_EVENTS, column: "timestamp", indexed: true }
|
|
15502
|
+
};
|
|
15503
|
+
/**
|
|
15504
|
+
* Expire signal events older than each table's `maxAge` by dropping whole
|
|
15505
|
+
* day partitions (native / pg_partman) or chunks (Timescale). Only
|
|
15506
|
+
* partitions wholly older than the cutoff are dropped, so the effective
|
|
15507
|
+
* granularity is one day. Each partition drop counts as one batch for
|
|
15508
|
+
* `maxBatches` / `maxRows` / abort-signal purposes; `deleted` reports the
|
|
15509
|
+
* row count of the dropped partitions.
|
|
15510
|
+
*/
|
|
15511
|
+
async prune(policies, options) {
|
|
15512
|
+
return this.#run("VNEXT_PRUNE", async () => {
|
|
15513
|
+
const mode = this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning);
|
|
15514
|
+
const results = [];
|
|
15515
|
+
const now = Date.now();
|
|
15516
|
+
for (const [key, entry] of Object.entries(_ObservabilityStoragePostgresVNext.retentionTables)) {
|
|
15517
|
+
const policy = policies[key];
|
|
15518
|
+
if (!policy) continue;
|
|
15519
|
+
if (options?.signal?.aborted) {
|
|
15520
|
+
results.push({ domain: "observability", table: entry.table, deleted: 0, done: false });
|
|
15521
|
+
continue;
|
|
15522
|
+
}
|
|
15523
|
+
const cutoff = retentionCutoff(policy, now);
|
|
15524
|
+
const args = { client: this.#client, schema: this.#schema, table: entry.table, cutoff, options };
|
|
15525
|
+
const outcome = mode === "timescale" ? await pruneTimescaleTable(args) : await prunePartitionedTable(args);
|
|
15526
|
+
results.push({ domain: "observability", table: entry.table, deleted: outcome.deleted, done: outcome.done });
|
|
15527
|
+
}
|
|
15528
|
+
return results;
|
|
15529
|
+
});
|
|
15530
|
+
}
|
|
14845
15531
|
get observabilityStrategy() {
|
|
14846
15532
|
return { preferred: "insert-only", supported: ["insert-only"] };
|
|
14847
15533
|
}
|
|
@@ -15786,6 +16472,15 @@ var SchedulesPG = class _SchedulesPG extends SchedulesStorage {
|
|
|
15786
16472
|
#indexes;
|
|
15787
16473
|
/** Tables managed by this domain */
|
|
15788
16474
|
static MANAGED_TABLES = [TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS];
|
|
16475
|
+
/**
|
|
16476
|
+
* The fire/run history (`schedule_triggers`, one row per fire) is the growth
|
|
16477
|
+
* table; schedule definitions are config and excluded. Anchored on
|
|
16478
|
+
* `actual_fire_at`, a bigint epoch-ms column (numeric comparison, not
|
|
16479
|
+
* timestamptz).
|
|
16480
|
+
*/
|
|
16481
|
+
static retentionTables = {
|
|
16482
|
+
triggers: { table: TABLE_SCHEDULE_TRIGGERS, column: "actual_fire_at", indexed: true, anchorType: "epoch-ms" }
|
|
16483
|
+
};
|
|
15789
16484
|
constructor(config) {
|
|
15790
16485
|
super();
|
|
15791
16486
|
const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
|
|
@@ -15807,6 +16502,43 @@ var SchedulesPG = class _SchedulesPG extends SchedulesStorage {
|
|
|
15807
16502
|
await this.createDefaultIndexes();
|
|
15808
16503
|
await this.createCustomIndexes();
|
|
15809
16504
|
}
|
|
16505
|
+
/**
|
|
16506
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
16507
|
+
* anchor column so age-based `prune()` deletes stay fast. The default
|
|
16508
|
+
* composite index leads with `schedule_id`, so a bare `actual_fire_at` range
|
|
16509
|
+
* scan can't use it. Called from the prune path (not init) so only
|
|
16510
|
+
* deployments that configure retention pay the index's write/disk overhead.
|
|
16511
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
16512
|
+
* so its supporting index is not part of the default index set.
|
|
16513
|
+
*/
|
|
16514
|
+
async ensureRetentionIndexes(policies) {
|
|
16515
|
+
const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
16516
|
+
for (const [key, entry] of Object.entries(_SchedulesPG.retentionTables)) {
|
|
16517
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
16518
|
+
try {
|
|
16519
|
+
await this.#db.ensureIndex({
|
|
16520
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
16521
|
+
tableName: entry.table,
|
|
16522
|
+
column: entry.column
|
|
16523
|
+
});
|
|
16524
|
+
} catch (error) {
|
|
16525
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
16526
|
+
}
|
|
16527
|
+
}
|
|
16528
|
+
}
|
|
16529
|
+
/**
|
|
16530
|
+
* Delete trigger (fire history) rows whose `actual_fire_at` is older than the
|
|
16531
|
+
* `triggers` policy's `maxAge`, batched. Schedule definitions are never pruned.
|
|
16532
|
+
*/
|
|
16533
|
+
async prune(policies, options) {
|
|
16534
|
+
await this.ensureRetentionIndexes(policies);
|
|
16535
|
+
const targets = resolveTargets({
|
|
16536
|
+
policies,
|
|
16537
|
+
descriptor: _SchedulesPG.retentionTables,
|
|
16538
|
+
order: ["triggers"]
|
|
16539
|
+
});
|
|
16540
|
+
return runPrune({ db: this.#db, domain: "schedules", targets, options });
|
|
16541
|
+
}
|
|
15810
16542
|
/**
|
|
15811
16543
|
* Returns default index definitions for the schedules domain.
|
|
15812
16544
|
* @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
|
|
@@ -16139,6 +16871,11 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16139
16871
|
tableName: TABLE_SCORER_DEFINITIONS,
|
|
16140
16872
|
schema: TABLE_SCHEMAS[TABLE_SCORER_DEFINITIONS]
|
|
16141
16873
|
});
|
|
16874
|
+
await this.#db.alterTable({
|
|
16875
|
+
tableName: TABLE_SCORER_DEFINITIONS,
|
|
16876
|
+
schema: TABLE_SCHEMAS[TABLE_SCORER_DEFINITIONS],
|
|
16877
|
+
ifNotExists: ["organizationId", "projectId"]
|
|
16878
|
+
});
|
|
16142
16879
|
await this.#db.createTable({
|
|
16143
16880
|
tableName: TABLE_SCORER_DEFINITION_VERSIONS,
|
|
16144
16881
|
schema: TABLE_SCHEMAS[TABLE_SCORER_DEFINITION_VERSIONS]
|
|
@@ -16194,14 +16931,16 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16194
16931
|
const nowIso = now.toISOString();
|
|
16195
16932
|
await this.#db.client.none(
|
|
16196
16933
|
`INSERT INTO ${tableName} (
|
|
16197
|
-
id, status, "activeVersionId", "authorId", metadata,
|
|
16934
|
+
id, status, "activeVersionId", "authorId", "organizationId", "projectId", metadata,
|
|
16198
16935
|
"createdAt", "createdAtZ", "updatedAt", "updatedAtZ"
|
|
16199
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
16936
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
|
16200
16937
|
[
|
|
16201
16938
|
scorerDefinition.id,
|
|
16202
16939
|
"draft",
|
|
16203
16940
|
null,
|
|
16204
16941
|
scorerDefinition.authorId ?? null,
|
|
16942
|
+
scorerDefinition.organizationId ?? null,
|
|
16943
|
+
scorerDefinition.projectId ?? null,
|
|
16205
16944
|
scorerDefinition.metadata ? JSON.stringify(scorerDefinition.metadata) : null,
|
|
16206
16945
|
nowIso,
|
|
16207
16946
|
nowIso,
|
|
@@ -16209,7 +16948,14 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16209
16948
|
nowIso
|
|
16210
16949
|
]
|
|
16211
16950
|
);
|
|
16212
|
-
const {
|
|
16951
|
+
const {
|
|
16952
|
+
id: _id,
|
|
16953
|
+
authorId: _authorId,
|
|
16954
|
+
organizationId: _organizationId,
|
|
16955
|
+
projectId: _projectId,
|
|
16956
|
+
metadata: _metadata,
|
|
16957
|
+
...snapshotConfig
|
|
16958
|
+
} = scorerDefinition;
|
|
16213
16959
|
const versionId = crypto.randomUUID();
|
|
16214
16960
|
await this.createVersion({
|
|
16215
16961
|
id: versionId,
|
|
@@ -16224,6 +16970,8 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16224
16970
|
status: "draft",
|
|
16225
16971
|
activeVersionId: void 0,
|
|
16226
16972
|
authorId: scorerDefinition.authorId,
|
|
16973
|
+
organizationId: scorerDefinition.organizationId,
|
|
16974
|
+
projectId: scorerDefinition.projectId,
|
|
16227
16975
|
metadata: scorerDefinition.metadata,
|
|
16228
16976
|
createdAt: now,
|
|
16229
16977
|
updatedAt: now
|
|
@@ -16337,7 +17085,16 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16337
17085
|
}
|
|
16338
17086
|
}
|
|
16339
17087
|
async list(args) {
|
|
16340
|
-
const {
|
|
17088
|
+
const {
|
|
17089
|
+
page = 0,
|
|
17090
|
+
perPage: perPageInput,
|
|
17091
|
+
orderBy,
|
|
17092
|
+
authorId,
|
|
17093
|
+
organizationId,
|
|
17094
|
+
projectId,
|
|
17095
|
+
metadata,
|
|
17096
|
+
status
|
|
17097
|
+
} = args || {};
|
|
16341
17098
|
const { field, direction } = this.parseOrderBy(orderBy);
|
|
16342
17099
|
if (page < 0) {
|
|
16343
17100
|
throw new MastraError(
|
|
@@ -16365,6 +17122,14 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16365
17122
|
conditions.push(`"authorId" = $${paramIdx++}`);
|
|
16366
17123
|
queryParams.push(authorId);
|
|
16367
17124
|
}
|
|
17125
|
+
if (organizationId !== void 0) {
|
|
17126
|
+
conditions.push(`"organizationId" = $${paramIdx++}`);
|
|
17127
|
+
queryParams.push(organizationId);
|
|
17128
|
+
}
|
|
17129
|
+
if (projectId !== void 0) {
|
|
17130
|
+
conditions.push(`"projectId" = $${paramIdx++}`);
|
|
17131
|
+
queryParams.push(projectId);
|
|
17132
|
+
}
|
|
16368
17133
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
16369
17134
|
conditions.push(`metadata @> $${paramIdx++}::jsonb`);
|
|
16370
17135
|
queryParams.push(JSON.stringify(metadata));
|
|
@@ -16691,6 +17456,8 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16691
17456
|
status: row.status,
|
|
16692
17457
|
activeVersionId: row.activeVersionId,
|
|
16693
17458
|
authorId: row.authorId,
|
|
17459
|
+
organizationId: row.organizationId,
|
|
17460
|
+
projectId: row.projectId,
|
|
16694
17461
|
metadata: parseJsonResilient(row.metadata),
|
|
16695
17462
|
createdAt: new Date(row.createdAtZ || row.createdAt),
|
|
16696
17463
|
updatedAt: new Date(row.updatedAtZ || row.updatedAt)
|
|
@@ -16715,6 +17482,18 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
|
|
|
16715
17482
|
};
|
|
16716
17483
|
}
|
|
16717
17484
|
};
|
|
17485
|
+
function applyTenancyFilters(conditions, queryParams, startIndex, filters) {
|
|
17486
|
+
let paramIndex = startIndex;
|
|
17487
|
+
if (filters?.organizationId !== void 0) {
|
|
17488
|
+
conditions.push(`"organizationId" = $${paramIndex++}`);
|
|
17489
|
+
queryParams.push(filters.organizationId);
|
|
17490
|
+
}
|
|
17491
|
+
if (filters?.projectId !== void 0) {
|
|
17492
|
+
conditions.push(`"projectId" = $${paramIndex++}`);
|
|
17493
|
+
queryParams.push(filters.projectId);
|
|
17494
|
+
}
|
|
17495
|
+
return paramIndex;
|
|
17496
|
+
}
|
|
16718
17497
|
function getSchemaName6(schema) {
|
|
16719
17498
|
return schema ? `"${schema}"` : '"public"';
|
|
16720
17499
|
}
|
|
@@ -16737,6 +17516,13 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
16737
17516
|
#indexes;
|
|
16738
17517
|
/** Tables managed by this domain */
|
|
16739
17518
|
static MANAGED_TABLES = [TABLE_SCORERS];
|
|
17519
|
+
/**
|
|
17520
|
+
* Scorer results accumulate as evals run. Single table, anchored on the
|
|
17521
|
+
* timezone-aware `createdAtZ` mirror column (kept in sync by triggers).
|
|
17522
|
+
*/
|
|
17523
|
+
static retentionTables = {
|
|
17524
|
+
scorers: { table: TABLE_SCORERS, column: "createdAtZ", indexed: true }
|
|
17525
|
+
};
|
|
16740
17526
|
constructor(config) {
|
|
16741
17527
|
super();
|
|
16742
17528
|
const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
|
|
@@ -16750,11 +17536,35 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
16750
17536
|
await this.#db.alterTable({
|
|
16751
17537
|
tableName: TABLE_SCORERS,
|
|
16752
17538
|
schema: TABLE_SCHEMAS[TABLE_SCORERS],
|
|
16753
|
-
ifNotExists: ["spanId", "requestContext"]
|
|
17539
|
+
ifNotExists: ["spanId", "requestContext", "organizationId", "projectId", "batchId", "datasetId", "datasetItemId"]
|
|
16754
17540
|
});
|
|
16755
17541
|
await this.createDefaultIndexes();
|
|
16756
17542
|
await this.createCustomIndexes();
|
|
16757
17543
|
}
|
|
17544
|
+
/**
|
|
17545
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
17546
|
+
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
17547
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
17548
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
17549
|
+
* logged and pruning proceeds (correct, just slower).
|
|
17550
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
17551
|
+
* so its supporting index is not part of the default index set.
|
|
17552
|
+
*/
|
|
17553
|
+
async ensureRetentionIndexes(policies) {
|
|
17554
|
+
const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
17555
|
+
for (const [key, entry] of Object.entries(_ScoresPG.retentionTables)) {
|
|
17556
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
17557
|
+
try {
|
|
17558
|
+
await this.#db.ensureIndex({
|
|
17559
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
17560
|
+
tableName: entry.table,
|
|
17561
|
+
column: entry.column
|
|
17562
|
+
});
|
|
17563
|
+
} catch (error) {
|
|
17564
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
17565
|
+
}
|
|
17566
|
+
}
|
|
17567
|
+
}
|
|
16758
17568
|
/**
|
|
16759
17569
|
* Returns default index definitions for the scores domain tables.
|
|
16760
17570
|
* @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
|
|
@@ -16829,6 +17639,16 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
16829
17639
|
async dangerouslyClearAll() {
|
|
16830
17640
|
await this.#db.clearTable({ tableName: TABLE_SCORERS });
|
|
16831
17641
|
}
|
|
17642
|
+
/** Delete scorer results older than the `scorers` policy's `maxAge`, batched. */
|
|
17643
|
+
async prune(policies, options) {
|
|
17644
|
+
await this.ensureRetentionIndexes(policies);
|
|
17645
|
+
const targets = resolveTargets({
|
|
17646
|
+
policies,
|
|
17647
|
+
descriptor: _ScoresPG.retentionTables,
|
|
17648
|
+
order: ["scorers"]
|
|
17649
|
+
});
|
|
17650
|
+
return runPrune({ db: this.#db, domain: "scores", targets, options });
|
|
17651
|
+
}
|
|
16832
17652
|
async getScoreById({ id }) {
|
|
16833
17653
|
try {
|
|
16834
17654
|
const result = await this.#db.client.oneOrNone(
|
|
@@ -16852,7 +17672,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
16852
17672
|
pagination,
|
|
16853
17673
|
entityId,
|
|
16854
17674
|
entityType,
|
|
16855
|
-
source
|
|
17675
|
+
source,
|
|
17676
|
+
filters
|
|
16856
17677
|
}) {
|
|
16857
17678
|
try {
|
|
16858
17679
|
const conditions = [`"scorerId" = $1`];
|
|
@@ -16870,6 +17691,7 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
16870
17691
|
conditions.push(`"source" = $${paramIndex++}`);
|
|
16871
17692
|
queryParams.push(source);
|
|
16872
17693
|
}
|
|
17694
|
+
paramIndex = applyTenancyFilters(conditions, queryParams, paramIndex, filters);
|
|
16873
17695
|
const whereClause = conditions.join(" AND ");
|
|
16874
17696
|
const total = await this.#db.client.oneOrNone(
|
|
16875
17697
|
`SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
|
|
@@ -16983,12 +17805,17 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
16983
17805
|
}
|
|
16984
17806
|
async listScoresByRunId({
|
|
16985
17807
|
runId,
|
|
16986
|
-
pagination
|
|
17808
|
+
pagination,
|
|
17809
|
+
filters
|
|
16987
17810
|
}) {
|
|
16988
17811
|
try {
|
|
17812
|
+
const conditions = [`"runId" = $1`];
|
|
17813
|
+
const queryParams = [runId];
|
|
17814
|
+
let paramIndex = applyTenancyFilters(conditions, queryParams, 2, filters);
|
|
17815
|
+
const whereClause = conditions.join(" AND ");
|
|
16989
17816
|
const total = await this.#db.client.oneOrNone(
|
|
16990
|
-
`SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE
|
|
16991
|
-
|
|
17817
|
+
`SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
|
|
17818
|
+
queryParams
|
|
16992
17819
|
);
|
|
16993
17820
|
const { page, perPage: perPageInput } = pagination;
|
|
16994
17821
|
const perPage = normalizePerPage(perPageInput, 100);
|
|
@@ -17007,8 +17834,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
17007
17834
|
const limitValue = perPageInput === false ? Number(total?.count) : perPage;
|
|
17008
17835
|
const end = perPageInput === false ? Number(total?.count) : start + perPage;
|
|
17009
17836
|
const result = await this.#db.client.manyOrNone(
|
|
17010
|
-
`SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "
|
|
17011
|
-
[
|
|
17837
|
+
`SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
17838
|
+
[...queryParams, limitValue, start]
|
|
17012
17839
|
);
|
|
17013
17840
|
return {
|
|
17014
17841
|
pagination: {
|
|
@@ -17033,12 +17860,17 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
17033
17860
|
async listScoresByEntityId({
|
|
17034
17861
|
entityId,
|
|
17035
17862
|
entityType,
|
|
17036
|
-
pagination
|
|
17863
|
+
pagination,
|
|
17864
|
+
filters
|
|
17037
17865
|
}) {
|
|
17038
17866
|
try {
|
|
17867
|
+
const conditions = [`"entityId" = $1`, `"entityType" = $2`];
|
|
17868
|
+
const queryParams = [entityId, entityType];
|
|
17869
|
+
let paramIndex = applyTenancyFilters(conditions, queryParams, 3, filters);
|
|
17870
|
+
const whereClause = conditions.join(" AND ");
|
|
17039
17871
|
const total = await this.#db.client.oneOrNone(
|
|
17040
|
-
`SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE
|
|
17041
|
-
|
|
17872
|
+
`SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
|
|
17873
|
+
queryParams
|
|
17042
17874
|
);
|
|
17043
17875
|
const { page, perPage: perPageInput } = pagination;
|
|
17044
17876
|
const perPage = normalizePerPage(perPageInput, 100);
|
|
@@ -17057,8 +17889,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
17057
17889
|
const limitValue = perPageInput === false ? Number(total?.count) : perPage;
|
|
17058
17890
|
const end = perPageInput === false ? Number(total?.count) : start + perPage;
|
|
17059
17891
|
const result = await this.#db.client.manyOrNone(
|
|
17060
|
-
`SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE
|
|
17061
|
-
[
|
|
17892
|
+
`SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
17893
|
+
[...queryParams, limitValue, start]
|
|
17062
17894
|
);
|
|
17063
17895
|
return {
|
|
17064
17896
|
pagination: {
|
|
@@ -17083,13 +17915,18 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
17083
17915
|
async listScoresBySpan({
|
|
17084
17916
|
traceId,
|
|
17085
17917
|
spanId,
|
|
17086
|
-
pagination
|
|
17918
|
+
pagination,
|
|
17919
|
+
filters
|
|
17087
17920
|
}) {
|
|
17088
17921
|
try {
|
|
17089
17922
|
const tableName = getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) });
|
|
17923
|
+
const conditions = [`"traceId" = $1`, `"spanId" = $2`];
|
|
17924
|
+
const queryParams = [traceId, spanId];
|
|
17925
|
+
let paramIndex = applyTenancyFilters(conditions, queryParams, 3, filters);
|
|
17926
|
+
const whereClause = conditions.join(" AND ");
|
|
17090
17927
|
const countSQLResult = await this.#db.client.oneOrNone(
|
|
17091
|
-
`SELECT COUNT(*) as count FROM ${tableName} WHERE
|
|
17092
|
-
|
|
17928
|
+
`SELECT COUNT(*) as count FROM ${tableName} WHERE ${whereClause}`,
|
|
17929
|
+
queryParams
|
|
17093
17930
|
);
|
|
17094
17931
|
const total = Number(countSQLResult?.count ?? 0);
|
|
17095
17932
|
const { page, perPage: perPageInput } = pagination;
|
|
@@ -17098,8 +17935,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
|
|
|
17098
17935
|
const limitValue = perPageInput === false ? total : perPage;
|
|
17099
17936
|
const end = perPageInput === false ? total : start + perPage;
|
|
17100
17937
|
const result = await this.#db.client.manyOrNone(
|
|
17101
|
-
`SELECT * FROM ${tableName} WHERE
|
|
17102
|
-
[
|
|
17938
|
+
`SELECT * FROM ${tableName} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
17939
|
+
[...queryParams, limitValue, start]
|
|
17103
17940
|
);
|
|
17104
17941
|
const hasMore = end < total;
|
|
17105
17942
|
const scores = result.map((row) => transformScoreRow(row)) ?? [];
|
|
@@ -18150,6 +18987,14 @@ var WorkflowsPG = class _WorkflowsPG extends WorkflowsStorage {
|
|
|
18150
18987
|
#indexes;
|
|
18151
18988
|
/** Tables managed by this domain */
|
|
18152
18989
|
static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT];
|
|
18990
|
+
/**
|
|
18991
|
+
* Workflow run snapshots accumulate as runs execute. Anchored on the
|
|
18992
|
+
* timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
|
|
18993
|
+
* long-running runs are not pruned by start age.
|
|
18994
|
+
*/
|
|
18995
|
+
static retentionTables = {
|
|
18996
|
+
workflowSnapshot: { table: TABLE_WORKFLOW_SNAPSHOT, column: "updatedAtZ", indexed: true }
|
|
18997
|
+
};
|
|
18153
18998
|
constructor(config) {
|
|
18154
18999
|
super();
|
|
18155
19000
|
const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
|
|
@@ -18221,6 +19066,40 @@ var WorkflowsPG = class _WorkflowsPG extends WorkflowsStorage {
|
|
|
18221
19066
|
await this.createDefaultIndexes();
|
|
18222
19067
|
await this.createCustomIndexes();
|
|
18223
19068
|
}
|
|
19069
|
+
/**
|
|
19070
|
+
* Lazily ensures a btree index exists on each configured policy's retention
|
|
19071
|
+
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
19072
|
+
* Called from the prune path (not init) so only deployments that configure
|
|
19073
|
+
* retention pay the index's write/disk overhead. Best-effort: failures are
|
|
19074
|
+
* logged and pruning proceeds (correct, just slower).
|
|
19075
|
+
* Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
|
|
19076
|
+
* so its supporting index is not part of the default index set.
|
|
19077
|
+
*/
|
|
19078
|
+
async ensureRetentionIndexes(policies) {
|
|
19079
|
+
const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
|
|
19080
|
+
for (const [key, entry] of Object.entries(_WorkflowsPG.retentionTables)) {
|
|
19081
|
+
if (!entry.indexed || !policies[key]) continue;
|
|
19082
|
+
try {
|
|
19083
|
+
await this.#db.ensureIndex({
|
|
19084
|
+
indexName: `${prefix}mastra_${key}_retention_idx`,
|
|
19085
|
+
tableName: entry.table,
|
|
19086
|
+
column: entry.column
|
|
19087
|
+
});
|
|
19088
|
+
} catch (error) {
|
|
19089
|
+
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
19090
|
+
}
|
|
19091
|
+
}
|
|
19092
|
+
}
|
|
19093
|
+
/** Delete workflow run snapshots older than the `workflowSnapshot` policy's `maxAge`, batched. */
|
|
19094
|
+
async prune(policies, options) {
|
|
19095
|
+
await this.ensureRetentionIndexes(policies);
|
|
19096
|
+
const targets = resolveTargets({
|
|
19097
|
+
policies,
|
|
19098
|
+
descriptor: _WorkflowsPG.retentionTables,
|
|
19099
|
+
order: ["workflowSnapshot"]
|
|
19100
|
+
});
|
|
19101
|
+
return runPrune({ db: this.#db, domain: "workflows", targets, options });
|
|
19102
|
+
}
|
|
18224
19103
|
/**
|
|
18225
19104
|
* Creates custom user-defined indexes for this domain's tables.
|
|
18226
19105
|
*/
|
|
@@ -19334,7 +20213,7 @@ var PostgresStore = class extends MastraCompositeStore {
|
|
|
19334
20213
|
constructor(config) {
|
|
19335
20214
|
try {
|
|
19336
20215
|
validateConfig("PostgresStore", config);
|
|
19337
|
-
super({ id: config.id, name: "PostgresStore", disableInit: config.disableInit });
|
|
20216
|
+
super({ id: config.id, name: "PostgresStore", disableInit: config.disableInit, retention: config.retention });
|
|
19338
20217
|
this.schema = parseSqlIdentifier(config.schemaName || "public", "schema name");
|
|
19339
20218
|
if (isPoolConfig(config)) {
|
|
19340
20219
|
this.#pool = config.pool;
|