@mastra/pg 1.33.0-alpha.3 → 1.34.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +584 -271
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +586 -273
- package/dist/index.js.map +1 -1
- package/dist/storage/client.d.ts +1 -0
- package/dist/storage/client.d.ts.map +1 -1
- package/dist/storage/db/index-build.d.ts +5 -0
- package/dist/storage/db/index-build.d.ts.map +1 -0
- package/dist/storage/db/index.d.ts +0 -8
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/db/pg-errors.d.ts +6 -0
- package/dist/storage/db/pg-errors.d.ts.map +1 -1
- package/dist/storage/db/schema-snapshot.d.ts +4 -7
- package/dist/storage/db/schema-snapshot.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts +8 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/feedback.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +9 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/logs.d.ts +2 -1
- package/dist/storage/domains/observability/v-next/logs.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/metrics.d.ts +2 -1
- package/dist/storage/domains/observability/v-next/metrics.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/partitioning.d.ts +29 -19
- package/dist/storage/domains/observability/v-next/partitioning.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/preview-compat.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/preview-compat.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/scores.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts +37 -1
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/span-query.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/span-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/sql.d.ts +12 -0
- package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/thread-query.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/thread-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/trace-query.d.ts +3 -1
- package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/vector/sql-builder.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/docs/SKILL.md +0 -48
- package/dist/docs/assets/SOURCE_MAP.json +0 -6
- package/dist/docs/references/docs-deployment-workers.md +0 -404
- package/dist/docs/references/docs-memory-semantic-recall.md +0 -481
- package/dist/docs/references/docs-memory-working-memory.md +0 -450
- package/dist/docs/references/docs-storage.md +0 -224
- package/dist/docs/references/integrations-databases-clickhouse-managed-postgres.md +0 -82
- package/dist/docs/references/integrations-databases-dynamodb.md +0 -286
- package/dist/docs/references/integrations-databases-neon.md +0 -222
- package/dist/docs/references/integrations-databases-postgresql.md +0 -561
- package/dist/docs/references/reference-memory-memory-class.md +0 -158
- package/dist/docs/references/reference-processors-message-history-processor.md +0 -89
- package/dist/docs/references/reference-processors-semantic-recall-processor.md +0 -119
- package/dist/docs/references/reference-processors-working-memory-processor.md +0 -154
- package/dist/docs/references/reference-rag-chunking-and-embedding.md +0 -184
- package/dist/docs/references/reference-rag-metadata-filters.md +0 -238
- package/dist/docs/references/reference-rag-overview.md +0 -76
- package/dist/docs/references/reference-rag-retrieval.md +0 -668
- package/dist/docs/references/reference-rag-vector-databases.md +0 -808
- package/dist/docs/references/reference-storage-composite.md +0 -433
- package/dist/docs/references/reference-storage-retention.md +0 -310
- package/dist/docs/references/reference-tools-vector-query-tool.md +0 -500
- package/dist/docs/references/reference-vectors-pg.md +0 -439
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
2
2
|
import * as coreStorage from "@mastra/core/storage";
|
|
3
|
-
import { AgentsStorage, BRANCH_SPAN_TYPES, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, EntityType, ExperimentsStorage, FactoryStorage, FavoritesStorage, KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, assertWorkflowStepGuard, calculatePagination, canonicalizeKnowledgeScope, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, isWorkflowStepConflictError, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseDuration, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
|
|
3
|
+
import { AgentsStorage, BRANCH_SPAN_TYPES, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, EntityType, ExperimentsStorage, FactoryStorage, FavoritesStorage, KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, assertWorkflowStepGuard, calculatePagination, canonicalizeKnowledgeScope, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, getStringAttribute, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, isWorkflowStepConflictError, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseDuration, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
|
|
4
4
|
import { parseFieldKey, parseSqlIdentifier } from "@mastra/core/utils";
|
|
5
5
|
import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/vector";
|
|
6
6
|
import { Mutex } from "async-mutex";
|
|
7
7
|
import * as pg$1 from "pg";
|
|
8
|
-
import pg, { Pool, defaults } from "pg";
|
|
8
|
+
import pg, { Pool, defaults, escapeIdentifier } from "pg";
|
|
9
9
|
import xxhash from "xxhash-wasm";
|
|
10
10
|
import { parse } from "pg-connection-string";
|
|
11
11
|
import { BaseFilterTranslator } from "@mastra/core/vector/filter";
|
|
12
12
|
import { MastraBase } from "@mastra/core/base";
|
|
13
13
|
import { createHash, randomUUID } from "crypto";
|
|
14
|
+
import { setTimeout as setTimeout$1 } from "timers/promises";
|
|
14
15
|
import { coreFeatures } from "@mastra/core/features";
|
|
15
16
|
import { MessageList } from "@mastra/core/agent";
|
|
16
17
|
import { saveScorePayloadSchema } from "@mastra/core/evals";
|
|
@@ -239,7 +240,7 @@ const createNumericOperator = (symbol) => {
|
|
|
239
240
|
sql: `(CASE WHEN jsonb_typeof(${jsonExtract}) = 'number' THEN (${textExtract})::numeric ${symbol} $${paramIndex}::numeric ELSE NULL END)`,
|
|
240
241
|
needsValue: true
|
|
241
242
|
};
|
|
242
|
-
|
|
243
|
+
return {
|
|
243
244
|
sql: `${textExtract} ${symbol} $${paramIndex}::text`,
|
|
244
245
|
needsValue: true
|
|
245
246
|
};
|
|
@@ -2253,6 +2254,9 @@ var PinnedClientAdapter = class {
|
|
|
2253
2254
|
this.#tail = next.catch(() => void 0);
|
|
2254
2255
|
return next;
|
|
2255
2256
|
}
|
|
2257
|
+
async drain() {
|
|
2258
|
+
await this.#tail;
|
|
2259
|
+
}
|
|
2256
2260
|
connect() {
|
|
2257
2261
|
throw new Error("PinnedClientAdapter.connect() is not supported during PostgresStore.init(). All DDL must flow through the pinned client.");
|
|
2258
2262
|
}
|
|
@@ -2435,6 +2439,39 @@ function buildConstraintName({ baseName, schemaName, maxLength = 63, hashWhenTru
|
|
|
2435
2439
|
return truncateIdentifier(fullName, maxLength);
|
|
2436
2440
|
}
|
|
2437
2441
|
// src/storage/db/pg-errors.ts
|
|
2442
|
+
/**
|
|
2443
|
+
* Postgres error classifiers for concurrent DDL.
|
|
2444
|
+
*
|
|
2445
|
+
* `CREATE SCHEMA / TABLE / INDEX IF NOT EXISTS` checks aren't atomic against
|
|
2446
|
+
* concurrent backends. Two callers racing past the existence probe can both
|
|
2447
|
+
* reach the catalog insert; the loser sees a duplicate-object error. These
|
|
2448
|
+
* helpers identify the exact codes we treat as "already exists by the time we
|
|
2449
|
+
* look" so every other error surfaces normally.
|
|
2450
|
+
*/
|
|
2451
|
+
var IndexBuildError = class extends Error {
|
|
2452
|
+
constructor(indexName, schemaName, state, cause) {
|
|
2453
|
+
super(`Could not create index ${schemaName}.${indexName}: ${state}`, { cause });
|
|
2454
|
+
this.name = "IndexBuildError";
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
function* causes(error) {
|
|
2458
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2459
|
+
while (typeof error === "object" && error !== null && !seen.has(error)) {
|
|
2460
|
+
seen.add(error);
|
|
2461
|
+
yield error;
|
|
2462
|
+
error = "cause" in error ? error.cause : void 0;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
/** Index recovery owns its retry budget, even through an outer MastraError. */
|
|
2466
|
+
function isRetryableDeadlock(error) {
|
|
2467
|
+
for (const cause of causes(error)) if (cause instanceof IndexBuildError) return false;
|
|
2468
|
+
for (const cause of causes(error)) if ("code" in cause && cause.code === "40P01") return true;
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
async function backoffAfterDeadlock(attempt) {
|
|
2472
|
+
const base = 50 * 2 ** (attempt - 1);
|
|
2473
|
+
await new Promise((resolve) => setTimeout(resolve, base + Math.random() * base * 2));
|
|
2474
|
+
}
|
|
2438
2475
|
function asPgError(error) {
|
|
2439
2476
|
return error ?? {};
|
|
2440
2477
|
}
|
|
@@ -2462,6 +2499,99 @@ function isDuplicateSchemaError(error) {
|
|
|
2462
2499
|
if (code === "23505" && constraint === "pg_namespace_nspname_index") return true;
|
|
2463
2500
|
return /schema .* already exists/i.test(message);
|
|
2464
2501
|
}
|
|
2502
|
+
// src/storage/db/index-build.ts
|
|
2503
|
+
/** A cold snapshot cannot arbitrate DDL submitted by independent processes. */
|
|
2504
|
+
async function buildIndex(client, schemaName, indexName, build, logger) {
|
|
2505
|
+
const deadline = performance.now() + 3e4;
|
|
2506
|
+
let attempts = 0;
|
|
2507
|
+
let failure;
|
|
2508
|
+
let state = "not built";
|
|
2509
|
+
let repairFailures = 0;
|
|
2510
|
+
const observe = (db = client) => db.oneOrNone(`SELECT c.oid, i.indrelid AS table_oid, t.relname AS table_name,
|
|
2511
|
+
tn.nspname AS table_schema, i.indisvalid AS valid
|
|
2512
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
2513
|
+
LEFT JOIN pg_index i ON i.indexrelid = c.oid
|
|
2514
|
+
LEFT JOIN pg_class t ON t.oid = i.indrelid
|
|
2515
|
+
LEFT JOIN pg_namespace tn ON tn.oid = t.relnamespace
|
|
2516
|
+
WHERE n.nspname = $1 AND c.relname = $2`, [schemaName, indexName]);
|
|
2517
|
+
while (true) {
|
|
2518
|
+
const relation = await observe();
|
|
2519
|
+
if (relation?.valid) return true;
|
|
2520
|
+
if (relation && relation.valid === null) throw new IndexBuildError(indexName, schemaName, "name belongs to a non-index relation", failure);
|
|
2521
|
+
if (relation) {
|
|
2522
|
+
if (await client.oneOrNone(`SELECT p.pid FROM pg_stat_progress_create_index p
|
|
2523
|
+
WHERE p.datid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
2524
|
+
AND p.pid <> pg_backend_pid()
|
|
2525
|
+
AND (p.index_relid = $1 OR ((p.index_relid IS NULL OR p.index_relid = 0) AND EXISTS (
|
|
2526
|
+
SELECT 1 FROM pg_locks l WHERE l.pid = p.pid AND l.database = p.datid
|
|
2527
|
+
AND l.locktype = 'relation' AND l.relation = $2 AND l.granted
|
|
2528
|
+
AND l.mode IN ('ShareUpdateExclusiveLock', 'ShareLock')
|
|
2529
|
+
)))
|
|
2530
|
+
LIMIT 1`, [relation.oid, relation.table_oid])) {
|
|
2531
|
+
logger.warn(`Index ${indexName} is being built by another session; skipping`);
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
if (performance.now() >= deadline && state === "table busy during invalid-index repair") {
|
|
2535
|
+
logger.warn(`Index ${indexName} could not be repaired because its table is busy; skipping`);
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
if (attempts >= 3 || performance.now() >= deadline) break;
|
|
2539
|
+
state = "abandoned invalid index";
|
|
2540
|
+
attempts++;
|
|
2541
|
+
let lockUnavailable = false;
|
|
2542
|
+
try {
|
|
2543
|
+
const result = await client.tx(async (tx) => {
|
|
2544
|
+
const table = `${escapeIdentifier(relation.table_schema)}.${escapeIdentifier(relation.table_name)}`;
|
|
2545
|
+
await tx.none(`SET LOCAL lock_timeout = '100ms'`);
|
|
2546
|
+
try {
|
|
2547
|
+
await tx.none(`LOCK TABLE ${table} IN ACCESS EXCLUSIVE MODE`);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
lockUnavailable = typeof error === "object" && error !== null && "code" in error && error.code === "55P03";
|
|
2550
|
+
throw error;
|
|
2551
|
+
}
|
|
2552
|
+
const current = await observe(tx);
|
|
2553
|
+
if (current?.valid) return "valid";
|
|
2554
|
+
const locked = await tx.one("SELECT to_regclass($1)::oid AS oid", [table]);
|
|
2555
|
+
if (!current || current.oid !== relation.oid || current.table_oid !== locked.oid) return "changed";
|
|
2556
|
+
logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
|
|
2557
|
+
await tx.none(`DROP INDEX ${escapeIdentifier(schemaName)}.${escapeIdentifier(indexName)}`);
|
|
2558
|
+
return "dropped";
|
|
2559
|
+
});
|
|
2560
|
+
if (result === "valid") return true;
|
|
2561
|
+
if (result === "changed") {
|
|
2562
|
+
attempts--;
|
|
2563
|
+
if (performance.now() >= deadline) break;
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
attempts--;
|
|
2568
|
+
if (lockUnavailable) {
|
|
2569
|
+
state = "table busy during invalid-index repair";
|
|
2570
|
+
failure ??= error;
|
|
2571
|
+
await setTimeout$1(100);
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (!isDuplicateRelationError(error) && !isRetryableDeadlock(error) && !(typeof error === "object" && error !== null && "code" in error && error.code === "42P01")) throw error;
|
|
2575
|
+
failure ??= error;
|
|
2576
|
+
if (++repairFailures >= 3 || performance.now() >= deadline) break;
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
if (attempts >= 3 || performance.now() >= deadline) break;
|
|
2581
|
+
attempts++;
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
await build();
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
if (!isDuplicateRelationError(error) && !isRetryableDeadlock(error)) throw error;
|
|
2587
|
+
failure ??= error;
|
|
2588
|
+
const deadlock = isRetryableDeadlock(error);
|
|
2589
|
+
state = deadlock ? "index build deadlock" : "competing index creation";
|
|
2590
|
+
if (deadlock && attempts < 3 && performance.now() < deadline) await backoffAfterDeadlock(attempts);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
throw new IndexBuildError(indexName, schemaName, state, failure);
|
|
2594
|
+
}
|
|
2465
2595
|
// src/storage/db/sanitize-json.ts
|
|
2466
2596
|
const repair = (value) => value.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "�").replaceAll("\0", "");
|
|
2467
2597
|
function repairJson(value) {
|
|
@@ -2517,9 +2647,10 @@ async function loadSchemaSnapshot(client, schemaName) {
|
|
|
2517
2647
|
AND c.relkind IN ('r', 'p')
|
|
2518
2648
|
AND a.attnum > 0
|
|
2519
2649
|
AND NOT a.attisdropped`, [schema]),
|
|
2520
|
-
client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
|
|
2650
|
+
client.manyOrNone(`SELECT c.relname AS indexname, t.relname AS table_name, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
|
|
2521
2651
|
FROM pg_catalog.pg_index i
|
|
2522
2652
|
JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
|
|
2653
|
+
JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
|
|
2523
2654
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
2524
2655
|
WHERE n.nspname = $1`, [schema])
|
|
2525
2656
|
]);
|
|
@@ -2550,13 +2681,11 @@ async function loadSchemaSnapshot(client, schemaName) {
|
|
|
2550
2681
|
}
|
|
2551
2682
|
const indexes = /* @__PURE__ */ new Set();
|
|
2552
2683
|
const replicaIdentityIndexes = /* @__PURE__ */ new Set();
|
|
2553
|
-
const primaryKeyIndexes = /* @__PURE__ */ new
|
|
2554
|
-
const invalidIndexes = /* @__PURE__ */ new Set();
|
|
2684
|
+
const primaryKeyIndexes = /* @__PURE__ */ new Map();
|
|
2555
2685
|
for (const row of indexRows) {
|
|
2556
2686
|
if (row.is_valid) indexes.add(row.indexname);
|
|
2557
|
-
else invalidIndexes.add(row.indexname);
|
|
2558
2687
|
if (row.is_replica_identity) replicaIdentityIndexes.add(row.indexname);
|
|
2559
|
-
if (row.is_primary) primaryKeyIndexes.
|
|
2688
|
+
if (row.is_primary) primaryKeyIndexes.set(row.indexname.toLowerCase(), row.table_name);
|
|
2560
2689
|
}
|
|
2561
2690
|
return {
|
|
2562
2691
|
schemaName: schema,
|
|
@@ -2566,8 +2695,7 @@ async function loadSchemaSnapshot(client, schemaName) {
|
|
|
2566
2695
|
notNullColumns,
|
|
2567
2696
|
indexes,
|
|
2568
2697
|
replicaIdentityIndexes,
|
|
2569
|
-
primaryKeyIndexes
|
|
2570
|
-
invalidIndexes
|
|
2698
|
+
primaryKeyIndexes
|
|
2571
2699
|
};
|
|
2572
2700
|
}
|
|
2573
2701
|
// src/storage/db/index.ts
|
|
@@ -3479,14 +3607,17 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3479
3607
|
baseName: "mastra_ai_spans_traceid_spanid_pk",
|
|
3480
3608
|
schemaName: (this.schemaName ? schemaNamePrefix(this.schemaName) : "") || void 0
|
|
3481
3609
|
});
|
|
3482
|
-
const
|
|
3610
|
+
const fullTableName = getTableName$6({
|
|
3611
|
+
indexName: TABLE_SPANS,
|
|
3612
|
+
schemaName: getSchemaName$6(this.schemaName)
|
|
3613
|
+
});
|
|
3483
3614
|
const snapshot = this.schemaSnapshot;
|
|
3484
|
-
if (snapshot) return snapshot.primaryKeyIndexes.
|
|
3485
|
-
return this.spansPrimaryKeyExistsLive(constraintName,
|
|
3615
|
+
if (snapshot) return snapshot.primaryKeyIndexes.get(constraintName.toLowerCase()) === TABLE_SPANS;
|
|
3616
|
+
return this.spansPrimaryKeyExistsLive(constraintName, fullTableName);
|
|
3486
3617
|
}
|
|
3487
3618
|
/** Live-catalog variant of {@link spansPrimaryKeyExists}, bypassing the snapshot. */
|
|
3488
|
-
async spansPrimaryKeyExistsLive(constraintName,
|
|
3489
|
-
return (await this.client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND
|
|
3619
|
+
async spansPrimaryKeyExistsLive(constraintName, fullTableName) {
|
|
3620
|
+
return (await this.client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND contype = 'p' AND conrelid = to_regclass($2)) as exists`, [constraintName, fullTableName]))?.exists ?? false;
|
|
3490
3621
|
}
|
|
3491
3622
|
/**
|
|
3492
3623
|
* Adds the PRIMARY KEY constraint on (traceId, spanId) to the spans table.
|
|
@@ -3501,13 +3632,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3501
3632
|
baseName: "mastra_ai_spans_traceid_spanid_pk",
|
|
3502
3633
|
schemaName: (this.schemaName ? schemaNamePrefix(this.schemaName) : "") || void 0
|
|
3503
3634
|
});
|
|
3504
|
-
const schemaFilter = this.schemaName || "public";
|
|
3505
3635
|
try {
|
|
3506
|
-
if (
|
|
3507
|
-
SELECT EXISTS (
|
|
3508
|
-
SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND connamespace = (SELECT oid FROM pg_namespace WHERE nspname = $2)
|
|
3509
|
-
) as exists
|
|
3510
|
-
`, [constraintName, schemaFilter]))?.exists) {
|
|
3636
|
+
if (await this.spansPrimaryKeyExistsLive(constraintName, fullTableName)) {
|
|
3511
3637
|
this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} already exists on ${fullTableName}`);
|
|
3512
3638
|
return;
|
|
3513
3639
|
}
|
|
@@ -3516,12 +3642,13 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3516
3642
|
ADD CONSTRAINT ${constraintName}
|
|
3517
3643
|
PRIMARY KEY ("traceId", "spanId")
|
|
3518
3644
|
`);
|
|
3519
|
-
this.schemaSnapshot?.primaryKeyIndexes.
|
|
3645
|
+
this.schemaSnapshot?.primaryKeyIndexes.set(constraintName.toLowerCase(), TABLE_SPANS);
|
|
3520
3646
|
this.logger?.info?.(`Added PRIMARY KEY constraint ${constraintName} to ${fullTableName}`);
|
|
3521
3647
|
} catch (error) {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3648
|
+
const primaryKeyRace = typeof error === "object" && error !== null && "code" in error && error.code === "42P16";
|
|
3649
|
+
if (isDuplicateRelationError(error) || primaryKeyRace) {
|
|
3650
|
+
if (await this.spansPrimaryKeyExistsLive(constraintName, fullTableName)) {
|
|
3651
|
+
this.schemaSnapshot?.primaryKeyIndexes.set(constraintName.toLowerCase(), TABLE_SPANS);
|
|
3525
3652
|
this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} was created by another process`);
|
|
3526
3653
|
return;
|
|
3527
3654
|
}
|
|
@@ -3738,7 +3865,6 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3738
3865
|
});
|
|
3739
3866
|
const snapshot = this.schemaSnapshot;
|
|
3740
3867
|
if (snapshot?.indexes.has(name)) return;
|
|
3741
|
-
if (!await this.prepareIndexBuild(name, schemaName)) return;
|
|
3742
3868
|
const uniqueStr = unique ? "UNIQUE " : "";
|
|
3743
3869
|
const concurrentStr = concurrent ? "CONCURRENTLY " : "";
|
|
3744
3870
|
const methodStr = method !== "btree" ? `USING ${method} ` : "";
|
|
@@ -3756,9 +3882,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3756
3882
|
const tablespaceStr = tablespace ? ` TABLESPACE ${tablespace}` : "";
|
|
3757
3883
|
let withStr = "";
|
|
3758
3884
|
if (storage && Object.keys(storage).length > 0) withStr = ` WITH (${Object.entries(storage).map(([key, value]) => `${key} = ${value}`).join(", ")})`;
|
|
3759
|
-
const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}${`"${parseSqlIdentifier(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
|
|
3760
|
-
await this.timedIndexBuild(name, sql);
|
|
3761
|
-
snapshot?.indexes.add(name);
|
|
3885
|
+
const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}IF NOT EXISTS ${`"${parseSqlIdentifier(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
|
|
3886
|
+
if (await buildIndex(this.client, schemaName, name, () => this.timedIndexBuild(name, sql), this.logger)) snapshot?.indexes.add(name);
|
|
3762
3887
|
} catch (error) {
|
|
3763
3888
|
throw new MastraError({
|
|
3764
3889
|
id: createStorageErrorId("PG", "INDEX_CREATE", "FAILED"),
|
|
@@ -3783,36 +3908,7 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3783
3908
|
async createIndexFromStatement(indexName, sql) {
|
|
3784
3909
|
const snapshot = this.schemaSnapshot;
|
|
3785
3910
|
if (snapshot?.indexes.has(indexName)) return;
|
|
3786
|
-
if (
|
|
3787
|
-
await this.timedIndexBuild(indexName, sql);
|
|
3788
|
-
snapshot?.indexes.add(indexName);
|
|
3789
|
-
}
|
|
3790
|
-
/**
|
|
3791
|
-
* Decides whether `indexName` still needs building. Returns false when a valid
|
|
3792
|
-
* index already exists, or when another session is currently building it.
|
|
3793
|
-
* An invalid index left by an interrupted `CREATE INDEX CONCURRENTLY` is
|
|
3794
|
-
* dropped (concurrently) so the caller rebuilds it — `IF NOT EXISTS` would
|
|
3795
|
-
* otherwise keep the broken index forever.
|
|
3796
|
-
*/
|
|
3797
|
-
async prepareIndexBuild(indexName, schemaName) {
|
|
3798
|
-
const snapshot = this.schemaSnapshot;
|
|
3799
|
-
if (snapshot && !snapshot.invalidIndexes.has(indexName)) return true;
|
|
3800
|
-
const existing = await this.client.oneOrNone(`SELECT i.indisvalid AS is_valid,
|
|
3801
|
-
EXISTS (SELECT 1 FROM pg_stat_progress_create_index p WHERE p.index_relid = c.oid) AS building
|
|
3802
|
-
FROM pg_catalog.pg_index i
|
|
3803
|
-
JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
|
|
3804
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
3805
|
-
WHERE c.relname = $1 AND n.nspname = $2`, [indexName, schemaName]);
|
|
3806
|
-
if (!existing) return true;
|
|
3807
|
-
if (existing.is_valid) return false;
|
|
3808
|
-
if (existing.building) {
|
|
3809
|
-
this.logger.warn(`Index ${indexName} is being built by another session; skipping`);
|
|
3810
|
-
return false;
|
|
3811
|
-
}
|
|
3812
|
-
this.logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
|
|
3813
|
-
const quotedIndexName = `"${parseSqlIdentifier(indexName, "index name")}"`;
|
|
3814
|
-
await this.client.none(`DROP INDEX CONCURRENTLY IF EXISTS ${getSchemaName$6(this.schemaName)}.${quotedIndexName}`);
|
|
3815
|
-
return true;
|
|
3911
|
+
if (await buildIndex(this.client, this.schemaName || "public", indexName, () => this.timedIndexBuild(indexName, sql), this.logger)) snapshot?.indexes.add(indexName);
|
|
3816
3912
|
}
|
|
3817
3913
|
async timedIndexBuild(indexName, sql) {
|
|
3818
3914
|
const startedAt = Date.now();
|
|
@@ -16103,6 +16199,51 @@ const SPAN_EVENT_COLUMNS = [
|
|
|
16103
16199
|
type: "double precision",
|
|
16104
16200
|
nullable: true
|
|
16105
16201
|
},
|
|
16202
|
+
{
|
|
16203
|
+
name: "externalParentSpanId",
|
|
16204
|
+
type: "text",
|
|
16205
|
+
nullable: true
|
|
16206
|
+
},
|
|
16207
|
+
{
|
|
16208
|
+
name: "resumedFromSpanId",
|
|
16209
|
+
type: "text",
|
|
16210
|
+
nullable: true
|
|
16211
|
+
},
|
|
16212
|
+
{
|
|
16213
|
+
name: "model",
|
|
16214
|
+
type: "text",
|
|
16215
|
+
nullable: true
|
|
16216
|
+
},
|
|
16217
|
+
{
|
|
16218
|
+
name: "provider",
|
|
16219
|
+
type: "text",
|
|
16220
|
+
nullable: true
|
|
16221
|
+
},
|
|
16222
|
+
{
|
|
16223
|
+
name: "outcome",
|
|
16224
|
+
type: "text",
|
|
16225
|
+
nullable: true
|
|
16226
|
+
},
|
|
16227
|
+
{
|
|
16228
|
+
name: "stopReason",
|
|
16229
|
+
type: "text",
|
|
16230
|
+
nullable: true
|
|
16231
|
+
},
|
|
16232
|
+
{
|
|
16233
|
+
name: "timeToFirstTokenMs",
|
|
16234
|
+
type: "double precision",
|
|
16235
|
+
nullable: true
|
|
16236
|
+
},
|
|
16237
|
+
{
|
|
16238
|
+
name: "errorCount",
|
|
16239
|
+
type: "integer",
|
|
16240
|
+
nullable: true
|
|
16241
|
+
},
|
|
16242
|
+
{
|
|
16243
|
+
name: "stopReasonContext",
|
|
16244
|
+
type: "jsonb",
|
|
16245
|
+
nullable: true
|
|
16246
|
+
},
|
|
16106
16247
|
{
|
|
16107
16248
|
name: "tags",
|
|
16108
16249
|
type: "text[]",
|
|
@@ -16811,6 +16952,26 @@ function allTableDDL(schema, mode) {
|
|
|
16811
16952
|
];
|
|
16812
16953
|
}
|
|
16813
16954
|
/**
|
|
16955
|
+
* Span event columns added after the table first shipped, with their types. All
|
|
16956
|
+
* nullable with no default, so each ALTER is a catalog-only change; rows written
|
|
16957
|
+
* before it read NULL and readers fall back to the payload or attributes.
|
|
16958
|
+
*/
|
|
16959
|
+
const ADDED_SPAN_COLUMNS = [
|
|
16960
|
+
["inputPreview", "text"],
|
|
16961
|
+
["outputPreview", "text"],
|
|
16962
|
+
["errorPreview", "text"],
|
|
16963
|
+
["timeToFirstChunkMs", "double precision"],
|
|
16964
|
+
["externalParentSpanId", "text"],
|
|
16965
|
+
["resumedFromSpanId", "text"],
|
|
16966
|
+
["model", "text"],
|
|
16967
|
+
["provider", "text"],
|
|
16968
|
+
["outcome", "text"],
|
|
16969
|
+
["stopReason", "text"],
|
|
16970
|
+
["timeToFirstTokenMs", "double precision"],
|
|
16971
|
+
["errorCount", "integer"],
|
|
16972
|
+
["stopReasonContext", "jsonb"]
|
|
16973
|
+
];
|
|
16974
|
+
/**
|
|
16814
16975
|
* Additive column migrations for tables created by an older version of this
|
|
16815
16976
|
* schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
|
|
16816
16977
|
* columns have to be added out of band.
|
|
@@ -16827,20 +16988,11 @@ function additiveColumns(schema) {
|
|
|
16827
16988
|
column: "isPending",
|
|
16828
16989
|
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
|
|
16829
16990
|
},
|
|
16830
|
-
...[
|
|
16831
|
-
"inputPreview",
|
|
16832
|
-
"outputPreview",
|
|
16833
|
-
"errorPreview"
|
|
16834
|
-
].map((column) => ({
|
|
16991
|
+
...ADDED_SPAN_COLUMNS.map(([column, type]) => ({
|
|
16835
16992
|
table: TABLE_SPAN_EVENTS,
|
|
16836
16993
|
column,
|
|
16837
|
-
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "${column}"
|
|
16994
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "${column}" ${type}`
|
|
16838
16995
|
})),
|
|
16839
|
-
{
|
|
16840
|
-
table: TABLE_SPAN_EVENTS,
|
|
16841
|
-
column: "timeToFirstChunkMs",
|
|
16842
|
-
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "timeToFirstChunkMs" double precision`
|
|
16843
|
-
},
|
|
16844
16996
|
{
|
|
16845
16997
|
table: TABLE_FEEDBACK_EVENTS,
|
|
16846
16998
|
column: "reviewStatus",
|
|
@@ -17243,6 +17395,203 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
|
|
|
17243
17395
|
reviewStatus: args.reviewStatus
|
|
17244
17396
|
};
|
|
17245
17397
|
}
|
|
17398
|
+
// src/storage/domains/observability/v-next/partitioning.ts
|
|
17399
|
+
const DEFAULT_FUTURE_DAYS = 14;
|
|
17400
|
+
async function detectTimescale(client) {
|
|
17401
|
+
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
|
|
17402
|
+
return Boolean(row?.exists);
|
|
17403
|
+
}
|
|
17404
|
+
async function detectPartman(client) {
|
|
17405
|
+
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
|
|
17406
|
+
return Boolean(row?.exists);
|
|
17407
|
+
}
|
|
17408
|
+
async function getPartmanVersion(client) {
|
|
17409
|
+
return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
|
|
17410
|
+
}
|
|
17411
|
+
function getPartmanMajor(version) {
|
|
17412
|
+
if (!version) return null;
|
|
17413
|
+
const match = /^(\d+)/.exec(version);
|
|
17414
|
+
return match ? Number(match[1]) : null;
|
|
17415
|
+
}
|
|
17416
|
+
async function resolveMode(client, options = {}) {
|
|
17417
|
+
if (options.mode && options.mode !== "auto") return options.mode;
|
|
17418
|
+
if (await detectTimescale(client)) return "timescale";
|
|
17419
|
+
if (await detectPartman(client)) return "partman";
|
|
17420
|
+
return "native";
|
|
17421
|
+
}
|
|
17422
|
+
function dayBounds(d) {
|
|
17423
|
+
const year = d.getUTCFullYear();
|
|
17424
|
+
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
17425
|
+
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
17426
|
+
const start = `${year}-${month}-${day} 00:00:00+00`;
|
|
17427
|
+
const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
|
|
17428
|
+
return {
|
|
17429
|
+
start,
|
|
17430
|
+
end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
|
|
17431
|
+
suffix: `${year}${month}${day}`
|
|
17432
|
+
};
|
|
17433
|
+
}
|
|
17434
|
+
function partitionName(table, suffix) {
|
|
17435
|
+
return `${table}_p${suffix}`;
|
|
17436
|
+
}
|
|
17437
|
+
/**
|
|
17438
|
+
* Creates daily partition tables in [today - 1 day, today + futureDays].
|
|
17439
|
+
* Idempotent: uses CREATE TABLE IF NOT EXISTS.
|
|
17440
|
+
*/
|
|
17441
|
+
async function ensureNativePartitions(client, schema, options = {}) {
|
|
17442
|
+
const windowDays = partitionWindow(options);
|
|
17443
|
+
for (const table of ALL_SIGNAL_TABLES) await ensureDayPartitions(client, schema, table, windowDays);
|
|
17444
|
+
}
|
|
17445
|
+
/** The UTC days init() pre-creates: yesterday (optional) through today + futureDays. */
|
|
17446
|
+
function partitionWindow(options = {}, nowMs = Date.now()) {
|
|
17447
|
+
const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
|
|
17448
|
+
const start = options.includeYesterday ?? true ? -1 : 0;
|
|
17449
|
+
const days = [];
|
|
17450
|
+
for (let i = start; i <= futureDays; i++) days.push(new Date(nowMs + i * 864e5));
|
|
17451
|
+
return days;
|
|
17452
|
+
}
|
|
17453
|
+
/**
|
|
17454
|
+
* Keeps only the days inside the current partition window. The write path
|
|
17455
|
+
* creates partitions on demand within that window only, so a row with a
|
|
17456
|
+
* client-supplied timestamp far in the past or future cannot make the
|
|
17457
|
+
* database grow a table per day it names.
|
|
17458
|
+
*/
|
|
17459
|
+
function withinPartitionWindow(days, options = {}) {
|
|
17460
|
+
const allowed = new Set(partitionWindow(options).map((d) => dayBounds(d).suffix));
|
|
17461
|
+
return days.filter((d) => allowed.has(dayBounds(d).suffix));
|
|
17462
|
+
}
|
|
17463
|
+
/**
|
|
17464
|
+
* Creates and attaches a partition of `schema.table` for each UTC day in
|
|
17465
|
+
* `days` that has none. Each day is a `CREATE TABLE (LIKE parent)` followed by
|
|
17466
|
+
* `ATTACH PARTITION`, in one transaction, so a failed attach leaves no
|
|
17467
|
+
* detached child behind. When either step fails, the catalog is read again: a
|
|
17468
|
+
* day that a concurrent caller attached meanwhile counts as done, and any
|
|
17469
|
+
* other failure is rethrown.
|
|
17470
|
+
*/
|
|
17471
|
+
async function ensureDayPartitions(client, schema, table, days) {
|
|
17472
|
+
const parent = qualifiedTable(schema, table);
|
|
17473
|
+
const attached = await attachedPartitionNames(client, schema, table);
|
|
17474
|
+
for (const d of days) {
|
|
17475
|
+
const { start: partStart, end: partEnd, suffix } = dayBounds(d);
|
|
17476
|
+
const childName = partitionName(table, suffix);
|
|
17477
|
+
if (attached.has(childName)) continue;
|
|
17478
|
+
const child = qualifiedName(schema, childName);
|
|
17479
|
+
try {
|
|
17480
|
+
await client.tx(async (t) => {
|
|
17481
|
+
await t.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
|
|
17482
|
+
await t.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
|
|
17483
|
+
FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
|
|
17484
|
+
});
|
|
17485
|
+
} catch (error) {
|
|
17486
|
+
if (!(await attachedPartitionNames(client, schema, table)).has(childName)) throw error;
|
|
17487
|
+
}
|
|
17488
|
+
}
|
|
17489
|
+
}
|
|
17490
|
+
/** Names of the children attached to `schema.table`, read from the catalog so no search_path can shadow it. */
|
|
17491
|
+
async function attachedPartitionNames(client, schema, table) {
|
|
17492
|
+
const rows = await client.manyOrNone(`SELECT c.relname
|
|
17493
|
+
FROM pg_catalog.pg_inherits i
|
|
17494
|
+
JOIN pg_catalog.pg_class c ON c.oid = i.inhrelid
|
|
17495
|
+
WHERE i.inhparent = (
|
|
17496
|
+
SELECT p.oid
|
|
17497
|
+
FROM pg_catalog.pg_class p
|
|
17498
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace
|
|
17499
|
+
WHERE n.nspname = $1 AND p.relname = $2
|
|
17500
|
+
)`, [schema, table]);
|
|
17501
|
+
return new Set(rows.map((r) => r.relname));
|
|
17502
|
+
}
|
|
17503
|
+
/**
|
|
17504
|
+
* `23514` raised by `ExecFindPartition`: the row's day has no attached
|
|
17505
|
+
* partition. The message is matched as a fallback only, because Postgres
|
|
17506
|
+
* translates it when `lc_messages` is not English.
|
|
17507
|
+
*/
|
|
17508
|
+
function isMissingPartitionError(error) {
|
|
17509
|
+
const { code, routine, message = "" } = error ?? {};
|
|
17510
|
+
return code === "23514" && (routine === "ExecFindPartition" || /no partition of relation/i.test(message));
|
|
17511
|
+
}
|
|
17512
|
+
/**
|
|
17513
|
+
* Calls `create_hypertable()` for each signal table.
|
|
17514
|
+
*
|
|
17515
|
+
* Postgres declarative partitioning and Timescale hypertables are mutually
|
|
17516
|
+
* exclusive, so the base DDL must have been generated with TableDDLMode
|
|
17517
|
+
* 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
|
|
17518
|
+
* idempotent across re-inits.
|
|
17519
|
+
*/
|
|
17520
|
+
async function ensureTimescaleHypertables(client, schema) {
|
|
17521
|
+
for (const table of ALL_SIGNAL_TABLES) {
|
|
17522
|
+
const tableExpr = qualifiedTable(schema, table);
|
|
17523
|
+
const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
|
|
17524
|
+
await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
|
|
17525
|
+
}
|
|
17526
|
+
}
|
|
17527
|
+
/**
|
|
17528
|
+
* Registers each signal table with pg_partman as a daily-interval parent and
|
|
17529
|
+
* lets partman create the child partitions.
|
|
17530
|
+
*
|
|
17531
|
+
* Retention is intentionally NOT configured here — it stays opt-in via the
|
|
17532
|
+
* future Mastra CLI surface.
|
|
17533
|
+
*
|
|
17534
|
+
* Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
|
|
17535
|
+
* a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
|
|
17536
|
+
* adapter run against current arm64-friendly images without breaking existing
|
|
17537
|
+
* 4.x deployments.
|
|
17538
|
+
*/
|
|
17539
|
+
async function partmanParentRegistered(client, schema, table) {
|
|
17540
|
+
const row = await client.oneOrNone(`SELECT EXISTS (
|
|
17541
|
+
SELECT 1 FROM partman.part_config
|
|
17542
|
+
WHERE parent_table = format('%I.%I', $1::text, $2::text)
|
|
17543
|
+
) AS "exists"`, [schema, table]);
|
|
17544
|
+
return Boolean(row?.exists);
|
|
17545
|
+
}
|
|
17546
|
+
async function ensurePartmanHypertables(client, schema) {
|
|
17547
|
+
const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
|
|
17548
|
+
for (const table of ALL_SIGNAL_TABLES) {
|
|
17549
|
+
if (await partmanParentRegistered(client, schema, table)) continue;
|
|
17550
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
17551
|
+
if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
|
|
17552
|
+
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
17553
|
+
p_control := $3,
|
|
17554
|
+
p_interval := '1 day',
|
|
17555
|
+
p_type := 'range'
|
|
17556
|
+
)`, [
|
|
17557
|
+
schema,
|
|
17558
|
+
table,
|
|
17559
|
+
SIGNAL_TIME_COLUMN[table]
|
|
17560
|
+
]);
|
|
17561
|
+
else await client.none(`SELECT partman.create_parent(
|
|
17562
|
+
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
17563
|
+
p_control := $3,
|
|
17564
|
+
p_type := 'native',
|
|
17565
|
+
p_interval := '1 day'
|
|
17566
|
+
)`, [
|
|
17567
|
+
schema,
|
|
17568
|
+
table,
|
|
17569
|
+
SIGNAL_TIME_COLUMN[table]
|
|
17570
|
+
]);
|
|
17571
|
+
break;
|
|
17572
|
+
} catch (error) {
|
|
17573
|
+
const message = error?.message ?? "";
|
|
17574
|
+
if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
|
|
17575
|
+
if (!/deadlock detected/i.test(message)) throw error;
|
|
17576
|
+
if (await partmanParentRegistered(client, schema, table)) break;
|
|
17577
|
+
if (attempt === 2) throw error;
|
|
17578
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
17579
|
+
}
|
|
17580
|
+
}
|
|
17581
|
+
}
|
|
17582
|
+
async function setupPartitioning(client, schema, options = {}) {
|
|
17583
|
+
switch (await resolveMode(client, options)) {
|
|
17584
|
+
case "timescale":
|
|
17585
|
+
await ensureTimescaleHypertables(client, schema);
|
|
17586
|
+
return "timescale";
|
|
17587
|
+
case "partman":
|
|
17588
|
+
await ensurePartmanHypertables(client, schema);
|
|
17589
|
+
return "partman";
|
|
17590
|
+
case "native":
|
|
17591
|
+
await ensureNativePartitions(client, schema, options);
|
|
17592
|
+
return "native";
|
|
17593
|
+
}
|
|
17594
|
+
}
|
|
17246
17595
|
// src/storage/domains/observability/v-next/sql.ts
|
|
17247
17596
|
/**
|
|
17248
17597
|
* SQL helpers for the v-next Postgres observability domain.
|
|
@@ -17323,6 +17672,54 @@ ${onConflict}`,
|
|
|
17323
17672
|
values
|
|
17324
17673
|
};
|
|
17325
17674
|
}
|
|
17675
|
+
/**
|
|
17676
|
+
* Insert into a signal table. In native partition mode, a row whose day is
|
|
17677
|
+
* inside the partition window but has no partition yet gets one created, then
|
|
17678
|
+
* the insert runs once more. TimescaleDB and pg_partman own their partitions,
|
|
17679
|
+
* so in those modes the original error propagates untouched. The mode is the
|
|
17680
|
+
* one the store resolved; a store that never ran init() (`disableInit`)
|
|
17681
|
+
* detects it here, after the failed insert, so the hot path stays one query.
|
|
17682
|
+
*/
|
|
17683
|
+
async function insertRows(client, schema, table, records, partitioning, onConflict) {
|
|
17684
|
+
const insert = buildInsert(schema, table, records, onConflict);
|
|
17685
|
+
if (!insert) return;
|
|
17686
|
+
try {
|
|
17687
|
+
await client.query(insert.text, insert.values);
|
|
17688
|
+
} catch (error) {
|
|
17689
|
+
if (!isMissingPartitionError(error) || await resolveMode(client, partitioning) !== "native") throw error;
|
|
17690
|
+
const days = withinPartitionWindow(recordDays(records, SIGNAL_TIME_COLUMN[table]), partitioning);
|
|
17691
|
+
if (days.length === 0) throw error;
|
|
17692
|
+
for (const day of days) await ensureDayPartitions(client, schema, table, [day]).catch((cause) => {
|
|
17693
|
+
if (!isInsufficientPrivilegeError(cause)) throw cause;
|
|
17694
|
+
throw new Error(partitionCreateFailure({
|
|
17695
|
+
table,
|
|
17696
|
+
day,
|
|
17697
|
+
cause
|
|
17698
|
+
}), { cause });
|
|
17699
|
+
});
|
|
17700
|
+
await client.query(insert.text, insert.values);
|
|
17701
|
+
}
|
|
17702
|
+
}
|
|
17703
|
+
/** `42501`: the role may not create the child, or does not own the parent it must attach to. */
|
|
17704
|
+
function isInsufficientPrivilegeError(error) {
|
|
17705
|
+
return error?.code === "42501";
|
|
17706
|
+
}
|
|
17707
|
+
function partitionCreateFailure({ table, day, cause }) {
|
|
17708
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
17709
|
+
return `Could not create the ${table} partition for ${day.toISOString().slice(0, 10)} (UTC): ${reason}. Create the partitions with a role that can create tables in the schema and owns the observability tables, for example by calling ensurePartitions() on a schedule.`;
|
|
17710
|
+
}
|
|
17711
|
+
function recordDays(records, timeColumn) {
|
|
17712
|
+
const days = /* @__PURE__ */ new Map();
|
|
17713
|
+
for (const record of records) {
|
|
17714
|
+
const value = record[timeColumn];
|
|
17715
|
+
if (value == null) continue;
|
|
17716
|
+
const d = new Date(value);
|
|
17717
|
+
if (isNaN(d.getTime())) continue;
|
|
17718
|
+
const day = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
17719
|
+
days.set(day.toISOString(), day);
|
|
17720
|
+
}
|
|
17721
|
+
return [...days.values()];
|
|
17722
|
+
}
|
|
17326
17723
|
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
17327
17724
|
const SPAN_KEY_COLUMNS = [
|
|
17328
17725
|
"traceId",
|
|
@@ -17393,6 +17790,11 @@ const PROMOTED_KEYS = /* @__PURE__ */ new Set([
|
|
|
17393
17790
|
"executionSource",
|
|
17394
17791
|
"serviceName"
|
|
17395
17792
|
]);
|
|
17793
|
+
function nullableNumber$1(value) {
|
|
17794
|
+
if (value == null || value === "") return null;
|
|
17795
|
+
const n = Number(value);
|
|
17796
|
+
return Number.isNaN(n) ? null : n;
|
|
17797
|
+
}
|
|
17396
17798
|
function nullableString(value) {
|
|
17397
17799
|
if (typeof value === "string") return value === "" ? null : value;
|
|
17398
17800
|
if (value == null) return null;
|
|
@@ -17554,6 +17956,15 @@ function spanRecordToRow(span) {
|
|
|
17554
17956
|
startedAt: toIsoOrDate(span.startedAt),
|
|
17555
17957
|
endedAt: toIsoOrDate(endedAt),
|
|
17556
17958
|
timeToFirstChunkMs: span.timeToFirstChunkMs ?? null,
|
|
17959
|
+
externalParentSpanId: span.externalParentSpanId ?? null,
|
|
17960
|
+
resumedFromSpanId: span.resumedFromSpanId ?? null,
|
|
17961
|
+
model: span.model ?? null,
|
|
17962
|
+
provider: span.provider ?? null,
|
|
17963
|
+
outcome: span.outcome ?? null,
|
|
17964
|
+
stopReason: span.stopReason ?? null,
|
|
17965
|
+
stopReasonContext: jsonField(span.stopReasonContext),
|
|
17966
|
+
timeToFirstTokenMs: span.timeToFirstTokenMs ?? null,
|
|
17967
|
+
errorCount: span.errorCount ?? null,
|
|
17557
17968
|
tags: normalizeTags(span.tags),
|
|
17558
17969
|
metadataSearch: buildMetadataSearch(metadata),
|
|
17559
17970
|
metadataRaw: jsonField(metadata),
|
|
@@ -17583,6 +17994,7 @@ function rowToSpanRecord(row) {
|
|
|
17583
17994
|
const startedAt = toDate(row.startedAt);
|
|
17584
17995
|
const endedAt = spanRowEndedAt(row, startedAt);
|
|
17585
17996
|
const error = parsedJson(row.error);
|
|
17997
|
+
const attributes = parsedJson(row.attributes);
|
|
17586
17998
|
const { executionSource, ...ctx } = rowToCommonContext(row);
|
|
17587
17999
|
return {
|
|
17588
18000
|
...ctx,
|
|
@@ -17595,10 +18007,19 @@ function rowToSpanRecord(row) {
|
|
|
17595
18007
|
isEvent: Boolean(row.isEvent),
|
|
17596
18008
|
startedAt,
|
|
17597
18009
|
endedAt,
|
|
18010
|
+
externalParentSpanId: nullableString(row.externalParentSpanId),
|
|
18011
|
+
resumedFromSpanId: nullableString(row.resumedFromSpanId),
|
|
18012
|
+
model: nullableString(row.model) ?? getStringAttribute(attributes, "model"),
|
|
18013
|
+
provider: nullableString(row.provider) ?? getStringAttribute(attributes, "provider"),
|
|
18014
|
+
outcome: nullableString(row.outcome),
|
|
18015
|
+
stopReason: nullableString(row.stopReason),
|
|
18016
|
+
stopReasonContext: parsedJson(row.stopReasonContext) ?? null,
|
|
18017
|
+
timeToFirstTokenMs: nullableNumber$1(row.timeToFirstTokenMs),
|
|
18018
|
+
errorCount: nullableNumber$1(row.errorCount),
|
|
17598
18019
|
tags: normalizeTags(row.tags),
|
|
17599
18020
|
metadata: parsedJson(row.metadataRaw) ?? void 0,
|
|
17600
18021
|
scope: parsedJson(row.scope) ?? void 0,
|
|
17601
|
-
attributes:
|
|
18022
|
+
attributes: attributes ?? void 0,
|
|
17602
18023
|
links: parsedJson(row.links) ?? void 0,
|
|
17603
18024
|
input: parsedJson(row.input) ?? void 0,
|
|
17604
18025
|
output: parsedJson(row.output) ?? void 0,
|
|
@@ -18209,16 +18630,14 @@ function pushFeedbackIdentity(acc, feedbackType, feedbackSource) {
|
|
|
18209
18630
|
}
|
|
18210
18631
|
acc.conditions.push(`"valueNumber" IS NOT NULL`);
|
|
18211
18632
|
}
|
|
18212
|
-
async function createFeedback(client, schema, args) {
|
|
18633
|
+
async function createFeedback(client, schema, args, partitioning) {
|
|
18213
18634
|
const row = feedbackRecordToRow(args.feedback);
|
|
18214
|
-
|
|
18215
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18635
|
+
await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, [row], partitioning, FEEDBACK_UPSERT_CLAUSE);
|
|
18216
18636
|
}
|
|
18217
|
-
async function batchCreateFeedback(client, schema, args) {
|
|
18637
|
+
async function batchCreateFeedback(client, schema, args, partitioning) {
|
|
18218
18638
|
if (args.feedbacks.length === 0) return;
|
|
18219
18639
|
const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
|
|
18220
|
-
|
|
18221
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18640
|
+
await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
|
|
18222
18641
|
}
|
|
18223
18642
|
async function updateFeedbackReviewStatus(client, schema, args) {
|
|
18224
18643
|
const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
|
|
@@ -18416,11 +18835,10 @@ function applyLogFilters(acc, filters) {
|
|
|
18416
18835
|
applyCommonFilters(acc, filters);
|
|
18417
18836
|
applySingleOrArrayFilter(acc, "level", filters?.level);
|
|
18418
18837
|
}
|
|
18419
|
-
async function batchCreateLogs(client, schema, args) {
|
|
18838
|
+
async function batchCreateLogs(client, schema, args, partitioning) {
|
|
18420
18839
|
if (args.logs.length === 0) return;
|
|
18421
18840
|
const rows = args.logs.map(logRecordToRow);
|
|
18422
|
-
|
|
18423
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18841
|
+
await insertRows(client, schema, TABLE_LOG_EVENTS, rows, partitioning);
|
|
18424
18842
|
}
|
|
18425
18843
|
async function listLogs(client, schema, args) {
|
|
18426
18844
|
const { mode, filters, pagination, orderBy, after, limit } = listLogsArgsSchema.parse(args);
|
|
@@ -18491,11 +18909,10 @@ function pushMetricNameFilter(acc, names) {
|
|
|
18491
18909
|
acc.conditions.push(`"name" IN (${placeholders})`);
|
|
18492
18910
|
acc.params.push(...names);
|
|
18493
18911
|
}
|
|
18494
|
-
async function batchCreateMetrics(client, schema, args) {
|
|
18912
|
+
async function batchCreateMetrics(client, schema, args, partitioning) {
|
|
18495
18913
|
if (args.metrics.length === 0) return;
|
|
18496
18914
|
const rows = args.metrics.map(metricRecordToRow);
|
|
18497
|
-
|
|
18498
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18915
|
+
await insertRows(client, schema, TABLE_METRIC_EVENTS, rows, partitioning);
|
|
18499
18916
|
}
|
|
18500
18917
|
async function listMetrics(client, schema, args) {
|
|
18501
18918
|
const { mode, filters, pagination, orderBy, after, limit } = listMetricsArgsSchema.parse(args);
|
|
@@ -18697,165 +19114,6 @@ async function getMetricPercentiles(client, schema, args) {
|
|
|
18697
19114
|
`;
|
|
18698
19115
|
return { series: percentileSeriesFromRows(await client.manyOrNone(sql, acc.params), args.percentiles) };
|
|
18699
19116
|
}
|
|
18700
|
-
// src/storage/domains/observability/v-next/partitioning.ts
|
|
18701
|
-
const DEFAULT_FUTURE_DAYS = 14;
|
|
18702
|
-
async function detectTimescale(client) {
|
|
18703
|
-
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
|
|
18704
|
-
return Boolean(row?.exists);
|
|
18705
|
-
}
|
|
18706
|
-
async function detectPartman(client) {
|
|
18707
|
-
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
|
|
18708
|
-
return Boolean(row?.exists);
|
|
18709
|
-
}
|
|
18710
|
-
async function getPartmanVersion(client) {
|
|
18711
|
-
return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
|
|
18712
|
-
}
|
|
18713
|
-
function getPartmanMajor(version) {
|
|
18714
|
-
if (!version) return null;
|
|
18715
|
-
const match = /^(\d+)/.exec(version);
|
|
18716
|
-
return match ? Number(match[1]) : null;
|
|
18717
|
-
}
|
|
18718
|
-
async function resolveMode(client, options = {}) {
|
|
18719
|
-
if (options.mode && options.mode !== "auto") return options.mode;
|
|
18720
|
-
if (await detectTimescale(client)) return "timescale";
|
|
18721
|
-
if (await detectPartman(client)) return "partman";
|
|
18722
|
-
return "native";
|
|
18723
|
-
}
|
|
18724
|
-
function dayBounds(d) {
|
|
18725
|
-
const year = d.getUTCFullYear();
|
|
18726
|
-
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
18727
|
-
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
18728
|
-
const start = `${year}-${month}-${day} 00:00:00+00`;
|
|
18729
|
-
const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
|
|
18730
|
-
return {
|
|
18731
|
-
start,
|
|
18732
|
-
end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
|
|
18733
|
-
suffix: `${year}${month}${day}`
|
|
18734
|
-
};
|
|
18735
|
-
}
|
|
18736
|
-
function partitionName(table, suffix) {
|
|
18737
|
-
return `${table}_p${suffix}`;
|
|
18738
|
-
}
|
|
18739
|
-
/**
|
|
18740
|
-
* Creates daily partition tables in [today - 1 day, today + futureDays].
|
|
18741
|
-
* Idempotent: uses CREATE TABLE IF NOT EXISTS.
|
|
18742
|
-
*/
|
|
18743
|
-
async function ensureNativePartitions(client, schema, options = {}) {
|
|
18744
|
-
const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
|
|
18745
|
-
const start = options.includeYesterday ?? true ? -1 : 0;
|
|
18746
|
-
const baseNowMs = Date.now();
|
|
18747
|
-
for (const table of ALL_SIGNAL_TABLES) for (let i = start; i <= futureDays; i++) {
|
|
18748
|
-
const { start: partStart, end: partEnd, suffix } = dayBounds(new Date(baseNowMs + i * 864e5));
|
|
18749
|
-
const child = qualifiedName(schema, partitionName(table, suffix));
|
|
18750
|
-
const parent = qualifiedTable(schema, table);
|
|
18751
|
-
try {
|
|
18752
|
-
await client.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
|
|
18753
|
-
} catch (error) {
|
|
18754
|
-
if (!isDuplicateRelationError(error)) throw error;
|
|
18755
|
-
}
|
|
18756
|
-
try {
|
|
18757
|
-
await client.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
|
|
18758
|
-
FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
|
|
18759
|
-
} catch (error) {
|
|
18760
|
-
if (!isAlreadyAttachedError(error)) throw error;
|
|
18761
|
-
}
|
|
18762
|
-
}
|
|
18763
|
-
}
|
|
18764
|
-
/**
|
|
18765
|
-
* `ALTER TABLE ATTACH PARTITION` fails with `42P17 invalid_object_definition`
|
|
18766
|
-
* when the target table is already a partition of *any* parent (including
|
|
18767
|
-
* the one we want). On older PG it can also surface as 42710 or a generic
|
|
18768
|
-
* message. We err on the side of treating any "already a partition" wording
|
|
18769
|
-
* as success so concurrent inits converge cleanly.
|
|
18770
|
-
*/
|
|
18771
|
-
function isAlreadyAttachedError(error) {
|
|
18772
|
-
const code = error?.code;
|
|
18773
|
-
const message = error?.message ?? "";
|
|
18774
|
-
if (code === "42P17" || code === "42710") return /is already a partition/i.test(message) || /already a partition/i.test(message);
|
|
18775
|
-
return /is already a partition/i.test(message);
|
|
18776
|
-
}
|
|
18777
|
-
/**
|
|
18778
|
-
* Calls `create_hypertable()` for each signal table.
|
|
18779
|
-
*
|
|
18780
|
-
* Postgres declarative partitioning and Timescale hypertables are mutually
|
|
18781
|
-
* exclusive, so the base DDL must have been generated with TableDDLMode
|
|
18782
|
-
* 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
|
|
18783
|
-
* idempotent across re-inits.
|
|
18784
|
-
*/
|
|
18785
|
-
async function ensureTimescaleHypertables(client, schema) {
|
|
18786
|
-
for (const table of ALL_SIGNAL_TABLES) {
|
|
18787
|
-
const tableExpr = qualifiedTable(schema, table);
|
|
18788
|
-
const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
|
|
18789
|
-
await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
|
|
18790
|
-
}
|
|
18791
|
-
}
|
|
18792
|
-
/**
|
|
18793
|
-
* Registers each signal table with pg_partman as a daily-interval parent and
|
|
18794
|
-
* lets partman create the child partitions.
|
|
18795
|
-
*
|
|
18796
|
-
* Retention is intentionally NOT configured here — it stays opt-in via the
|
|
18797
|
-
* future Mastra CLI surface.
|
|
18798
|
-
*
|
|
18799
|
-
* Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
|
|
18800
|
-
* a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
|
|
18801
|
-
* adapter run against current arm64-friendly images without breaking existing
|
|
18802
|
-
* 4.x deployments.
|
|
18803
|
-
*/
|
|
18804
|
-
async function ensurePartmanHypertables(client, schema) {
|
|
18805
|
-
const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
|
|
18806
|
-
for (const table of ALL_SIGNAL_TABLES) {
|
|
18807
|
-
if ((await client.oneOrNone(`SELECT EXISTS (
|
|
18808
|
-
SELECT 1 FROM partman.part_config
|
|
18809
|
-
WHERE parent_table = format('%I.%I', $1::text, $2::text)
|
|
18810
|
-
) AS "exists"`, [schema, table]))?.exists) continue;
|
|
18811
|
-
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
18812
|
-
if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
|
|
18813
|
-
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
18814
|
-
p_control := $3,
|
|
18815
|
-
p_interval := '1 day',
|
|
18816
|
-
p_type := 'range'
|
|
18817
|
-
)`, [
|
|
18818
|
-
schema,
|
|
18819
|
-
table,
|
|
18820
|
-
SIGNAL_TIME_COLUMN[table]
|
|
18821
|
-
]);
|
|
18822
|
-
else await client.none(`SELECT partman.create_parent(
|
|
18823
|
-
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
18824
|
-
p_control := $3,
|
|
18825
|
-
p_type := 'native',
|
|
18826
|
-
p_interval := '1 day'
|
|
18827
|
-
)`, [
|
|
18828
|
-
schema,
|
|
18829
|
-
table,
|
|
18830
|
-
SIGNAL_TIME_COLUMN[table]
|
|
18831
|
-
]);
|
|
18832
|
-
break;
|
|
18833
|
-
} catch (error) {
|
|
18834
|
-
const message = error?.message ?? "";
|
|
18835
|
-
if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
|
|
18836
|
-
if (!/deadlock detected/i.test(message)) throw error;
|
|
18837
|
-
if ((await client.oneOrNone(`SELECT EXISTS (
|
|
18838
|
-
SELECT 1 FROM partman.part_config
|
|
18839
|
-
WHERE parent_table = format('%I.%I', $1::text, $2::text)
|
|
18840
|
-
) AS "exists"`, [schema, table]))?.exists) break;
|
|
18841
|
-
if (attempt === 2) throw error;
|
|
18842
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
18843
|
-
}
|
|
18844
|
-
}
|
|
18845
|
-
}
|
|
18846
|
-
async function setupPartitioning(client, schema, options = {}) {
|
|
18847
|
-
switch (await resolveMode(client, options)) {
|
|
18848
|
-
case "timescale":
|
|
18849
|
-
await ensureTimescaleHypertables(client, schema);
|
|
18850
|
-
return "timescale";
|
|
18851
|
-
case "partman":
|
|
18852
|
-
await ensurePartmanHypertables(client, schema);
|
|
18853
|
-
return "partman";
|
|
18854
|
-
case "native":
|
|
18855
|
-
await ensureNativePartitions(client, schema, options);
|
|
18856
|
-
return "native";
|
|
18857
|
-
}
|
|
18858
|
-
}
|
|
18859
19117
|
// src/storage/domains/observability/v-next/retention.ts
|
|
18860
19118
|
/**
|
|
18861
19119
|
* Age-based retention for the v-next Postgres observability domain.
|
|
@@ -19021,16 +19279,14 @@ function collapseExactScoreConflicts(rows) {
|
|
|
19021
19279
|
}
|
|
19022
19280
|
return [...records.values()];
|
|
19023
19281
|
}
|
|
19024
|
-
async function createScore(client, schema, args) {
|
|
19282
|
+
async function createScore(client, schema, args, partitioning) {
|
|
19025
19283
|
const row = scoreRecordToRow(args.score);
|
|
19026
|
-
|
|
19027
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
19284
|
+
await insertRows(client, schema, TABLE_SCORE_EVENTS, [row], partitioning, scoreRewriteConflict(row));
|
|
19028
19285
|
}
|
|
19029
|
-
async function batchCreateScores(client, schema, args) {
|
|
19286
|
+
async function batchCreateScores(client, schema, args, partitioning) {
|
|
19030
19287
|
if (args.scores.length === 0) return;
|
|
19031
19288
|
const rows = collapseExactScoreConflicts(args.scores.map(scoreRecordToRow));
|
|
19032
|
-
|
|
19033
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
19289
|
+
await insertRows(client, schema, TABLE_SCORE_EVENTS, rows, partitioning, scoreRewriteConflict(rows[0]));
|
|
19034
19290
|
}
|
|
19035
19291
|
/**
|
|
19036
19292
|
* Delete score events by scoreId. Optional `organizationId` and `resourceId`
|
|
@@ -19261,7 +19517,15 @@ const TRACE_STATUS_SQL = `CASE WHEN r."error" IS NOT NULL THEN 'error' ELSE 'suc
|
|
|
19261
19517
|
const timeToFirstChunkSupported = "timeToFirstChunkMs" in coreStorage.TRACE_QUERY_FIELD_REGISTRY.trace;
|
|
19262
19518
|
/** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
|
|
19263
19519
|
const timeToFirstChunkSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
|
|
19264
|
-
|
|
19520
|
+
/** Same rule for time to first token: returned and sorted only when core knows the field. */
|
|
19521
|
+
const timeToFirstTokenSupported = "timeToFirstTokenMs" in coreStorage.TRACE_QUERY_FIELD_REGISTRY.trace;
|
|
19522
|
+
const timeToFirstTokenSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstTokenMs") === true;
|
|
19523
|
+
const timingFeatures = [
|
|
19524
|
+
...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [],
|
|
19525
|
+
...timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : [],
|
|
19526
|
+
...timeToFirstTokenSupported ? ["trace-query-time-to-first-token"] : [],
|
|
19527
|
+
...timeToFirstTokenSortSupported ? ["trace-query-time-to-first-token-sort"] : []
|
|
19528
|
+
];
|
|
19265
19529
|
/**
|
|
19266
19530
|
* Keyset bounds for a cursor's sort value: terms for rows strictly past it in `direction`, and the
|
|
19267
19531
|
* condition for rows level with it. Empty values sort last in both directions, so they are past
|
|
@@ -19294,6 +19558,7 @@ const TRACE_FIELDS = {
|
|
|
19294
19558
|
endedAt: "r.\"endedAt\"",
|
|
19295
19559
|
durationMs: durationMsSql("r.\"startedAt\"", "r.\"endedAt\""),
|
|
19296
19560
|
timeToFirstChunkMs: "r.\"timeToFirstChunkMs\"",
|
|
19561
|
+
timeToFirstTokenMs: "r.\"timeToFirstTokenMs\"",
|
|
19297
19562
|
entityId: "r.\"entityId\"",
|
|
19298
19563
|
entityName: "r.\"entityName\"",
|
|
19299
19564
|
entityType: "r.\"entityType\"",
|
|
@@ -19312,6 +19577,7 @@ const SPAN_FIELDS = {
|
|
|
19312
19577
|
endedAt: "s.\"endedAt\"",
|
|
19313
19578
|
durationMs: "s.\"durationMs\"",
|
|
19314
19579
|
timeToFirstChunkMs: "s.\"timeToFirstChunkMs\"",
|
|
19580
|
+
timeToFirstTokenMs: "s.\"timeToFirstTokenMs\"",
|
|
19315
19581
|
status: "s.\"status\"",
|
|
19316
19582
|
error: "s.\"error\"",
|
|
19317
19583
|
entityType: "s.\"entityType\"",
|
|
@@ -19382,6 +19648,7 @@ const TRACE_SELECT = `
|
|
|
19382
19648
|
r."startedAt" AS "startedAt",
|
|
19383
19649
|
r."endedAt" AS "endedAt",
|
|
19384
19650
|
r."timeToFirstChunkMs" AS "timeToFirstChunkMs",
|
|
19651
|
+
r."timeToFirstTokenMs" AS "timeToFirstTokenMs",
|
|
19385
19652
|
r."entityName" AS "entityName",
|
|
19386
19653
|
r."entityType" AS "entityType",
|
|
19387
19654
|
r."environment" AS "environment",
|
|
@@ -19751,6 +20018,7 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
|
|
|
19751
20018
|
ELSE ${durationMsSql("s.\"startedAt\"", "s.\"endedAt\"")}
|
|
19752
20019
|
END AS "durationMs",
|
|
19753
20020
|
s."timeToFirstChunkMs",
|
|
20021
|
+
s."timeToFirstTokenMs",
|
|
19754
20022
|
CASE WHEN s."error" IS NOT NULL THEN 'error' ELSE 'success' END AS "status",
|
|
19755
20023
|
s."error",
|
|
19756
20024
|
s."entityType",
|
|
@@ -20125,6 +20393,7 @@ function traceRowToResult(row) {
|
|
|
20125
20393
|
startedAt: asIsoTimestamp$1(row.startedAt),
|
|
20126
20394
|
endedAt: asIsoTimestamp$1(row.endedAt),
|
|
20127
20395
|
...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) },
|
|
20396
|
+
...timeToFirstTokenSupported && { timeToFirstTokenMs: row.timeToFirstTokenMs == null ? null : Number(row.timeToFirstTokenMs) },
|
|
20128
20397
|
entityName: row.entityName == null ? null : String(row.entityName),
|
|
20129
20398
|
entityType: row.entityType == null ? null : String(row.entityType),
|
|
20130
20399
|
environment: row.environment == null ? null : String(row.environment),
|
|
@@ -20280,7 +20549,8 @@ const fields = [
|
|
|
20280
20549
|
"experimentId",
|
|
20281
20550
|
"environment",
|
|
20282
20551
|
"executionSource",
|
|
20283
|
-
"timeToFirstChunkMs"
|
|
20552
|
+
"timeToFirstChunkMs",
|
|
20553
|
+
"timeToFirstTokenMs"
|
|
20284
20554
|
];
|
|
20285
20555
|
const identities = identityFields.map((field) => `"${field}"`).join(", ");
|
|
20286
20556
|
const identityOrder = identityFields.map((field) => `"${field}" COLLATE "C" ASC NULLS LAST`).join(", ");
|
|
@@ -20412,9 +20682,10 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20412
20682
|
return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
|
|
20413
20683
|
const query = compilePostgresSpanQuery(schema, plan);
|
|
20414
20684
|
const rows = await tx.any(query.text, query.values);
|
|
20415
|
-
const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, ...row }) => ({
|
|
20685
|
+
const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, timeToFirstTokenMs, ...row }) => ({
|
|
20416
20686
|
...row,
|
|
20417
20687
|
...timeToFirstChunkSupported && { timeToFirstChunkMs },
|
|
20688
|
+
...timeToFirstTokenSupported && { timeToFirstTokenMs },
|
|
20418
20689
|
startedAt: startedAt.toISOString(),
|
|
20419
20690
|
endedAt: endedAt.toISOString()
|
|
20420
20691
|
}));
|
|
@@ -20546,7 +20817,11 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
|
|
|
20546
20817
|
}
|
|
20547
20818
|
const qualify = plan.timeRange.mode === "traces" ? "s.\"inWindow\"" : `s."${plan.timeRange.field}" >= $1 AND s."${plan.timeRange.field}" < $2`;
|
|
20548
20819
|
const filtered = predicateFields(plan.where);
|
|
20549
|
-
const firstContext = [
|
|
20820
|
+
const firstContext = [
|
|
20821
|
+
...contextFields,
|
|
20822
|
+
"timeToFirstChunkMs",
|
|
20823
|
+
"timeToFirstTokenMs"
|
|
20824
|
+
].filter((field) => field === "resourceId" || filtered.has(field));
|
|
20550
20825
|
const lastFields = [
|
|
20551
20826
|
"traceId",
|
|
20552
20827
|
"spanId",
|
|
@@ -20560,7 +20835,7 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
|
|
|
20560
20835
|
text: `WITH ${scopedRoots}, ${strategy === "candidates" ? `${candidates}, ` : ""}current_roots AS (
|
|
20561
20836
|
SELECT DISTINCT ON ("organizationId", "traceId")
|
|
20562
20837
|
"organizationId", "threadId", "traceId", "spanId", "cursorId", "startedAt", "endedAt", "isPending",
|
|
20563
|
-
${firstContext.map((field) => field === "timeToFirstChunkMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "
|
|
20838
|
+
${firstContext.map((field) => field === "timeToFirstChunkMs" || field === "timeToFirstTokenMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "${field}" END AS "${field}"` : `"${field}"`).join(", ")}
|
|
20564
20839
|
${filtered.has("status") ? ", CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status" : ""}
|
|
20565
20840
|
FROM scoped_roots ${strategy === "candidates" ? `WHERE (${identity("traceId")}) IN (SELECT ${identity("traceId")} FROM candidate_traces)` : ""}
|
|
20566
20841
|
ORDER BY "organizationId", "traceId", "startedAt" ASC, "spanId" COLLATE "C", "cursorId" DESC
|
|
@@ -20626,6 +20901,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
|
|
|
20626
20901
|
}));
|
|
20627
20902
|
const payloads = await run(`SELECT "organizationId", "traceId", "cursorId", ${contextFields.map((field) => `"${field}"`).join(", ")},
|
|
20628
20903
|
CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs",
|
|
20904
|
+
CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstTokenMs" END AS "timeToFirstTokenMs",
|
|
20629
20905
|
CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
|
|
20630
20906
|
${[
|
|
20631
20907
|
"input",
|
|
@@ -20654,6 +20930,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
|
|
|
20654
20930
|
endedAt: row.endedAt.toISOString(),
|
|
20655
20931
|
traceCount: Number(row.traceCount),
|
|
20656
20932
|
timeToFirstChunkMs: first.timeToFirstChunkMs,
|
|
20933
|
+
...timeToFirstTokenSupported && { timeToFirstTokenMs: first.timeToFirstTokenMs },
|
|
20657
20934
|
inputPreview: readPreview(first.inputPreview, first.input),
|
|
20658
20935
|
outputPreview: readPreview(last.outputPreview, last.output),
|
|
20659
20936
|
errorPreview: readPreview(last.errorPreview, last.error)
|
|
@@ -21610,10 +21887,9 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
|
|
|
21610
21887
|
return encodeDeltaCursor(await readSafeXactHorizon(client), 0);
|
|
21611
21888
|
}
|
|
21612
21889
|
// src/storage/domains/observability/v-next/tracing.ts
|
|
21613
|
-
async function writeSpanRows(client, schema, rows) {
|
|
21890
|
+
async function writeSpanRows(client, schema, rows, partitioning) {
|
|
21614
21891
|
const deduped = dedupeSpanRows(rows);
|
|
21615
|
-
|
|
21616
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
21892
|
+
await insertRows(client, schema, TABLE_SPAN_EVENTS, deduped, partitioning, spanConflictClause(Object.keys(deduped[0] ?? {})));
|
|
21617
21893
|
}
|
|
21618
21894
|
/**
|
|
21619
21895
|
* Collapse rows that share a primary key within a single batch. Postgres
|
|
@@ -21633,12 +21909,12 @@ function dedupeSpanRows(rows) {
|
|
|
21633
21909
|
}
|
|
21634
21910
|
return [...byKey.values()];
|
|
21635
21911
|
}
|
|
21636
|
-
async function createSpan(client, schema, args) {
|
|
21637
|
-
await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
|
|
21912
|
+
async function createSpan(client, schema, args, partitioning) {
|
|
21913
|
+
await writeSpanRows(client, schema, [spanRecordToRow(args.span)], partitioning);
|
|
21638
21914
|
}
|
|
21639
|
-
async function batchCreateSpans(client, schema, args) {
|
|
21915
|
+
async function batchCreateSpans(client, schema, args, partitioning) {
|
|
21640
21916
|
if (args.records.length === 0) return;
|
|
21641
|
-
await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
|
|
21917
|
+
await writeSpanRows(client, schema, args.records.map(spanRecordToRow), partitioning);
|
|
21642
21918
|
}
|
|
21643
21919
|
async function getSpans(client, schema, args) {
|
|
21644
21920
|
if (args.spanIds.length === 0) return {
|
|
@@ -21859,6 +22135,18 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
21859
22135
|
return this.#partitionMode;
|
|
21860
22136
|
}
|
|
21861
22137
|
/**
|
|
22138
|
+
* Create any missing day partitions from yesterday through today +
|
|
22139
|
+
* `futureDays`, without the rest of init(). For apps whose runtime role
|
|
22140
|
+
* cannot run DDL: call it on a schedule with a role that owns the
|
|
22141
|
+
* observability tables. Safe to call repeatedly on one instance. Does
|
|
22142
|
+
* nothing with TimescaleDB or pg_partman, which create their own partitions.
|
|
22143
|
+
*/
|
|
22144
|
+
async ensurePartitions() {
|
|
22145
|
+
return this.#run("VNEXT_ENSURE_PARTITIONS", async () => {
|
|
22146
|
+
if ((this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning)) === "native") await ensureNativePartitions(this.#client, this.#schema, this.#partitioning);
|
|
22147
|
+
});
|
|
22148
|
+
}
|
|
22149
|
+
/**
|
|
21862
22150
|
* All five signal tables are insert-only growth tables. The anchor column is
|
|
21863
22151
|
* each table's partition / chunk key, so age-based expiry drops whole day
|
|
21864
22152
|
* partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
|
|
@@ -21962,7 +22250,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
21962
22250
|
"trace-query-tenant-scope",
|
|
21963
22251
|
"feedback",
|
|
21964
22252
|
"trace-query-context-ids",
|
|
21965
|
-
...
|
|
22253
|
+
...timingFeatures,
|
|
21966
22254
|
"trace-query-root-span-fields",
|
|
21967
22255
|
"trace-query-root-entity-id"
|
|
21968
22256
|
];
|
|
@@ -21987,11 +22275,23 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
21987
22275
|
"trace-query-tenant-scope",
|
|
21988
22276
|
"feedback",
|
|
21989
22277
|
"trace-query-context-ids",
|
|
21990
|
-
...
|
|
22278
|
+
...timingFeatures,
|
|
21991
22279
|
"trace-query-root-span-fields",
|
|
21992
22280
|
"trace-query-root-entity-id"
|
|
21993
22281
|
];
|
|
21994
22282
|
}
|
|
22283
|
+
/**
|
|
22284
|
+
* Partitioning options for the write path, with `mode` fixed to the mode
|
|
22285
|
+
* this store resolved (in init(), or lazily in prune()) so the write path
|
|
22286
|
+
* does not detect it again. Until then the configured mode stands in and
|
|
22287
|
+
* the write path resolves it itself when a partition is missing.
|
|
22288
|
+
*/
|
|
22289
|
+
#writePartitioning() {
|
|
22290
|
+
return {
|
|
22291
|
+
...this.#partitioning,
|
|
22292
|
+
mode: this.#partitionMode ?? this.#partitioning.mode
|
|
22293
|
+
};
|
|
22294
|
+
}
|
|
21995
22295
|
async #run(op, fn, details) {
|
|
21996
22296
|
try {
|
|
21997
22297
|
return await fn();
|
|
@@ -22000,13 +22300,13 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22000
22300
|
}
|
|
22001
22301
|
}
|
|
22002
22302
|
async createSpan(args) {
|
|
22003
|
-
await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args), {
|
|
22303
|
+
await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args, this.#writePartitioning()), {
|
|
22004
22304
|
traceId: args.span.traceId,
|
|
22005
22305
|
spanId: args.span.spanId
|
|
22006
22306
|
});
|
|
22007
22307
|
}
|
|
22008
22308
|
async batchCreateSpans(args) {
|
|
22009
|
-
await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args), { count: args.records.length });
|
|
22309
|
+
await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.records.length });
|
|
22010
22310
|
}
|
|
22011
22311
|
async getSpan(args) {
|
|
22012
22312
|
return this.#run("GET_SPAN", () => getSpan(this.#readClient, this.#schema, args), {
|
|
@@ -22060,22 +22360,22 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22060
22360
|
return this.#run("LIST_BRANCHES", () => listBranches(this.#readClient, this.#schema, args));
|
|
22061
22361
|
}
|
|
22062
22362
|
async batchCreateLogs(args) {
|
|
22063
|
-
await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args), { count: args.logs.length });
|
|
22363
|
+
await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.logs.length });
|
|
22064
22364
|
}
|
|
22065
22365
|
async batchCreateMetrics(args) {
|
|
22066
|
-
await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args), { count: args.metrics.length });
|
|
22366
|
+
await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.metrics.length });
|
|
22067
22367
|
}
|
|
22068
22368
|
async createScore(args) {
|
|
22069
|
-
await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args));
|
|
22369
|
+
await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args, this.#writePartitioning()));
|
|
22070
22370
|
}
|
|
22071
22371
|
async batchCreateScores(args) {
|
|
22072
|
-
await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args), { count: args.scores.length });
|
|
22372
|
+
await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.scores.length });
|
|
22073
22373
|
}
|
|
22074
22374
|
async createFeedback(args) {
|
|
22075
|
-
await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args));
|
|
22375
|
+
await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args, this.#writePartitioning()));
|
|
22076
22376
|
}
|
|
22077
22377
|
async batchCreateFeedback(args) {
|
|
22078
|
-
await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args), { count: args.feedbacks.length });
|
|
22378
|
+
await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.feedbacks.length });
|
|
22079
22379
|
}
|
|
22080
22380
|
async deleteScores(args) {
|
|
22081
22381
|
await this.#run("DELETE_SCORES", () => deleteScores(this.#client, this.#schema, args), { count: args.scoreIds.length });
|
|
@@ -27485,12 +27785,20 @@ var PostgresStore = class extends MastraCompositeStore {
|
|
|
27485
27785
|
}
|
|
27486
27786
|
async #runPinnedInit() {
|
|
27487
27787
|
let pinnedClient;
|
|
27788
|
+
let pinned;
|
|
27488
27789
|
try {
|
|
27489
27790
|
pinnedClient = await this.#writePool.connect();
|
|
27490
|
-
|
|
27791
|
+
pinned = new PinnedClientAdapter(this.#writePool, pinnedClient);
|
|
27491
27792
|
this.#db.pin(pinned);
|
|
27492
|
-
|
|
27493
|
-
|
|
27793
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
27794
|
+
this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
|
|
27795
|
+
const failures = (await Promise.allSettled(Object.entries(this.stores).filter(([name, store]) => name !== "knowledge" && store).map(([, store]) => Promise.resolve().then(() => store.init())))).filter((result) => result.status === "rejected");
|
|
27796
|
+
if (failures.length === 0) break;
|
|
27797
|
+
const terminal = failures.find((result) => !isRetryableDeadlock(result.reason));
|
|
27798
|
+
if (terminal || attempt === 3) throw (terminal ?? failures[0]).reason;
|
|
27799
|
+
this.#db.setSchemaSnapshot(null);
|
|
27800
|
+
await backoffAfterDeadlock(attempt);
|
|
27801
|
+
}
|
|
27494
27802
|
this.isInitialized = true;
|
|
27495
27803
|
} catch (error) {
|
|
27496
27804
|
this.#initPromise = null;
|
|
@@ -27501,6 +27809,7 @@ var PostgresStore = class extends MastraCompositeStore {
|
|
|
27501
27809
|
category: ErrorCategory.THIRD_PARTY
|
|
27502
27810
|
}, error);
|
|
27503
27811
|
} finally {
|
|
27812
|
+
await pinned?.drain();
|
|
27504
27813
|
this.#db.setSchemaSnapshot(null);
|
|
27505
27814
|
if (pinnedClient) {
|
|
27506
27815
|
this.#db.unpin();
|
|
@@ -27655,6 +27964,10 @@ var PostgresStoreVNext = class extends PostgresStore {
|
|
|
27655
27964
|
owned: true
|
|
27656
27965
|
};
|
|
27657
27966
|
}
|
|
27967
|
+
/** See {@link ObservabilityStoragePostgresVNext.ensurePartitions}. */
|
|
27968
|
+
async ensurePartitions() {
|
|
27969
|
+
await this.stores.observability.ensurePartitions();
|
|
27970
|
+
}
|
|
27658
27971
|
/**
|
|
27659
27972
|
* Closes both the primary pool (when owned) and the observability pool
|
|
27660
27973
|
* (when this store created it). Safe to call multiple times.
|