@mastra/pg 1.31.1 → 1.31.2-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/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/index.cjs +456 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +457 -148
- package/dist/index.js.map +1 -1
- package/dist/storage/db/sanitize-json.d.ts +2 -0
- package/dist/storage/db/sanitize-json.d.ts.map +1 -1
- package/dist/storage/domains/notifications/index.d.ts +9 -0
- package/dist/storage/domains/notifications/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/helpers.d.ts +7 -0
- package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +2 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/preview-compat.d.ts +47 -0
- package/dist/storage/domains/observability/v-next/preview-compat.d.ts.map +1 -0
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts +12 -0
- 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 +2 -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 +11 -0
- package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/traces.d.ts +3 -1
- package/dist/storage/domains/observability/v-next/traces.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, calculatePagination, canonicalizeKnowledgeScope, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, 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, calculatePagination, canonicalizeKnowledgeScope, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, 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";
|
|
@@ -2448,6 +2448,10 @@ function repairJson(value) {
|
|
|
2448
2448
|
}
|
|
2449
2449
|
return value;
|
|
2450
2450
|
}
|
|
2451
|
+
/** Repair text for a PostgreSQL `text` column, which rejects NUL characters. */
|
|
2452
|
+
function toPgText(value) {
|
|
2453
|
+
return repair(value);
|
|
2454
|
+
}
|
|
2451
2455
|
/** Serialize JSON values without emitting Unicode sequences PostgreSQL rejects. */
|
|
2452
2456
|
function toPgJson(value) {
|
|
2453
2457
|
const json = JSON.stringify(value);
|
|
@@ -14539,6 +14543,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
|
|
|
14539
14543
|
};
|
|
14540
14544
|
//#endregion
|
|
14541
14545
|
//#region src/storage/domains/notifications/index.ts
|
|
14546
|
+
const COMPOSITE_PRIMARY_KEY$1 = TABLE_CONFIGS[TABLE_NOTIFICATIONS].compositePrimaryKey;
|
|
14542
14547
|
const statusTimestamp = (status, now) => {
|
|
14543
14548
|
if (status === "delivered") return { deliveredAt: now };
|
|
14544
14549
|
if (status === "seen") return { seenAt: now };
|
|
@@ -14650,11 +14655,58 @@ var NotificationsPG = class NotificationsPG extends NotificationsStorage {
|
|
|
14650
14655
|
async init() {
|
|
14651
14656
|
await this.#db.createTable({
|
|
14652
14657
|
tableName: TABLE_NOTIFICATIONS,
|
|
14653
|
-
schema: TABLE_SCHEMAS[TABLE_NOTIFICATIONS]
|
|
14658
|
+
schema: TABLE_SCHEMAS[TABLE_NOTIFICATIONS],
|
|
14659
|
+
compositePrimaryKey: COMPOSITE_PRIMARY_KEY$1
|
|
14654
14660
|
});
|
|
14661
|
+
await this.ensurePrimaryKey();
|
|
14655
14662
|
await this.createDefaultIndexes();
|
|
14656
14663
|
await this.createCustomIndexes();
|
|
14657
14664
|
}
|
|
14665
|
+
async #hasPrimaryKey() {
|
|
14666
|
+
return !!(await this.#db.client.oneOrNone(`SELECT EXISTS (
|
|
14667
|
+
SELECT 1 FROM pg_index i
|
|
14668
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
14669
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
14670
|
+
WHERE c.relname = $1 AND n.nspname = $2 AND i.indisprimary
|
|
14671
|
+
) AS exists`, [TABLE_NOTIFICATIONS, this.#schema]))?.exists;
|
|
14672
|
+
}
|
|
14673
|
+
/**
|
|
14674
|
+
* Tables created before the composite primary key was declared have no key at
|
|
14675
|
+
* all, which allows duplicate ids and leaves the table without a replica
|
|
14676
|
+
* identity (UPDATE/DELETE fail under logical replication). `createTable` is a
|
|
14677
|
+
* no-op for existing tables, so add the key here. If duplicate rows already
|
|
14678
|
+
* exist, or the role does not own the table, the key cannot be added; warn with
|
|
14679
|
+
* remediation instead of blocking init.
|
|
14680
|
+
*/
|
|
14681
|
+
async ensurePrimaryKey() {
|
|
14682
|
+
if (await this.#hasPrimaryKey()) return;
|
|
14683
|
+
const tableName = getTableName$5({
|
|
14684
|
+
indexName: TABLE_NOTIFICATIONS,
|
|
14685
|
+
schemaName: getSchemaName$5(this.#schema)
|
|
14686
|
+
});
|
|
14687
|
+
const keyColumns = COMPOSITE_PRIMARY_KEY$1.map((column) => `"${parseSqlIdentifier(column, "column name")}"`).join(", ");
|
|
14688
|
+
const remediation = `ALTER TABLE ${tableName} ADD PRIMARY KEY (${keyColumns});`;
|
|
14689
|
+
if (await this.#db.client.oneOrNone(`SELECT 1 FROM ${tableName} GROUP BY ${keyColumns} HAVING count(*) > 1 LIMIT 1`)) {
|
|
14690
|
+
this.logger?.warn?.(`${tableName} contains duplicate (${keyColumns}) rows, so its PRIMARY KEY could not be added. Remove the duplicates, then run: ${remediation}`);
|
|
14691
|
+
return;
|
|
14692
|
+
}
|
|
14693
|
+
try {
|
|
14694
|
+
await this.#db.client.none(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${keyColumns})`);
|
|
14695
|
+
this.logger?.info?.(`Added PRIMARY KEY (${keyColumns}) to ${tableName}`);
|
|
14696
|
+
} catch (error) {
|
|
14697
|
+
if (await this.#hasPrimaryKey()) return;
|
|
14698
|
+
if (error?.code === "42501") {
|
|
14699
|
+
this.logger?.warn?.(`Insufficient privilege to add PRIMARY KEY (${keyColumns}) to ${tableName}. Ask the table owner to run: ${remediation}`);
|
|
14700
|
+
return;
|
|
14701
|
+
}
|
|
14702
|
+
throw new MastraError({
|
|
14703
|
+
id: createStorageErrorId("PG", "ADD_NOTIFICATIONS_PRIMARY_KEY", "FAILED"),
|
|
14704
|
+
domain: ErrorDomain.STORAGE,
|
|
14705
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
14706
|
+
details: { tableName }
|
|
14707
|
+
}, error);
|
|
14708
|
+
}
|
|
14709
|
+
}
|
|
14658
14710
|
/**
|
|
14659
14711
|
* Lazily ensures a btree index exists on each configured policy's retention
|
|
14660
14712
|
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
@@ -14738,6 +14790,7 @@ var NotificationsPG = class NotificationsPG extends NotificationsStorage {
|
|
|
14738
14790
|
tableName: TABLE_NOTIFICATIONS,
|
|
14739
14791
|
schema: TABLE_SCHEMAS[TABLE_NOTIFICATIONS],
|
|
14740
14792
|
schemaName,
|
|
14793
|
+
compositePrimaryKey: COMPOSITE_PRIMARY_KEY$1,
|
|
14741
14794
|
includeAllConstraints: true
|
|
14742
14795
|
}));
|
|
14743
14796
|
for (const idx of NotificationsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
|
|
@@ -16002,6 +16055,21 @@ const SPAN_EVENT_COLUMNS = [
|
|
|
16002
16055
|
name: "requestContext",
|
|
16003
16056
|
type: "jsonb",
|
|
16004
16057
|
nullable: true
|
|
16058
|
+
},
|
|
16059
|
+
{
|
|
16060
|
+
name: "inputPreview",
|
|
16061
|
+
type: "text",
|
|
16062
|
+
nullable: true
|
|
16063
|
+
},
|
|
16064
|
+
{
|
|
16065
|
+
name: "outputPreview",
|
|
16066
|
+
type: "text",
|
|
16067
|
+
nullable: true
|
|
16068
|
+
},
|
|
16069
|
+
{
|
|
16070
|
+
name: "errorPreview",
|
|
16071
|
+
type: "text",
|
|
16072
|
+
nullable: true
|
|
16005
16073
|
}
|
|
16006
16074
|
];
|
|
16007
16075
|
const METRIC_EVENT_COLUMNS = [
|
|
@@ -16657,15 +16725,27 @@ function allTableDDL(schema, mode) {
|
|
|
16657
16725
|
* default does not rewrite the table, and the ALTER cascades to partitions.
|
|
16658
16726
|
*/
|
|
16659
16727
|
function additiveColumns(schema) {
|
|
16660
|
-
return [
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16728
|
+
return [
|
|
16729
|
+
{
|
|
16730
|
+
table: TABLE_SPAN_EVENTS,
|
|
16731
|
+
column: "isPending",
|
|
16732
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
|
|
16733
|
+
},
|
|
16734
|
+
...[
|
|
16735
|
+
"inputPreview",
|
|
16736
|
+
"outputPreview",
|
|
16737
|
+
"errorPreview"
|
|
16738
|
+
].map((column) => ({
|
|
16739
|
+
table: TABLE_SPAN_EVENTS,
|
|
16740
|
+
column,
|
|
16741
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "${column}" text`
|
|
16742
|
+
})),
|
|
16743
|
+
{
|
|
16744
|
+
table: TABLE_FEEDBACK_EVENTS,
|
|
16745
|
+
column: "reviewStatus",
|
|
16746
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)} ADD COLUMN IF NOT EXISTS "reviewStatus" text NOT NULL DEFAULT 'needs-review'`
|
|
16747
|
+
}
|
|
16748
|
+
];
|
|
16669
16749
|
}
|
|
16670
16750
|
/** Existence probe for an additive column. Params: schema, table, column. */
|
|
16671
16751
|
const columnExistsSQL = `SELECT 1 FROM information_schema.columns
|
|
@@ -16987,6 +17067,46 @@ function whereOrEmpty(acc) {
|
|
|
16987
17067
|
return acc.conditions.length ? `WHERE ${acc.conditions.join(" AND ")}` : "";
|
|
16988
17068
|
}
|
|
16989
17069
|
//#endregion
|
|
17070
|
+
//#region src/storage/domains/observability/v-next/preview-compat.ts
|
|
17071
|
+
const generator = coreStorage.buildStoredPreview;
|
|
17072
|
+
const canGeneratePreviews = typeof generator === "function";
|
|
17073
|
+
/** SQL detection only; preview formatting stays in TypeScript. Empty strings are intentional. */
|
|
17074
|
+
function missingPreviewSql(column) {
|
|
17075
|
+
return canGeneratePreviews ? `${column} IS NULL` : "FALSE";
|
|
17076
|
+
}
|
|
17077
|
+
function needsPreview(value) {
|
|
17078
|
+
return canGeneratePreviews && value == null;
|
|
17079
|
+
}
|
|
17080
|
+
function storedPreview(preview, value, serialized) {
|
|
17081
|
+
return canGeneratePreviews ? generator(preview, value, serialized) : preview ?? null;
|
|
17082
|
+
}
|
|
17083
|
+
function readPreview(preview, payload, jsonEncoded = false) {
|
|
17084
|
+
const supplied = preview;
|
|
17085
|
+
if (!canGeneratePreviews) return supplied ?? null;
|
|
17086
|
+
if (supplied != null) return supplied.trim() || null;
|
|
17087
|
+
let value = payload;
|
|
17088
|
+
if (needsPreview(preview) && jsonEncoded && typeof payload === "string") try {
|
|
17089
|
+
value = JSON.parse(payload);
|
|
17090
|
+
} catch {
|
|
17091
|
+
value = null;
|
|
17092
|
+
}
|
|
17093
|
+
return storedPreview(supplied, value, jsonEncoded && typeof payload === "string" ? payload : void 0) || null;
|
|
17094
|
+
}
|
|
17095
|
+
/** Older response builders may shorten previews or turn empty strings into null. */
|
|
17096
|
+
function buildSpanQueryResponse(...args) {
|
|
17097
|
+
if (canGeneratePreviews) return coreStorage.buildSpanQueryResponse(...args);
|
|
17098
|
+
const [plan, selected, payloads, metrics] = args;
|
|
17099
|
+
const response = coreStorage.buildSpanQueryResponse(plan, selected, [], metrics);
|
|
17100
|
+
const byIdentity = new Map(payloads.map((payload) => [coreStorage.spanQueryIdentityKey(payload), payload]));
|
|
17101
|
+
for (const span of response.spans) {
|
|
17102
|
+
const payload = byIdentity.get(coreStorage.spanQueryIdentityKey(span));
|
|
17103
|
+
span.inputPreview = payload?.inputPreview ?? null;
|
|
17104
|
+
span.outputPreview = payload?.outputPreview ?? null;
|
|
17105
|
+
span.errorPreview = payload?.errorPreview ?? null;
|
|
17106
|
+
}
|
|
17107
|
+
return response;
|
|
17108
|
+
}
|
|
17109
|
+
//#endregion
|
|
16990
17110
|
//#region src/storage/domains/observability/v-next/review-status.ts
|
|
16991
17111
|
const FEEDBACK_REVIEW_STATUSES = ["needs-review", "reviewed"];
|
|
16992
17112
|
function isFeedbackReviewStatus(value) {
|
|
@@ -17012,6 +17132,137 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
|
|
|
17012
17132
|
};
|
|
17013
17133
|
}
|
|
17014
17134
|
//#endregion
|
|
17135
|
+
//#region src/storage/domains/observability/v-next/sql.ts
|
|
17136
|
+
/**
|
|
17137
|
+
* SQL helpers for the v-next Postgres observability domain.
|
|
17138
|
+
*
|
|
17139
|
+
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
17140
|
+
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
17141
|
+
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
17142
|
+
*/
|
|
17143
|
+
/**
|
|
17144
|
+
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
17145
|
+
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
17146
|
+
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
17147
|
+
*
|
|
17148
|
+
* The result is sanitized because Postgres rejects some sequences that are
|
|
17149
|
+
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
17150
|
+
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
17151
|
+
* multi-row statement, so one bad value would otherwise reject the whole
|
|
17152
|
+
* batch of observability events.
|
|
17153
|
+
*/
|
|
17154
|
+
function encodeJsonb(value) {
|
|
17155
|
+
if (value === null || value === void 0) return null;
|
|
17156
|
+
if (value instanceof EncodedJsonb) return value.json;
|
|
17157
|
+
return toPgJson(value);
|
|
17158
|
+
}
|
|
17159
|
+
/**
|
|
17160
|
+
* A jsonb value the caller already serialized with `toPgJson`, so `buildInsert`
|
|
17161
|
+
* sends it as is instead of serializing it again. Span writes serialize their
|
|
17162
|
+
* payloads up front because the stored previews are built from that same text.
|
|
17163
|
+
*/
|
|
17164
|
+
var EncodedJsonb = class {
|
|
17165
|
+
json;
|
|
17166
|
+
constructor(json) {
|
|
17167
|
+
this.json = json;
|
|
17168
|
+
}
|
|
17169
|
+
};
|
|
17170
|
+
/** Serialize a value for a jsonb column once, for callers that also need the text. */
|
|
17171
|
+
function encodeJsonbValue(value) {
|
|
17172
|
+
if (value === null || value === void 0) return null;
|
|
17173
|
+
const json = toPgJson(value);
|
|
17174
|
+
return json === void 0 ? null : new EncodedJsonb(json);
|
|
17175
|
+
}
|
|
17176
|
+
/**
|
|
17177
|
+
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
17178
|
+
*
|
|
17179
|
+
* @param schema Schema name.
|
|
17180
|
+
* @param table Table name.
|
|
17181
|
+
* @param records Array of records (each is a column-name → value object).
|
|
17182
|
+
* All records must have identical key sets.
|
|
17183
|
+
* @returns { text, values } ready to pass to `client.query`.
|
|
17184
|
+
*/
|
|
17185
|
+
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
17186
|
+
if (records.length === 0) return null;
|
|
17187
|
+
const columns = Object.keys(records[0]).map((c) => parseSqlIdentifier(c, "column name"));
|
|
17188
|
+
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
17189
|
+
const values = [];
|
|
17190
|
+
const rowPlaceholders = [];
|
|
17191
|
+
let p = 1;
|
|
17192
|
+
for (const record of records) {
|
|
17193
|
+
const placeholders = columns.map((col) => {
|
|
17194
|
+
const raw = record[col];
|
|
17195
|
+
if (JSONB_COLUMNS.has(col)) {
|
|
17196
|
+
values.push(encodeJsonb(raw));
|
|
17197
|
+
return `$${p++}::jsonb`;
|
|
17198
|
+
}
|
|
17199
|
+
if (TEXT_ARRAY_COLUMNS.has(col)) {
|
|
17200
|
+
values.push(Array.isArray(raw) ? raw : []);
|
|
17201
|
+
return `$${p++}::text[]`;
|
|
17202
|
+
}
|
|
17203
|
+
values.push(raw === void 0 ? null : raw);
|
|
17204
|
+
return `$${p++}`;
|
|
17205
|
+
});
|
|
17206
|
+
rowPlaceholders.push(`(${placeholders.join(", ")})`);
|
|
17207
|
+
}
|
|
17208
|
+
return {
|
|
17209
|
+
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
17210
|
+
VALUES ${rowPlaceholders.join(", ")}
|
|
17211
|
+
${onConflict}`,
|
|
17212
|
+
values
|
|
17213
|
+
};
|
|
17214
|
+
}
|
|
17215
|
+
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
17216
|
+
const SPAN_KEY_COLUMNS = [
|
|
17217
|
+
"traceId",
|
|
17218
|
+
"spanId",
|
|
17219
|
+
"endedAt"
|
|
17220
|
+
];
|
|
17221
|
+
/**
|
|
17222
|
+
* Conflict clause for span writes.
|
|
17223
|
+
*
|
|
17224
|
+
* A span is written twice under the event-sourced strategy: once when it starts
|
|
17225
|
+
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
17226
|
+
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
17227
|
+
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
17228
|
+
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
17229
|
+
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
17230
|
+
* end row overwrites the pending one instead.
|
|
17231
|
+
*
|
|
17232
|
+
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
17233
|
+
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
17234
|
+
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
17235
|
+
* span to pending.
|
|
17236
|
+
*/
|
|
17237
|
+
function spanConflictClause(columns) {
|
|
17238
|
+
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
17239
|
+
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
17240
|
+
}
|
|
17241
|
+
/**
|
|
17242
|
+
* Ordering that picks the winning row for a span under the event-sourced write
|
|
17243
|
+
* model. An ended row always beats a pending row for the same span; among rows
|
|
17244
|
+
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
17245
|
+
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
17246
|
+
* the grouping key to collapse a span's rows down to one.
|
|
17247
|
+
*/
|
|
17248
|
+
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
17249
|
+
/**
|
|
17250
|
+
* Standard SELECT column list for tracing tables. The select projects every
|
|
17251
|
+
* column the row→record converters expect.
|
|
17252
|
+
*/
|
|
17253
|
+
const SPAN_SELECT_COLUMNS = `
|
|
17254
|
+
${buildSelectColumns(SPAN_EVENT_COLUMNS)}`;
|
|
17255
|
+
const SPAN_LIGHT_SELECT_COLUMNS = `
|
|
17256
|
+
${buildNamedSelectColumns(SPAN_LIGHT_SELECT_COLUMN_NAMES)}`;
|
|
17257
|
+
const METRIC_SELECT_COLUMNS = `
|
|
17258
|
+
${buildSelectColumns(METRIC_EVENT_COLUMNS)}`;
|
|
17259
|
+
const LOG_SELECT_COLUMNS = `
|
|
17260
|
+
${buildSelectColumns(LOG_EVENT_COLUMNS)}`;
|
|
17261
|
+
const SCORE_SELECT_COLUMNS = `
|
|
17262
|
+
${buildSelectColumns(SCORE_EVENT_COLUMNS)}`;
|
|
17263
|
+
const FEEDBACK_SELECT_COLUMNS = `
|
|
17264
|
+
${buildSelectColumns(FEEDBACK_EVENT_COLUMNS)}`;
|
|
17265
|
+
//#endregion
|
|
17015
17266
|
//#region src/storage/domains/observability/v-next/helpers.ts
|
|
17016
17267
|
const PROMOTED_KEYS = /* @__PURE__ */ new Set([
|
|
17017
17268
|
"experimentId",
|
|
@@ -17161,10 +17412,26 @@ function commonContextToRow(record) {
|
|
|
17161
17412
|
serviceName: record.serviceName ?? null
|
|
17162
17413
|
};
|
|
17163
17414
|
}
|
|
17415
|
+
/**
|
|
17416
|
+
* Serialize a span payload for its jsonb column and build its stored preview from the same value.
|
|
17417
|
+
* Nonblank upstream previews win (trimmed and capped like generated ones). The preview goes into a `text` column, which
|
|
17418
|
+
* rejects NUL, so it gets the same repair `toPgJson` applies to the payload.
|
|
17419
|
+
*/
|
|
17420
|
+
function spanPayload(value, upstream) {
|
|
17421
|
+
const encoded = encodeJsonbValue(value);
|
|
17422
|
+
const preview = storedPreview(upstream, value, encoded?.json);
|
|
17423
|
+
return {
|
|
17424
|
+
encoded,
|
|
17425
|
+
preview: preview === null ? null : toPgText(preview)
|
|
17426
|
+
};
|
|
17427
|
+
}
|
|
17164
17428
|
function spanRecordToRow(span) {
|
|
17165
17429
|
const isPending = !span.isEvent && span.endedAt == null;
|
|
17166
17430
|
const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
|
|
17167
17431
|
const metadata = span.metadata ?? null;
|
|
17432
|
+
const input = spanPayload(span.input, span.inputPreview);
|
|
17433
|
+
const output = spanPayload(span.output, span.outputPreview);
|
|
17434
|
+
const error = spanPayload(span.error, span.errorPreview);
|
|
17168
17435
|
return {
|
|
17169
17436
|
...commonContextToRow(span),
|
|
17170
17437
|
traceId: span.traceId,
|
|
@@ -17182,10 +17449,13 @@ function spanRecordToRow(span) {
|
|
|
17182
17449
|
scope: jsonField(span.scope),
|
|
17183
17450
|
attributes: jsonField(span.attributes),
|
|
17184
17451
|
links: jsonField(span.links),
|
|
17185
|
-
input:
|
|
17186
|
-
output:
|
|
17187
|
-
error:
|
|
17188
|
-
requestContext: jsonField(span.requestContext)
|
|
17452
|
+
input: input.encoded,
|
|
17453
|
+
output: output.encoded,
|
|
17454
|
+
error: error.encoded,
|
|
17455
|
+
requestContext: jsonField(span.requestContext),
|
|
17456
|
+
inputPreview: input.preview,
|
|
17457
|
+
outputPreview: output.preview,
|
|
17458
|
+
errorPreview: error.preview
|
|
17189
17459
|
};
|
|
17190
17460
|
}
|
|
17191
17461
|
/**
|
|
@@ -17252,6 +17522,32 @@ function rowToLightSpanRecord(row) {
|
|
|
17252
17522
|
updatedAt: null
|
|
17253
17523
|
};
|
|
17254
17524
|
}
|
|
17525
|
+
/** A stored preview as lists return it: `''` (nothing to preview) and NULL become null. */
|
|
17526
|
+
function listPreview(value) {
|
|
17527
|
+
return readPreview(value);
|
|
17528
|
+
}
|
|
17529
|
+
/**
|
|
17530
|
+
* Build a trace-list {@link LightSpanRecord} from a row that projected the light trace-list columns
|
|
17531
|
+
* (see `TRACE_LIGHT_SELECT_COLUMNS` in traces.ts): the timeline fields plus the memory context,
|
|
17532
|
+
* metadata and the stored previews. Callers fill in previews for rows written before they were
|
|
17533
|
+
* stored first.
|
|
17534
|
+
*/
|
|
17535
|
+
function rowToLightTraceRecord(row) {
|
|
17536
|
+
const record = rowToLightSpanRecord(row);
|
|
17537
|
+
return {
|
|
17538
|
+
...record,
|
|
17539
|
+
status: computeTraceStatus({
|
|
17540
|
+
error: record.error,
|
|
17541
|
+
endedAt: record.endedAt
|
|
17542
|
+
}),
|
|
17543
|
+
threadId: row.threadId == null ? null : String(row.threadId),
|
|
17544
|
+
resourceId: row.resourceId == null ? null : String(row.resourceId),
|
|
17545
|
+
metadata: parsedJson(row.metadataRaw) ?? void 0,
|
|
17546
|
+
inputPreview: listPreview(row.inputPreview),
|
|
17547
|
+
outputPreview: listPreview(row.outputPreview),
|
|
17548
|
+
errorPreview: listPreview(row.errorPreview)
|
|
17549
|
+
};
|
|
17550
|
+
}
|
|
17255
17551
|
function logRecordToRow(log) {
|
|
17256
17552
|
return {
|
|
17257
17553
|
...commonContextToRow(log),
|
|
@@ -17767,119 +18063,6 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
|
|
|
17767
18063
|
"requestContext"
|
|
17768
18064
|
]);
|
|
17769
18065
|
//#endregion
|
|
17770
|
-
//#region src/storage/domains/observability/v-next/sql.ts
|
|
17771
|
-
/**
|
|
17772
|
-
* SQL helpers for the v-next Postgres observability domain.
|
|
17773
|
-
*
|
|
17774
|
-
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
17775
|
-
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
17776
|
-
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
17777
|
-
*/
|
|
17778
|
-
/**
|
|
17779
|
-
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
17780
|
-
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
17781
|
-
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
17782
|
-
*
|
|
17783
|
-
* The result is sanitized because Postgres rejects some sequences that are
|
|
17784
|
-
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
17785
|
-
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
17786
|
-
* multi-row statement, so one bad value would otherwise reject the whole
|
|
17787
|
-
* batch of observability events.
|
|
17788
|
-
*/
|
|
17789
|
-
function encodeJsonb(value) {
|
|
17790
|
-
if (value === null || value === void 0) return null;
|
|
17791
|
-
return toPgJson(value);
|
|
17792
|
-
}
|
|
17793
|
-
/**
|
|
17794
|
-
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
17795
|
-
*
|
|
17796
|
-
* @param schema Schema name.
|
|
17797
|
-
* @param table Table name.
|
|
17798
|
-
* @param records Array of records (each is a column-name → value object).
|
|
17799
|
-
* All records must have identical key sets.
|
|
17800
|
-
* @returns { text, values } ready to pass to `client.query`.
|
|
17801
|
-
*/
|
|
17802
|
-
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
17803
|
-
if (records.length === 0) return null;
|
|
17804
|
-
const columns = Object.keys(records[0]).map((c) => parseSqlIdentifier(c, "column name"));
|
|
17805
|
-
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
17806
|
-
const values = [];
|
|
17807
|
-
const rowPlaceholders = [];
|
|
17808
|
-
let p = 1;
|
|
17809
|
-
for (const record of records) {
|
|
17810
|
-
const placeholders = columns.map((col) => {
|
|
17811
|
-
const raw = record[col];
|
|
17812
|
-
if (JSONB_COLUMNS.has(col)) {
|
|
17813
|
-
values.push(encodeJsonb(raw));
|
|
17814
|
-
return `$${p++}::jsonb`;
|
|
17815
|
-
}
|
|
17816
|
-
if (TEXT_ARRAY_COLUMNS.has(col)) {
|
|
17817
|
-
values.push(Array.isArray(raw) ? raw : []);
|
|
17818
|
-
return `$${p++}::text[]`;
|
|
17819
|
-
}
|
|
17820
|
-
values.push(raw === void 0 ? null : raw);
|
|
17821
|
-
return `$${p++}`;
|
|
17822
|
-
});
|
|
17823
|
-
rowPlaceholders.push(`(${placeholders.join(", ")})`);
|
|
17824
|
-
}
|
|
17825
|
-
return {
|
|
17826
|
-
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
17827
|
-
VALUES ${rowPlaceholders.join(", ")}
|
|
17828
|
-
${onConflict}`,
|
|
17829
|
-
values
|
|
17830
|
-
};
|
|
17831
|
-
}
|
|
17832
|
-
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
17833
|
-
const SPAN_KEY_COLUMNS = [
|
|
17834
|
-
"traceId",
|
|
17835
|
-
"spanId",
|
|
17836
|
-
"endedAt"
|
|
17837
|
-
];
|
|
17838
|
-
/**
|
|
17839
|
-
* Conflict clause for span writes.
|
|
17840
|
-
*
|
|
17841
|
-
* A span is written twice under the event-sourced strategy: once when it starts
|
|
17842
|
-
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
17843
|
-
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
17844
|
-
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
17845
|
-
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
17846
|
-
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
17847
|
-
* end row overwrites the pending one instead.
|
|
17848
|
-
*
|
|
17849
|
-
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
17850
|
-
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
17851
|
-
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
17852
|
-
* span to pending.
|
|
17853
|
-
*/
|
|
17854
|
-
function spanConflictClause(columns) {
|
|
17855
|
-
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
17856
|
-
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
17857
|
-
}
|
|
17858
|
-
/**
|
|
17859
|
-
* Ordering that picks the winning row for a span under the event-sourced write
|
|
17860
|
-
* model. An ended row always beats a pending row for the same span; among rows
|
|
17861
|
-
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
17862
|
-
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
17863
|
-
* the grouping key to collapse a span's rows down to one.
|
|
17864
|
-
*/
|
|
17865
|
-
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
17866
|
-
/**
|
|
17867
|
-
* Standard SELECT column list for tracing tables. The select projects every
|
|
17868
|
-
* column the row→record converters expect.
|
|
17869
|
-
*/
|
|
17870
|
-
const SPAN_SELECT_COLUMNS = `
|
|
17871
|
-
${buildSelectColumns(SPAN_EVENT_COLUMNS)}`;
|
|
17872
|
-
const SPAN_LIGHT_SELECT_COLUMNS = `
|
|
17873
|
-
${buildNamedSelectColumns(SPAN_LIGHT_SELECT_COLUMN_NAMES)}`;
|
|
17874
|
-
const METRIC_SELECT_COLUMNS = `
|
|
17875
|
-
${buildSelectColumns(METRIC_EVENT_COLUMNS)}`;
|
|
17876
|
-
const LOG_SELECT_COLUMNS = `
|
|
17877
|
-
${buildSelectColumns(LOG_EVENT_COLUMNS)}`;
|
|
17878
|
-
const SCORE_SELECT_COLUMNS = `
|
|
17879
|
-
${buildSelectColumns(SCORE_EVENT_COLUMNS)}`;
|
|
17880
|
-
const FEEDBACK_SELECT_COLUMNS = `
|
|
17881
|
-
${buildSelectColumns(FEEDBACK_EVENT_COLUMNS)}`;
|
|
17882
|
-
//#endregion
|
|
17883
18066
|
//#region src/storage/domains/observability/v-next/feedback.ts
|
|
17884
18067
|
/**
|
|
17885
18068
|
* Feedback operations for the v-next Postgres observability domain.
|
|
@@ -19038,7 +19221,10 @@ const TRACE_SELECT = `
|
|
|
19038
19221
|
r."entityId" AS "entityId",
|
|
19039
19222
|
r."parentSpanId" AS "parentSpanId",
|
|
19040
19223
|
r."metadataRaw" AS "metadata",
|
|
19041
|
-
r."
|
|
19224
|
+
r."inputPreview" AS "inputPreview",
|
|
19225
|
+
r."outputPreview" AS "outputPreview",
|
|
19226
|
+
r."errorPreview" AS "errorPreview",
|
|
19227
|
+
r."cursorId" AS "cursorId",
|
|
19042
19228
|
r."threadId" AS "threadId",
|
|
19043
19229
|
r."resourceId" AS "resourceId",
|
|
19044
19230
|
r."startedAt" AS "startedAt",
|
|
@@ -19496,7 +19682,7 @@ function compilePostgresTraceQuery(schema, plan, mode = "data", safeHorizon) {
|
|
|
19496
19682
|
safeHorizon
|
|
19497
19683
|
};
|
|
19498
19684
|
}
|
|
19499
|
-
const { ctes, values } = compilePostgresTraceCandidates(schema, plan, `${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.\"xactId\"
|
|
19685
|
+
const { ctes, values } = compilePostgresTraceCandidates(schema, plan, `${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.\"xactId\"" : ""}`, deltaWindow);
|
|
19500
19686
|
const candidates = `WITH ${ctes.join(",\n")}`;
|
|
19501
19687
|
if (plan.paginationMode === "page" && mode === "count") return {
|
|
19502
19688
|
text: `${candidates}
|
|
@@ -19738,6 +19924,34 @@ async function setRemainingTimeout(transaction, deadline) {
|
|
|
19738
19924
|
if (remainingTimeoutMs <= 0) throw new coreStorage.TraceQueryExecutionError();
|
|
19739
19925
|
await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${remainingTimeoutMs}ms`]);
|
|
19740
19926
|
}
|
|
19927
|
+
const PREVIEW_PAYLOADS$2 = [
|
|
19928
|
+
["inputPreview", "input"],
|
|
19929
|
+
["outputPreview", "output"],
|
|
19930
|
+
["errorPreview", "error"]
|
|
19931
|
+
];
|
|
19932
|
+
/**
|
|
19933
|
+
* Fill the previews of root rows written before the preview columns existed (stored preview NULL)
|
|
19934
|
+
* by reading just those rows' payloads, and only the payloads whose preview is missing. Rows are
|
|
19935
|
+
* keyed by `cursorId`, which identifies the exact root row the list picked.
|
|
19936
|
+
*
|
|
19937
|
+
* jsonb comes back decoded, so the preview is built with `buildPreview(value)`. jsonb also
|
|
19938
|
+
* normalizes object key order, so a compact-JSON fallback preview can differ from the one the
|
|
19939
|
+
* writer would have stored from the original value. That is acceptable for these old rows.
|
|
19940
|
+
*/
|
|
19941
|
+
async function fillMissingTracePreviews(transaction, schema, rows, deadline) {
|
|
19942
|
+
const missing = rows.filter((row) => PREVIEW_PAYLOADS$2.some(([preview]) => needsPreview(row[preview])));
|
|
19943
|
+
if (!missing.length) return;
|
|
19944
|
+
await setRemainingTimeout(transaction, deadline);
|
|
19945
|
+
const payloads = await transaction.any(`SELECT e."cursorId"::text AS "cursorId",
|
|
19946
|
+
${PREVIEW_PAYLOADS$2.map(([preview, payload]) => `${missing.some((row) => needsPreview(row[preview])) ? `e."${payload}"` : "NULL"} AS "${payload}"`).join(",\n ")}
|
|
19947
|
+
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} e
|
|
19948
|
+
WHERE (e."traceId", e."cursorId") IN (SELECT * FROM unnest($1::text[], $2::bigint[]))`, [missing.map((row) => String(row.traceId)), missing.map((row) => String(row.cursorId))]);
|
|
19949
|
+
const byCursor = new Map(payloads.map((payload) => [String(payload.cursorId), payload]));
|
|
19950
|
+
for (const row of missing) {
|
|
19951
|
+
const payload = byCursor.get(String(row.cursorId));
|
|
19952
|
+
for (const [preview, column] of PREVIEW_PAYLOADS$2) if (needsPreview(row[preview])) row[preview] = readPreview(row[preview], payload?.[column]);
|
|
19953
|
+
}
|
|
19954
|
+
}
|
|
19741
19955
|
function traceRowToResult(row) {
|
|
19742
19956
|
return {
|
|
19743
19957
|
traceId: String(row.traceId),
|
|
@@ -19747,7 +19961,9 @@ function traceRowToResult(row) {
|
|
|
19747
19961
|
parentSpanId: row.parentSpanId ?? null,
|
|
19748
19962
|
createdAt: asIsoTimestamp$1(row.startedAt),
|
|
19749
19963
|
metadata: row.metadata ?? null,
|
|
19750
|
-
inputPreview:
|
|
19964
|
+
inputPreview: readPreview(row.inputPreview),
|
|
19965
|
+
outputPreview: readPreview(row.outputPreview),
|
|
19966
|
+
errorPreview: readPreview(row.errorPreview),
|
|
19751
19967
|
threadId: row.threadId == null ? null : String(row.threadId),
|
|
19752
19968
|
resourceId: row.resourceId == null ? null : String(row.resourceId),
|
|
19753
19969
|
startedAt: asIsoTimestamp$1(row.startedAt),
|
|
@@ -19775,6 +19991,7 @@ async function queryTraces(client, schema, plan, timeoutMs) {
|
|
|
19775
19991
|
rows = await transaction.any(query.text, query.values);
|
|
19776
19992
|
}
|
|
19777
19993
|
const visible = rows.slice(0, plan.limit);
|
|
19994
|
+
await fillMissingTracePreviews(transaction, schema, visible, deadline);
|
|
19778
19995
|
const last = visible.at(-1);
|
|
19779
19996
|
return coreStorage.traceQueryResponseSchema.parse({
|
|
19780
19997
|
traces: visible.map(traceRowToResult),
|
|
@@ -19804,6 +20021,7 @@ async function queryTraces(client, schema, plan, timeoutMs) {
|
|
|
19804
20021
|
if (remainingTimeoutMs <= 0) throw new coreStorage.TraceQueryExecutionError();
|
|
19805
20022
|
await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${remainingTimeoutMs}ms`]);
|
|
19806
20023
|
const rows = await transaction.any(dataQuery.text, dataQuery.values);
|
|
20024
|
+
await fillMissingTracePreviews(transaction, schema, rows, deadline);
|
|
19807
20025
|
return {
|
|
19808
20026
|
total: Number(countRows[0]?.count ?? 0),
|
|
19809
20027
|
rows,
|
|
@@ -19823,7 +20041,12 @@ async function queryTraces(client, schema, plan, timeoutMs) {
|
|
|
19823
20041
|
});
|
|
19824
20042
|
}
|
|
19825
20043
|
const query = compilePostgresTraceQuery(schema, plan);
|
|
19826
|
-
const
|
|
20044
|
+
const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
|
|
20045
|
+
const rows = await runWithPostgresTraceQueryTimeout(client, timeoutMs, async (transaction) => {
|
|
20046
|
+
const rows = await transaction.any(query.text, query.values);
|
|
20047
|
+
if (plan.result === "traces") await fillMissingTracePreviews(transaction, schema, rows.slice(0, plan.limit), deadline);
|
|
20048
|
+
return rows;
|
|
20049
|
+
});
|
|
19827
20050
|
const visibleRows = rows.slice(0, plan.limit);
|
|
19828
20051
|
if (plan.result === "groups") {
|
|
19829
20052
|
const groups = visibleRows.map((row) => ({ threadId: String(row.threadId) }));
|
|
@@ -19980,6 +20203,29 @@ function compilePostgresSpanQuery(schema, plan) {
|
|
|
19980
20203
|
values
|
|
19981
20204
|
};
|
|
19982
20205
|
}
|
|
20206
|
+
const PREVIEW_PAYLOADS$1 = [
|
|
20207
|
+
["inputPreview", "input"],
|
|
20208
|
+
["outputPreview", "output"],
|
|
20209
|
+
["errorPreview", "error"]
|
|
20210
|
+
];
|
|
20211
|
+
/**
|
|
20212
|
+
* Stored previews of a hydrated row, built from the payload where the stored preview is NULL.
|
|
20213
|
+
* jsonb comes back decoded, so the fallback uses `buildPreview(value)`. jsonb normalizes object key
|
|
20214
|
+
* order, so a compact-JSON fallback preview can differ from what the writer would have stored from
|
|
20215
|
+
* the original value; that is acceptable for rows written before the preview columns existed.
|
|
20216
|
+
*/
|
|
20217
|
+
function toSpanQueryPayload(row) {
|
|
20218
|
+
const [inputPreview, outputPreview, errorPreview] = PREVIEW_PAYLOADS$1.map(([preview, payload]) => readPreview(row[preview], row[payload]));
|
|
20219
|
+
return {
|
|
20220
|
+
organizationId: row.organizationId ?? null,
|
|
20221
|
+
resourceId: row.resourceId ?? null,
|
|
20222
|
+
traceId: String(row.traceId),
|
|
20223
|
+
spanId: String(row.spanId),
|
|
20224
|
+
inputPreview,
|
|
20225
|
+
outputPreview,
|
|
20226
|
+
errorPreview
|
|
20227
|
+
};
|
|
20228
|
+
}
|
|
19983
20229
|
async function querySpans(client, schema, plan, timeoutMs) {
|
|
19984
20230
|
const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
|
|
19985
20231
|
return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
|
|
@@ -20004,9 +20250,9 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20004
20250
|
payloadValues.push(row.version);
|
|
20005
20251
|
return `(${key} AND "cursorId" = $${payloadValues.length}::bigint)`;
|
|
20006
20252
|
}).join(" OR ");
|
|
20007
|
-
const payloads = await tx.any(`SELECT ${identities},
|
|
20008
|
-
|
|
20009
|
-
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues);
|
|
20253
|
+
const payloads = (await tx.any(`SELECT ${identities},
|
|
20254
|
+
${PREVIEW_PAYLOADS$1.map(([preview, payload]) => `"${preview}", CASE WHEN ${missingPreviewSql(`"${preview}"`)} THEN "${payload}" END AS "${payload}"`).join(",\n ")}
|
|
20255
|
+
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues)).map(toSpanQueryPayload);
|
|
20010
20256
|
await remaining();
|
|
20011
20257
|
const metricValues = [];
|
|
20012
20258
|
const metricWhere = selectedKeys(page, metricValues);
|
|
@@ -20017,7 +20263,7 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20017
20263
|
ORDER BY ${identities}, "metricId", "cursorId" DESC) m
|
|
20018
20264
|
LIMIT ${coreStorage.SPAN_QUERY_MAX_COST_METRICS + 1}`, metricValues);
|
|
20019
20265
|
if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
|
|
20020
|
-
return
|
|
20266
|
+
return buildSpanQueryResponse(plan, selected, payloads, metrics);
|
|
20021
20267
|
}, { repeatableRead: true });
|
|
20022
20268
|
}
|
|
20023
20269
|
//#endregion
|
|
@@ -20482,16 +20728,73 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
20482
20728
|
* array-of-strings representation here.
|
|
20483
20729
|
*/
|
|
20484
20730
|
const SPAN_SELECT_COLUMNS_ALIASED = SPAN_SELECT_COLUMNS.replace(/\n/g, " ").split(",").map((c) => `r.${c.trim()}`).join(", ");
|
|
20731
|
+
const FULL_PROJECTION = {
|
|
20732
|
+
columns: SPAN_SELECT_COLUMNS_ALIASED,
|
|
20733
|
+
mapRows: (_client, _span, rows) => toTraceSpans(rows.map(rowToSpanRecord))
|
|
20734
|
+
};
|
|
20735
|
+
/**
|
|
20736
|
+
* Columns a light trace list renders: the timeline fields, memory context, metadata and the stored
|
|
20737
|
+
* previews. The `input` / `output` payloads stay off the read path; rows written before the
|
|
20738
|
+
* previews were stored get theirs from the payloads, read for just those rows.
|
|
20739
|
+
*/
|
|
20740
|
+
const TRACE_LIGHT_SELECT_COLUMNS = [
|
|
20741
|
+
...SPAN_LIGHT_SELECT_COLUMN_NAMES,
|
|
20742
|
+
"threadId",
|
|
20743
|
+
"resourceId",
|
|
20744
|
+
"metadataRaw",
|
|
20745
|
+
"inputPreview",
|
|
20746
|
+
"outputPreview",
|
|
20747
|
+
"errorPreview"
|
|
20748
|
+
].map((column) => `r."${column}"`).join(", ");
|
|
20749
|
+
const PREVIEW_PAYLOADS = [
|
|
20750
|
+
["inputPreview", "input"],
|
|
20751
|
+
["outputPreview", "output"],
|
|
20752
|
+
["errorPreview", "error"]
|
|
20753
|
+
];
|
|
20754
|
+
/**
|
|
20755
|
+
* Builds the previews of rows written before previews were stored (NULL preview) from those rows'
|
|
20756
|
+
* payloads. Only the flagged rows' payloads are read, so this fades out as old rows age out.
|
|
20757
|
+
*
|
|
20758
|
+
* `jsonb` comes back decoded with its keys reordered, so a compact-JSON preview built here can
|
|
20759
|
+
* differ in key order from the one the writer would have stored. Acceptable for old rows.
|
|
20760
|
+
*/
|
|
20761
|
+
async function fillMissingPreviews(client, span, rows) {
|
|
20762
|
+
const missing = rows.filter((row) => PREVIEW_PAYLOADS.some(([preview]) => needsPreview(row[preview])));
|
|
20763
|
+
if (!missing.length) return;
|
|
20764
|
+
const payloads = await client.manyOrNone(`SELECT e."cursorId"::text AS "cursorId",
|
|
20765
|
+
${PREVIEW_PAYLOADS.map(([preview, payload]) => `${missing.some((row) => needsPreview(row[preview])) ? `e."${payload}"` : "NULL"} AS "${payload}"`).join(",\n ")}
|
|
20766
|
+
FROM ${span} e
|
|
20767
|
+
WHERE (e."traceId", e."cursorId") IN (SELECT * FROM unnest($1::text[], $2::bigint[]))`, [missing.map((row) => String(row.traceId)), missing.map((row) => String(row.cursorId))]);
|
|
20768
|
+
const byCursor = new Map(payloads.map((payload) => [String(payload.cursorId), payload]));
|
|
20769
|
+
for (const row of missing) {
|
|
20770
|
+
const payload = byCursor.get(String(row.cursorId));
|
|
20771
|
+
for (const [preview, column] of PREVIEW_PAYLOADS) if (needsPreview(row[preview])) row[preview] = readPreview(row[preview], payload?.[column]);
|
|
20772
|
+
}
|
|
20773
|
+
}
|
|
20774
|
+
const LIGHT_PROJECTION = {
|
|
20775
|
+
columns: TRACE_LIGHT_SELECT_COLUMNS,
|
|
20776
|
+
mapRows: async (client, span, rows) => {
|
|
20777
|
+
await fillMissingPreviews(client, span, rows);
|
|
20778
|
+
return rows.map(rowToLightTraceRecord);
|
|
20779
|
+
}
|
|
20780
|
+
};
|
|
20485
20781
|
async function listTraces(client, schema, args) {
|
|
20782
|
+
return listTraceRows(client, schema, args, FULL_PROJECTION);
|
|
20783
|
+
}
|
|
20784
|
+
/** Trace list projecting only the columns a list renders, with stored previews instead of payloads. */
|
|
20785
|
+
async function listTracesLight(client, schema, args) {
|
|
20786
|
+
return listTraceRows(client, schema, args, LIGHT_PROJECTION);
|
|
20787
|
+
}
|
|
20788
|
+
async function listTraceRows(client, schema, args, projection) {
|
|
20486
20789
|
const { mode, filters, pagination, orderBy, after, limit } = listTracesArgsSchema.parse(args);
|
|
20487
20790
|
const span = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
20488
20791
|
if (mode === "delta") {
|
|
20489
20792
|
assertDeltaPollingEnabled();
|
|
20490
|
-
return listTracesDelta(client, span, filters, after, limit);
|
|
20793
|
+
return listTracesDelta(client, span, projection, filters, after, limit);
|
|
20491
20794
|
}
|
|
20492
|
-
return listTracesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
20795
|
+
return listTracesPage(client, span, projection, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
20493
20796
|
}
|
|
20494
|
-
async function listTracesPage(client, span, filters, page, perPage, orderField, orderDir) {
|
|
20797
|
+
async function listTracesPage(client, span, projection, filters, page, perPage, orderField, orderDir) {
|
|
20495
20798
|
const { conditions, params, nextParamIdx } = buildListTracesFilters(filters, span, 1);
|
|
20496
20799
|
let i = nextParamIdx;
|
|
20497
20800
|
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
@@ -20499,15 +20802,18 @@ async function listTracesPage(client, span, filters, page, perPage, orderField,
|
|
|
20499
20802
|
const countRow = await client.oneOrNone(`SELECT COUNT(*)::text AS count FROM ${span} r ${whereClause}`, params);
|
|
20500
20803
|
const count = Number(countRow?.count ?? 0);
|
|
20501
20804
|
let spans = [];
|
|
20502
|
-
if (count > 0)
|
|
20805
|
+
if (count > 0) {
|
|
20806
|
+
const rows = await client.manyOrNone(`SELECT ${projection.columns}
|
|
20503
20807
|
FROM ${span} r
|
|
20504
20808
|
${whereClause}
|
|
20505
20809
|
${orderClause}
|
|
20506
20810
|
LIMIT $${i++} OFFSET $${i++}`, [
|
|
20507
|
-
|
|
20508
|
-
|
|
20509
|
-
|
|
20510
|
-
|
|
20811
|
+
...params,
|
|
20812
|
+
perPage,
|
|
20813
|
+
page * perPage
|
|
20814
|
+
]);
|
|
20815
|
+
spans = await projection.mapRows(client, span, rows);
|
|
20816
|
+
}
|
|
20511
20817
|
const deltaCursor = deltaPollingFeatureEnabled() ? await readTracesStreamHeadCursor(client, span, filters) : void 0;
|
|
20512
20818
|
return {
|
|
20513
20819
|
pagination: {
|
|
@@ -20516,11 +20822,11 @@ async function listTracesPage(client, span, filters, page, perPage, orderField,
|
|
|
20516
20822
|
perPage,
|
|
20517
20823
|
hasMore: (page + 1) * perPage < count
|
|
20518
20824
|
},
|
|
20519
|
-
spans
|
|
20825
|
+
spans,
|
|
20520
20826
|
...deltaCursor !== void 0 ? { deltaCursor } : {}
|
|
20521
20827
|
};
|
|
20522
20828
|
}
|
|
20523
|
-
async function listTracesDelta(client, span, filters, after, limit) {
|
|
20829
|
+
async function listTracesDelta(client, span, projection, filters, after, limit) {
|
|
20524
20830
|
if (after === void 0) {
|
|
20525
20831
|
const deltaCursor = await readTracesStreamHeadCursor(client, span, filters);
|
|
20526
20832
|
return {
|
|
@@ -20540,7 +20846,7 @@ async function listTracesDelta(client, span, filters, after, limit) {
|
|
|
20540
20846
|
params.push(afterCursor.xactId, afterCursor.cursorId);
|
|
20541
20847
|
conditions.push(`r."xactId" < $${i++}::xid8`);
|
|
20542
20848
|
params.push(safeHorizon);
|
|
20543
|
-
const rows = await client.manyOrNone(`SELECT ${
|
|
20849
|
+
const rows = await client.manyOrNone(`SELECT ${projection.columns}
|
|
20544
20850
|
FROM ${span} r
|
|
20545
20851
|
WHERE ${conditions.join(" AND ")}
|
|
20546
20852
|
ORDER BY r."xactId" ASC, r."cursorId" ASC
|
|
@@ -20549,7 +20855,7 @@ async function listTracesDelta(client, span, filters, after, limit) {
|
|
|
20549
20855
|
const visible = rows.slice(0, limit);
|
|
20550
20856
|
const deltaCursor = visible.length > 0 ? encodeDeltaCursor(visible[visible.length - 1].xactId, visible[visible.length - 1].cursorId) : encodeDeltaCursor(safeHorizon, 0);
|
|
20551
20857
|
return {
|
|
20552
|
-
spans:
|
|
20858
|
+
spans: await projection.mapRows(client, span, visible),
|
|
20553
20859
|
delta: {
|
|
20554
20860
|
limit,
|
|
20555
20861
|
hasMore
|
|
@@ -21260,6 +21566,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
21260
21566
|
async listTraces(args) {
|
|
21261
21567
|
return this.#run("LIST_TRACES", () => listTraces(this.#readClient, this.#schema, args));
|
|
21262
21568
|
}
|
|
21569
|
+
async listTracesLight(args) {
|
|
21570
|
+
return this.#run("LIST_TRACES_LIGHT", () => listTracesLight(this.#readClient, this.#schema, args));
|
|
21571
|
+
}
|
|
21263
21572
|
async querySpans(plan) {
|
|
21264
21573
|
return this.#run("QUERY_SPANS", () => querySpans(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
|
|
21265
21574
|
}
|