@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.cjs
CHANGED
|
@@ -2472,6 +2472,10 @@ function repairJson(value) {
|
|
|
2472
2472
|
}
|
|
2473
2473
|
return value;
|
|
2474
2474
|
}
|
|
2475
|
+
/** Repair text for a PostgreSQL `text` column, which rejects NUL characters. */
|
|
2476
|
+
function toPgText(value) {
|
|
2477
|
+
return repair(value);
|
|
2478
|
+
}
|
|
2475
2479
|
/** Serialize JSON values without emitting Unicode sequences PostgreSQL rejects. */
|
|
2476
2480
|
function toPgJson(value) {
|
|
2477
2481
|
const json = JSON.stringify(value);
|
|
@@ -14563,6 +14567,7 @@ var MemoryPG = class MemoryPG extends _mastra_core_storage.MemoryStorage {
|
|
|
14563
14567
|
};
|
|
14564
14568
|
//#endregion
|
|
14565
14569
|
//#region src/storage/domains/notifications/index.ts
|
|
14570
|
+
const COMPOSITE_PRIMARY_KEY$1 = _mastra_core_storage.TABLE_CONFIGS[_mastra_core_storage.TABLE_NOTIFICATIONS].compositePrimaryKey;
|
|
14566
14571
|
const statusTimestamp = (status, now) => {
|
|
14567
14572
|
if (status === "delivered") return { deliveredAt: now };
|
|
14568
14573
|
if (status === "seen") return { seenAt: now };
|
|
@@ -14674,11 +14679,58 @@ var NotificationsPG = class NotificationsPG extends _mastra_core_storage.Notific
|
|
|
14674
14679
|
async init() {
|
|
14675
14680
|
await this.#db.createTable({
|
|
14676
14681
|
tableName: _mastra_core_storage.TABLE_NOTIFICATIONS,
|
|
14677
|
-
schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_NOTIFICATIONS]
|
|
14682
|
+
schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_NOTIFICATIONS],
|
|
14683
|
+
compositePrimaryKey: COMPOSITE_PRIMARY_KEY$1
|
|
14678
14684
|
});
|
|
14685
|
+
await this.ensurePrimaryKey();
|
|
14679
14686
|
await this.createDefaultIndexes();
|
|
14680
14687
|
await this.createCustomIndexes();
|
|
14681
14688
|
}
|
|
14689
|
+
async #hasPrimaryKey() {
|
|
14690
|
+
return !!(await this.#db.client.oneOrNone(`SELECT EXISTS (
|
|
14691
|
+
SELECT 1 FROM pg_index i
|
|
14692
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
14693
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
14694
|
+
WHERE c.relname = $1 AND n.nspname = $2 AND i.indisprimary
|
|
14695
|
+
) AS exists`, [_mastra_core_storage.TABLE_NOTIFICATIONS, this.#schema]))?.exists;
|
|
14696
|
+
}
|
|
14697
|
+
/**
|
|
14698
|
+
* Tables created before the composite primary key was declared have no key at
|
|
14699
|
+
* all, which allows duplicate ids and leaves the table without a replica
|
|
14700
|
+
* identity (UPDATE/DELETE fail under logical replication). `createTable` is a
|
|
14701
|
+
* no-op for existing tables, so add the key here. If duplicate rows already
|
|
14702
|
+
* exist, or the role does not own the table, the key cannot be added; warn with
|
|
14703
|
+
* remediation instead of blocking init.
|
|
14704
|
+
*/
|
|
14705
|
+
async ensurePrimaryKey() {
|
|
14706
|
+
if (await this.#hasPrimaryKey()) return;
|
|
14707
|
+
const tableName = getTableName$5({
|
|
14708
|
+
indexName: _mastra_core_storage.TABLE_NOTIFICATIONS,
|
|
14709
|
+
schemaName: getSchemaName$5(this.#schema)
|
|
14710
|
+
});
|
|
14711
|
+
const keyColumns = COMPOSITE_PRIMARY_KEY$1.map((column) => `"${(0, _mastra_core_utils.parseSqlIdentifier)(column, "column name")}"`).join(", ");
|
|
14712
|
+
const remediation = `ALTER TABLE ${tableName} ADD PRIMARY KEY (${keyColumns});`;
|
|
14713
|
+
if (await this.#db.client.oneOrNone(`SELECT 1 FROM ${tableName} GROUP BY ${keyColumns} HAVING count(*) > 1 LIMIT 1`)) {
|
|
14714
|
+
this.logger?.warn?.(`${tableName} contains duplicate (${keyColumns}) rows, so its PRIMARY KEY could not be added. Remove the duplicates, then run: ${remediation}`);
|
|
14715
|
+
return;
|
|
14716
|
+
}
|
|
14717
|
+
try {
|
|
14718
|
+
await this.#db.client.none(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${keyColumns})`);
|
|
14719
|
+
this.logger?.info?.(`Added PRIMARY KEY (${keyColumns}) to ${tableName}`);
|
|
14720
|
+
} catch (error) {
|
|
14721
|
+
if (await this.#hasPrimaryKey()) return;
|
|
14722
|
+
if (error?.code === "42501") {
|
|
14723
|
+
this.logger?.warn?.(`Insufficient privilege to add PRIMARY KEY (${keyColumns}) to ${tableName}. Ask the table owner to run: ${remediation}`);
|
|
14724
|
+
return;
|
|
14725
|
+
}
|
|
14726
|
+
throw new _mastra_core_error.MastraError({
|
|
14727
|
+
id: (0, _mastra_core_storage.createStorageErrorId)("PG", "ADD_NOTIFICATIONS_PRIMARY_KEY", "FAILED"),
|
|
14728
|
+
domain: _mastra_core_error.ErrorDomain.STORAGE,
|
|
14729
|
+
category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
|
|
14730
|
+
details: { tableName }
|
|
14731
|
+
}, error);
|
|
14732
|
+
}
|
|
14733
|
+
}
|
|
14682
14734
|
/**
|
|
14683
14735
|
* Lazily ensures a btree index exists on each configured policy's retention
|
|
14684
14736
|
* anchor column so age-based `prune()` deletes stay fast on large tables.
|
|
@@ -14762,6 +14814,7 @@ var NotificationsPG = class NotificationsPG extends _mastra_core_storage.Notific
|
|
|
14762
14814
|
tableName: _mastra_core_storage.TABLE_NOTIFICATIONS,
|
|
14763
14815
|
schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_NOTIFICATIONS],
|
|
14764
14816
|
schemaName,
|
|
14817
|
+
compositePrimaryKey: COMPOSITE_PRIMARY_KEY$1,
|
|
14765
14818
|
includeAllConstraints: true
|
|
14766
14819
|
}));
|
|
14767
14820
|
for (const idx of NotificationsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
|
|
@@ -16026,6 +16079,21 @@ const SPAN_EVENT_COLUMNS = [
|
|
|
16026
16079
|
name: "requestContext",
|
|
16027
16080
|
type: "jsonb",
|
|
16028
16081
|
nullable: true
|
|
16082
|
+
},
|
|
16083
|
+
{
|
|
16084
|
+
name: "inputPreview",
|
|
16085
|
+
type: "text",
|
|
16086
|
+
nullable: true
|
|
16087
|
+
},
|
|
16088
|
+
{
|
|
16089
|
+
name: "outputPreview",
|
|
16090
|
+
type: "text",
|
|
16091
|
+
nullable: true
|
|
16092
|
+
},
|
|
16093
|
+
{
|
|
16094
|
+
name: "errorPreview",
|
|
16095
|
+
type: "text",
|
|
16096
|
+
nullable: true
|
|
16029
16097
|
}
|
|
16030
16098
|
];
|
|
16031
16099
|
const METRIC_EVENT_COLUMNS = [
|
|
@@ -16681,15 +16749,27 @@ function allTableDDL(schema, mode) {
|
|
|
16681
16749
|
* default does not rewrite the table, and the ALTER cascades to partitions.
|
|
16682
16750
|
*/
|
|
16683
16751
|
function additiveColumns(schema) {
|
|
16684
|
-
return [
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16752
|
+
return [
|
|
16753
|
+
{
|
|
16754
|
+
table: TABLE_SPAN_EVENTS,
|
|
16755
|
+
column: "isPending",
|
|
16756
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
|
|
16757
|
+
},
|
|
16758
|
+
...[
|
|
16759
|
+
"inputPreview",
|
|
16760
|
+
"outputPreview",
|
|
16761
|
+
"errorPreview"
|
|
16762
|
+
].map((column) => ({
|
|
16763
|
+
table: TABLE_SPAN_EVENTS,
|
|
16764
|
+
column,
|
|
16765
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "${column}" text`
|
|
16766
|
+
})),
|
|
16767
|
+
{
|
|
16768
|
+
table: TABLE_FEEDBACK_EVENTS,
|
|
16769
|
+
column: "reviewStatus",
|
|
16770
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)} ADD COLUMN IF NOT EXISTS "reviewStatus" text NOT NULL DEFAULT 'needs-review'`
|
|
16771
|
+
}
|
|
16772
|
+
];
|
|
16693
16773
|
}
|
|
16694
16774
|
/** Existence probe for an additive column. Params: schema, table, column. */
|
|
16695
16775
|
const columnExistsSQL = `SELECT 1 FROM information_schema.columns
|
|
@@ -17011,6 +17091,46 @@ function whereOrEmpty(acc) {
|
|
|
17011
17091
|
return acc.conditions.length ? `WHERE ${acc.conditions.join(" AND ")}` : "";
|
|
17012
17092
|
}
|
|
17013
17093
|
//#endregion
|
|
17094
|
+
//#region src/storage/domains/observability/v-next/preview-compat.ts
|
|
17095
|
+
const generator = _mastra_core_storage.buildStoredPreview;
|
|
17096
|
+
const canGeneratePreviews = typeof generator === "function";
|
|
17097
|
+
/** SQL detection only; preview formatting stays in TypeScript. Empty strings are intentional. */
|
|
17098
|
+
function missingPreviewSql(column) {
|
|
17099
|
+
return canGeneratePreviews ? `${column} IS NULL` : "FALSE";
|
|
17100
|
+
}
|
|
17101
|
+
function needsPreview(value) {
|
|
17102
|
+
return canGeneratePreviews && value == null;
|
|
17103
|
+
}
|
|
17104
|
+
function storedPreview(preview, value, serialized) {
|
|
17105
|
+
return canGeneratePreviews ? generator(preview, value, serialized) : preview ?? null;
|
|
17106
|
+
}
|
|
17107
|
+
function readPreview(preview, payload, jsonEncoded = false) {
|
|
17108
|
+
const supplied = preview;
|
|
17109
|
+
if (!canGeneratePreviews) return supplied ?? null;
|
|
17110
|
+
if (supplied != null) return supplied.trim() || null;
|
|
17111
|
+
let value = payload;
|
|
17112
|
+
if (needsPreview(preview) && jsonEncoded && typeof payload === "string") try {
|
|
17113
|
+
value = JSON.parse(payload);
|
|
17114
|
+
} catch {
|
|
17115
|
+
value = null;
|
|
17116
|
+
}
|
|
17117
|
+
return storedPreview(supplied, value, jsonEncoded && typeof payload === "string" ? payload : void 0) || null;
|
|
17118
|
+
}
|
|
17119
|
+
/** Older response builders may shorten previews or turn empty strings into null. */
|
|
17120
|
+
function buildSpanQueryResponse(...args) {
|
|
17121
|
+
if (canGeneratePreviews) return _mastra_core_storage.buildSpanQueryResponse(...args);
|
|
17122
|
+
const [plan, selected, payloads, metrics] = args;
|
|
17123
|
+
const response = _mastra_core_storage.buildSpanQueryResponse(plan, selected, [], metrics);
|
|
17124
|
+
const byIdentity = new Map(payloads.map((payload) => [_mastra_core_storage.spanQueryIdentityKey(payload), payload]));
|
|
17125
|
+
for (const span of response.spans) {
|
|
17126
|
+
const payload = byIdentity.get(_mastra_core_storage.spanQueryIdentityKey(span));
|
|
17127
|
+
span.inputPreview = payload?.inputPreview ?? null;
|
|
17128
|
+
span.outputPreview = payload?.outputPreview ?? null;
|
|
17129
|
+
span.errorPreview = payload?.errorPreview ?? null;
|
|
17130
|
+
}
|
|
17131
|
+
return response;
|
|
17132
|
+
}
|
|
17133
|
+
//#endregion
|
|
17014
17134
|
//#region src/storage/domains/observability/v-next/review-status.ts
|
|
17015
17135
|
const FEEDBACK_REVIEW_STATUSES = ["needs-review", "reviewed"];
|
|
17016
17136
|
function isFeedbackReviewStatus(value) {
|
|
@@ -17036,6 +17156,137 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
|
|
|
17036
17156
|
};
|
|
17037
17157
|
}
|
|
17038
17158
|
//#endregion
|
|
17159
|
+
//#region src/storage/domains/observability/v-next/sql.ts
|
|
17160
|
+
/**
|
|
17161
|
+
* SQL helpers for the v-next Postgres observability domain.
|
|
17162
|
+
*
|
|
17163
|
+
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
17164
|
+
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
17165
|
+
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
17166
|
+
*/
|
|
17167
|
+
/**
|
|
17168
|
+
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
17169
|
+
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
17170
|
+
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
17171
|
+
*
|
|
17172
|
+
* The result is sanitized because Postgres rejects some sequences that are
|
|
17173
|
+
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
17174
|
+
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
17175
|
+
* multi-row statement, so one bad value would otherwise reject the whole
|
|
17176
|
+
* batch of observability events.
|
|
17177
|
+
*/
|
|
17178
|
+
function encodeJsonb(value) {
|
|
17179
|
+
if (value === null || value === void 0) return null;
|
|
17180
|
+
if (value instanceof EncodedJsonb) return value.json;
|
|
17181
|
+
return toPgJson(value);
|
|
17182
|
+
}
|
|
17183
|
+
/**
|
|
17184
|
+
* A jsonb value the caller already serialized with `toPgJson`, so `buildInsert`
|
|
17185
|
+
* sends it as is instead of serializing it again. Span writes serialize their
|
|
17186
|
+
* payloads up front because the stored previews are built from that same text.
|
|
17187
|
+
*/
|
|
17188
|
+
var EncodedJsonb = class {
|
|
17189
|
+
json;
|
|
17190
|
+
constructor(json) {
|
|
17191
|
+
this.json = json;
|
|
17192
|
+
}
|
|
17193
|
+
};
|
|
17194
|
+
/** Serialize a value for a jsonb column once, for callers that also need the text. */
|
|
17195
|
+
function encodeJsonbValue(value) {
|
|
17196
|
+
if (value === null || value === void 0) return null;
|
|
17197
|
+
const json = toPgJson(value);
|
|
17198
|
+
return json === void 0 ? null : new EncodedJsonb(json);
|
|
17199
|
+
}
|
|
17200
|
+
/**
|
|
17201
|
+
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
17202
|
+
*
|
|
17203
|
+
* @param schema Schema name.
|
|
17204
|
+
* @param table Table name.
|
|
17205
|
+
* @param records Array of records (each is a column-name → value object).
|
|
17206
|
+
* All records must have identical key sets.
|
|
17207
|
+
* @returns { text, values } ready to pass to `client.query`.
|
|
17208
|
+
*/
|
|
17209
|
+
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
17210
|
+
if (records.length === 0) return null;
|
|
17211
|
+
const columns = Object.keys(records[0]).map((c) => (0, _mastra_core_utils.parseSqlIdentifier)(c, "column name"));
|
|
17212
|
+
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
17213
|
+
const values = [];
|
|
17214
|
+
const rowPlaceholders = [];
|
|
17215
|
+
let p = 1;
|
|
17216
|
+
for (const record of records) {
|
|
17217
|
+
const placeholders = columns.map((col) => {
|
|
17218
|
+
const raw = record[col];
|
|
17219
|
+
if (JSONB_COLUMNS.has(col)) {
|
|
17220
|
+
values.push(encodeJsonb(raw));
|
|
17221
|
+
return `$${p++}::jsonb`;
|
|
17222
|
+
}
|
|
17223
|
+
if (TEXT_ARRAY_COLUMNS.has(col)) {
|
|
17224
|
+
values.push(Array.isArray(raw) ? raw : []);
|
|
17225
|
+
return `$${p++}::text[]`;
|
|
17226
|
+
}
|
|
17227
|
+
values.push(raw === void 0 ? null : raw);
|
|
17228
|
+
return `$${p++}`;
|
|
17229
|
+
});
|
|
17230
|
+
rowPlaceholders.push(`(${placeholders.join(", ")})`);
|
|
17231
|
+
}
|
|
17232
|
+
return {
|
|
17233
|
+
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
17234
|
+
VALUES ${rowPlaceholders.join(", ")}
|
|
17235
|
+
${onConflict}`,
|
|
17236
|
+
values
|
|
17237
|
+
};
|
|
17238
|
+
}
|
|
17239
|
+
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
17240
|
+
const SPAN_KEY_COLUMNS = [
|
|
17241
|
+
"traceId",
|
|
17242
|
+
"spanId",
|
|
17243
|
+
"endedAt"
|
|
17244
|
+
];
|
|
17245
|
+
/**
|
|
17246
|
+
* Conflict clause for span writes.
|
|
17247
|
+
*
|
|
17248
|
+
* A span is written twice under the event-sourced strategy: once when it starts
|
|
17249
|
+
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
17250
|
+
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
17251
|
+
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
17252
|
+
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
17253
|
+
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
17254
|
+
* end row overwrites the pending one instead.
|
|
17255
|
+
*
|
|
17256
|
+
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
17257
|
+
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
17258
|
+
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
17259
|
+
* span to pending.
|
|
17260
|
+
*/
|
|
17261
|
+
function spanConflictClause(columns) {
|
|
17262
|
+
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
17263
|
+
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
17264
|
+
}
|
|
17265
|
+
/**
|
|
17266
|
+
* Ordering that picks the winning row for a span under the event-sourced write
|
|
17267
|
+
* model. An ended row always beats a pending row for the same span; among rows
|
|
17268
|
+
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
17269
|
+
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
17270
|
+
* the grouping key to collapse a span's rows down to one.
|
|
17271
|
+
*/
|
|
17272
|
+
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
17273
|
+
/**
|
|
17274
|
+
* Standard SELECT column list for tracing tables. The select projects every
|
|
17275
|
+
* column the row→record converters expect.
|
|
17276
|
+
*/
|
|
17277
|
+
const SPAN_SELECT_COLUMNS = `
|
|
17278
|
+
${buildSelectColumns(SPAN_EVENT_COLUMNS)}`;
|
|
17279
|
+
const SPAN_LIGHT_SELECT_COLUMNS = `
|
|
17280
|
+
${buildNamedSelectColumns(SPAN_LIGHT_SELECT_COLUMN_NAMES)}`;
|
|
17281
|
+
const METRIC_SELECT_COLUMNS = `
|
|
17282
|
+
${buildSelectColumns(METRIC_EVENT_COLUMNS)}`;
|
|
17283
|
+
const LOG_SELECT_COLUMNS = `
|
|
17284
|
+
${buildSelectColumns(LOG_EVENT_COLUMNS)}`;
|
|
17285
|
+
const SCORE_SELECT_COLUMNS = `
|
|
17286
|
+
${buildSelectColumns(SCORE_EVENT_COLUMNS)}`;
|
|
17287
|
+
const FEEDBACK_SELECT_COLUMNS = `
|
|
17288
|
+
${buildSelectColumns(FEEDBACK_EVENT_COLUMNS)}`;
|
|
17289
|
+
//#endregion
|
|
17039
17290
|
//#region src/storage/domains/observability/v-next/helpers.ts
|
|
17040
17291
|
const PROMOTED_KEYS = /* @__PURE__ */ new Set([
|
|
17041
17292
|
"experimentId",
|
|
@@ -17185,10 +17436,26 @@ function commonContextToRow(record) {
|
|
|
17185
17436
|
serviceName: record.serviceName ?? null
|
|
17186
17437
|
};
|
|
17187
17438
|
}
|
|
17439
|
+
/**
|
|
17440
|
+
* Serialize a span payload for its jsonb column and build its stored preview from the same value.
|
|
17441
|
+
* Nonblank upstream previews win (trimmed and capped like generated ones). The preview goes into a `text` column, which
|
|
17442
|
+
* rejects NUL, so it gets the same repair `toPgJson` applies to the payload.
|
|
17443
|
+
*/
|
|
17444
|
+
function spanPayload(value, upstream) {
|
|
17445
|
+
const encoded = encodeJsonbValue(value);
|
|
17446
|
+
const preview = storedPreview(upstream, value, encoded?.json);
|
|
17447
|
+
return {
|
|
17448
|
+
encoded,
|
|
17449
|
+
preview: preview === null ? null : toPgText(preview)
|
|
17450
|
+
};
|
|
17451
|
+
}
|
|
17188
17452
|
function spanRecordToRow(span) {
|
|
17189
17453
|
const isPending = !span.isEvent && span.endedAt == null;
|
|
17190
17454
|
const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
|
|
17191
17455
|
const metadata = span.metadata ?? null;
|
|
17456
|
+
const input = spanPayload(span.input, span.inputPreview);
|
|
17457
|
+
const output = spanPayload(span.output, span.outputPreview);
|
|
17458
|
+
const error = spanPayload(span.error, span.errorPreview);
|
|
17192
17459
|
return {
|
|
17193
17460
|
...commonContextToRow(span),
|
|
17194
17461
|
traceId: span.traceId,
|
|
@@ -17206,10 +17473,13 @@ function spanRecordToRow(span) {
|
|
|
17206
17473
|
scope: jsonField(span.scope),
|
|
17207
17474
|
attributes: jsonField(span.attributes),
|
|
17208
17475
|
links: jsonField(span.links),
|
|
17209
|
-
input:
|
|
17210
|
-
output:
|
|
17211
|
-
error:
|
|
17212
|
-
requestContext: jsonField(span.requestContext)
|
|
17476
|
+
input: input.encoded,
|
|
17477
|
+
output: output.encoded,
|
|
17478
|
+
error: error.encoded,
|
|
17479
|
+
requestContext: jsonField(span.requestContext),
|
|
17480
|
+
inputPreview: input.preview,
|
|
17481
|
+
outputPreview: output.preview,
|
|
17482
|
+
errorPreview: error.preview
|
|
17213
17483
|
};
|
|
17214
17484
|
}
|
|
17215
17485
|
/**
|
|
@@ -17276,6 +17546,32 @@ function rowToLightSpanRecord(row) {
|
|
|
17276
17546
|
updatedAt: null
|
|
17277
17547
|
};
|
|
17278
17548
|
}
|
|
17549
|
+
/** A stored preview as lists return it: `''` (nothing to preview) and NULL become null. */
|
|
17550
|
+
function listPreview(value) {
|
|
17551
|
+
return readPreview(value);
|
|
17552
|
+
}
|
|
17553
|
+
/**
|
|
17554
|
+
* Build a trace-list {@link LightSpanRecord} from a row that projected the light trace-list columns
|
|
17555
|
+
* (see `TRACE_LIGHT_SELECT_COLUMNS` in traces.ts): the timeline fields plus the memory context,
|
|
17556
|
+
* metadata and the stored previews. Callers fill in previews for rows written before they were
|
|
17557
|
+
* stored first.
|
|
17558
|
+
*/
|
|
17559
|
+
function rowToLightTraceRecord(row) {
|
|
17560
|
+
const record = rowToLightSpanRecord(row);
|
|
17561
|
+
return {
|
|
17562
|
+
...record,
|
|
17563
|
+
status: (0, _mastra_core_storage.computeTraceStatus)({
|
|
17564
|
+
error: record.error,
|
|
17565
|
+
endedAt: record.endedAt
|
|
17566
|
+
}),
|
|
17567
|
+
threadId: row.threadId == null ? null : String(row.threadId),
|
|
17568
|
+
resourceId: row.resourceId == null ? null : String(row.resourceId),
|
|
17569
|
+
metadata: parsedJson(row.metadataRaw) ?? void 0,
|
|
17570
|
+
inputPreview: listPreview(row.inputPreview),
|
|
17571
|
+
outputPreview: listPreview(row.outputPreview),
|
|
17572
|
+
errorPreview: listPreview(row.errorPreview)
|
|
17573
|
+
};
|
|
17574
|
+
}
|
|
17279
17575
|
function logRecordToRow(log) {
|
|
17280
17576
|
return {
|
|
17281
17577
|
...commonContextToRow(log),
|
|
@@ -17791,119 +18087,6 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
|
|
|
17791
18087
|
"requestContext"
|
|
17792
18088
|
]);
|
|
17793
18089
|
//#endregion
|
|
17794
|
-
//#region src/storage/domains/observability/v-next/sql.ts
|
|
17795
|
-
/**
|
|
17796
|
-
* SQL helpers for the v-next Postgres observability domain.
|
|
17797
|
-
*
|
|
17798
|
-
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
17799
|
-
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
17800
|
-
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
17801
|
-
*/
|
|
17802
|
-
/**
|
|
17803
|
-
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
17804
|
-
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
17805
|
-
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
17806
|
-
*
|
|
17807
|
-
* The result is sanitized because Postgres rejects some sequences that are
|
|
17808
|
-
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
17809
|
-
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
17810
|
-
* multi-row statement, so one bad value would otherwise reject the whole
|
|
17811
|
-
* batch of observability events.
|
|
17812
|
-
*/
|
|
17813
|
-
function encodeJsonb(value) {
|
|
17814
|
-
if (value === null || value === void 0) return null;
|
|
17815
|
-
return toPgJson(value);
|
|
17816
|
-
}
|
|
17817
|
-
/**
|
|
17818
|
-
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
17819
|
-
*
|
|
17820
|
-
* @param schema Schema name.
|
|
17821
|
-
* @param table Table name.
|
|
17822
|
-
* @param records Array of records (each is a column-name → value object).
|
|
17823
|
-
* All records must have identical key sets.
|
|
17824
|
-
* @returns { text, values } ready to pass to `client.query`.
|
|
17825
|
-
*/
|
|
17826
|
-
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
17827
|
-
if (records.length === 0) return null;
|
|
17828
|
-
const columns = Object.keys(records[0]).map((c) => (0, _mastra_core_utils.parseSqlIdentifier)(c, "column name"));
|
|
17829
|
-
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
17830
|
-
const values = [];
|
|
17831
|
-
const rowPlaceholders = [];
|
|
17832
|
-
let p = 1;
|
|
17833
|
-
for (const record of records) {
|
|
17834
|
-
const placeholders = columns.map((col) => {
|
|
17835
|
-
const raw = record[col];
|
|
17836
|
-
if (JSONB_COLUMNS.has(col)) {
|
|
17837
|
-
values.push(encodeJsonb(raw));
|
|
17838
|
-
return `$${p++}::jsonb`;
|
|
17839
|
-
}
|
|
17840
|
-
if (TEXT_ARRAY_COLUMNS.has(col)) {
|
|
17841
|
-
values.push(Array.isArray(raw) ? raw : []);
|
|
17842
|
-
return `$${p++}::text[]`;
|
|
17843
|
-
}
|
|
17844
|
-
values.push(raw === void 0 ? null : raw);
|
|
17845
|
-
return `$${p++}`;
|
|
17846
|
-
});
|
|
17847
|
-
rowPlaceholders.push(`(${placeholders.join(", ")})`);
|
|
17848
|
-
}
|
|
17849
|
-
return {
|
|
17850
|
-
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
17851
|
-
VALUES ${rowPlaceholders.join(", ")}
|
|
17852
|
-
${onConflict}`,
|
|
17853
|
-
values
|
|
17854
|
-
};
|
|
17855
|
-
}
|
|
17856
|
-
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
17857
|
-
const SPAN_KEY_COLUMNS = [
|
|
17858
|
-
"traceId",
|
|
17859
|
-
"spanId",
|
|
17860
|
-
"endedAt"
|
|
17861
|
-
];
|
|
17862
|
-
/**
|
|
17863
|
-
* Conflict clause for span writes.
|
|
17864
|
-
*
|
|
17865
|
-
* A span is written twice under the event-sourced strategy: once when it starts
|
|
17866
|
-
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
17867
|
-
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
17868
|
-
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
17869
|
-
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
17870
|
-
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
17871
|
-
* end row overwrites the pending one instead.
|
|
17872
|
-
*
|
|
17873
|
-
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
17874
|
-
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
17875
|
-
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
17876
|
-
* span to pending.
|
|
17877
|
-
*/
|
|
17878
|
-
function spanConflictClause(columns) {
|
|
17879
|
-
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
17880
|
-
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
17881
|
-
}
|
|
17882
|
-
/**
|
|
17883
|
-
* Ordering that picks the winning row for a span under the event-sourced write
|
|
17884
|
-
* model. An ended row always beats a pending row for the same span; among rows
|
|
17885
|
-
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
17886
|
-
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
17887
|
-
* the grouping key to collapse a span's rows down to one.
|
|
17888
|
-
*/
|
|
17889
|
-
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
17890
|
-
/**
|
|
17891
|
-
* Standard SELECT column list for tracing tables. The select projects every
|
|
17892
|
-
* column the row→record converters expect.
|
|
17893
|
-
*/
|
|
17894
|
-
const SPAN_SELECT_COLUMNS = `
|
|
17895
|
-
${buildSelectColumns(SPAN_EVENT_COLUMNS)}`;
|
|
17896
|
-
const SPAN_LIGHT_SELECT_COLUMNS = `
|
|
17897
|
-
${buildNamedSelectColumns(SPAN_LIGHT_SELECT_COLUMN_NAMES)}`;
|
|
17898
|
-
const METRIC_SELECT_COLUMNS = `
|
|
17899
|
-
${buildSelectColumns(METRIC_EVENT_COLUMNS)}`;
|
|
17900
|
-
const LOG_SELECT_COLUMNS = `
|
|
17901
|
-
${buildSelectColumns(LOG_EVENT_COLUMNS)}`;
|
|
17902
|
-
const SCORE_SELECT_COLUMNS = `
|
|
17903
|
-
${buildSelectColumns(SCORE_EVENT_COLUMNS)}`;
|
|
17904
|
-
const FEEDBACK_SELECT_COLUMNS = `
|
|
17905
|
-
${buildSelectColumns(FEEDBACK_EVENT_COLUMNS)}`;
|
|
17906
|
-
//#endregion
|
|
17907
18090
|
//#region src/storage/domains/observability/v-next/feedback.ts
|
|
17908
18091
|
/**
|
|
17909
18092
|
* Feedback operations for the v-next Postgres observability domain.
|
|
@@ -19062,7 +19245,10 @@ const TRACE_SELECT = `
|
|
|
19062
19245
|
r."entityId" AS "entityId",
|
|
19063
19246
|
r."parentSpanId" AS "parentSpanId",
|
|
19064
19247
|
r."metadataRaw" AS "metadata",
|
|
19065
|
-
r."
|
|
19248
|
+
r."inputPreview" AS "inputPreview",
|
|
19249
|
+
r."outputPreview" AS "outputPreview",
|
|
19250
|
+
r."errorPreview" AS "errorPreview",
|
|
19251
|
+
r."cursorId" AS "cursorId",
|
|
19066
19252
|
r."threadId" AS "threadId",
|
|
19067
19253
|
r."resourceId" AS "resourceId",
|
|
19068
19254
|
r."startedAt" AS "startedAt",
|
|
@@ -19520,7 +19706,7 @@ function compilePostgresTraceQuery(schema, plan, mode = "data", safeHorizon) {
|
|
|
19520
19706
|
safeHorizon
|
|
19521
19707
|
};
|
|
19522
19708
|
}
|
|
19523
|
-
const { ctes, values } = compilePostgresTraceCandidates(schema, plan, `${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.\"xactId\"
|
|
19709
|
+
const { ctes, values } = compilePostgresTraceCandidates(schema, plan, `${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.\"xactId\"" : ""}`, deltaWindow);
|
|
19524
19710
|
const candidates = `WITH ${ctes.join(",\n")}`;
|
|
19525
19711
|
if (plan.paginationMode === "page" && mode === "count") return {
|
|
19526
19712
|
text: `${candidates}
|
|
@@ -19762,6 +19948,34 @@ async function setRemainingTimeout(transaction, deadline) {
|
|
|
19762
19948
|
if (remainingTimeoutMs <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
|
|
19763
19949
|
await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${remainingTimeoutMs}ms`]);
|
|
19764
19950
|
}
|
|
19951
|
+
const PREVIEW_PAYLOADS$2 = [
|
|
19952
|
+
["inputPreview", "input"],
|
|
19953
|
+
["outputPreview", "output"],
|
|
19954
|
+
["errorPreview", "error"]
|
|
19955
|
+
];
|
|
19956
|
+
/**
|
|
19957
|
+
* Fill the previews of root rows written before the preview columns existed (stored preview NULL)
|
|
19958
|
+
* by reading just those rows' payloads, and only the payloads whose preview is missing. Rows are
|
|
19959
|
+
* keyed by `cursorId`, which identifies the exact root row the list picked.
|
|
19960
|
+
*
|
|
19961
|
+
* jsonb comes back decoded, so the preview is built with `buildPreview(value)`. jsonb also
|
|
19962
|
+
* normalizes object key order, so a compact-JSON fallback preview can differ from the one the
|
|
19963
|
+
* writer would have stored from the original value. That is acceptable for these old rows.
|
|
19964
|
+
*/
|
|
19965
|
+
async function fillMissingTracePreviews(transaction, schema, rows, deadline) {
|
|
19966
|
+
const missing = rows.filter((row) => PREVIEW_PAYLOADS$2.some(([preview]) => needsPreview(row[preview])));
|
|
19967
|
+
if (!missing.length) return;
|
|
19968
|
+
await setRemainingTimeout(transaction, deadline);
|
|
19969
|
+
const payloads = await transaction.any(`SELECT e."cursorId"::text AS "cursorId",
|
|
19970
|
+
${PREVIEW_PAYLOADS$2.map(([preview, payload]) => `${missing.some((row) => needsPreview(row[preview])) ? `e."${payload}"` : "NULL"} AS "${payload}"`).join(",\n ")}
|
|
19971
|
+
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} e
|
|
19972
|
+
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))]);
|
|
19973
|
+
const byCursor = new Map(payloads.map((payload) => [String(payload.cursorId), payload]));
|
|
19974
|
+
for (const row of missing) {
|
|
19975
|
+
const payload = byCursor.get(String(row.cursorId));
|
|
19976
|
+
for (const [preview, column] of PREVIEW_PAYLOADS$2) if (needsPreview(row[preview])) row[preview] = readPreview(row[preview], payload?.[column]);
|
|
19977
|
+
}
|
|
19978
|
+
}
|
|
19765
19979
|
function traceRowToResult(row) {
|
|
19766
19980
|
return {
|
|
19767
19981
|
traceId: String(row.traceId),
|
|
@@ -19771,7 +19985,9 @@ function traceRowToResult(row) {
|
|
|
19771
19985
|
parentSpanId: row.parentSpanId ?? null,
|
|
19772
19986
|
createdAt: asIsoTimestamp$1(row.startedAt),
|
|
19773
19987
|
metadata: row.metadata ?? null,
|
|
19774
|
-
inputPreview:
|
|
19988
|
+
inputPreview: readPreview(row.inputPreview),
|
|
19989
|
+
outputPreview: readPreview(row.outputPreview),
|
|
19990
|
+
errorPreview: readPreview(row.errorPreview),
|
|
19775
19991
|
threadId: row.threadId == null ? null : String(row.threadId),
|
|
19776
19992
|
resourceId: row.resourceId == null ? null : String(row.resourceId),
|
|
19777
19993
|
startedAt: asIsoTimestamp$1(row.startedAt),
|
|
@@ -19799,6 +20015,7 @@ async function queryTraces(client, schema, plan, timeoutMs) {
|
|
|
19799
20015
|
rows = await transaction.any(query.text, query.values);
|
|
19800
20016
|
}
|
|
19801
20017
|
const visible = rows.slice(0, plan.limit);
|
|
20018
|
+
await fillMissingTracePreviews(transaction, schema, visible, deadline);
|
|
19802
20019
|
const last = visible.at(-1);
|
|
19803
20020
|
return _mastra_core_storage.traceQueryResponseSchema.parse({
|
|
19804
20021
|
traces: visible.map(traceRowToResult),
|
|
@@ -19828,6 +20045,7 @@ async function queryTraces(client, schema, plan, timeoutMs) {
|
|
|
19828
20045
|
if (remainingTimeoutMs <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
|
|
19829
20046
|
await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${remainingTimeoutMs}ms`]);
|
|
19830
20047
|
const rows = await transaction.any(dataQuery.text, dataQuery.values);
|
|
20048
|
+
await fillMissingTracePreviews(transaction, schema, rows, deadline);
|
|
19831
20049
|
return {
|
|
19832
20050
|
total: Number(countRows[0]?.count ?? 0),
|
|
19833
20051
|
rows,
|
|
@@ -19847,7 +20065,12 @@ async function queryTraces(client, schema, plan, timeoutMs) {
|
|
|
19847
20065
|
});
|
|
19848
20066
|
}
|
|
19849
20067
|
const query = compilePostgresTraceQuery(schema, plan);
|
|
19850
|
-
const
|
|
20068
|
+
const deadline = performance.now() + _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
|
|
20069
|
+
const rows = await runWithPostgresTraceQueryTimeout(client, timeoutMs, async (transaction) => {
|
|
20070
|
+
const rows = await transaction.any(query.text, query.values);
|
|
20071
|
+
if (plan.result === "traces") await fillMissingTracePreviews(transaction, schema, rows.slice(0, plan.limit), deadline);
|
|
20072
|
+
return rows;
|
|
20073
|
+
});
|
|
19851
20074
|
const visibleRows = rows.slice(0, plan.limit);
|
|
19852
20075
|
if (plan.result === "groups") {
|
|
19853
20076
|
const groups = visibleRows.map((row) => ({ threadId: String(row.threadId) }));
|
|
@@ -20004,6 +20227,29 @@ function compilePostgresSpanQuery(schema, plan) {
|
|
|
20004
20227
|
values
|
|
20005
20228
|
};
|
|
20006
20229
|
}
|
|
20230
|
+
const PREVIEW_PAYLOADS$1 = [
|
|
20231
|
+
["inputPreview", "input"],
|
|
20232
|
+
["outputPreview", "output"],
|
|
20233
|
+
["errorPreview", "error"]
|
|
20234
|
+
];
|
|
20235
|
+
/**
|
|
20236
|
+
* Stored previews of a hydrated row, built from the payload where the stored preview is NULL.
|
|
20237
|
+
* jsonb comes back decoded, so the fallback uses `buildPreview(value)`. jsonb normalizes object key
|
|
20238
|
+
* order, so a compact-JSON fallback preview can differ from what the writer would have stored from
|
|
20239
|
+
* the original value; that is acceptable for rows written before the preview columns existed.
|
|
20240
|
+
*/
|
|
20241
|
+
function toSpanQueryPayload(row) {
|
|
20242
|
+
const [inputPreview, outputPreview, errorPreview] = PREVIEW_PAYLOADS$1.map(([preview, payload]) => readPreview(row[preview], row[payload]));
|
|
20243
|
+
return {
|
|
20244
|
+
organizationId: row.organizationId ?? null,
|
|
20245
|
+
resourceId: row.resourceId ?? null,
|
|
20246
|
+
traceId: String(row.traceId),
|
|
20247
|
+
spanId: String(row.spanId),
|
|
20248
|
+
inputPreview,
|
|
20249
|
+
outputPreview,
|
|
20250
|
+
errorPreview
|
|
20251
|
+
};
|
|
20252
|
+
}
|
|
20007
20253
|
async function querySpans(client, schema, plan, timeoutMs) {
|
|
20008
20254
|
const deadline = performance.now() + _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
|
|
20009
20255
|
return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
|
|
@@ -20028,9 +20274,9 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20028
20274
|
payloadValues.push(row.version);
|
|
20029
20275
|
return `(${key} AND "cursorId" = $${payloadValues.length}::bigint)`;
|
|
20030
20276
|
}).join(" OR ");
|
|
20031
|
-
const payloads = await tx.any(`SELECT ${identities},
|
|
20032
|
-
|
|
20033
|
-
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues);
|
|
20277
|
+
const payloads = (await tx.any(`SELECT ${identities},
|
|
20278
|
+
${PREVIEW_PAYLOADS$1.map(([preview, payload]) => `"${preview}", CASE WHEN ${missingPreviewSql(`"${preview}"`)} THEN "${payload}" END AS "${payload}"`).join(",\n ")}
|
|
20279
|
+
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues)).map(toSpanQueryPayload);
|
|
20034
20280
|
await remaining();
|
|
20035
20281
|
const metricValues = [];
|
|
20036
20282
|
const metricWhere = selectedKeys(page, metricValues);
|
|
@@ -20041,7 +20287,7 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20041
20287
|
ORDER BY ${identities}, "metricId", "cursorId" DESC) m
|
|
20042
20288
|
LIMIT ${_mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS + 1}`, metricValues);
|
|
20043
20289
|
if (metrics.length > _mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS) throw new _mastra_core_storage.TraceQueryResourceLimitError();
|
|
20044
|
-
return
|
|
20290
|
+
return buildSpanQueryResponse(plan, selected, payloads, metrics);
|
|
20045
20291
|
}, { repeatableRead: true });
|
|
20046
20292
|
}
|
|
20047
20293
|
//#endregion
|
|
@@ -20506,16 +20752,73 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
20506
20752
|
* array-of-strings representation here.
|
|
20507
20753
|
*/
|
|
20508
20754
|
const SPAN_SELECT_COLUMNS_ALIASED = SPAN_SELECT_COLUMNS.replace(/\n/g, " ").split(",").map((c) => `r.${c.trim()}`).join(", ");
|
|
20755
|
+
const FULL_PROJECTION = {
|
|
20756
|
+
columns: SPAN_SELECT_COLUMNS_ALIASED,
|
|
20757
|
+
mapRows: (_client, _span, rows) => (0, _mastra_core_storage.toTraceSpans)(rows.map(rowToSpanRecord))
|
|
20758
|
+
};
|
|
20759
|
+
/**
|
|
20760
|
+
* Columns a light trace list renders: the timeline fields, memory context, metadata and the stored
|
|
20761
|
+
* previews. The `input` / `output` payloads stay off the read path; rows written before the
|
|
20762
|
+
* previews were stored get theirs from the payloads, read for just those rows.
|
|
20763
|
+
*/
|
|
20764
|
+
const TRACE_LIGHT_SELECT_COLUMNS = [
|
|
20765
|
+
...SPAN_LIGHT_SELECT_COLUMN_NAMES,
|
|
20766
|
+
"threadId",
|
|
20767
|
+
"resourceId",
|
|
20768
|
+
"metadataRaw",
|
|
20769
|
+
"inputPreview",
|
|
20770
|
+
"outputPreview",
|
|
20771
|
+
"errorPreview"
|
|
20772
|
+
].map((column) => `r."${column}"`).join(", ");
|
|
20773
|
+
const PREVIEW_PAYLOADS = [
|
|
20774
|
+
["inputPreview", "input"],
|
|
20775
|
+
["outputPreview", "output"],
|
|
20776
|
+
["errorPreview", "error"]
|
|
20777
|
+
];
|
|
20778
|
+
/**
|
|
20779
|
+
* Builds the previews of rows written before previews were stored (NULL preview) from those rows'
|
|
20780
|
+
* payloads. Only the flagged rows' payloads are read, so this fades out as old rows age out.
|
|
20781
|
+
*
|
|
20782
|
+
* `jsonb` comes back decoded with its keys reordered, so a compact-JSON preview built here can
|
|
20783
|
+
* differ in key order from the one the writer would have stored. Acceptable for old rows.
|
|
20784
|
+
*/
|
|
20785
|
+
async function fillMissingPreviews(client, span, rows) {
|
|
20786
|
+
const missing = rows.filter((row) => PREVIEW_PAYLOADS.some(([preview]) => needsPreview(row[preview])));
|
|
20787
|
+
if (!missing.length) return;
|
|
20788
|
+
const payloads = await client.manyOrNone(`SELECT e."cursorId"::text AS "cursorId",
|
|
20789
|
+
${PREVIEW_PAYLOADS.map(([preview, payload]) => `${missing.some((row) => needsPreview(row[preview])) ? `e."${payload}"` : "NULL"} AS "${payload}"`).join(",\n ")}
|
|
20790
|
+
FROM ${span} e
|
|
20791
|
+
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))]);
|
|
20792
|
+
const byCursor = new Map(payloads.map((payload) => [String(payload.cursorId), payload]));
|
|
20793
|
+
for (const row of missing) {
|
|
20794
|
+
const payload = byCursor.get(String(row.cursorId));
|
|
20795
|
+
for (const [preview, column] of PREVIEW_PAYLOADS) if (needsPreview(row[preview])) row[preview] = readPreview(row[preview], payload?.[column]);
|
|
20796
|
+
}
|
|
20797
|
+
}
|
|
20798
|
+
const LIGHT_PROJECTION = {
|
|
20799
|
+
columns: TRACE_LIGHT_SELECT_COLUMNS,
|
|
20800
|
+
mapRows: async (client, span, rows) => {
|
|
20801
|
+
await fillMissingPreviews(client, span, rows);
|
|
20802
|
+
return rows.map(rowToLightTraceRecord);
|
|
20803
|
+
}
|
|
20804
|
+
};
|
|
20509
20805
|
async function listTraces(client, schema, args) {
|
|
20806
|
+
return listTraceRows(client, schema, args, FULL_PROJECTION);
|
|
20807
|
+
}
|
|
20808
|
+
/** Trace list projecting only the columns a list renders, with stored previews instead of payloads. */
|
|
20809
|
+
async function listTracesLight(client, schema, args) {
|
|
20810
|
+
return listTraceRows(client, schema, args, LIGHT_PROJECTION);
|
|
20811
|
+
}
|
|
20812
|
+
async function listTraceRows(client, schema, args, projection) {
|
|
20510
20813
|
const { mode, filters, pagination, orderBy, after, limit } = _mastra_core_storage.listTracesArgsSchema.parse(args);
|
|
20511
20814
|
const span = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
20512
20815
|
if (mode === "delta") {
|
|
20513
20816
|
assertDeltaPollingEnabled();
|
|
20514
|
-
return listTracesDelta(client, span, filters, after, limit);
|
|
20817
|
+
return listTracesDelta(client, span, projection, filters, after, limit);
|
|
20515
20818
|
}
|
|
20516
|
-
return listTracesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
20819
|
+
return listTracesPage(client, span, projection, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
20517
20820
|
}
|
|
20518
|
-
async function listTracesPage(client, span, filters, page, perPage, orderField, orderDir) {
|
|
20821
|
+
async function listTracesPage(client, span, projection, filters, page, perPage, orderField, orderDir) {
|
|
20519
20822
|
const { conditions, params, nextParamIdx } = buildListTracesFilters(filters, span, 1);
|
|
20520
20823
|
let i = nextParamIdx;
|
|
20521
20824
|
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
@@ -20523,15 +20826,18 @@ async function listTracesPage(client, span, filters, page, perPage, orderField,
|
|
|
20523
20826
|
const countRow = await client.oneOrNone(`SELECT COUNT(*)::text AS count FROM ${span} r ${whereClause}`, params);
|
|
20524
20827
|
const count = Number(countRow?.count ?? 0);
|
|
20525
20828
|
let spans = [];
|
|
20526
|
-
if (count > 0)
|
|
20829
|
+
if (count > 0) {
|
|
20830
|
+
const rows = await client.manyOrNone(`SELECT ${projection.columns}
|
|
20527
20831
|
FROM ${span} r
|
|
20528
20832
|
${whereClause}
|
|
20529
20833
|
${orderClause}
|
|
20530
20834
|
LIMIT $${i++} OFFSET $${i++}`, [
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
|
|
20534
|
-
|
|
20835
|
+
...params,
|
|
20836
|
+
perPage,
|
|
20837
|
+
page * perPage
|
|
20838
|
+
]);
|
|
20839
|
+
spans = await projection.mapRows(client, span, rows);
|
|
20840
|
+
}
|
|
20535
20841
|
const deltaCursor = deltaPollingFeatureEnabled() ? await readTracesStreamHeadCursor(client, span, filters) : void 0;
|
|
20536
20842
|
return {
|
|
20537
20843
|
pagination: {
|
|
@@ -20540,11 +20846,11 @@ async function listTracesPage(client, span, filters, page, perPage, orderField,
|
|
|
20540
20846
|
perPage,
|
|
20541
20847
|
hasMore: (page + 1) * perPage < count
|
|
20542
20848
|
},
|
|
20543
|
-
spans
|
|
20849
|
+
spans,
|
|
20544
20850
|
...deltaCursor !== void 0 ? { deltaCursor } : {}
|
|
20545
20851
|
};
|
|
20546
20852
|
}
|
|
20547
|
-
async function listTracesDelta(client, span, filters, after, limit) {
|
|
20853
|
+
async function listTracesDelta(client, span, projection, filters, after, limit) {
|
|
20548
20854
|
if (after === void 0) {
|
|
20549
20855
|
const deltaCursor = await readTracesStreamHeadCursor(client, span, filters);
|
|
20550
20856
|
return {
|
|
@@ -20564,7 +20870,7 @@ async function listTracesDelta(client, span, filters, after, limit) {
|
|
|
20564
20870
|
params.push(afterCursor.xactId, afterCursor.cursorId);
|
|
20565
20871
|
conditions.push(`r."xactId" < $${i++}::xid8`);
|
|
20566
20872
|
params.push(safeHorizon);
|
|
20567
|
-
const rows = await client.manyOrNone(`SELECT ${
|
|
20873
|
+
const rows = await client.manyOrNone(`SELECT ${projection.columns}
|
|
20568
20874
|
FROM ${span} r
|
|
20569
20875
|
WHERE ${conditions.join(" AND ")}
|
|
20570
20876
|
ORDER BY r."xactId" ASC, r."cursorId" ASC
|
|
@@ -20573,7 +20879,7 @@ async function listTracesDelta(client, span, filters, after, limit) {
|
|
|
20573
20879
|
const visible = rows.slice(0, limit);
|
|
20574
20880
|
const deltaCursor = visible.length > 0 ? encodeDeltaCursor(visible[visible.length - 1].xactId, visible[visible.length - 1].cursorId) : encodeDeltaCursor(safeHorizon, 0);
|
|
20575
20881
|
return {
|
|
20576
|
-
spans: (
|
|
20882
|
+
spans: await projection.mapRows(client, span, visible),
|
|
20577
20883
|
delta: {
|
|
20578
20884
|
limit,
|
|
20579
20885
|
hasMore
|
|
@@ -21284,6 +21590,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
21284
21590
|
async listTraces(args) {
|
|
21285
21591
|
return this.#run("LIST_TRACES", () => listTraces(this.#readClient, this.#schema, args));
|
|
21286
21592
|
}
|
|
21593
|
+
async listTracesLight(args) {
|
|
21594
|
+
return this.#run("LIST_TRACES_LIGHT", () => listTracesLight(this.#readClient, this.#schema, args));
|
|
21595
|
+
}
|
|
21287
21596
|
async querySpans(plan) {
|
|
21288
21597
|
return this.#run("QUERY_SPANS", () => querySpans(this.#readClient, this.#schema, plan, this.#traceQueryTimeoutMs));
|
|
21289
21598
|
}
|