@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.
Files changed (27) hide show
  1. package/dist/docs/SKILL.md +1 -1
  2. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  3. package/dist/index.cjs +456 -147
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +457 -148
  6. package/dist/index.js.map +1 -1
  7. package/dist/storage/db/sanitize-json.d.ts +2 -0
  8. package/dist/storage/db/sanitize-json.d.ts.map +1 -1
  9. package/dist/storage/domains/notifications/index.d.ts +9 -0
  10. package/dist/storage/domains/notifications/index.d.ts.map +1 -1
  11. package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
  12. package/dist/storage/domains/observability/v-next/helpers.d.ts +7 -0
  13. package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
  14. package/dist/storage/domains/observability/v-next/index.d.ts +2 -1
  15. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  16. package/dist/storage/domains/observability/v-next/preview-compat.d.ts +47 -0
  17. package/dist/storage/domains/observability/v-next/preview-compat.d.ts.map +1 -0
  18. package/dist/storage/domains/observability/v-next/signal-schema.d.ts +12 -0
  19. package/dist/storage/domains/observability/v-next/signal-schema.d.ts.map +1 -1
  20. package/dist/storage/domains/observability/v-next/span-query.d.ts +2 -0
  21. package/dist/storage/domains/observability/v-next/span-query.d.ts.map +1 -1
  22. package/dist/storage/domains/observability/v-next/sql.d.ts +11 -0
  23. package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
  24. package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
  25. package/dist/storage/domains/observability/v-next/traces.d.ts +3 -1
  26. package/dist/storage/domains/observability/v-next/traces.d.ts.map +1 -1
  27. 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
- table: TABLE_SPAN_EVENTS,
16662
- column: "isPending",
16663
- ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
16664
- }, {
16665
- table: TABLE_FEEDBACK_EVENTS,
16666
- column: "reviewStatus",
16667
- ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)} ADD COLUMN IF NOT EXISTS "reviewStatus" text NOT NULL DEFAULT 'needs-review'`
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: jsonField(span.input),
17186
- output: jsonField(span.output),
17187
- error: jsonField(span.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."input" AS "input",
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\", r.\"cursorId\"" : ""}`, deltaWindow);
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: coreStorage.buildInputPreview(row.input) ?? null,
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 rows = await runWithPostgresTraceQueryTimeout(client, timeoutMs, (transaction) => transaction.any(query.text, query.values));
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
- left(input::text, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS "inputPreview", left(output::text, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS "outputPreview"
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 coreStorage.buildSpanQueryResponse(plan, selected, payloads, metrics);
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) spans = (await client.manyOrNone(`SELECT ${SPAN_SELECT_COLUMNS_ALIASED}
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
- ...params,
20508
- perPage,
20509
- page * perPage
20510
- ])).map(rowToSpanRecord);
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: toTraceSpans(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 ${SPAN_SELECT_COLUMNS_ALIASED}
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: toTraceSpans(visible.map(rowToSpanRecord)),
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
  }