@mastra/duckdb 1.5.2-alpha.0 → 1.6.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3733 @@
1
+ import { t as DuckDBConnection } from "./db-Dl0fY488.js";
2
+ import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
+ import { BRANCH_SPAN_TYPES, METRIC_DISTINCT_COLUMNS, ObservabilityStorage, createStorageErrorId, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, toTraceSpans } from "@mastra/core/storage";
4
+ import { coreFeatures } from "@mastra/core/features";
5
+ import { EntityType } from "@mastra/core/observability";
6
+ import { parseFieldKey } from "@mastra/core/utils";
7
+ //#region src/storage/domains/observability/ddl.ts
8
+ /**
9
+ * DDL statements for DuckDB observability tables.
10
+ * All tables use append-only patterns with a single `timestamp` column.
11
+ *
12
+ * Column ordering convention:
13
+ * 1. Event metadata (eventType, timestamp)
14
+ * 2. IDs (trace, span, experiment, resource, run, session, etc.)
15
+ * 3. Entity hierarchy (entity, parent, root)
16
+ * 4. Context (user, org, environment, service, executionSource)
17
+ * 5. Domain-specific scalar fields
18
+ * 6. JSON fields (attributes, metadata, tags, input/output, etc.)
19
+ */
20
+ const SPAN_EVENTS_CURSOR_SEQUENCE_DDL = `
21
+ CREATE SEQUENCE IF NOT EXISTS span_events_cursor_id_seq START 1
22
+ `;
23
+ const METRIC_EVENTS_CURSOR_SEQUENCE_DDL = `
24
+ CREATE SEQUENCE IF NOT EXISTS metric_events_cursor_id_seq START 1
25
+ `;
26
+ const LOG_EVENTS_CURSOR_SEQUENCE_DDL = `
27
+ CREATE SEQUENCE IF NOT EXISTS log_events_cursor_id_seq START 1
28
+ `;
29
+ const SCORE_EVENTS_CURSOR_SEQUENCE_DDL = `
30
+ CREATE SEQUENCE IF NOT EXISTS score_events_cursor_id_seq START 1
31
+ `;
32
+ const FEEDBACK_EVENTS_CURSOR_SEQUENCE_DDL = `
33
+ CREATE SEQUENCE IF NOT EXISTS feedback_events_cursor_id_seq START 1
34
+ `;
35
+ /** DDL for the span_events append-only table. */
36
+ const SPAN_EVENTS_DDL = `
37
+ CREATE TABLE IF NOT EXISTS span_events (
38
+ -- Event metadata
39
+ eventType VARCHAR NOT NULL,
40
+ timestamp TIMESTAMP NOT NULL,
41
+ cursorId BIGINT,
42
+
43
+ -- IDs
44
+ traceId VARCHAR NOT NULL,
45
+ spanId VARCHAR NOT NULL,
46
+ parentSpanId VARCHAR,
47
+ experimentId VARCHAR,
48
+
49
+ -- Entity
50
+ entityType VARCHAR,
51
+ entityId VARCHAR,
52
+ entityName VARCHAR,
53
+ entityVersionId VARCHAR,
54
+
55
+ -- Context
56
+ userId VARCHAR,
57
+ organizationId VARCHAR,
58
+ resourceId VARCHAR,
59
+ runId VARCHAR,
60
+ sessionId VARCHAR,
61
+ threadId VARCHAR,
62
+ requestId VARCHAR,
63
+ environment VARCHAR,
64
+ source VARCHAR,
65
+ serviceName VARCHAR,
66
+ requestContext JSON,
67
+
68
+ -- Span-specific scalars
69
+ name VARCHAR,
70
+ spanType VARCHAR,
71
+ isEvent BOOLEAN,
72
+ endedAt TIMESTAMP,
73
+
74
+ -- JSON fields
75
+ attributes JSON,
76
+ metadata JSON,
77
+ tags JSON,
78
+ scope JSON,
79
+ links JSON,
80
+ input JSON,
81
+ output JSON,
82
+ error JSON
83
+ )`;
84
+ /** DDL for the metric_events append-only table. */
85
+ const METRIC_EVENTS_DDL = `
86
+ CREATE TABLE IF NOT EXISTS metric_events (
87
+ -- Event metadata
88
+ timestamp TIMESTAMP NOT NULL,
89
+ cursorId BIGINT,
90
+
91
+ -- IDs
92
+ metricId VARCHAR NOT NULL PRIMARY KEY,
93
+ traceId VARCHAR,
94
+ spanId VARCHAR,
95
+ experimentId VARCHAR,
96
+
97
+ -- Entity hierarchy
98
+ entityType VARCHAR,
99
+ entityId VARCHAR,
100
+ entityName VARCHAR,
101
+ entityVersionId VARCHAR,
102
+ parentEntityVersionId VARCHAR,
103
+ parentEntityType VARCHAR,
104
+ parentEntityId VARCHAR,
105
+ parentEntityName VARCHAR,
106
+ rootEntityVersionId VARCHAR,
107
+ rootEntityType VARCHAR,
108
+ rootEntityId VARCHAR,
109
+ rootEntityName VARCHAR,
110
+
111
+ -- Context
112
+ userId VARCHAR,
113
+ organizationId VARCHAR,
114
+ resourceId VARCHAR,
115
+ runId VARCHAR,
116
+ sessionId VARCHAR,
117
+ threadId VARCHAR,
118
+ requestId VARCHAR,
119
+ environment VARCHAR,
120
+ executionSource VARCHAR,
121
+ serviceName VARCHAR,
122
+
123
+ -- Metric-specific scalars
124
+ name VARCHAR NOT NULL,
125
+ value DOUBLE NOT NULL,
126
+ provider VARCHAR,
127
+ model VARCHAR,
128
+ estimatedCost DOUBLE,
129
+ costUnit VARCHAR,
130
+
131
+ -- JSON fields
132
+ tags JSON,
133
+ labels JSON,
134
+ costMetadata JSON,
135
+ metadata JSON,
136
+ scope JSON
137
+ )`;
138
+ /** DDL for the log_events append-only table. */
139
+ const LOG_EVENTS_DDL = `
140
+ CREATE TABLE IF NOT EXISTS log_events (
141
+ -- Event metadata
142
+ timestamp TIMESTAMP NOT NULL,
143
+ cursorId BIGINT,
144
+
145
+ -- IDs
146
+ logId VARCHAR NOT NULL PRIMARY KEY,
147
+ traceId VARCHAR,
148
+ spanId VARCHAR,
149
+ experimentId VARCHAR,
150
+
151
+ -- Entity hierarchy
152
+ entityType VARCHAR,
153
+ entityId VARCHAR,
154
+ entityName VARCHAR,
155
+ entityVersionId VARCHAR,
156
+ parentEntityVersionId VARCHAR,
157
+ parentEntityType VARCHAR,
158
+ parentEntityId VARCHAR,
159
+ parentEntityName VARCHAR,
160
+ rootEntityVersionId VARCHAR,
161
+ rootEntityType VARCHAR,
162
+ rootEntityId VARCHAR,
163
+ rootEntityName VARCHAR,
164
+
165
+ -- Context
166
+ userId VARCHAR,
167
+ organizationId VARCHAR,
168
+ resourceId VARCHAR,
169
+ runId VARCHAR,
170
+ sessionId VARCHAR,
171
+ threadId VARCHAR,
172
+ requestId VARCHAR,
173
+ environment VARCHAR,
174
+ executionSource VARCHAR,
175
+ serviceName VARCHAR,
176
+
177
+ -- Log-specific scalars
178
+ level VARCHAR NOT NULL,
179
+ message VARCHAR NOT NULL,
180
+
181
+ -- JSON fields
182
+ data JSON,
183
+ tags JSON,
184
+ metadata JSON,
185
+ scope JSON
186
+ )`;
187
+ /** DDL for the score_events append-only table. */
188
+ const SCORE_EVENTS_DDL = `
189
+ CREATE TABLE IF NOT EXISTS score_events (
190
+ -- Event metadata
191
+ timestamp TIMESTAMP NOT NULL,
192
+ cursorId BIGINT,
193
+
194
+ -- IDs
195
+ scoreId VARCHAR NOT NULL PRIMARY KEY,
196
+ traceId VARCHAR,
197
+ spanId VARCHAR,
198
+ experimentId VARCHAR,
199
+ scoreTraceId VARCHAR,
200
+
201
+ -- Entity hierarchy
202
+ entityType VARCHAR,
203
+ entityId VARCHAR,
204
+ entityName VARCHAR,
205
+ entityVersionId VARCHAR,
206
+ parentEntityVersionId VARCHAR,
207
+ parentEntityType VARCHAR,
208
+ parentEntityId VARCHAR,
209
+ parentEntityName VARCHAR,
210
+ rootEntityVersionId VARCHAR,
211
+ rootEntityType VARCHAR,
212
+ rootEntityId VARCHAR,
213
+ rootEntityName VARCHAR,
214
+
215
+ -- Context
216
+ userId VARCHAR,
217
+ organizationId VARCHAR,
218
+ resourceId VARCHAR,
219
+ runId VARCHAR,
220
+ sessionId VARCHAR,
221
+ threadId VARCHAR,
222
+ requestId VARCHAR,
223
+ environment VARCHAR,
224
+ executionSource VARCHAR,
225
+ serviceName VARCHAR,
226
+
227
+ -- Score-specific scalars
228
+ scorerId VARCHAR NOT NULL,
229
+ scorerVersion VARCHAR,
230
+ source VARCHAR,
231
+ scoreSource VARCHAR,
232
+ score DOUBLE NOT NULL,
233
+ reason VARCHAR,
234
+
235
+ -- JSON fields
236
+ tags JSON,
237
+ metadata JSON,
238
+ scope JSON
239
+ )`;
240
+ /** DDL for the feedback_events append-only table. */
241
+ const FEEDBACK_EVENTS_DDL = `
242
+ CREATE TABLE IF NOT EXISTS feedback_events (
243
+ -- Event metadata
244
+ timestamp TIMESTAMP NOT NULL,
245
+ cursorId BIGINT,
246
+
247
+ -- IDs
248
+ feedbackId VARCHAR NOT NULL PRIMARY KEY,
249
+ traceId VARCHAR,
250
+ spanId VARCHAR,
251
+ experimentId VARCHAR,
252
+ -- Entity hierarchy
253
+ entityType VARCHAR,
254
+ entityId VARCHAR,
255
+ entityName VARCHAR,
256
+ entityVersionId VARCHAR,
257
+ parentEntityVersionId VARCHAR,
258
+ parentEntityType VARCHAR,
259
+ parentEntityId VARCHAR,
260
+ parentEntityName VARCHAR,
261
+ rootEntityVersionId VARCHAR,
262
+ rootEntityType VARCHAR,
263
+ rootEntityId VARCHAR,
264
+ rootEntityName VARCHAR,
265
+
266
+ -- Context
267
+ userId VARCHAR,
268
+ organizationId VARCHAR,
269
+ resourceId VARCHAR,
270
+ runId VARCHAR,
271
+ sessionId VARCHAR,
272
+ threadId VARCHAR,
273
+ requestId VARCHAR,
274
+ environment VARCHAR,
275
+ executionSource VARCHAR,
276
+ serviceName VARCHAR,
277
+
278
+ -- Feedback actor / linkage
279
+ feedbackUserId VARCHAR,
280
+ sourceId VARCHAR,
281
+
282
+ -- Feedback-specific scalars
283
+ source VARCHAR,
284
+ feedbackSource VARCHAR NOT NULL,
285
+ feedbackType VARCHAR NOT NULL,
286
+ value VARCHAR NOT NULL,
287
+ comment VARCHAR,
288
+
289
+ -- JSON fields
290
+ tags JSON,
291
+ metadata JSON,
292
+ scope JSON
293
+ )`;
294
+ /** All observability DDL statements, in creation order. */
295
+ const ALL_DDL = [
296
+ SPAN_EVENTS_CURSOR_SEQUENCE_DDL,
297
+ METRIC_EVENTS_CURSOR_SEQUENCE_DDL,
298
+ LOG_EVENTS_CURSOR_SEQUENCE_DDL,
299
+ SCORE_EVENTS_CURSOR_SEQUENCE_DDL,
300
+ FEEDBACK_EVENTS_CURSOR_SEQUENCE_DDL,
301
+ SPAN_EVENTS_DDL,
302
+ METRIC_EVENTS_DDL,
303
+ LOG_EVENTS_DDL,
304
+ SCORE_EVENTS_DDL,
305
+ FEEDBACK_EVENTS_DDL
306
+ ];
307
+ /** Additive migrations for observability tables created by older versions. */
308
+ const ALL_MIGRATIONS = [
309
+ `CREATE SEQUENCE IF NOT EXISTS span_events_cursor_id_seq START 1`,
310
+ `CREATE SEQUENCE IF NOT EXISTS metric_events_cursor_id_seq START 1`,
311
+ `CREATE SEQUENCE IF NOT EXISTS log_events_cursor_id_seq START 1`,
312
+ `CREATE SEQUENCE IF NOT EXISTS score_events_cursor_id_seq START 1`,
313
+ `CREATE SEQUENCE IF NOT EXISTS feedback_events_cursor_id_seq START 1`,
314
+ `ALTER TABLE span_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
315
+ `ALTER TABLE span_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
316
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
317
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
318
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
319
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
320
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS experimentId VARCHAR`,
321
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
322
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
323
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
324
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
325
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
326
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
327
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS userId VARCHAR`,
328
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
329
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
330
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
331
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
332
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
333
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
334
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
335
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
336
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
337
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS costMetadata JSON`,
338
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS metadata JSON`,
339
+ `ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS scope JSON`,
340
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
341
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
342
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
343
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
344
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS experimentId VARCHAR`,
345
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
346
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
347
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
348
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
349
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
350
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
351
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS userId VARCHAR`,
352
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
353
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
354
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
355
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
356
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
357
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
358
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
359
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
360
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
361
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS tags JSON`,
362
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS metadata JSON`,
363
+ `ALTER TABLE log_events ADD COLUMN IF NOT EXISTS scope JSON`,
364
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
365
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
366
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
367
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
368
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityType VARCHAR`,
369
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityId VARCHAR`,
370
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityName VARCHAR`,
371
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
372
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
373
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
374
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
375
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
376
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
377
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS userId VARCHAR`,
378
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
379
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
380
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
381
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
382
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
383
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
384
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
385
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
386
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
387
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS tags JSON`,
388
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS scope JSON`,
389
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS source VARCHAR`,
390
+ `ALTER TABLE score_events ADD COLUMN IF NOT EXISTS scoreSource VARCHAR`,
391
+ `ALTER TABLE score_events ALTER COLUMN traceId DROP NOT NULL`,
392
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
393
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
394
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
395
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
396
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityType VARCHAR`,
397
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityId VARCHAR`,
398
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityName VARCHAR`,
399
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
400
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
401
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
402
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
403
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
404
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
405
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
406
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
407
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
408
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
409
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
410
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
411
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
412
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
413
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
414
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS feedbackUserId VARCHAR`,
415
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS sourceId VARCHAR`,
416
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS tags JSON`,
417
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS scope JSON`,
418
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS source VARCHAR`,
419
+ `ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS feedbackSource VARCHAR`,
420
+ `ALTER TABLE feedback_events ALTER COLUMN traceId DROP NOT NULL`
421
+ ];
422
+ //#endregion
423
+ //#region src/storage/domains/observability/discovery.ts
424
+ function unionDistinctQueries(selects, orderBy) {
425
+ return `${selects.join("\nUNION\n")}\nORDER BY ${orderBy}`;
426
+ }
427
+ /** Return distinct entity types across observability signals that carry them. */
428
+ async function getEntityTypes(db, _args) {
429
+ const rows = await db.query(unionDistinctQueries([
430
+ `SELECT entityType FROM span_events WHERE entityType IS NOT NULL`,
431
+ `SELECT entityType FROM metric_events WHERE entityType IS NOT NULL`,
432
+ `SELECT entityType FROM log_events WHERE entityType IS NOT NULL`
433
+ ], "entityType"));
434
+ const validTypes = new Set(Object.values(EntityType));
435
+ const typeSet = /* @__PURE__ */ new Set();
436
+ for (const row of rows) if (row.entityType && validTypes.has(row.entityType)) typeSet.add(row.entityType);
437
+ return { entityTypes: Array.from(typeSet).sort() };
438
+ }
439
+ /** Return distinct entity names across observability signals, optionally filtered by entity type. */
440
+ async function getEntityNames(db, args) {
441
+ const buildSelect = (table) => {
442
+ const conditions = [`entityName IS NOT NULL`];
443
+ if (args.entityType) conditions.push(`entityType = ?`);
444
+ return `SELECT entityName FROM ${table} WHERE ${conditions.join(" AND ")}`;
445
+ };
446
+ const params = args.entityType ? [
447
+ args.entityType,
448
+ args.entityType,
449
+ args.entityType
450
+ ] : [];
451
+ return { names: (await db.query(unionDistinctQueries([
452
+ buildSelect("span_events"),
453
+ buildSelect("metric_events"),
454
+ buildSelect("log_events")
455
+ ], "entityName"), params)).map((r) => r.entityName) };
456
+ }
457
+ /** Return distinct service names across observability signals. */
458
+ async function getServiceNames(db, _args) {
459
+ return { serviceNames: (await db.query(unionDistinctQueries([
460
+ `SELECT serviceName FROM span_events WHERE serviceName IS NOT NULL`,
461
+ `SELECT serviceName FROM metric_events WHERE serviceName IS NOT NULL`,
462
+ `SELECT serviceName FROM log_events WHERE serviceName IS NOT NULL`
463
+ ], "serviceName"))).map((r) => r.serviceName) };
464
+ }
465
+ /** Return distinct environment values across observability signals. */
466
+ async function getEnvironments(db, _args) {
467
+ return { environments: (await db.query(unionDistinctQueries([
468
+ `SELECT environment FROM span_events WHERE environment IS NOT NULL`,
469
+ `SELECT environment FROM metric_events WHERE environment IS NOT NULL`,
470
+ `SELECT environment FROM log_events WHERE environment IS NOT NULL`
471
+ ], "environment"))).map((r) => r.environment) };
472
+ }
473
+ /** Return distinct tags across observability signals, optionally filtered by entity type. */
474
+ async function getTags(db, args) {
475
+ const buildSelect = (table) => {
476
+ const conditions = [`tags IS NOT NULL`];
477
+ if (args.entityType) conditions.push(`entityType = ?`);
478
+ return `SELECT unnest(CAST(tags AS VARCHAR[])) AS tag FROM ${table} WHERE ${conditions.join(" AND ")}`;
479
+ };
480
+ const params = args.entityType ? [
481
+ args.entityType,
482
+ args.entityType,
483
+ args.entityType
484
+ ] : [];
485
+ return { tags: (await db.query(unionDistinctQueries([
486
+ buildSelect("span_events"),
487
+ buildSelect("metric_events"),
488
+ buildSelect("log_events")
489
+ ], "tag"), params)).map((r) => r.tag) };
490
+ }
491
+ //#endregion
492
+ //#region src/storage/domains/observability/filters.ts
493
+ function buildJsonPath(key) {
494
+ try {
495
+ return `$.${parseFieldKey(key)}`;
496
+ } catch {
497
+ return `$."${key.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
498
+ }
499
+ }
500
+ function normalizeJsonFilterValue(value) {
501
+ if (value === void 0) return null;
502
+ if (typeof value === "string") return value;
503
+ return JSON.stringify(value) ?? null;
504
+ }
505
+ function sanitizeColumn(column) {
506
+ return parseFieldKey(column);
507
+ }
508
+ /**
509
+ * Build a WHERE clause from a filter object.
510
+ * Returns { clause, params } for parameterized queries.
511
+ */
512
+ function buildWhereClause(filters, fieldMappings) {
513
+ if (!filters) return {
514
+ clause: "",
515
+ params: []
516
+ };
517
+ const conditions = [];
518
+ const params = [];
519
+ for (const [key, value] of Object.entries(filters)) {
520
+ if (value === void 0 || value === null) continue;
521
+ const column = sanitizeColumn(fieldMappings?.[key] ?? key);
522
+ if (key === "timestamp" || key === "startedAt" || key === "endedAt") {
523
+ const dateRange = value;
524
+ if (dateRange.start) {
525
+ const op = dateRange.startExclusive ? ">" : ">=";
526
+ conditions.push(`${column} ${op} ?`);
527
+ params.push(dateRange.start);
528
+ }
529
+ if (dateRange.end) {
530
+ const op = dateRange.endExclusive ? "<" : "<=";
531
+ conditions.push(`${column} ${op} ?`);
532
+ params.push(dateRange.end);
533
+ }
534
+ continue;
535
+ }
536
+ if (key === "labels") {
537
+ const labelsObj = value;
538
+ for (const [labelKey, labelValue] of Object.entries(labelsObj)) {
539
+ conditions.push(`json_extract_string(${column}, ?) = ?`);
540
+ params.push(buildJsonPath(labelKey), labelValue);
541
+ }
542
+ continue;
543
+ }
544
+ if (key === "tags") {
545
+ const tags = value;
546
+ for (const tag of tags) {
547
+ conditions.push(`list_contains(CAST(${column} AS VARCHAR[]), ?)`);
548
+ params.push(tag);
549
+ }
550
+ continue;
551
+ }
552
+ if (key === "status") {
553
+ const status = value;
554
+ if (status === "error") conditions.push(`error IS NOT NULL`);
555
+ else if (status === "running") conditions.push(`endedAt IS NULL AND error IS NULL`);
556
+ else if (status === "success") conditions.push(`endedAt IS NOT NULL AND error IS NULL`);
557
+ continue;
558
+ }
559
+ if (key === "hasChildError") continue;
560
+ if (key === "metadata" || key === "scope") {
561
+ const jsonObj = value;
562
+ for (const [jsonKey, jsonValue] of Object.entries(jsonObj)) {
563
+ const normalized = normalizeJsonFilterValue(jsonValue);
564
+ if (normalized === null) continue;
565
+ conditions.push(`json_extract_string(${column}, ?) = ?`);
566
+ params.push(buildJsonPath(jsonKey), normalized);
567
+ }
568
+ continue;
569
+ }
570
+ if (Array.isArray(value)) {
571
+ if (value.length === 0) continue;
572
+ const placeholders = value.map(() => "?").join(", ");
573
+ conditions.push(`${column} IN (${placeholders})`);
574
+ params.push(...value);
575
+ continue;
576
+ }
577
+ conditions.push(`${column} = ?`);
578
+ params.push(value);
579
+ }
580
+ return {
581
+ clause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
582
+ params
583
+ };
584
+ }
585
+ /**
586
+ * Build an ORDER BY clause from orderBy config.
587
+ */
588
+ function buildOrderByClause(orderBy) {
589
+ if (!orderBy) return "";
590
+ const dir = orderBy.direction.toUpperCase();
591
+ if (dir !== "ASC" && dir !== "DESC") throw new Error(`Invalid sort direction: ${orderBy.direction}`);
592
+ return `ORDER BY ${parseFieldKey(orderBy.field)} ${dir}`;
593
+ }
594
+ /**
595
+ * Build a LIMIT/OFFSET clause from pagination config.
596
+ */
597
+ function buildPaginationClause(pagination) {
598
+ if (!pagination) return {
599
+ clause: "",
600
+ params: []
601
+ };
602
+ if (!Number.isInteger(pagination.page) || pagination.page < 0) throw new Error(`Invalid page: ${pagination.page}`);
603
+ if (!Number.isInteger(pagination.perPage) || pagination.perPage <= 0) throw new Error(`Invalid perPage: ${pagination.perPage}`);
604
+ const offset = pagination.page * pagination.perPage;
605
+ return {
606
+ clause: `LIMIT ? OFFSET ?`,
607
+ params: [pagination.perPage, offset]
608
+ };
609
+ }
610
+ //#endregion
611
+ //#region src/storage/domains/observability/helpers.ts
612
+ /** Shorthand for {@link DuckDBConnection.sqlValue}. */
613
+ const v = DuckDBConnection.sqlValue;
614
+ /** Serialize a value to JSON then SQL-escape it, or return 'NULL'. */
615
+ function jsonV(val) {
616
+ if (val === null || val === void 0) return "NULL";
617
+ return DuckDBConnection.sqlValue(JSON.stringify(val));
618
+ }
619
+ /** Coerce a value to a Date. Throws if value is nullish. */
620
+ function toDate(val) {
621
+ if (val === null || val === void 0) throw new Error("Expected date value but received null/undefined");
622
+ const date = val instanceof Date ? val : new Date(String(val));
623
+ if (Number.isNaN(date.getTime())) throw new Error("Expected valid date but received invalid date");
624
+ return date;
625
+ }
626
+ /** Coerce a value to a Date, returning null for nullish values. */
627
+ function toDateOrNull(val) {
628
+ if (val === null || val === void 0) return null;
629
+ return val instanceof Date ? val : new Date(String(val));
630
+ }
631
+ /** Parse a JSON string, returning the original value if parsing fails. */
632
+ function parseJson(value) {
633
+ if (value === null || value === void 0) return null;
634
+ if (typeof value === "string") try {
635
+ return JSON.parse(value);
636
+ } catch {
637
+ return value;
638
+ }
639
+ return value;
640
+ }
641
+ /** Parse a JSON string and return the result only if it is an array. */
642
+ function parseJsonArray(value) {
643
+ if (value === null || value === void 0) return null;
644
+ const parsed = parseJson(value);
645
+ return Array.isArray(parsed) ? parsed : null;
646
+ }
647
+ //#endregion
648
+ //#region src/storage/domains/observability/polling.ts
649
+ const OBSERVABILITY_DELTA_POLLING_FEATURE = "observability-delta-polling";
650
+ function deltaPollingFeatureEnabled() {
651
+ return coreFeatures.has(OBSERVABILITY_DELTA_POLLING_FEATURE);
652
+ }
653
+ function assertDeltaPollingEnabled() {
654
+ if (deltaPollingFeatureEnabled()) return;
655
+ throw new MastraError({
656
+ id: "OBSERVABILITY_DELTA_POLLING_NOT_SUPPORTED",
657
+ domain: ErrorDomain.MASTRA_OBSERVABILITY,
658
+ category: ErrorCategory.SYSTEM,
659
+ text: "This storage provider does not support observability delta polling"
660
+ });
661
+ }
662
+ function encodeDeltaCursor(value) {
663
+ return String(value ?? 0);
664
+ }
665
+ function validateCursorId(cursor) {
666
+ if (!/^\d+$/.test(cursor)) throw new MastraError({
667
+ id: "OBSERVABILITY_INVALID_DELTA_CURSOR",
668
+ domain: ErrorDomain.MASTRA_OBSERVABILITY,
669
+ category: ErrorCategory.USER,
670
+ text: "Invalid observability delta cursor"
671
+ });
672
+ return cursor;
673
+ }
674
+ function extendWhereClause(baseClause, extraConditions) {
675
+ const conditions = extraConditions.filter(Boolean);
676
+ if (conditions.length === 0) return baseClause;
677
+ if (!baseClause) return `WHERE ${conditions.join(" AND ")}`;
678
+ return `${baseClause} AND ${conditions.join(" AND ")}`;
679
+ }
680
+ //#endregion
681
+ //#region src/storage/domains/observability/feedback.ts
682
+ const FEEDBACK_GROUP_BY_COLUMNS = /* @__PURE__ */ new Set([
683
+ "timestamp",
684
+ "traceId",
685
+ "spanId",
686
+ "experimentId",
687
+ "entityType",
688
+ "entityId",
689
+ "entityName",
690
+ "entityVersionId",
691
+ "parentEntityVersionId",
692
+ "parentEntityType",
693
+ "parentEntityId",
694
+ "parentEntityName",
695
+ "rootEntityVersionId",
696
+ "rootEntityType",
697
+ "rootEntityId",
698
+ "rootEntityName",
699
+ "userId",
700
+ "organizationId",
701
+ "resourceId",
702
+ "runId",
703
+ "sessionId",
704
+ "threadId",
705
+ "requestId",
706
+ "environment",
707
+ "executionSource",
708
+ "serviceName",
709
+ "feedbackUserId",
710
+ "sourceId",
711
+ "feedbackSource",
712
+ "feedbackType",
713
+ "value",
714
+ "comment"
715
+ ]);
716
+ function getAggregationSql$2(aggregation, measure = "TRY_CAST(value AS DOUBLE)") {
717
+ switch (aggregation) {
718
+ case "sum": return `SUM(${measure})`;
719
+ case "avg": return `AVG(${measure})`;
720
+ case "min": return `MIN(${measure})`;
721
+ case "max": return `MAX(${measure})`;
722
+ case "count": return `CAST(COUNT(${measure}) AS DOUBLE)`;
723
+ case "last": return `arg_max(${measure}, timestamp)`;
724
+ default: return `SUM(${measure})`;
725
+ }
726
+ }
727
+ function getIntervalSql$2(interval) {
728
+ switch (interval) {
729
+ case "1m": return "1 minute";
730
+ case "5m": return "5 minutes";
731
+ case "15m": return "15 minutes";
732
+ case "1h": return "1 hour";
733
+ case "1d": return "1 day";
734
+ default: return "1 hour";
735
+ }
736
+ }
737
+ function getValidatedPercentiles$1(percentiles) {
738
+ if (!Array.isArray(percentiles) || percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
739
+ return percentiles.map((percentile) => {
740
+ if (!Number.isFinite(percentile) || percentile < 0 || percentile > 1) throw new Error("Percentiles must be finite numbers between 0 and 1.");
741
+ return percentile;
742
+ });
743
+ }
744
+ function buildFeedbackWhereClause(args, includeNumericGuard = false) {
745
+ const conditions = ["feedbackType = ?"];
746
+ const params = [args.feedbackType];
747
+ if (args.feedbackSource !== void 0) {
748
+ conditions.push("feedbackSource = ?");
749
+ params.push(args.feedbackSource);
750
+ }
751
+ const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, { source: "feedbackSource" });
752
+ if (filterClause) {
753
+ conditions.push(filterClause.replace("WHERE ", ""));
754
+ params.push(...filterParams);
755
+ }
756
+ if (includeNumericGuard) conditions.push("TRY_CAST(value AS DOUBLE) IS NOT NULL");
757
+ return {
758
+ clause: `WHERE ${conditions.join(" AND ")}`,
759
+ params
760
+ };
761
+ }
762
+ function resolveFeedbackGroupBy(groupBy) {
763
+ return groupBy.map((key, index) => {
764
+ const column = parseFieldKey(key);
765
+ if (!FEEDBACK_GROUP_BY_COLUMNS.has(column)) throw new Error(`Invalid groupBy column(s): ${key}`);
766
+ const alias = `group_by_${index}`;
767
+ return {
768
+ key,
769
+ selectSql: `${column} AS ${alias}`,
770
+ groupSql: alias
771
+ };
772
+ });
773
+ }
774
+ function toSeriesName$1(values) {
775
+ return values.map((value) => value === null || value === void 0 ? "" : String(value)).join("|");
776
+ }
777
+ function rowToFeedbackRecord(row) {
778
+ const rawValue = row.value;
779
+ let value = rawValue;
780
+ const numValue = Number(rawValue);
781
+ if (!isNaN(numValue)) value = numValue;
782
+ return {
783
+ feedbackId: row.feedbackId,
784
+ timestamp: toDate(row.timestamp),
785
+ traceId: row.traceId ?? null,
786
+ spanId: row.spanId ?? null,
787
+ experimentId: row.experimentId ?? null,
788
+ entityType: row.entityType ?? null,
789
+ entityId: row.entityId ?? null,
790
+ entityName: row.entityName ?? null,
791
+ entityVersionId: row.entityVersionId ?? null,
792
+ parentEntityVersionId: row.parentEntityVersionId ?? null,
793
+ parentEntityType: row.parentEntityType ?? null,
794
+ parentEntityId: row.parentEntityId ?? null,
795
+ parentEntityName: row.parentEntityName ?? null,
796
+ rootEntityVersionId: row.rootEntityVersionId ?? null,
797
+ rootEntityType: row.rootEntityType ?? null,
798
+ rootEntityId: row.rootEntityId ?? null,
799
+ rootEntityName: row.rootEntityName ?? null,
800
+ userId: row.userId ?? null,
801
+ organizationId: row.organizationId ?? null,
802
+ resourceId: row.resourceId ?? null,
803
+ runId: row.runId ?? null,
804
+ sessionId: row.sessionId ?? null,
805
+ threadId: row.threadId ?? null,
806
+ requestId: row.requestId ?? null,
807
+ environment: row.environment ?? null,
808
+ executionSource: row.executionSource ?? null,
809
+ serviceName: row.serviceName ?? null,
810
+ feedbackUserId: row.feedbackUserId ?? null,
811
+ sourceId: row.sourceId ?? null,
812
+ source: row.feedbackSource,
813
+ feedbackSource: row.feedbackSource,
814
+ feedbackType: row.feedbackType,
815
+ value,
816
+ comment: row.comment ?? null,
817
+ tags: parseJsonArray(row.tags),
818
+ metadata: parseJson(row.metadata),
819
+ scope: parseJson(row.scope)
820
+ };
821
+ }
822
+ function getComparisonDateRange$1(comparePeriod, timestamp) {
823
+ if (!timestamp.start || !timestamp.end) return null;
824
+ const duration = timestamp.end.getTime() - timestamp.start.getTime();
825
+ switch (comparePeriod) {
826
+ case "previous_period": return {
827
+ start: new Date(timestamp.start.getTime() - duration),
828
+ end: new Date(timestamp.end.getTime() - duration),
829
+ startExclusive: timestamp.startExclusive,
830
+ endExclusive: timestamp.endExclusive
831
+ };
832
+ case "previous_day": return {
833
+ start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 864e5),
834
+ end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 864e5),
835
+ startExclusive: timestamp.startExclusive,
836
+ endExclusive: timestamp.endExclusive
837
+ };
838
+ case "previous_week": return {
839
+ start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 6048e5),
840
+ end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 6048e5),
841
+ startExclusive: timestamp.startExclusive,
842
+ endExclusive: timestamp.endExclusive
843
+ };
844
+ }
845
+ }
846
+ /** Insert a single feedback event. */
847
+ async function createFeedback(db, args) {
848
+ const f = args.feedback;
849
+ const feedbackSource = f.feedbackSource ?? f.source ?? "";
850
+ const feedbackUserId = f.feedbackUserId ?? f.userId ?? null;
851
+ await db.execute(`INSERT INTO feedback_events (
852
+ feedbackId, timestamp, cursorId, traceId, spanId, experimentId,
853
+ entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
854
+ userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
855
+ feedbackUserId, sourceId, feedbackSource, feedbackType, value, comment, tags, metadata, scope
856
+ )
857
+ VALUES (${[
858
+ v(f.feedbackId),
859
+ v(f.timestamp),
860
+ "nextval('feedback_events_cursor_id_seq')",
861
+ v(f.traceId),
862
+ v(f.spanId ?? null),
863
+ v(f.experimentId ?? null),
864
+ v(f.entityType ?? null),
865
+ v(f.entityId ?? null),
866
+ v(f.entityName ?? null),
867
+ v(f.entityVersionId ?? null),
868
+ v(f.parentEntityVersionId ?? null),
869
+ v(f.parentEntityType ?? null),
870
+ v(f.parentEntityId ?? null),
871
+ v(f.parentEntityName ?? null),
872
+ v(f.rootEntityVersionId ?? null),
873
+ v(f.rootEntityType ?? null),
874
+ v(f.rootEntityId ?? null),
875
+ v(f.rootEntityName ?? null),
876
+ v(f.userId ?? null),
877
+ v(f.organizationId ?? null),
878
+ v(f.resourceId ?? null),
879
+ v(f.runId ?? null),
880
+ v(f.sessionId ?? null),
881
+ v(f.threadId ?? null),
882
+ v(f.requestId ?? null),
883
+ v(f.environment ?? null),
884
+ v(f.executionSource ?? null),
885
+ v(f.serviceName ?? null),
886
+ v(feedbackUserId),
887
+ v(f.sourceId ?? null),
888
+ v(feedbackSource),
889
+ v(f.feedbackType),
890
+ v(String(f.value)),
891
+ v(f.comment ?? null),
892
+ jsonV(f.tags ?? null),
893
+ jsonV(f.metadata),
894
+ jsonV(f.scope ?? null)
895
+ ].join(", ")})
896
+ ON CONFLICT DO NOTHING`);
897
+ }
898
+ /** Insert multiple feedback events in a single statement. */
899
+ async function batchCreateFeedback(db, args) {
900
+ if (args.feedbacks.length === 0) return;
901
+ const tuples = args.feedbacks.map((f) => {
902
+ const legacyFeedback = f;
903
+ const feedbackSource = legacyFeedback.feedbackSource ?? legacyFeedback.source ?? "";
904
+ const feedbackUserId = legacyFeedback.feedbackUserId ?? legacyFeedback.userId ?? null;
905
+ return `(${[
906
+ v(legacyFeedback.feedbackId),
907
+ v(legacyFeedback.timestamp),
908
+ "nextval('feedback_events_cursor_id_seq')",
909
+ v(legacyFeedback.traceId),
910
+ v(legacyFeedback.spanId ?? null),
911
+ v(legacyFeedback.experimentId ?? null),
912
+ v(legacyFeedback.entityType ?? null),
913
+ v(legacyFeedback.entityId ?? null),
914
+ v(legacyFeedback.entityName ?? null),
915
+ v(legacyFeedback.entityVersionId ?? null),
916
+ v(legacyFeedback.parentEntityVersionId ?? null),
917
+ v(legacyFeedback.parentEntityType ?? null),
918
+ v(legacyFeedback.parentEntityId ?? null),
919
+ v(legacyFeedback.parentEntityName ?? null),
920
+ v(legacyFeedback.rootEntityVersionId ?? null),
921
+ v(legacyFeedback.rootEntityType ?? null),
922
+ v(legacyFeedback.rootEntityId ?? null),
923
+ v(legacyFeedback.rootEntityName ?? null),
924
+ v(legacyFeedback.userId ?? null),
925
+ v(legacyFeedback.organizationId ?? null),
926
+ v(legacyFeedback.resourceId ?? null),
927
+ v(legacyFeedback.runId ?? null),
928
+ v(legacyFeedback.sessionId ?? null),
929
+ v(legacyFeedback.threadId ?? null),
930
+ v(legacyFeedback.requestId ?? null),
931
+ v(legacyFeedback.environment ?? null),
932
+ v(legacyFeedback.executionSource ?? null),
933
+ v(legacyFeedback.serviceName ?? null),
934
+ v(feedbackUserId),
935
+ v(legacyFeedback.sourceId ?? null),
936
+ v(feedbackSource),
937
+ v(legacyFeedback.feedbackType),
938
+ v(String(legacyFeedback.value)),
939
+ v(legacyFeedback.comment ?? null),
940
+ jsonV(legacyFeedback.tags ?? null),
941
+ jsonV(legacyFeedback.metadata),
942
+ jsonV(legacyFeedback.scope ?? null)
943
+ ].join(", ")})`;
944
+ });
945
+ await db.execute(`INSERT INTO feedback_events (
946
+ feedbackId, timestamp, cursorId, traceId, spanId, experimentId,
947
+ entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
948
+ userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
949
+ feedbackUserId, sourceId, feedbackSource, feedbackType, value, comment, tags, metadata, scope
950
+ )
951
+ VALUES ${tuples.join(",\n ")}
952
+ ON CONFLICT DO NOTHING`);
953
+ }
954
+ /** Query feedback events with filtering, ordering, and pagination. */
955
+ async function listFeedback(db, args) {
956
+ const { mode, filters, pagination, orderBy, after, limit } = listFeedbackArgsSchema.parse(args);
957
+ const page = Number(pagination.page);
958
+ const perPage = Number(pagination.perPage);
959
+ const { clause: filterClause, params: filterParams } = buildWhereClause(filters, { source: "feedbackSource" });
960
+ if (mode === "delta") {
961
+ assertDeltaPollingEnabled();
962
+ const streamHeadCursor = await getStreamHeadCursor$3(db);
963
+ if (after === void 0) return {
964
+ feedback: [],
965
+ delta: {
966
+ limit,
967
+ hasMore: false
968
+ },
969
+ deltaCursor: streamHeadCursor
970
+ };
971
+ const afterCursorId = validateCursorId(after);
972
+ const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
973
+ const rows = await db.query(`SELECT * FROM feedback_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
974
+ ...filterParams,
975
+ afterCursorId,
976
+ limit + 1
977
+ ]);
978
+ const visibleRows = rows.slice(0, limit).map((row) => ({
979
+ cursorId: row.cursorId,
980
+ feedback: rowToFeedbackRecord(row)
981
+ }));
982
+ return {
983
+ feedback: visibleRows.map((row) => row.feedback),
984
+ delta: {
985
+ limit,
986
+ hasMore: rows.length > limit
987
+ },
988
+ deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
989
+ };
990
+ }
991
+ const orderByClause = buildOrderByClause(orderBy);
992
+ const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
993
+ page,
994
+ perPage
995
+ });
996
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor$3(db, filterClause, filterParams) : void 0;
997
+ const countResult = await db.query(`SELECT COUNT(*) as total FROM feedback_events ${filterClause}`, filterParams);
998
+ const total = Number(countResult[0]?.total ?? 0);
999
+ const rows = await db.query(`SELECT * FROM feedback_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams]);
1000
+ return {
1001
+ pagination: {
1002
+ total,
1003
+ page,
1004
+ perPage,
1005
+ hasMore: (page + 1) * perPage < total
1006
+ },
1007
+ feedback: rows.map((row) => rowToFeedbackRecord(row)),
1008
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
1009
+ };
1010
+ }
1011
+ async function getDeltaCursor$3(db, filterClause, filterParams) {
1012
+ const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM feedback_events ${filterClause}`, filterParams))[0]?.cursorId;
1013
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
1014
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM feedback_events`))[0]?.cursorId);
1015
+ }
1016
+ async function getStreamHeadCursor$3(db) {
1017
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM feedback_events`))[0]?.cursorId);
1018
+ }
1019
+ async function getFeedbackAggregate(db, args) {
1020
+ const aggSql = getAggregationSql$2(args.aggregation);
1021
+ const { clause, params } = buildFeedbackWhereClause(args, true);
1022
+ const rows = await db.query(`SELECT ${aggSql} AS value FROM feedback_events ${clause}`, params);
1023
+ const value = rows[0]?.value === null || rows[0]?.value === void 0 ? null : Number(rows[0]?.value);
1024
+ if (args.comparePeriod && args.filters?.timestamp) {
1025
+ const previousTimestamp = getComparisonDateRange$1(args.comparePeriod, args.filters.timestamp);
1026
+ if (previousTimestamp) {
1027
+ const previousWhere = buildFeedbackWhereClause({
1028
+ ...args,
1029
+ filters: {
1030
+ ...args.filters ?? {},
1031
+ timestamp: previousTimestamp
1032
+ }
1033
+ }, true);
1034
+ const prevRows = await db.query(`SELECT ${aggSql} AS value FROM feedback_events ${previousWhere.clause}`, previousWhere.params);
1035
+ const previousValue = prevRows[0]?.value === null || prevRows[0]?.value === void 0 ? null : Number(prevRows[0]?.value);
1036
+ let changePercent = null;
1037
+ if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
1038
+ return {
1039
+ value,
1040
+ previousValue,
1041
+ changePercent
1042
+ };
1043
+ }
1044
+ }
1045
+ return { value };
1046
+ }
1047
+ async function getFeedbackBreakdown(db, args) {
1048
+ const aggSql = getAggregationSql$2(args.aggregation);
1049
+ const { clause, params } = buildFeedbackWhereClause(args, true);
1050
+ const resolvedGroupBy = resolveFeedbackGroupBy(args.groupBy);
1051
+ const sql = `SELECT ${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")}, ${aggSql} AS value FROM feedback_events ${clause} GROUP BY ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")} ORDER BY value DESC`;
1052
+ return { groups: (await db.query(sql, params)).map((row) => ({
1053
+ dimensions: Object.fromEntries(resolvedGroupBy.map((entry, index) => {
1054
+ const value = row[`group_by_${index}`];
1055
+ return [entry.key, value === null || value === void 0 ? null : String(value)];
1056
+ })),
1057
+ value: Number(row.value ?? 0)
1058
+ })) };
1059
+ }
1060
+ async function getFeedbackTimeSeries(db, args) {
1061
+ const aggSql = getAggregationSql$2(args.aggregation);
1062
+ const intervalSql = getIntervalSql$2(args.interval);
1063
+ const { clause, params } = buildFeedbackWhereClause(args, true);
1064
+ if (args.groupBy && args.groupBy.length > 0) {
1065
+ const resolvedGroupBy = resolveFeedbackGroupBy(args.groupBy);
1066
+ const sql = `
1067
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
1068
+ ${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")},
1069
+ ${aggSql} AS value
1070
+ FROM feedback_events ${clause}
1071
+ GROUP BY bucket, ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")}
1072
+ ORDER BY bucket
1073
+ `;
1074
+ const rows = await db.query(sql, params);
1075
+ const seriesMap = /* @__PURE__ */ new Map();
1076
+ for (const row of rows) {
1077
+ const groupValues = resolvedGroupBy.map((_, index) => row[`group_by_${index}`]);
1078
+ const key = JSON.stringify(groupValues);
1079
+ if (!seriesMap.has(key)) seriesMap.set(key, {
1080
+ name: toSeriesName$1(groupValues),
1081
+ points: []
1082
+ });
1083
+ seriesMap.get(key).points.push({
1084
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
1085
+ value: Number(row.value ?? 0)
1086
+ });
1087
+ }
1088
+ return { series: Array.from(seriesMap.values()) };
1089
+ }
1090
+ const rows = await db.query(`
1091
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
1092
+ ${aggSql} AS value
1093
+ FROM feedback_events ${clause}
1094
+ GROUP BY bucket
1095
+ ORDER BY bucket
1096
+ `, params);
1097
+ return { series: [{
1098
+ name: args.feedbackSource ? `${args.feedbackType}|${args.feedbackSource}` : args.feedbackType,
1099
+ points: rows.map((row) => ({
1100
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
1101
+ value: Number(row.value ?? 0)
1102
+ }))
1103
+ }] };
1104
+ }
1105
+ async function getFeedbackPercentiles(db, args) {
1106
+ const intervalSql = getIntervalSql$2(args.interval);
1107
+ const { clause, params } = buildFeedbackWhereClause(args, true);
1108
+ const percentiles = getValidatedPercentiles$1(args.percentiles);
1109
+ const series = [];
1110
+ for (const percentile of percentiles) {
1111
+ const rows = await db.query(`
1112
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
1113
+ percentile_cont(${percentile}) WITHIN GROUP (ORDER BY TRY_CAST(value AS DOUBLE)) AS pvalue
1114
+ FROM feedback_events ${clause}
1115
+ GROUP BY bucket
1116
+ ORDER BY bucket
1117
+ `, params);
1118
+ series.push({
1119
+ percentile,
1120
+ points: rows.map((row) => ({
1121
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
1122
+ value: Number(row.pvalue ?? 0)
1123
+ }))
1124
+ });
1125
+ }
1126
+ return { series };
1127
+ }
1128
+ //#endregion
1129
+ //#region src/storage/domains/observability/logs.ts
1130
+ const COLUMNS_SQL$1 = [
1131
+ "logId",
1132
+ "timestamp",
1133
+ "cursorId",
1134
+ "level",
1135
+ "message",
1136
+ "data",
1137
+ "traceId",
1138
+ "spanId",
1139
+ "entityType",
1140
+ "entityId",
1141
+ "entityName",
1142
+ "entityVersionId",
1143
+ "parentEntityVersionId",
1144
+ "parentEntityType",
1145
+ "parentEntityId",
1146
+ "parentEntityName",
1147
+ "rootEntityVersionId",
1148
+ "rootEntityType",
1149
+ "rootEntityId",
1150
+ "rootEntityName",
1151
+ "userId",
1152
+ "organizationId",
1153
+ "resourceId",
1154
+ "runId",
1155
+ "sessionId",
1156
+ "threadId",
1157
+ "requestId",
1158
+ "environment",
1159
+ "executionSource",
1160
+ "serviceName",
1161
+ "experimentId",
1162
+ "tags",
1163
+ "metadata",
1164
+ "scope"
1165
+ ].join(", ");
1166
+ function rowToLogRecord(row) {
1167
+ return {
1168
+ logId: row.logId,
1169
+ timestamp: toDate(row.timestamp),
1170
+ level: row.level,
1171
+ message: row.message,
1172
+ data: parseJson(row.data),
1173
+ traceId: row.traceId ?? null,
1174
+ spanId: row.spanId ?? null,
1175
+ entityType: row.entityType ?? null,
1176
+ entityId: row.entityId ?? null,
1177
+ entityName: row.entityName ?? null,
1178
+ entityVersionId: row.entityVersionId ?? null,
1179
+ parentEntityVersionId: row.parentEntityVersionId ?? null,
1180
+ parentEntityType: row.parentEntityType ?? null,
1181
+ parentEntityId: row.parentEntityId ?? null,
1182
+ parentEntityName: row.parentEntityName ?? null,
1183
+ rootEntityVersionId: row.rootEntityVersionId ?? null,
1184
+ rootEntityType: row.rootEntityType ?? null,
1185
+ rootEntityId: row.rootEntityId ?? null,
1186
+ rootEntityName: row.rootEntityName ?? null,
1187
+ userId: row.userId ?? null,
1188
+ organizationId: row.organizationId ?? null,
1189
+ resourceId: row.resourceId ?? null,
1190
+ runId: row.runId ?? null,
1191
+ sessionId: row.sessionId ?? null,
1192
+ threadId: row.threadId ?? null,
1193
+ requestId: row.requestId ?? null,
1194
+ environment: row.environment ?? null,
1195
+ executionSource: row.executionSource ?? null,
1196
+ serviceName: row.serviceName ?? null,
1197
+ experimentId: row.experimentId ?? null,
1198
+ tags: parseJsonArray(row.tags),
1199
+ metadata: parseJson(row.metadata),
1200
+ scope: parseJson(row.scope)
1201
+ };
1202
+ }
1203
+ /** Insert multiple log events in a single statement. */
1204
+ async function batchCreateLogs(db, args) {
1205
+ if (args.logs.length === 0) return;
1206
+ const tuples = args.logs.map((log) => {
1207
+ return `(${[
1208
+ v(log.logId),
1209
+ v(log.timestamp),
1210
+ "nextval('log_events_cursor_id_seq')",
1211
+ v(log.level),
1212
+ v(log.message),
1213
+ jsonV(log.data),
1214
+ v(log.traceId ?? null),
1215
+ v(log.spanId ?? null),
1216
+ v(log.entityType ?? null),
1217
+ v(log.entityId ?? null),
1218
+ v(log.entityName ?? null),
1219
+ v(log.entityVersionId ?? null),
1220
+ v(log.parentEntityVersionId ?? null),
1221
+ v(log.parentEntityType ?? null),
1222
+ v(log.parentEntityId ?? null),
1223
+ v(log.parentEntityName ?? null),
1224
+ v(log.rootEntityVersionId ?? null),
1225
+ v(log.rootEntityType ?? null),
1226
+ v(log.rootEntityId ?? null),
1227
+ v(log.rootEntityName ?? null),
1228
+ v(log.userId ?? null),
1229
+ v(log.organizationId ?? null),
1230
+ v(log.resourceId ?? null),
1231
+ v(log.runId ?? null),
1232
+ v(log.sessionId ?? null),
1233
+ v(log.threadId ?? null),
1234
+ v(log.requestId ?? null),
1235
+ v(log.environment ?? null),
1236
+ v(log.executionSource ?? null),
1237
+ v(log.serviceName ?? null),
1238
+ v(log.experimentId ?? null),
1239
+ jsonV(log.tags),
1240
+ jsonV(log.metadata),
1241
+ jsonV(log.scope)
1242
+ ].join(", ")})`;
1243
+ });
1244
+ await db.execute(`INSERT INTO log_events (${COLUMNS_SQL$1}) VALUES ${tuples.join(",\n")} ON CONFLICT DO NOTHING`);
1245
+ }
1246
+ /** Query log events with filtering, ordering, and pagination. */
1247
+ async function listLogs(db, args) {
1248
+ const { mode, filters, pagination, orderBy, after, limit } = listLogsArgsSchema.parse(args);
1249
+ const filterRecord = filters;
1250
+ const page = Number(pagination.page);
1251
+ const perPage = Number(pagination.perPage);
1252
+ const { clause: filterClause, params: filterParams } = buildWhereClause(filterRecord);
1253
+ if (mode === "delta") {
1254
+ assertDeltaPollingEnabled();
1255
+ const streamHeadCursor = await getStreamHeadCursor$2(db);
1256
+ if (after === void 0) return {
1257
+ logs: [],
1258
+ delta: {
1259
+ limit,
1260
+ hasMore: false
1261
+ },
1262
+ deltaCursor: streamHeadCursor
1263
+ };
1264
+ const afterCursorId = validateCursorId(after);
1265
+ const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
1266
+ const rows = await db.query(`SELECT * FROM log_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
1267
+ ...filterParams,
1268
+ afterCursorId,
1269
+ limit + 1
1270
+ ]);
1271
+ const visibleRows = rows.slice(0, limit).map((row) => ({
1272
+ cursorId: row.cursorId,
1273
+ log: rowToLogRecord(row)
1274
+ }));
1275
+ return {
1276
+ logs: visibleRows.map((row) => row.log),
1277
+ delta: {
1278
+ limit,
1279
+ hasMore: rows.length > limit
1280
+ },
1281
+ deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
1282
+ };
1283
+ }
1284
+ const orderByClause = buildOrderByClause(orderBy);
1285
+ const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
1286
+ page,
1287
+ perPage
1288
+ });
1289
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor$2(db, filterClause, filterParams) : void 0;
1290
+ const countResult = await db.query(`SELECT COUNT(*) as total FROM log_events ${filterClause}`, filterParams);
1291
+ const total = Number(countResult[0]?.total ?? 0);
1292
+ const logs = (await db.query(`SELECT * FROM log_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams])).map((row) => rowToLogRecord(row));
1293
+ return {
1294
+ pagination: {
1295
+ total,
1296
+ page,
1297
+ perPage,
1298
+ hasMore: (page + 1) * perPage < total
1299
+ },
1300
+ logs,
1301
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
1302
+ };
1303
+ }
1304
+ async function getDeltaCursor$2(db, filterClause, filterParams) {
1305
+ const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM log_events ${filterClause}`, filterParams))[0]?.cursorId;
1306
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
1307
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM log_events`))[0]?.cursorId);
1308
+ }
1309
+ async function getStreamHeadCursor$2(db) {
1310
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM log_events`))[0]?.cursorId);
1311
+ }
1312
+ //#endregion
1313
+ //#region src/storage/domains/observability/metrics.ts
1314
+ const METRIC_FILTER_FIELD_MAPPINGS = { traceIds: "traceId" };
1315
+ function resolveDistinctColumnSql(distinctColumn) {
1316
+ if (!distinctColumn) throw new Error(`count_distinct aggregation requires a 'distinctColumn' argument`);
1317
+ if (!METRIC_DISTINCT_COLUMNS.includes(distinctColumn)) throw new Error(`Invalid distinctColumn: ${distinctColumn}`);
1318
+ return parseFieldKey(distinctColumn);
1319
+ }
1320
+ function getAggregationSql$1(aggregation, measure = "value", distinctColumn) {
1321
+ switch (aggregation) {
1322
+ case "sum": return `SUM(${measure})`;
1323
+ case "avg": return `AVG(${measure})`;
1324
+ case "min": return `MIN(${measure})`;
1325
+ case "max": return `MAX(${measure})`;
1326
+ case "count": return `CAST(COUNT(${measure}) AS DOUBLE)`;
1327
+ case "count_distinct": return `CAST(approx_count_distinct(${resolveDistinctColumnSql(distinctColumn)}) AS DOUBLE)`;
1328
+ case "last": return `arg_max(${measure}, timestamp)`;
1329
+ default: return `SUM(${measure})`;
1330
+ }
1331
+ }
1332
+ function getIntervalSql$1(interval) {
1333
+ switch (interval) {
1334
+ case "1m": return "1 minute";
1335
+ case "5m": return "5 minutes";
1336
+ case "15m": return "15 minutes";
1337
+ case "1h": return "1 hour";
1338
+ case "1d": return "1 day";
1339
+ default: return "1 hour";
1340
+ }
1341
+ }
1342
+ function buildMetricNameFilter(name) {
1343
+ if (Array.isArray(name)) return {
1344
+ clause: `name IN (${name.map(() => "?").join(", ")})`,
1345
+ params: name
1346
+ };
1347
+ return {
1348
+ clause: `name = ?`,
1349
+ params: [name]
1350
+ };
1351
+ }
1352
+ const METRIC_COLUMNS = [
1353
+ "metricId",
1354
+ "timestamp",
1355
+ "cursorId",
1356
+ "name",
1357
+ "value",
1358
+ "traceId",
1359
+ "spanId",
1360
+ "entityType",
1361
+ "entityId",
1362
+ "entityName",
1363
+ "entityVersionId",
1364
+ "parentEntityVersionId",
1365
+ "parentEntityType",
1366
+ "parentEntityId",
1367
+ "parentEntityName",
1368
+ "rootEntityVersionId",
1369
+ "rootEntityType",
1370
+ "rootEntityId",
1371
+ "rootEntityName",
1372
+ "userId",
1373
+ "organizationId",
1374
+ "resourceId",
1375
+ "runId",
1376
+ "sessionId",
1377
+ "threadId",
1378
+ "requestId",
1379
+ "environment",
1380
+ "executionSource",
1381
+ "serviceName",
1382
+ "experimentId",
1383
+ "provider",
1384
+ "model",
1385
+ "estimatedCost",
1386
+ "costUnit",
1387
+ "tags",
1388
+ "labels",
1389
+ "costMetadata",
1390
+ "metadata",
1391
+ "scope"
1392
+ ];
1393
+ const METRIC_COLUMNS_SQL = METRIC_COLUMNS.join(", ");
1394
+ const METRIC_COLUMN_SET = new Set(METRIC_COLUMNS);
1395
+ const METRIC_LABEL_ONLY_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
1396
+ "metadata",
1397
+ "scope",
1398
+ "costMetadata",
1399
+ "tags"
1400
+ ]);
1401
+ function buildGroupByAlias(index) {
1402
+ return `group_by_${index}`;
1403
+ }
1404
+ function toSeriesDisplayValue(value) {
1405
+ return value === null || value === void 0 ? "" : String(value);
1406
+ }
1407
+ function getCostSummarySelect(prefix = "") {
1408
+ const ref = (column) => `${prefix}${column}`;
1409
+ return [
1410
+ `SUM(${ref("estimatedCost")}) FILTER (WHERE ${ref("estimatedCost")} IS NOT NULL) AS estimatedCost`,
1411
+ `,`,
1412
+ `CASE`,
1413
+ ` WHEN COUNT(DISTINCT ${ref("costUnit")}) FILTER (WHERE ${ref("costUnit")} IS NOT NULL) = 1`,
1414
+ ` THEN MIN(${ref("costUnit")}) FILTER (WHERE ${ref("costUnit")} IS NOT NULL)`,
1415
+ ` ELSE NULL`,
1416
+ `END AS costUnit`
1417
+ ].join(" ");
1418
+ }
1419
+ function normalizeCostSummaryRow(row) {
1420
+ return {
1421
+ estimatedCost: row.estimatedCost === null || row.estimatedCost === void 0 ? null : Number(row.estimatedCost),
1422
+ costUnit: row.costUnit === null || row.costUnit === void 0 ? null : String(row.costUnit)
1423
+ };
1424
+ }
1425
+ function buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams) {
1426
+ const conditions = [nameClause];
1427
+ const params = [...nameParams];
1428
+ if (filterClause) {
1429
+ conditions.push(filterClause.replace("WHERE ", ""));
1430
+ params.push(...filterParams);
1431
+ }
1432
+ return {
1433
+ clause: `WHERE ${conditions.join(" AND ")}`,
1434
+ params
1435
+ };
1436
+ }
1437
+ function resolveGroupBy(groupBy) {
1438
+ return groupBy.map((key, index) => {
1439
+ if (METRIC_COLUMN_SET.has(key)) {
1440
+ const parsed = parseFieldKey(key);
1441
+ if (METRIC_LABEL_ONLY_GROUP_BY_EXCLUDED.has(parsed)) throw new Error(`Invalid groupBy column(s): ${key}`);
1442
+ return {
1443
+ kind: "column",
1444
+ key,
1445
+ selectSql: `${parsed} AS "${key}"`,
1446
+ groupSql: parsed,
1447
+ resultKey: key
1448
+ };
1449
+ }
1450
+ const labelExpr = `json_extract_string(labels, '${buildJsonPath(key).replace(/'/g, "''")}')`;
1451
+ const alias = buildGroupByAlias(index);
1452
+ return {
1453
+ kind: "label",
1454
+ key,
1455
+ selectSql: `${labelExpr} AS ${alias}`,
1456
+ groupSql: alias,
1457
+ resultKey: alias
1458
+ };
1459
+ });
1460
+ }
1461
+ function rowToMetricRecord(row) {
1462
+ return {
1463
+ metricId: row.metricId,
1464
+ timestamp: toDate(row.timestamp),
1465
+ name: row.name,
1466
+ value: Number(row.value),
1467
+ traceId: row.traceId ?? null,
1468
+ spanId: row.spanId ?? null,
1469
+ entityType: row.entityType ?? null,
1470
+ entityId: row.entityId ?? null,
1471
+ entityName: row.entityName ?? null,
1472
+ entityVersionId: row.entityVersionId ?? null,
1473
+ parentEntityVersionId: row.parentEntityVersionId ?? null,
1474
+ parentEntityType: row.parentEntityType ?? null,
1475
+ parentEntityId: row.parentEntityId ?? null,
1476
+ parentEntityName: row.parentEntityName ?? null,
1477
+ rootEntityVersionId: row.rootEntityVersionId ?? null,
1478
+ rootEntityType: row.rootEntityType ?? null,
1479
+ rootEntityId: row.rootEntityId ?? null,
1480
+ rootEntityName: row.rootEntityName ?? null,
1481
+ userId: row.userId ?? null,
1482
+ organizationId: row.organizationId ?? null,
1483
+ resourceId: row.resourceId ?? null,
1484
+ runId: row.runId ?? null,
1485
+ sessionId: row.sessionId ?? null,
1486
+ threadId: row.threadId ?? null,
1487
+ requestId: row.requestId ?? null,
1488
+ environment: row.environment ?? null,
1489
+ executionSource: row.executionSource ?? null,
1490
+ serviceName: row.serviceName ?? null,
1491
+ experimentId: row.experimentId ?? null,
1492
+ provider: row.provider ?? null,
1493
+ model: row.model ?? null,
1494
+ estimatedCost: row.estimatedCost === null || row.estimatedCost === void 0 ? null : Number(row.estimatedCost),
1495
+ costUnit: row.costUnit ?? null,
1496
+ costMetadata: parseJson(row.costMetadata),
1497
+ tags: parseJsonArray(row.tags),
1498
+ labels: parseJson(row.labels) ?? {},
1499
+ metadata: parseJson(row.metadata),
1500
+ scope: parseJson(row.scope)
1501
+ };
1502
+ }
1503
+ /** Insert multiple metric events in a single statement. */
1504
+ async function batchCreateMetrics(db, args) {
1505
+ if (args.metrics.length === 0) return;
1506
+ const tuples = args.metrics.map((m) => {
1507
+ return `(${[
1508
+ v(m.metricId),
1509
+ v(m.timestamp),
1510
+ "nextval('metric_events_cursor_id_seq')",
1511
+ v(m.name),
1512
+ v(m.value),
1513
+ v(m.traceId ?? null),
1514
+ v(m.spanId ?? null),
1515
+ v(m.entityType ?? null),
1516
+ v(m.entityId ?? null),
1517
+ v(m.entityName ?? null),
1518
+ v(m.entityVersionId ?? null),
1519
+ v(m.parentEntityVersionId ?? null),
1520
+ v(m.parentEntityType ?? null),
1521
+ v(m.parentEntityId ?? null),
1522
+ v(m.parentEntityName ?? null),
1523
+ v(m.rootEntityVersionId ?? null),
1524
+ v(m.rootEntityType ?? null),
1525
+ v(m.rootEntityId ?? null),
1526
+ v(m.rootEntityName ?? null),
1527
+ v(m.userId ?? null),
1528
+ v(m.organizationId ?? null),
1529
+ v(m.resourceId ?? null),
1530
+ v(m.runId ?? null),
1531
+ v(m.sessionId ?? null),
1532
+ v(m.threadId ?? null),
1533
+ v(m.requestId ?? null),
1534
+ v(m.environment ?? null),
1535
+ v(m.executionSource ?? null),
1536
+ v(m.serviceName ?? null),
1537
+ v(m.experimentId ?? null),
1538
+ v(m.provider ?? null),
1539
+ v(m.model ?? null),
1540
+ v(m.estimatedCost ?? null),
1541
+ v(m.costUnit ?? null),
1542
+ jsonV(m.tags ?? null),
1543
+ v(JSON.stringify(m.labels ?? {})),
1544
+ jsonV(m.costMetadata ?? null),
1545
+ jsonV(m.metadata ?? null),
1546
+ jsonV(m.scope ?? null)
1547
+ ].join(", ")})`;
1548
+ });
1549
+ await db.execute(`INSERT INTO metric_events (${METRIC_COLUMNS_SQL}) VALUES ${tuples.join(",\n")} ON CONFLICT DO NOTHING`);
1550
+ }
1551
+ /** Query metric events with filtering, ordering, and pagination. */
1552
+ async function listMetrics(db, args) {
1553
+ const { mode, filters, pagination, orderBy, after, limit } = listMetricsArgsSchema.parse(args);
1554
+ const filterRecord = filters;
1555
+ const page = Number(pagination.page);
1556
+ const perPage = Number(pagination.perPage);
1557
+ const { clause: filterClause, params: filterParams } = buildWhereClause(filterRecord, METRIC_FILTER_FIELD_MAPPINGS);
1558
+ if (mode === "delta") {
1559
+ assertDeltaPollingEnabled();
1560
+ const streamHeadCursor = await getStreamHeadCursor$1(db);
1561
+ if (after === void 0) return {
1562
+ metrics: [],
1563
+ delta: {
1564
+ limit,
1565
+ hasMore: false
1566
+ },
1567
+ deltaCursor: streamHeadCursor
1568
+ };
1569
+ const afterCursorId = validateCursorId(after);
1570
+ const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
1571
+ const rows = await db.query(`SELECT * FROM metric_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
1572
+ ...filterParams,
1573
+ afterCursorId,
1574
+ limit + 1
1575
+ ]);
1576
+ const visibleRows = rows.slice(0, limit).map((row) => ({
1577
+ cursorId: row.cursorId,
1578
+ metric: rowToMetricRecord(row)
1579
+ }));
1580
+ return {
1581
+ metrics: visibleRows.map((row) => row.metric),
1582
+ delta: {
1583
+ limit,
1584
+ hasMore: rows.length > limit
1585
+ },
1586
+ deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
1587
+ };
1588
+ }
1589
+ const orderByClause = buildOrderByClause(orderBy);
1590
+ const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
1591
+ page,
1592
+ perPage
1593
+ });
1594
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor$1(db, filterClause, filterParams) : void 0;
1595
+ const countResult = await db.query(`SELECT COUNT(*) AS total FROM metric_events ${filterClause}`, filterParams);
1596
+ const total = Number(countResult[0]?.total ?? 0);
1597
+ const rows = await db.query(`SELECT * FROM metric_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams]);
1598
+ return {
1599
+ pagination: {
1600
+ total,
1601
+ page,
1602
+ perPage,
1603
+ hasMore: (page + 1) * perPage < total
1604
+ },
1605
+ metrics: rows.map((row) => rowToMetricRecord(row)),
1606
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
1607
+ };
1608
+ }
1609
+ async function getDeltaCursor$1(db, filterClause, filterParams) {
1610
+ const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM metric_events ${filterClause}`, filterParams))[0]?.cursorId;
1611
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
1612
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM metric_events`))[0]?.cursorId);
1613
+ }
1614
+ async function getStreamHeadCursor$1(db) {
1615
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM metric_events`))[0]?.cursorId);
1616
+ }
1617
+ /** Compute an aggregate value (sum, avg, min, max, etc.) for a metric, with optional period comparison. */
1618
+ async function getMetricAggregate(db, args) {
1619
+ const aggSql = getAggregationSql$1(args.aggregation, "value", args.distinctColumn);
1620
+ const { clause: nameClause, params: nameParams } = buildMetricNameFilter(args.name);
1621
+ const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, METRIC_FILTER_FIELD_MAPPINGS);
1622
+ const { clause: whereClause, params: allParams } = buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams);
1623
+ const sql = `SELECT ${aggSql} AS value, ${getCostSummarySelect()} FROM metric_events ${whereClause}`;
1624
+ const row = (await db.query(sql, allParams))[0] ?? {};
1625
+ const value = row.value === null || row.value === void 0 ? null : Number(row.value);
1626
+ const costSummary = normalizeCostSummaryRow(row);
1627
+ if (args.comparePeriod && args.filters?.timestamp) {
1628
+ const ts = args.filters.timestamp;
1629
+ if (ts.start && ts.end) {
1630
+ const duration = ts.end.getTime() - ts.start.getTime();
1631
+ let prevStart;
1632
+ let prevEnd;
1633
+ switch (args.comparePeriod) {
1634
+ case "previous_period":
1635
+ prevStart = new Date(ts.start.getTime() - duration);
1636
+ prevEnd = new Date(ts.end.getTime() - duration);
1637
+ break;
1638
+ case "previous_day":
1639
+ prevStart = /* @__PURE__ */ new Date(ts.start.getTime() - 864e5);
1640
+ prevEnd = /* @__PURE__ */ new Date(ts.end.getTime() - 864e5);
1641
+ break;
1642
+ case "previous_week":
1643
+ prevStart = /* @__PURE__ */ new Date(ts.start.getTime() - 6048e5);
1644
+ prevEnd = /* @__PURE__ */ new Date(ts.end.getTime() - 6048e5);
1645
+ break;
1646
+ default:
1647
+ prevStart = new Date(ts.start.getTime() - duration);
1648
+ prevEnd = new Date(ts.end.getTime() - duration);
1649
+ }
1650
+ const { clause: prevFilterClause, params: prevFilterParams } = buildWhereClause({
1651
+ ...args.filters ?? {},
1652
+ timestamp: {
1653
+ start: prevStart,
1654
+ end: prevEnd,
1655
+ startExclusive: ts.startExclusive,
1656
+ endExclusive: ts.endExclusive
1657
+ }
1658
+ }, METRIC_FILTER_FIELD_MAPPINGS);
1659
+ const { clause: prevWhereClause, params: prevParams } = buildCombinedWhereClause(nameClause, nameParams, prevFilterClause, prevFilterParams);
1660
+ const prevSql = `SELECT ${aggSql} AS value, ${getCostSummarySelect()} FROM metric_events ${prevWhereClause}`;
1661
+ const prevRow = (await db.query(prevSql, prevParams))[0] ?? {};
1662
+ const previousValue = prevRow.value === null || prevRow.value === void 0 ? null : Number(prevRow.value);
1663
+ const previousCostSummary = normalizeCostSummaryRow(prevRow);
1664
+ let changePercent = null;
1665
+ if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
1666
+ let costChangePercent = null;
1667
+ if (previousCostSummary.estimatedCost !== null && previousCostSummary.estimatedCost !== 0 && costSummary.estimatedCost !== null) costChangePercent = (costSummary.estimatedCost - previousCostSummary.estimatedCost) / Math.abs(previousCostSummary.estimatedCost) * 100;
1668
+ return {
1669
+ value,
1670
+ estimatedCost: costSummary.estimatedCost,
1671
+ costUnit: costSummary.costUnit,
1672
+ previousValue,
1673
+ previousEstimatedCost: previousCostSummary.estimatedCost,
1674
+ changePercent,
1675
+ costChangePercent
1676
+ };
1677
+ }
1678
+ }
1679
+ return {
1680
+ value,
1681
+ estimatedCost: costSummary.estimatedCost,
1682
+ costUnit: costSummary.costUnit
1683
+ };
1684
+ }
1685
+ /** Aggregate a metric grouped by one or more dimensions. */
1686
+ async function getMetricBreakdown(db, args) {
1687
+ const aggSql = getAggregationSql$1(args.aggregation, "value", args.distinctColumn);
1688
+ const { clause: nameClause, params: nameParams } = buildMetricNameFilter(args.name);
1689
+ const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, METRIC_FILTER_FIELD_MAPPINGS);
1690
+ const { clause: whereClause, params: allParams } = buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams);
1691
+ const resolvedGroupBy = resolveGroupBy(args.groupBy);
1692
+ const selectGroupBy = resolvedGroupBy.map((entry) => entry.selectSql).join(", ");
1693
+ const groupByCols = resolvedGroupBy.map((entry) => entry.groupSql).join(", ");
1694
+ const orderDirection = args.orderDirection === "ASC" ? "ASC" : "DESC";
1695
+ const limitClause = typeof args.limit === "number" ? `LIMIT ?` : "";
1696
+ const limitParams = typeof args.limit === "number" ? [args.limit] : [];
1697
+ const sql = `SELECT ${selectGroupBy}, ${aggSql} AS value, ${getCostSummarySelect()} FROM metric_events ${whereClause} GROUP BY ${groupByCols} ORDER BY value ${orderDirection} ${limitClause}`;
1698
+ return { groups: (await db.query(sql, [...allParams, ...limitParams])).map((row) => {
1699
+ const dimensions = {};
1700
+ for (const entry of resolvedGroupBy) {
1701
+ const value = row[entry.resultKey];
1702
+ dimensions[entry.key] = value === null || value === void 0 ? null : String(value);
1703
+ }
1704
+ const costSummary = normalizeCostSummaryRow(row);
1705
+ return {
1706
+ dimensions,
1707
+ value: Number(row.value ?? 0),
1708
+ estimatedCost: costSummary.estimatedCost,
1709
+ costUnit: costSummary.costUnit
1710
+ };
1711
+ }) };
1712
+ }
1713
+ /** Aggregate a metric into time-bucketed series, with optional group-by dimensions. */
1714
+ async function getMetricTimeSeries(db, args) {
1715
+ const aggSql = getAggregationSql$1(args.aggregation, "value", args.distinctColumn);
1716
+ const intervalSql = getIntervalSql$1(args.interval);
1717
+ const { clause: nameClause, params: nameParams } = buildMetricNameFilter(args.name);
1718
+ const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, METRIC_FILTER_FIELD_MAPPINGS);
1719
+ const { clause: whereClause, params: allParams } = buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams);
1720
+ if (args.groupBy && args.groupBy.length > 0) {
1721
+ const resolvedGroupBy = resolveGroupBy(args.groupBy);
1722
+ const selectGroupBy = resolvedGroupBy.map((entry) => entry.selectSql).join(", ");
1723
+ const groupByCols = resolvedGroupBy.map((entry) => entry.groupSql).join(", ");
1724
+ const sql = `
1725
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
1726
+ ${selectGroupBy},
1727
+ ${aggSql} AS value,
1728
+ ${getCostSummarySelect()}
1729
+ FROM metric_events ${whereClause}
1730
+ GROUP BY bucket, ${groupByCols}
1731
+ ORDER BY bucket
1732
+ `;
1733
+ const rows = await db.query(sql, allParams);
1734
+ const seriesMap = /* @__PURE__ */ new Map();
1735
+ for (const row of rows) {
1736
+ const dimensionValues = resolvedGroupBy.map((entry) => row[entry.resultKey]);
1737
+ const seriesKey = JSON.stringify(dimensionValues);
1738
+ const name = dimensionValues.map(toSeriesDisplayValue).join("|");
1739
+ const costSummary = normalizeCostSummaryRow(row);
1740
+ if (!seriesMap.has(seriesKey)) seriesMap.set(seriesKey, {
1741
+ name,
1742
+ costUnits: /* @__PURE__ */ new Set(),
1743
+ points: []
1744
+ });
1745
+ if (costSummary.costUnit) seriesMap.get(seriesKey).costUnits.add(costSummary.costUnit);
1746
+ seriesMap.get(seriesKey).points.push({
1747
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
1748
+ value: Number(row.value ?? 0),
1749
+ estimatedCost: costSummary.estimatedCost
1750
+ });
1751
+ }
1752
+ return { series: Array.from(seriesMap.values()).map((series) => ({
1753
+ name: series.name,
1754
+ costUnit: series.costUnits.size === 1 ? Array.from(series.costUnits)[0] : null,
1755
+ points: series.points
1756
+ })) };
1757
+ }
1758
+ const sql = `
1759
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
1760
+ ${aggSql} AS value,
1761
+ ${getCostSummarySelect()}
1762
+ FROM metric_events ${whereClause}
1763
+ GROUP BY bucket
1764
+ ORDER BY bucket
1765
+ `;
1766
+ const rows = await db.query(sql, allParams);
1767
+ const metricName = Array.isArray(args.name) ? args.name.join(",") : args.name;
1768
+ const overallCostUnits = new Set(rows.map((row) => row.costUnit).filter((value) => typeof value === "string"));
1769
+ return { series: [{
1770
+ name: metricName,
1771
+ costUnit: overallCostUnits.size === 1 ? Array.from(overallCostUnits)[0] : null,
1772
+ points: rows.map((row) => {
1773
+ const costSummary = normalizeCostSummaryRow(row);
1774
+ return {
1775
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
1776
+ value: Number(row.value ?? 0),
1777
+ estimatedCost: costSummary.estimatedCost
1778
+ };
1779
+ })
1780
+ }] };
1781
+ }
1782
+ /** Compute percentile time series for a metric using `percentile_cont`. */
1783
+ async function getMetricPercentiles(db, args) {
1784
+ const intervalSql = getIntervalSql$1(args.interval);
1785
+ const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, METRIC_FILTER_FIELD_MAPPINGS);
1786
+ const allConditions = [`name = ?`];
1787
+ const allParams = [args.name];
1788
+ if (filterClause) {
1789
+ allConditions.push(filterClause.replace("WHERE ", ""));
1790
+ allParams.push(...filterParams);
1791
+ }
1792
+ const whereClause = `WHERE ${allConditions.join(" AND ")}`;
1793
+ const series = [];
1794
+ for (const p of args.percentiles) {
1795
+ const sql = `
1796
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
1797
+ percentile_cont(${p}) WITHIN GROUP (ORDER BY value) AS pvalue
1798
+ FROM metric_events ${whereClause}
1799
+ GROUP BY bucket
1800
+ ORDER BY bucket
1801
+ `;
1802
+ const rows = await db.query(sql, allParams);
1803
+ series.push({
1804
+ percentile: p,
1805
+ points: rows.map((row) => ({
1806
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
1807
+ value: Number(row.pvalue ?? 0)
1808
+ }))
1809
+ });
1810
+ }
1811
+ return { series };
1812
+ }
1813
+ /** Return distinct metric names, optionally filtered by prefix. */
1814
+ async function getMetricNames(db, args) {
1815
+ const conditions = [];
1816
+ const params = [];
1817
+ if (args.prefix) {
1818
+ conditions.push(`name LIKE ?`);
1819
+ params.push(`${args.prefix}%`);
1820
+ }
1821
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1822
+ const limitClause = args.limit ? `LIMIT ?` : "";
1823
+ if (args.limit) params.push(args.limit);
1824
+ return { names: (await db.query(`SELECT DISTINCT name FROM metric_events ${whereClause} ORDER BY name ${limitClause}`, params)).map((r) => r.name) };
1825
+ }
1826
+ /** Return distinct label keys for a given metric name. */
1827
+ async function getMetricLabelKeys(db, args) {
1828
+ return { keys: (await db.query(`SELECT DISTINCT unnest(json_keys(labels)) AS key FROM metric_events WHERE name = ? AND labels IS NOT NULL`, [args.metricName])).map((r) => r.key) };
1829
+ }
1830
+ /** Return distinct values for a specific label key on a metric. */
1831
+ async function getMetricLabelValues(db, args) {
1832
+ const labelPath = buildJsonPath(args.labelKey);
1833
+ const conditions = [`name = ?`, `json_extract_string(labels, ?) IS NOT NULL`];
1834
+ const params = [args.metricName, labelPath];
1835
+ if (args.prefix) {
1836
+ conditions.push(`json_extract_string(labels, ?) LIKE ?`);
1837
+ params.push(labelPath, `${args.prefix}%`);
1838
+ }
1839
+ const limitClause = args.limit ? `LIMIT ?` : "";
1840
+ if (args.limit) params.push(args.limit);
1841
+ return { values: (await db.query(`SELECT DISTINCT json_extract_string(labels, ?) AS val FROM metric_events WHERE ${conditions.join(" AND ")} ORDER BY val ${limitClause}`, [labelPath, ...params])).map((r) => r.val) };
1842
+ }
1843
+ //#endregion
1844
+ //#region src/storage/domains/observability/migration.ts
1845
+ const CURSOR_ID_TABLES = [
1846
+ "span_events",
1847
+ "metric_events",
1848
+ "log_events",
1849
+ "score_events",
1850
+ "feedback_events"
1851
+ ];
1852
+ /**
1853
+ * Drop any leftover `DEFAULT nextval(...)` on observability `cursorId` columns.
1854
+ *
1855
+ * A previous version of the migration set this default via
1856
+ * `ALTER COLUMN cursorId SET DEFAULT nextval(...)`. DuckDB WAL replay cannot
1857
+ * bind that function expression before the default database is attached, so
1858
+ * affected databases fail to reopen. Insert paths now write cursor IDs
1859
+ * explicitly, so the catalog default is unnecessary and should be removed.
1860
+ *
1861
+ * We query `information_schema` first and only emit the `ALTER` for tables that
1862
+ * actually carry the bad default; this avoids writing redundant SetDefault
1863
+ * entries to the WAL on every startup for healthy databases.
1864
+ */
1865
+ async function dropLegacyCursorIdDefaults(db) {
1866
+ const rows = await db.query(`SELECT table_name FROM information_schema.columns
1867
+ WHERE column_name = 'cursorId'
1868
+ AND column_default IS NOT NULL
1869
+ AND table_name IN (${CURSOR_ID_TABLES.map((t) => `'${t}'`).join(", ")})`);
1870
+ if (rows.length === 0) return;
1871
+ await db.executeBatch(rows.map((row) => `ALTER TABLE ${row.table_name} ALTER COLUMN cursorId DROP DEFAULT`));
1872
+ }
1873
+ const SIGNAL_MIGRATIONS = [
1874
+ {
1875
+ table: "metric_events",
1876
+ createDDL: METRIC_EVENTS_DDL,
1877
+ idColumn: "metricId",
1878
+ cursorSequenceDDL: METRIC_EVENTS_CURSOR_SEQUENCE_DDL
1879
+ },
1880
+ {
1881
+ table: "log_events",
1882
+ createDDL: LOG_EVENTS_DDL,
1883
+ idColumn: "logId",
1884
+ cursorSequenceDDL: LOG_EVENTS_CURSOR_SEQUENCE_DDL
1885
+ },
1886
+ {
1887
+ table: "score_events",
1888
+ createDDL: SCORE_EVENTS_DDL,
1889
+ idColumn: "scoreId",
1890
+ cursorSequenceDDL: SCORE_EVENTS_CURSOR_SEQUENCE_DDL
1891
+ },
1892
+ {
1893
+ table: "feedback_events",
1894
+ createDDL: FEEDBACK_EVENTS_DDL,
1895
+ idColumn: "feedbackId",
1896
+ cursorSequenceDDL: FEEDBACK_EVENTS_CURSOR_SEQUENCE_DDL
1897
+ }
1898
+ ];
1899
+ async function tableExists(db, table) {
1900
+ return (await db.query(`SELECT table_name FROM information_schema.tables WHERE table_name = ?`, [table])).length > 0;
1901
+ }
1902
+ async function hasPrimaryKey(db, table) {
1903
+ return (await db.query(`SELECT constraint_type FROM information_schema.table_constraints
1904
+ WHERE table_name = ? AND constraint_type = 'PRIMARY KEY'`, [table])).length > 0;
1905
+ }
1906
+ async function getColumns(db, table) {
1907
+ return (await db.query(`SELECT column_name FROM information_schema.columns WHERE table_name = ?`, [table])).map((r) => r.column_name);
1908
+ }
1909
+ function buildTemporaryTableDDL(createDDL, table, tempTable) {
1910
+ return createDDL.replace(`CREATE TABLE IF NOT EXISTS ${table}`, `CREATE TABLE ${tempTable}`);
1911
+ }
1912
+ async function dropTableIfExists(db, table) {
1913
+ if (await tableExists(db, table)) await db.execute(`DROP TABLE ${table}`);
1914
+ }
1915
+ function createMigrationError(args, error) {
1916
+ return new MastraError({
1917
+ id: createStorageErrorId("DUCKDB", "MIGRATE_SIGNAL_TABLES", "FAILED"),
1918
+ domain: ErrorDomain.STORAGE,
1919
+ category: ErrorCategory.THIRD_PARTY,
1920
+ details: args
1921
+ }, error);
1922
+ }
1923
+ async function checkSignalTablesMigrationStatus(db) {
1924
+ const tables = [];
1925
+ for (const { table, idColumn } of SIGNAL_MIGRATIONS) {
1926
+ if (!await tableExists(db, table)) continue;
1927
+ if (await hasPrimaryKey(db, table)) continue;
1928
+ tables.push({
1929
+ table,
1930
+ idColumn
1931
+ });
1932
+ }
1933
+ return {
1934
+ needsMigration: tables.length > 0,
1935
+ tables
1936
+ };
1937
+ }
1938
+ /**
1939
+ * Migrate signal tables to a schema with PRIMARY KEY + NOT NULL on the signal ID
1940
+ * without dropping data. Copy-and-swap: create temp → INSERT…SELECT
1941
+ * (generating IDs) → rename old to backup → rename temp to live → drop backup.
1942
+ * The live table is only touched during the final swap step.
1943
+ */
1944
+ async function migrateSignalTables(db, logger) {
1945
+ for (const { table, createDDL, idColumn, cursorSequenceDDL } of SIGNAL_MIGRATIONS) {
1946
+ if (!await tableExists(db, table)) continue;
1947
+ if (await hasPrimaryKey(db, table)) continue;
1948
+ logger?.info?.(`Migrating ${table} to schema with ${idColumn} PRIMARY KEY`);
1949
+ const temp = `${table}_migrating_${Date.now()}`;
1950
+ const backup = `${table}_backup_${Date.now()}`;
1951
+ let originalRenamed = false;
1952
+ let swapCompleted = false;
1953
+ try {
1954
+ await db.execute(cursorSequenceDDL);
1955
+ await db.execute(buildTemporaryTableDDL(createDDL, table, temp));
1956
+ const newColumns = await getColumns(db, temp);
1957
+ const currentColumns = new Set(await getColumns(db, table));
1958
+ const columnList = newColumns.map((c) => `"${c}"`).join(", ");
1959
+ const selectExprs = newColumns.map((c) => {
1960
+ if (c === idColumn) return currentColumns.has(c) ? `COALESCE(NULLIF("${c}", ''), CAST(uuid() AS VARCHAR)) AS "${c}"` : `CAST(uuid() AS VARCHAR) AS "${c}"`;
1961
+ return currentColumns.has(c) ? `"${c}"` : `NULL AS "${c}"`;
1962
+ }).join(", ");
1963
+ await db.execute(`INSERT INTO ${temp} (${columnList}) SELECT ${selectExprs} FROM ${table}`);
1964
+ await db.execute(`ALTER TABLE ${table} RENAME TO ${backup}`);
1965
+ originalRenamed = true;
1966
+ await db.execute(`ALTER TABLE ${temp} RENAME TO ${table}`);
1967
+ swapCompleted = true;
1968
+ try {
1969
+ await db.execute(`DROP TABLE ${backup}`);
1970
+ } catch (cleanupError) {
1971
+ logger?.warn?.(`Migration of ${table} completed, but failed to drop backup ${backup}: ${cleanupError.message}`);
1972
+ }
1973
+ logger?.info?.(`Successfully migrated ${table}`);
1974
+ } catch (error) {
1975
+ logger?.error?.(`Migration of ${table} failed: ${error.message}`);
1976
+ try {
1977
+ await dropTableIfExists(db, temp);
1978
+ } catch (restoreError) {
1979
+ logger?.error?.(`Failed to clean up temporary table ${temp}: ${restoreError.message}`);
1980
+ }
1981
+ if (originalRenamed && !swapCompleted) try {
1982
+ await db.execute(`ALTER TABLE ${backup} RENAME TO ${table}`);
1983
+ } catch (restoreError) {
1984
+ logger?.error?.(`Failed to restore original table ${table} from backup ${backup}: ${restoreError.message}`);
1985
+ }
1986
+ throw createMigrationError({
1987
+ table,
1988
+ idColumn
1989
+ }, error);
1990
+ }
1991
+ }
1992
+ }
1993
+ //#endregion
1994
+ //#region src/storage/domains/observability/scores.ts
1995
+ const SCORE_GROUP_BY_COLUMNS = /* @__PURE__ */ new Set([
1996
+ "timestamp",
1997
+ "traceId",
1998
+ "spanId",
1999
+ "experimentId",
2000
+ "scoreTraceId",
2001
+ "entityType",
2002
+ "entityId",
2003
+ "entityName",
2004
+ "entityVersionId",
2005
+ "parentEntityVersionId",
2006
+ "parentEntityType",
2007
+ "parentEntityId",
2008
+ "parentEntityName",
2009
+ "rootEntityVersionId",
2010
+ "rootEntityType",
2011
+ "rootEntityId",
2012
+ "rootEntityName",
2013
+ "userId",
2014
+ "organizationId",
2015
+ "resourceId",
2016
+ "runId",
2017
+ "sessionId",
2018
+ "threadId",
2019
+ "requestId",
2020
+ "environment",
2021
+ "executionSource",
2022
+ "serviceName",
2023
+ "scorerId",
2024
+ "scorerVersion",
2025
+ "scoreSource",
2026
+ "score",
2027
+ "reason"
2028
+ ]);
2029
+ function getAggregationSql(aggregation, measure = "score") {
2030
+ switch (aggregation) {
2031
+ case "sum": return `SUM(${measure})`;
2032
+ case "avg": return `AVG(${measure})`;
2033
+ case "min": return `MIN(${measure})`;
2034
+ case "max": return `MAX(${measure})`;
2035
+ case "count": return `CAST(COUNT(${measure}) AS DOUBLE)`;
2036
+ case "last": return `arg_max(${measure}, timestamp)`;
2037
+ default: return `SUM(${measure})`;
2038
+ }
2039
+ }
2040
+ function getIntervalSql(interval) {
2041
+ switch (interval) {
2042
+ case "1m": return "1 minute";
2043
+ case "5m": return "5 minutes";
2044
+ case "15m": return "15 minutes";
2045
+ case "1h": return "1 hour";
2046
+ case "1d": return "1 day";
2047
+ default: return "1 hour";
2048
+ }
2049
+ }
2050
+ function getValidatedPercentiles(percentiles) {
2051
+ if (!Array.isArray(percentiles) || percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
2052
+ return percentiles.map((percentile) => {
2053
+ if (!Number.isFinite(percentile) || percentile < 0 || percentile > 1) throw new Error("Percentiles must be finite numbers between 0 and 1.");
2054
+ return percentile;
2055
+ });
2056
+ }
2057
+ function buildScoreWhereClause(args) {
2058
+ const conditions = ["scorerId = ?"];
2059
+ const params = [args.scorerId];
2060
+ if (args.scoreSource !== void 0) {
2061
+ conditions.push("scoreSource = ?");
2062
+ params.push(args.scoreSource);
2063
+ }
2064
+ const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, { source: "scoreSource" });
2065
+ if (filterClause) {
2066
+ conditions.push(filterClause.replace("WHERE ", ""));
2067
+ params.push(...filterParams);
2068
+ }
2069
+ return {
2070
+ clause: `WHERE ${conditions.join(" AND ")}`,
2071
+ params
2072
+ };
2073
+ }
2074
+ function resolveScoreGroupBy(groupBy) {
2075
+ return groupBy.map((key, index) => {
2076
+ const column = parseFieldKey(key);
2077
+ if (!SCORE_GROUP_BY_COLUMNS.has(column)) throw new Error(`Invalid groupBy column(s): ${key}`);
2078
+ const alias = `group_by_${index}`;
2079
+ return {
2080
+ key,
2081
+ selectSql: `${column} AS ${alias}`,
2082
+ groupSql: alias
2083
+ };
2084
+ });
2085
+ }
2086
+ function toSeriesName(values) {
2087
+ return values.map((value) => value === null || value === void 0 ? "" : String(value)).join("|");
2088
+ }
2089
+ function rowToScoreRecord(row) {
2090
+ return {
2091
+ scoreId: row.scoreId,
2092
+ timestamp: toDate(row.timestamp),
2093
+ traceId: row.traceId ?? null,
2094
+ spanId: row.spanId ?? null,
2095
+ experimentId: row.experimentId ?? null,
2096
+ scoreTraceId: row.scoreTraceId ?? null,
2097
+ entityType: row.entityType ?? null,
2098
+ entityId: row.entityId ?? null,
2099
+ entityName: row.entityName ?? null,
2100
+ entityVersionId: row.entityVersionId ?? null,
2101
+ parentEntityVersionId: row.parentEntityVersionId ?? null,
2102
+ parentEntityType: row.parentEntityType ?? null,
2103
+ parentEntityId: row.parentEntityId ?? null,
2104
+ parentEntityName: row.parentEntityName ?? null,
2105
+ rootEntityVersionId: row.rootEntityVersionId ?? null,
2106
+ rootEntityType: row.rootEntityType ?? null,
2107
+ rootEntityId: row.rootEntityId ?? null,
2108
+ rootEntityName: row.rootEntityName ?? null,
2109
+ userId: row.userId ?? null,
2110
+ organizationId: row.organizationId ?? null,
2111
+ resourceId: row.resourceId ?? null,
2112
+ runId: row.runId ?? null,
2113
+ sessionId: row.sessionId ?? null,
2114
+ threadId: row.threadId ?? null,
2115
+ requestId: row.requestId ?? null,
2116
+ environment: row.environment ?? null,
2117
+ executionSource: row.executionSource ?? null,
2118
+ serviceName: row.serviceName ?? null,
2119
+ scorerId: row.scorerId,
2120
+ scorerVersion: row.scorerVersion ?? null,
2121
+ source: row.scoreSource ?? null,
2122
+ scoreSource: row.scoreSource ?? null,
2123
+ score: Number(row.score),
2124
+ reason: row.reason ?? null,
2125
+ tags: parseJsonArray(row.tags),
2126
+ metadata: parseJson(row.metadata),
2127
+ scope: parseJson(row.scope)
2128
+ };
2129
+ }
2130
+ function getComparisonDateRange(comparePeriod, timestamp) {
2131
+ if (!timestamp.start || !timestamp.end) return null;
2132
+ const duration = timestamp.end.getTime() - timestamp.start.getTime();
2133
+ switch (comparePeriod) {
2134
+ case "previous_period": return {
2135
+ start: new Date(timestamp.start.getTime() - duration),
2136
+ end: new Date(timestamp.end.getTime() - duration),
2137
+ startExclusive: timestamp.startExclusive,
2138
+ endExclusive: timestamp.endExclusive
2139
+ };
2140
+ case "previous_day": return {
2141
+ start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 864e5),
2142
+ end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 864e5),
2143
+ startExclusive: timestamp.startExclusive,
2144
+ endExclusive: timestamp.endExclusive
2145
+ };
2146
+ case "previous_week": return {
2147
+ start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 6048e5),
2148
+ end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 6048e5),
2149
+ startExclusive: timestamp.startExclusive,
2150
+ endExclusive: timestamp.endExclusive
2151
+ };
2152
+ }
2153
+ }
2154
+ /** Insert a single score event. */
2155
+ async function createScore(db, args) {
2156
+ const s = args.score;
2157
+ const scoreSource = s.scoreSource ?? s.source ?? null;
2158
+ await db.execute(`INSERT INTO score_events (
2159
+ scoreId, timestamp, cursorId, traceId, spanId, experimentId, scoreTraceId,
2160
+ entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
2161
+ userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
2162
+ scorerId, scorerVersion, scoreSource, score, reason, tags, metadata, scope
2163
+ )
2164
+ VALUES (${[
2165
+ v(s.scoreId),
2166
+ v(s.timestamp),
2167
+ "nextval('score_events_cursor_id_seq')",
2168
+ v(s.traceId),
2169
+ v(s.spanId ?? null),
2170
+ v(s.experimentId ?? null),
2171
+ v(s.scoreTraceId ?? null),
2172
+ v(s.entityType ?? null),
2173
+ v(s.entityId ?? null),
2174
+ v(s.entityName ?? null),
2175
+ v(s.entityVersionId ?? null),
2176
+ v(s.parentEntityVersionId ?? null),
2177
+ v(s.parentEntityType ?? null),
2178
+ v(s.parentEntityId ?? null),
2179
+ v(s.parentEntityName ?? null),
2180
+ v(s.rootEntityVersionId ?? null),
2181
+ v(s.rootEntityType ?? null),
2182
+ v(s.rootEntityId ?? null),
2183
+ v(s.rootEntityName ?? null),
2184
+ v(s.userId ?? null),
2185
+ v(s.organizationId ?? null),
2186
+ v(s.resourceId ?? null),
2187
+ v(s.runId ?? null),
2188
+ v(s.sessionId ?? null),
2189
+ v(s.threadId ?? null),
2190
+ v(s.requestId ?? null),
2191
+ v(s.environment ?? null),
2192
+ v(s.executionSource ?? null),
2193
+ v(s.serviceName ?? null),
2194
+ v(s.scorerId),
2195
+ v(s.scorerVersion ?? null),
2196
+ v(scoreSource),
2197
+ v(s.score),
2198
+ v(s.reason ?? null),
2199
+ jsonV(s.tags ?? null),
2200
+ jsonV(s.metadata),
2201
+ jsonV(s.scope ?? null)
2202
+ ].join(", ")})
2203
+ ON CONFLICT DO NOTHING`);
2204
+ }
2205
+ /** Insert multiple score events in a single statement. */
2206
+ async function batchCreateScores(db, args) {
2207
+ if (args.scores.length === 0) return;
2208
+ const tuples = args.scores.map((s) => {
2209
+ const legacyScore = s;
2210
+ const scoreSource = legacyScore.scoreSource ?? legacyScore.source ?? null;
2211
+ return `(${[
2212
+ v(legacyScore.scoreId),
2213
+ v(legacyScore.timestamp),
2214
+ "nextval('score_events_cursor_id_seq')",
2215
+ v(legacyScore.traceId),
2216
+ v(legacyScore.spanId ?? null),
2217
+ v(legacyScore.experimentId ?? null),
2218
+ v(legacyScore.scoreTraceId ?? null),
2219
+ v(legacyScore.entityType ?? null),
2220
+ v(legacyScore.entityId ?? null),
2221
+ v(legacyScore.entityName ?? null),
2222
+ v(legacyScore.entityVersionId ?? null),
2223
+ v(legacyScore.parentEntityVersionId ?? null),
2224
+ v(legacyScore.parentEntityType ?? null),
2225
+ v(legacyScore.parentEntityId ?? null),
2226
+ v(legacyScore.parentEntityName ?? null),
2227
+ v(legacyScore.rootEntityVersionId ?? null),
2228
+ v(legacyScore.rootEntityType ?? null),
2229
+ v(legacyScore.rootEntityId ?? null),
2230
+ v(legacyScore.rootEntityName ?? null),
2231
+ v(legacyScore.userId ?? null),
2232
+ v(legacyScore.organizationId ?? null),
2233
+ v(legacyScore.resourceId ?? null),
2234
+ v(legacyScore.runId ?? null),
2235
+ v(legacyScore.sessionId ?? null),
2236
+ v(legacyScore.threadId ?? null),
2237
+ v(legacyScore.requestId ?? null),
2238
+ v(legacyScore.environment ?? null),
2239
+ v(legacyScore.executionSource ?? null),
2240
+ v(legacyScore.serviceName ?? null),
2241
+ v(legacyScore.scorerId),
2242
+ v(legacyScore.scorerVersion ?? null),
2243
+ v(scoreSource),
2244
+ v(legacyScore.score),
2245
+ v(legacyScore.reason ?? null),
2246
+ jsonV(legacyScore.tags ?? null),
2247
+ jsonV(legacyScore.metadata),
2248
+ jsonV(legacyScore.scope ?? null)
2249
+ ].join(", ")})`;
2250
+ });
2251
+ await db.execute(`INSERT INTO score_events (
2252
+ scoreId, timestamp, cursorId, traceId, spanId, experimentId, scoreTraceId,
2253
+ entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
2254
+ userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
2255
+ scorerId, scorerVersion, scoreSource, score, reason, tags, metadata, scope
2256
+ )
2257
+ VALUES ${tuples.join(",\n ")}
2258
+ ON CONFLICT DO NOTHING`);
2259
+ }
2260
+ /** Query score events with filtering, ordering, and pagination. */
2261
+ async function listScores(db, args) {
2262
+ const { mode, filters, pagination, orderBy, after, limit } = listScoresArgsSchema.parse(args);
2263
+ const page = Number(pagination.page);
2264
+ const perPage = Number(pagination.perPage);
2265
+ const { clause: filterClause, params: filterParams } = buildWhereClause(filters, { source: "scoreSource" });
2266
+ if (mode === "delta") {
2267
+ assertDeltaPollingEnabled();
2268
+ const streamHeadCursor = await getStreamHeadCursor(db);
2269
+ if (after === void 0) return {
2270
+ scores: [],
2271
+ delta: {
2272
+ limit,
2273
+ hasMore: false
2274
+ },
2275
+ deltaCursor: streamHeadCursor
2276
+ };
2277
+ const afterCursorId = validateCursorId(after);
2278
+ const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
2279
+ const rows = await db.query(`SELECT * FROM score_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
2280
+ ...filterParams,
2281
+ afterCursorId,
2282
+ limit + 1
2283
+ ]);
2284
+ const visibleRows = rows.slice(0, limit).map((row) => ({
2285
+ cursorId: row.cursorId,
2286
+ score: rowToScoreRecord(row)
2287
+ }));
2288
+ return {
2289
+ scores: visibleRows.map((row) => row.score),
2290
+ delta: {
2291
+ limit,
2292
+ hasMore: rows.length > limit
2293
+ },
2294
+ deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
2295
+ };
2296
+ }
2297
+ const orderByClause = buildOrderByClause(orderBy);
2298
+ const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
2299
+ page,
2300
+ perPage
2301
+ });
2302
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor(db, filterClause, filterParams) : void 0;
2303
+ const countResult = await db.query(`SELECT COUNT(*) as total FROM score_events ${filterClause}`, filterParams);
2304
+ const total = Number(countResult[0]?.total ?? 0);
2305
+ const rows = await db.query(`SELECT * FROM score_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams]);
2306
+ return {
2307
+ pagination: {
2308
+ total,
2309
+ page,
2310
+ perPage,
2311
+ hasMore: (page + 1) * perPage < total
2312
+ },
2313
+ scores: rows.map((row) => rowToScoreRecord(row)),
2314
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
2315
+ };
2316
+ }
2317
+ async function getDeltaCursor(db, filterClause, filterParams) {
2318
+ const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM score_events ${filterClause}`, filterParams))[0]?.cursorId;
2319
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
2320
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM score_events`))[0]?.cursorId);
2321
+ }
2322
+ async function getStreamHeadCursor(db) {
2323
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM score_events`))[0]?.cursorId);
2324
+ }
2325
+ async function getScoreById(db, scoreId) {
2326
+ const rows = await db.query(`SELECT * FROM score_events WHERE scoreId = ? LIMIT 1`, [scoreId]);
2327
+ return rows[0] ? rowToScoreRecord(rows[0]) : null;
2328
+ }
2329
+ async function getScoreAggregate(db, args) {
2330
+ const aggSql = getAggregationSql(args.aggregation);
2331
+ const { clause, params } = buildScoreWhereClause(args);
2332
+ const rows = await db.query(`SELECT ${aggSql} AS value FROM score_events ${clause}`, params);
2333
+ const value = rows[0]?.value === null || rows[0]?.value === void 0 ? null : Number(rows[0]?.value);
2334
+ if (args.comparePeriod && args.filters?.timestamp) {
2335
+ const previousTimestamp = getComparisonDateRange(args.comparePeriod, args.filters.timestamp);
2336
+ if (previousTimestamp) {
2337
+ const prevRows = await db.query(`SELECT ${aggSql} AS value FROM score_events ${buildScoreWhereClause({
2338
+ ...args,
2339
+ filters: {
2340
+ ...args.filters ?? {},
2341
+ timestamp: previousTimestamp
2342
+ }
2343
+ }).clause}`, buildScoreWhereClause({
2344
+ ...args,
2345
+ filters: {
2346
+ ...args.filters ?? {},
2347
+ timestamp: previousTimestamp
2348
+ }
2349
+ }).params);
2350
+ const previousValue = prevRows[0]?.value === null || prevRows[0]?.value === void 0 ? null : Number(prevRows[0]?.value);
2351
+ let changePercent = null;
2352
+ if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
2353
+ return {
2354
+ value,
2355
+ previousValue,
2356
+ changePercent
2357
+ };
2358
+ }
2359
+ }
2360
+ return { value };
2361
+ }
2362
+ async function getScoreBreakdown(db, args) {
2363
+ const aggSql = getAggregationSql(args.aggregation);
2364
+ const { clause, params } = buildScoreWhereClause(args);
2365
+ const resolvedGroupBy = resolveScoreGroupBy(args.groupBy);
2366
+ const sql = `SELECT ${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")}, ${aggSql} AS value FROM score_events ${clause} GROUP BY ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")} ORDER BY value DESC`;
2367
+ return { groups: (await db.query(sql, params)).map((row) => ({
2368
+ dimensions: Object.fromEntries(resolvedGroupBy.map((entry, index) => {
2369
+ const value = row[`group_by_${index}`];
2370
+ return [entry.key, value === null || value === void 0 ? null : String(value)];
2371
+ })),
2372
+ value: Number(row.value ?? 0)
2373
+ })) };
2374
+ }
2375
+ async function getScoreTimeSeries(db, args) {
2376
+ const aggSql = getAggregationSql(args.aggregation);
2377
+ const intervalSql = getIntervalSql(args.interval);
2378
+ const { clause, params } = buildScoreWhereClause(args);
2379
+ if (args.groupBy && args.groupBy.length > 0) {
2380
+ const resolvedGroupBy = resolveScoreGroupBy(args.groupBy);
2381
+ const sql = `
2382
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
2383
+ ${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")},
2384
+ ${aggSql} AS value
2385
+ FROM score_events ${clause}
2386
+ GROUP BY bucket, ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")}
2387
+ ORDER BY bucket
2388
+ `;
2389
+ const rows = await db.query(sql, params);
2390
+ const seriesMap = /* @__PURE__ */ new Map();
2391
+ for (const row of rows) {
2392
+ const groupValues = resolvedGroupBy.map((_, index) => row[`group_by_${index}`]);
2393
+ const key = JSON.stringify(groupValues);
2394
+ if (!seriesMap.has(key)) seriesMap.set(key, {
2395
+ name: toSeriesName(groupValues),
2396
+ points: []
2397
+ });
2398
+ seriesMap.get(key).points.push({
2399
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
2400
+ value: Number(row.value ?? 0)
2401
+ });
2402
+ }
2403
+ return { series: Array.from(seriesMap.values()) };
2404
+ }
2405
+ const rows = await db.query(`
2406
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
2407
+ ${aggSql} AS value
2408
+ FROM score_events ${clause}
2409
+ GROUP BY bucket
2410
+ ORDER BY bucket
2411
+ `, params);
2412
+ return { series: [{
2413
+ name: args.scoreSource ? `${args.scorerId}|${args.scoreSource}` : args.scorerId,
2414
+ points: rows.map((row) => ({
2415
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
2416
+ value: Number(row.value ?? 0)
2417
+ }))
2418
+ }] };
2419
+ }
2420
+ async function getScorePercentiles(db, args) {
2421
+ const intervalSql = getIntervalSql(args.interval);
2422
+ const { clause, params } = buildScoreWhereClause(args);
2423
+ const percentiles = getValidatedPercentiles(args.percentiles);
2424
+ const series = [];
2425
+ for (const percentile of percentiles) {
2426
+ const rows = await db.query(`
2427
+ SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
2428
+ percentile_cont(${percentile}) WITHIN GROUP (ORDER BY score) AS pvalue
2429
+ FROM score_events ${clause}
2430
+ GROUP BY bucket
2431
+ ORDER BY bucket
2432
+ `, params);
2433
+ series.push({
2434
+ percentile,
2435
+ points: rows.map((row) => ({
2436
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
2437
+ value: Number(row.pvalue ?? 0)
2438
+ }))
2439
+ });
2440
+ }
2441
+ return { series };
2442
+ }
2443
+ //#endregion
2444
+ //#region src/storage/domains/observability/tracing.ts
2445
+ const COLUMNS_SQL = [
2446
+ "eventType",
2447
+ "timestamp",
2448
+ "cursorId",
2449
+ "traceId",
2450
+ "spanId",
2451
+ "parentSpanId",
2452
+ "name",
2453
+ "spanType",
2454
+ "isEvent",
2455
+ "endedAt",
2456
+ "experimentId",
2457
+ "entityType",
2458
+ "entityId",
2459
+ "entityName",
2460
+ "entityVersionId",
2461
+ "userId",
2462
+ "organizationId",
2463
+ "resourceId",
2464
+ "runId",
2465
+ "sessionId",
2466
+ "threadId",
2467
+ "requestId",
2468
+ "environment",
2469
+ "source",
2470
+ "serviceName",
2471
+ "attributes",
2472
+ "metadata",
2473
+ "tags",
2474
+ "scope",
2475
+ "links",
2476
+ "input",
2477
+ "output",
2478
+ "error",
2479
+ "requestContext"
2480
+ ].join(", ");
2481
+ /**
2482
+ * Reconstruction query uses `arg_max(field, timestamp) FILTER (WHERE field IS NOT NULL)`
2483
+ * so that the final end event supplies the terminal span fields without wiping
2484
+ * stable values emitted on the start event.
2485
+ */
2486
+ function argMaxNonNull(col) {
2487
+ return `arg_max(${col}, timestamp) FILTER (WHERE ${col} IS NOT NULL) as ${col}`;
2488
+ }
2489
+ const SPAN_RECONSTRUCT_SELECT = `
2490
+ SELECT
2491
+ traceId, spanId,
2492
+ ${argMaxNonNull("name")},
2493
+ ${argMaxNonNull("spanType")},
2494
+ ${argMaxNonNull("parentSpanId")},
2495
+ ${argMaxNonNull("isEvent")},
2496
+ coalesce(min(timestamp) FILTER (WHERE eventType = 'start'), min(timestamp)) as startedAt,
2497
+ ${argMaxNonNull("endedAt")},
2498
+ ${argMaxNonNull("experimentId")},
2499
+ ${argMaxNonNull("entityType")},
2500
+ ${argMaxNonNull("entityId")},
2501
+ ${argMaxNonNull("entityName")},
2502
+ ${argMaxNonNull("entityVersionId")},
2503
+ ${argMaxNonNull("userId")},
2504
+ ${argMaxNonNull("organizationId")},
2505
+ ${argMaxNonNull("resourceId")},
2506
+ ${argMaxNonNull("runId")},
2507
+ ${argMaxNonNull("sessionId")},
2508
+ ${argMaxNonNull("threadId")},
2509
+ ${argMaxNonNull("requestId")},
2510
+ ${argMaxNonNull("environment")},
2511
+ ${argMaxNonNull("source")},
2512
+ ${argMaxNonNull("serviceName")},
2513
+ ${argMaxNonNull("attributes")},
2514
+ ${argMaxNonNull("metadata")},
2515
+ ${argMaxNonNull("tags")},
2516
+ ${argMaxNonNull("scope")},
2517
+ ${argMaxNonNull("links")},
2518
+ ${argMaxNonNull("input")},
2519
+ ${argMaxNonNull("output")},
2520
+ ${argMaxNonNull("error")},
2521
+ ${argMaxNonNull("requestContext")}
2522
+ FROM span_events
2523
+ `;
2524
+ /** Lightweight variant — only timeline-relevant columns. */
2525
+ const SPAN_RECONSTRUCT_SELECT_LIGHT = `
2526
+ SELECT
2527
+ traceId, spanId,
2528
+ ${argMaxNonNull("name")},
2529
+ ${argMaxNonNull("spanType")},
2530
+ ${argMaxNonNull("parentSpanId")},
2531
+ ${argMaxNonNull("isEvent")},
2532
+ coalesce(min(timestamp) FILTER (WHERE eventType = 'start'), min(timestamp)) as startedAt,
2533
+ ${argMaxNonNull("endedAt")},
2534
+ ${argMaxNonNull("entityType")},
2535
+ ${argMaxNonNull("entityId")},
2536
+ ${argMaxNonNull("entityName")},
2537
+ ${argMaxNonNull("error")}
2538
+ FROM span_events
2539
+ `;
2540
+ /**
2541
+ * Which reconstructed columns each post-aggregation filter key can reference.
2542
+ * `status` is derived from endedAt + error (see buildWhereClause).
2543
+ */
2544
+ const POSTAGG_FILTER_COLUMNS = {
2545
+ status: ["endedAt", "error"],
2546
+ endedAt: ["endedAt"],
2547
+ tags: ["tags"],
2548
+ metadata: ["metadata"],
2549
+ scope: ["scope"]
2550
+ };
2551
+ /**
2552
+ * Narrow reconstruction used by the slow list paths to evaluate
2553
+ * post-aggregation filters and ordering before pagination. Includes only the
2554
+ * columns the active filters and order field actually reference, so the
2555
+ * full-set aggregate never decompresses the heavy JSON payload columns
2556
+ * (attributes, links, input, output, requestContext) and skips even the
2557
+ * cheaper JSON columns (tags/metadata/scope) unless a filter needs them.
2558
+ */
2559
+ function buildPostAggReconstructSelect(postAgg, orderByField) {
2560
+ const columns = /* @__PURE__ */ new Set();
2561
+ if (orderByField === "endedAt") columns.add("endedAt");
2562
+ for (const key of Object.keys(postAgg)) for (const column of POSTAGG_FILTER_COLUMNS[key] ?? []) columns.add(column);
2563
+ return `
2564
+ SELECT
2565
+ traceId, spanId,
2566
+ ${[...columns].map((col) => `${argMaxNonNull(col)},`).join("\n ")}
2567
+ coalesce(min(timestamp) FILTER (WHERE eventType = 'start'), min(timestamp)) as startedAt
2568
+ FROM span_events
2569
+ `;
2570
+ }
2571
+ /**
2572
+ * Reconstruct spans for the `(traceId, spanId)` pairs selected by `anchorCte`,
2573
+ * scanning only events at/after the CTE's earliest `anchorStartedAt`.
2574
+ *
2575
+ * The `(traceId, spanId) IN (subquery)` semi-join alone cannot be pushed into
2576
+ * the table scan, so without a bound DuckDB decompresses every column of the
2577
+ * entire table just to emit one page of spans. The time bound is a plain range
2578
+ * predicate that zone maps can prune on (insertion order tracks event time).
2579
+ *
2580
+ * Correctness: span events are only 'start' (timestamp = startedAt) and 'end'
2581
+ * (timestamp = endedAt >= startedAt), so every event of an anchored span has
2582
+ * timestamp >= its start-row timestamp >= min(anchorStartedAt). An empty
2583
+ * anchor set makes the bound NULL, which matches the empty IN-list result.
2584
+ */
2585
+ function reconstructForAnchors(reconstructSelect, anchorCte) {
2586
+ return `
2587
+ ${reconstructSelect}
2588
+ WHERE timestamp >= (SELECT min(anchorStartedAt) FROM ${anchorCte})
2589
+ AND (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
2590
+ GROUP BY traceId, spanId`;
2591
+ }
2592
+ /**
2593
+ * Same time-bound trick as {@link reconstructForAnchors}, but with the bound
2594
+ * precomputed in JS and passed as a `?` parameter. The delta-poll query shape
2595
+ * references its candidate CTE multiple times, which makes DuckDB materialize
2596
+ * it and lose the dynamic-filter pushdown a scalar subquery bound relies on —
2597
+ * a literal parameter always reaches the scan as a plain range filter.
2598
+ */
2599
+ function reconstructForAnchorsWithBoundParam(reconstructSelect, anchorCte) {
2600
+ return `
2601
+ ${reconstructSelect}
2602
+ WHERE timestamp >= ?
2603
+ AND (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
2604
+ GROUP BY traceId, spanId`;
2605
+ }
2606
+ function rowToLightSpanRecord(row) {
2607
+ return {
2608
+ traceId: row.traceId,
2609
+ spanId: row.spanId,
2610
+ name: row.name,
2611
+ spanType: row.spanType,
2612
+ parentSpanId: row.parentSpanId ?? null,
2613
+ isEvent: row.isEvent,
2614
+ startedAt: toDate(row.startedAt),
2615
+ endedAt: toDateOrNull(row.endedAt),
2616
+ entityType: row.entityType ?? null,
2617
+ entityId: row.entityId ?? null,
2618
+ entityName: row.entityName ?? null,
2619
+ error: parseJson(row.error),
2620
+ createdAt: toDate(row.startedAt),
2621
+ updatedAt: toDateOrNull(row.endedAt)
2622
+ };
2623
+ }
2624
+ function rowToSpanRecord(row) {
2625
+ return {
2626
+ traceId: row.traceId,
2627
+ spanId: row.spanId,
2628
+ name: row.name,
2629
+ spanType: row.spanType,
2630
+ parentSpanId: row.parentSpanId ?? null,
2631
+ isEvent: row.isEvent,
2632
+ startedAt: toDate(row.startedAt),
2633
+ endedAt: toDateOrNull(row.endedAt),
2634
+ experimentId: row.experimentId ?? null,
2635
+ entityType: row.entityType ?? null,
2636
+ entityId: row.entityId ?? null,
2637
+ entityName: row.entityName ?? null,
2638
+ entityVersionId: row.entityVersionId ?? null,
2639
+ userId: row.userId ?? null,
2640
+ organizationId: row.organizationId ?? null,
2641
+ resourceId: row.resourceId ?? null,
2642
+ runId: row.runId ?? null,
2643
+ sessionId: row.sessionId ?? null,
2644
+ threadId: row.threadId ?? null,
2645
+ requestId: row.requestId ?? null,
2646
+ environment: row.environment ?? null,
2647
+ source: row.source ?? null,
2648
+ serviceName: row.serviceName ?? null,
2649
+ attributes: parseJson(row.attributes),
2650
+ metadata: parseJson(row.metadata),
2651
+ tags: parseJsonArray(row.tags),
2652
+ scope: parseJson(row.scope),
2653
+ links: parseJsonArray(row.links),
2654
+ input: parseJson(row.input),
2655
+ output: parseJson(row.output),
2656
+ error: parseJson(row.error),
2657
+ requestContext: parseJson(row.requestContext),
2658
+ createdAt: toDate(row.startedAt),
2659
+ updatedAt: null
2660
+ };
2661
+ }
2662
+ function buildHasChildErrorClause(hasChildError, rootAlias) {
2663
+ if (hasChildError === void 0) return "";
2664
+ const base = `SELECT 1 FROM span_events c WHERE c.traceId = ${rootAlias}.traceId AND c.spanId != ${rootAlias}.spanId AND c.error IS NOT NULL`;
2665
+ return hasChildError ? `EXISTS (${base})` : `NOT EXISTS (${base})`;
2666
+ }
2667
+ /**
2668
+ * Filter keys that can be evaluated directly against raw `span_events` start
2669
+ * rows. These are stable scalar columns whose value on the start row matches
2670
+ * the reconstructed span value, so pushing them down before reconstruction is
2671
+ * observation-equivalent to reconstructing first and filtering after.
2672
+ */
2673
+ const PREFILTER_KEYS = /* @__PURE__ */ new Set([
2674
+ "traceId",
2675
+ "spanId",
2676
+ "parentSpanId",
2677
+ "name",
2678
+ "spanType",
2679
+ "source",
2680
+ "entityType",
2681
+ "entityId",
2682
+ "entityName",
2683
+ "entityVersionId",
2684
+ "experimentId",
2685
+ "userId",
2686
+ "organizationId",
2687
+ "resourceId",
2688
+ "runId",
2689
+ "sessionId",
2690
+ "threadId",
2691
+ "requestId",
2692
+ "environment",
2693
+ "serviceName"
2694
+ ]);
2695
+ /**
2696
+ * Order-by fields whose start-row value matches the reconstructed root-span
2697
+ * value, so ordering inside the prefilter (before GROUP BY) yields the same
2698
+ * sequence as ordering on reconstructed rows. Anything outside this set must
2699
+ * fall back to the slow path so pagination stays correct.
2700
+ *
2701
+ * `endedAt` is intentionally excluded — start rows always have NULL `endedAt`,
2702
+ * so ordering by it on raw rows would compare NULLs and produce wrong pages.
2703
+ */
2704
+ const SAFE_PREFILTER_ORDER_FIELDS = /* @__PURE__ */ new Set(["startedAt"]);
2705
+ /**
2706
+ * Intersect the existing prefilter timestamp range with an incoming bound.
2707
+ * Each bound is an exact constraint on the start-row `timestamp`, so the
2708
+ * intersection is the **tighter** of the two on each side: the later `start`
2709
+ * wins, the earlier `end` wins. When two bounds tie on a side, the result is
2710
+ * exclusive if either input was exclusive (the union of exclusivity).
2711
+ *
2712
+ * Required for cases like `{ startedAt: { end: B }, endedAt: { end: C } }`:
2713
+ * both bound the start-row timestamp from above and we want `min(B, C)`,
2714
+ * regardless of insertion order.
2715
+ */
2716
+ function intersectTimestampRange(existing, incoming) {
2717
+ if (!existing) return { ...incoming };
2718
+ const merged = { ...existing };
2719
+ if (incoming.start !== void 0) {
2720
+ if (merged.start === void 0 || incoming.start.getTime() > merged.start.getTime()) {
2721
+ merged.start = incoming.start;
2722
+ merged.startExclusive = incoming.startExclusive;
2723
+ } else if (incoming.start.getTime() === merged.start.getTime()) merged.startExclusive = (merged.startExclusive ?? false) || (incoming.startExclusive ?? false);
2724
+ }
2725
+ if (incoming.end !== void 0) {
2726
+ if (merged.end === void 0 || incoming.end.getTime() < merged.end.getTime()) {
2727
+ merged.end = incoming.end;
2728
+ merged.endExclusive = incoming.endExclusive;
2729
+ } else if (incoming.end.getTime() === merged.end.getTime()) merged.endExclusive = (merged.endExclusive ?? false) || (incoming.endExclusive ?? false);
2730
+ }
2731
+ return merged;
2732
+ }
2733
+ /**
2734
+ * Split a span-anchor filter set into a `prefilter` half (pushed to raw
2735
+ * `span_events` start rows) and a `postAgg` half (applied after the
2736
+ * reconstruction GROUP BY). Used by both `listTraces` and `listBranches`.
2737
+ *
2738
+ * `hasChildError` is split out separately since it doesn't run via
2739
+ * `buildWhereClause` — `listTraces` wires it up via EXISTS, `listBranches`
2740
+ * never sees it (not in `branchesFilterSchema`).
2741
+ */
2742
+ function partitionAnchorFilters(filters) {
2743
+ const prefilter = {};
2744
+ const postAgg = {};
2745
+ let hasChildError;
2746
+ for (const [key, value] of Object.entries(filters)) {
2747
+ if (value === void 0 || value === null) continue;
2748
+ if (key === "hasChildError") {
2749
+ if (typeof value === "boolean") hasChildError = value;
2750
+ continue;
2751
+ }
2752
+ if (key === "startedAt") {
2753
+ prefilter.timestamp = intersectTimestampRange(prefilter.timestamp, value);
2754
+ continue;
2755
+ }
2756
+ if (key === "endedAt") {
2757
+ postAgg.endedAt = value;
2758
+ const dateRange = value;
2759
+ if (dateRange?.end) prefilter.timestamp = intersectTimestampRange(prefilter.timestamp, {
2760
+ end: dateRange.end,
2761
+ endExclusive: dateRange.endExclusive
2762
+ });
2763
+ continue;
2764
+ }
2765
+ if (PREFILTER_KEYS.has(key)) {
2766
+ prefilter[key] = value;
2767
+ continue;
2768
+ }
2769
+ postAgg[key] = value;
2770
+ }
2771
+ return {
2772
+ prefilter,
2773
+ postAgg,
2774
+ hasChildError
2775
+ };
2776
+ }
2777
+ function toValuesTuple(row) {
2778
+ return [
2779
+ v(row.eventType),
2780
+ v(row.timestamp),
2781
+ "nextval('span_events_cursor_id_seq')",
2782
+ v(row.traceId),
2783
+ v(row.spanId),
2784
+ v(row.parentSpanId),
2785
+ v(row.name),
2786
+ v(row.spanType),
2787
+ v(row.isEvent),
2788
+ v(row.endedAt),
2789
+ v(row.experimentId),
2790
+ v(row.entityType),
2791
+ v(row.entityId),
2792
+ v(row.entityName),
2793
+ v(row.entityVersionId),
2794
+ v(row.userId),
2795
+ v(row.organizationId),
2796
+ v(row.resourceId),
2797
+ v(row.runId),
2798
+ v(row.sessionId),
2799
+ v(row.threadId),
2800
+ v(row.requestId),
2801
+ v(row.environment),
2802
+ v(row.source),
2803
+ v(row.serviceName),
2804
+ jsonV(row.attributes),
2805
+ jsonV(row.metadata),
2806
+ jsonV(row.tags),
2807
+ jsonV(row.scope),
2808
+ jsonV(row.links),
2809
+ jsonV(row.input),
2810
+ jsonV(row.output),
2811
+ jsonV(row.error),
2812
+ jsonV(row.requestContext)
2813
+ ].join(", ");
2814
+ }
2815
+ async function insertSpanEvents(db, rows) {
2816
+ if (rows.length === 0) return;
2817
+ const tuples = rows.map((row) => `(${toValuesTuple(row)})`).join(",\n");
2818
+ await db.execute(`INSERT INTO span_events (${COLUMNS_SQL}) VALUES ${tuples}`);
2819
+ }
2820
+ function createStartSpanRow(s) {
2821
+ return {
2822
+ eventType: "start",
2823
+ timestamp: s.startedAt,
2824
+ traceId: s.traceId,
2825
+ spanId: s.spanId,
2826
+ parentSpanId: s.parentSpanId ?? null,
2827
+ name: s.name,
2828
+ spanType: s.spanType,
2829
+ isEvent: s.isEvent,
2830
+ endedAt: null,
2831
+ experimentId: s.experimentId ?? null,
2832
+ entityType: s.entityType ?? null,
2833
+ entityId: s.entityId ?? null,
2834
+ entityName: s.entityName ?? null,
2835
+ entityVersionId: s.entityVersionId ?? null,
2836
+ userId: s.userId ?? null,
2837
+ organizationId: s.organizationId ?? null,
2838
+ resourceId: s.resourceId ?? null,
2839
+ runId: s.runId ?? null,
2840
+ sessionId: s.sessionId ?? null,
2841
+ threadId: s.threadId ?? null,
2842
+ requestId: s.requestId ?? null,
2843
+ environment: s.environment ?? null,
2844
+ source: s.source ?? null,
2845
+ serviceName: s.serviceName ?? null,
2846
+ attributes: s.attributes ?? null,
2847
+ metadata: s.metadata ?? null,
2848
+ tags: s.tags ?? null,
2849
+ scope: s.scope ?? null,
2850
+ links: null,
2851
+ input: s.input ?? null,
2852
+ output: null,
2853
+ error: null,
2854
+ requestContext: s.requestContext ?? null
2855
+ };
2856
+ }
2857
+ function createEndSpanRow(s) {
2858
+ return {
2859
+ eventType: "end",
2860
+ timestamp: s.endedAt,
2861
+ traceId: s.traceId,
2862
+ spanId: s.spanId,
2863
+ parentSpanId: s.parentSpanId ?? null,
2864
+ name: s.name,
2865
+ spanType: s.spanType,
2866
+ isEvent: s.isEvent,
2867
+ endedAt: s.endedAt ?? null,
2868
+ experimentId: s.experimentId ?? null,
2869
+ entityType: s.entityType ?? null,
2870
+ entityId: s.entityId ?? null,
2871
+ entityName: s.entityName ?? null,
2872
+ entityVersionId: s.entityVersionId ?? null,
2873
+ userId: s.userId ?? null,
2874
+ organizationId: s.organizationId ?? null,
2875
+ resourceId: s.resourceId ?? null,
2876
+ runId: s.runId ?? null,
2877
+ sessionId: s.sessionId ?? null,
2878
+ threadId: s.threadId ?? null,
2879
+ requestId: s.requestId ?? null,
2880
+ environment: s.environment ?? null,
2881
+ source: s.source ?? null,
2882
+ serviceName: s.serviceName ?? null,
2883
+ attributes: s.attributes ?? null,
2884
+ metadata: s.metadata ?? null,
2885
+ tags: s.tags ?? null,
2886
+ scope: s.scope ?? null,
2887
+ links: s.links ?? null,
2888
+ input: s.input ?? null,
2889
+ output: s.output ?? null,
2890
+ error: s.error ?? null,
2891
+ requestContext: s.requestContext ?? null
2892
+ };
2893
+ }
2894
+ /** Insert a 'start' event for a new span. */
2895
+ async function createSpan(db, args) {
2896
+ const rows = [createStartSpanRow(args.span)];
2897
+ if (args.span.endedAt) rows.push(createEndSpanRow(args.span));
2898
+ await insertSpanEvents(db, rows);
2899
+ }
2900
+ /** Insert 'start' events for multiple spans in a single statement. */
2901
+ async function batchCreateSpans(db, args) {
2902
+ if (args.records.length === 0) return;
2903
+ await insertSpanEvents(db, args.records.flatMap((record) => {
2904
+ const events = [createStartSpanRow(record)];
2905
+ if (record.endedAt) events.push(createEndSpanRow(record));
2906
+ return events;
2907
+ }));
2908
+ }
2909
+ /** Delete all span events for the given trace IDs. */
2910
+ async function batchDeleteTraces(db, args) {
2911
+ if (args.traceIds.length === 0) return;
2912
+ const placeholders = args.traceIds.map(() => "?").join(", ");
2913
+ await db.execute(`DELETE FROM span_events WHERE traceId IN (${placeholders})`, args.traceIds);
2914
+ }
2915
+ /** Reconstruct a single span from its event history. */
2916
+ async function getSpan(db, args) {
2917
+ const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT} WHERE traceId = ? AND spanId = ? GROUP BY traceId, spanId`, [args.traceId, args.spanId]);
2918
+ if (rows.length === 0) return null;
2919
+ return { span: rowToSpanRecord(rows[0]) };
2920
+ }
2921
+ /** Reconstruct the root span (no parent) for a trace. */
2922
+ async function getRootSpan(db, args) {
2923
+ const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT} WHERE traceId = ? GROUP BY traceId, spanId HAVING arg_max(parentSpanId, timestamp) IS NULL LIMIT 1`, [args.traceId]);
2924
+ if (rows.length === 0) return null;
2925
+ return { span: rowToSpanRecord(rows[0]) };
2926
+ }
2927
+ /** Reconstruct all spans belonging to a trace. */
2928
+ async function getTrace(db, args) {
2929
+ const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT} WHERE traceId = ? GROUP BY traceId, spanId`, [args.traceId]);
2930
+ if (rows.length === 0) return null;
2931
+ return {
2932
+ traceId: args.traceId,
2933
+ spans: rows.map((row) => rowToSpanRecord(row))
2934
+ };
2935
+ }
2936
+ /** Reconstruct lightweight spans belonging to a trace (timeline fields only). */
2937
+ async function getTraceLight(db, args) {
2938
+ const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT_LIGHT} WHERE traceId = ? GROUP BY traceId, spanId`, [args.traceId]);
2939
+ if (rows.length === 0) return null;
2940
+ return {
2941
+ traceId: args.traceId,
2942
+ spans: rows.map((row) => rowToLightSpanRecord(row))
2943
+ };
2944
+ }
2945
+ /**
2946
+ * List root spans (traces) with filtering, ordering, and pagination.
2947
+ *
2948
+ * Instead of reconstructing every span in the table and then filtering, we:
2949
+ * 1. Pick candidate root `(traceId, spanId)` tuples from raw `span_events`
2950
+ * by looking only at rows where `eventType = 'start'` and
2951
+ * `parentSpanId IS NULL`. All scalar filters (entity*, *Id, service,
2952
+ * environment, startedAt range, ...) run here, against raw columns.
2953
+ * 2. Fully reconstruct spans only for that narrowed set, then apply
2954
+ * post-aggregation filters (status/tags/metadata/scope/endedAt/
2955
+ * hasChildError).
2956
+ *
2957
+ * When there are no post-aggregation filters, ordering + pagination happen
2958
+ * inside the prefilter CTE so reconstruction runs on at most `perPage` rows.
2959
+ */
2960
+ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
2961
+ const filters = args.filters ?? {};
2962
+ const page = Number(args.pagination?.page ?? 0);
2963
+ const perPage = Number(args.pagination?.perPage ?? 10);
2964
+ const orderBy = {
2965
+ field: args.orderBy?.field ?? "startedAt",
2966
+ direction: args.orderBy?.direction ?? "DESC"
2967
+ };
2968
+ const { prefilter, postAgg, hasChildError } = partitionAnchorFilters(filters);
2969
+ const { clause: prefilterClause, params: prefilterParams } = buildWhereClause(prefilter);
2970
+ const prefilterParts = [`eventType = 'start'`, `parentSpanId IS NULL`];
2971
+ if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
2972
+ const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
2973
+ const outerAlias = "outer_root";
2974
+ const orderDir = orderBy.direction.toUpperCase();
2975
+ if (orderDir !== "ASC" && orderDir !== "DESC") throw new Error(`Invalid sort direction: ${orderBy.direction}`);
2976
+ const canOrderInPrefilter = SAFE_PREFILTER_ORDER_FIELDS.has(orderBy.field);
2977
+ if (!(Object.keys(postAgg).length > 0 || hasChildError !== void 0 || !canOrderInPrefilter)) {
2978
+ const prefilterOrderBy = `ORDER BY timestamp ${orderDir}`;
2979
+ const offset = page * perPage;
2980
+ const countSql = `
2981
+ SELECT COUNT(*) as total
2982
+ FROM span_events AS ${outerAlias}
2983
+ ${prefilterWhere}
2984
+ `;
2985
+ const countResult = await db.query(countSql, prefilterParams);
2986
+ const total = Number(countResult[0]?.total ?? 0);
2987
+ const pageSql = `
2988
+ WITH page_roots AS (
2989
+ SELECT traceId, spanId, timestamp AS anchorStartedAt
2990
+ FROM span_events AS ${outerAlias}
2991
+ ${prefilterWhere}
2992
+ ${prefilterOrderBy}
2993
+ LIMIT ? OFFSET ?
2994
+ )
2995
+ ${reconstructForAnchors(reconstructSelect, "page_roots")}
2996
+ ${buildOrderByClause(orderBy)}
2997
+ `;
2998
+ const spans = (await db.query(pageSql, [
2999
+ ...prefilterParams,
3000
+ perPage,
3001
+ offset
3002
+ ])).map((row) => mapRow(row));
3003
+ return {
3004
+ pagination: {
3005
+ total,
3006
+ page,
3007
+ perPage,
3008
+ hasMore: (page + 1) * perPage < total
3009
+ },
3010
+ spans: toSpans(spans)
3011
+ };
3012
+ }
3013
+ const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
3014
+ const postAggParts = [];
3015
+ if (postAggClause) postAggParts.push(postAggClause.replace(/^WHERE\s+/i, ""));
3016
+ const childErrorClause = buildHasChildErrorClause(hasChildError, "root_spans");
3017
+ if (childErrorClause) postAggParts.push(childErrorClause);
3018
+ const postAggWhere = postAggParts.length > 0 ? `WHERE ${postAggParts.join(" AND ")}` : "";
3019
+ const cteSql = `
3020
+ WITH candidate_roots AS (
3021
+ SELECT traceId, spanId
3022
+ FROM span_events AS ${outerAlias}
3023
+ ${prefilterWhere}
3024
+ ),
3025
+ root_spans AS (
3026
+ ${buildPostAggReconstructSelect(postAgg, orderBy.field)}
3027
+ WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_roots)
3028
+ GROUP BY traceId, spanId
3029
+ )
3030
+ `;
3031
+ const orderByClause = buildOrderByClause(orderBy);
3032
+ const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
3033
+ page,
3034
+ perPage
3035
+ });
3036
+ const countSql = `
3037
+ ${cteSql}
3038
+ SELECT COUNT(*) as total FROM root_spans ${postAggWhere}
3039
+ `;
3040
+ const countResult = await db.query(countSql, [...prefilterParams, ...postAggParams]);
3041
+ const total = Number(countResult[0]?.total ?? 0);
3042
+ const dataSql = `
3043
+ ${cteSql},
3044
+ page_roots AS (
3045
+ SELECT traceId, spanId, startedAt AS anchorStartedAt
3046
+ FROM root_spans ${postAggWhere} ${orderByClause} ${paginationClause}
3047
+ )
3048
+ ${reconstructForAnchors(reconstructSelect, "page_roots")}
3049
+ ${orderByClause}
3050
+ `;
3051
+ const spans = (await db.query(dataSql, [
3052
+ ...prefilterParams,
3053
+ ...postAggParams,
3054
+ ...paginationParams
3055
+ ])).map((row) => mapRow(row));
3056
+ return {
3057
+ pagination: {
3058
+ total,
3059
+ page,
3060
+ perPage,
3061
+ hasMore: (page + 1) * perPage < total
3062
+ },
3063
+ spans: toSpans(spans)
3064
+ };
3065
+ }
3066
+ async function listTraces(db, args) {
3067
+ const { mode, filters, pagination, orderBy, after, limit } = listTracesArgsSchema.parse(args);
3068
+ const filterRecord = filters ?? {};
3069
+ if (mode === "delta") {
3070
+ assertDeltaPollingEnabled();
3071
+ const streamHeadCursor = await getTraceStreamHeadCursor(db);
3072
+ if (after === void 0) return {
3073
+ spans: [],
3074
+ delta: {
3075
+ limit,
3076
+ hasMore: false
3077
+ },
3078
+ deltaCursor: streamHeadCursor
3079
+ };
3080
+ const afterCursorId = validateCursorId(after);
3081
+ const { prefilter, postAgg, hasChildError } = partitionAnchorFilters(filterRecord);
3082
+ const { clause: prefilterClause, params: prefilterParams } = buildWhereClause(prefilter);
3083
+ const prefilterParts = [
3084
+ `eventType = 'start'`,
3085
+ `parentSpanId IS NULL`,
3086
+ `cursorId IS NOT NULL`,
3087
+ `cursorId > CAST(? AS BIGINT)`
3088
+ ];
3089
+ if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
3090
+ const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
3091
+ const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
3092
+ const postAggParts = [];
3093
+ if (postAggClause) postAggParts.push(postAggClause.replace(/^WHERE\s+/i, ""));
3094
+ const childErrorClause = buildHasChildErrorClause(hasChildError, "root_spans");
3095
+ if (childErrorClause) postAggParts.push(childErrorClause);
3096
+ const postAggWhere = postAggParts.length > 0 ? `WHERE ${postAggParts.join(" AND ")}` : "";
3097
+ const outerAlias = "outer_root";
3098
+ const minTs = (await db.query(`SELECT min(timestamp) as minTs FROM span_events AS ${outerAlias} ${prefilterWhere}`, [afterCursorId, ...prefilterParams]))[0]?.minTs ?? null;
3099
+ if (minTs === null) return {
3100
+ spans: [],
3101
+ delta: {
3102
+ limit,
3103
+ hasMore: false
3104
+ },
3105
+ deltaCursor: streamHeadCursor
3106
+ };
3107
+ const dataSql = `
3108
+ WITH candidate_roots AS (
3109
+ SELECT traceId, spanId, cursorId
3110
+ FROM span_events AS ${outerAlias}
3111
+ ${prefilterWhere}
3112
+ ),
3113
+ root_spans AS (
3114
+ SELECT reconstructed.*, candidate_roots.cursorId AS anchorCursorId
3115
+ FROM (
3116
+ ${reconstructForAnchorsWithBoundParam(SPAN_RECONSTRUCT_SELECT, "candidate_roots")}
3117
+ ) AS reconstructed
3118
+ INNER JOIN candidate_roots USING (traceId, spanId)
3119
+ )
3120
+ SELECT * FROM root_spans ${postAggWhere} ORDER BY anchorCursorId ASC LIMIT ?
3121
+ `;
3122
+ const rows = await db.query(dataSql, [
3123
+ afterCursorId,
3124
+ ...prefilterParams,
3125
+ minTs,
3126
+ ...postAggParams,
3127
+ limit + 1
3128
+ ]);
3129
+ const visibleRows = rows.slice(0, limit).map((row) => ({
3130
+ cursorId: row.anchorCursorId,
3131
+ span: rowToSpanRecord(row)
3132
+ }));
3133
+ return {
3134
+ spans: toTraceSpans(visibleRows.map((row) => row.span)),
3135
+ delta: {
3136
+ limit,
3137
+ hasMore: rows.length > limit
3138
+ },
3139
+ deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
3140
+ };
3141
+ }
3142
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getTraceDeltaCursor(db, filters) : void 0;
3143
+ const { pagination: resultPagination, spans } = await listTraceRows(db, {
3144
+ filters,
3145
+ pagination,
3146
+ orderBy
3147
+ }, SPAN_RECONSTRUCT_SELECT, rowToSpanRecord, toTraceSpans);
3148
+ return {
3149
+ pagination: resultPagination,
3150
+ spans,
3151
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
3152
+ };
3153
+ }
3154
+ async function listTracesLight(db, args) {
3155
+ return listTraceRows(db, args, SPAN_RECONSTRUCT_SELECT_LIGHT, rowToLightSpanRecord, (spans) => spans);
3156
+ }
3157
+ const BRANCH_SPAN_TYPE_PLACEHOLDERS = BRANCH_SPAN_TYPES.map(() => "?").join(", ");
3158
+ /**
3159
+ * Reconstruct multiple spans by spanId within a single trace. Single round-trip
3160
+ * fetch used by the optimized {@link import('@mastra/core/storage').getBranch}
3161
+ * path: getStructure walks the skeleton to identify branch spanIds, then this
3162
+ * pulls full data for only those spans instead of the whole trace.
3163
+ */
3164
+ async function getSpans(db, args) {
3165
+ if (args.spanIds.length === 0) return {
3166
+ traceId: args.traceId,
3167
+ spans: []
3168
+ };
3169
+ const placeholders = args.spanIds.map(() => "?").join(", ");
3170
+ const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT}
3171
+ WHERE traceId = ? AND spanId IN (${placeholders})
3172
+ GROUP BY traceId, spanId`, [args.traceId, ...args.spanIds]);
3173
+ return {
3174
+ traceId: args.traceId,
3175
+ spans: rows.map((row) => rowToSpanRecord(row))
3176
+ };
3177
+ }
3178
+ /**
3179
+ * List branch anchor spans (named-entity invocations) across all traces with
3180
+ * filtering, ordering, and pagination.
3181
+ *
3182
+ * Same two-stage strategy as `listTraces`:
3183
+ * 1. Pick candidate anchor `(traceId, spanId)` tuples from raw `span_events`
3184
+ * by looking only at `eventType = 'start'` rows whose `spanType` is in
3185
+ * {@link BRANCH_SPAN_TYPES}. Scalar filters (entity*, *Id, environment,
3186
+ * serviceName, startedAt range, ...) run here, against raw columns. This
3187
+ * avoids paying reconstruction cost for the high-volume sub-operation
3188
+ * events (MODEL_STEP, MODEL_CHUNK, ...) that are never anchors.
3189
+ * 2. Reconstruct full span data only for that narrowed set, then apply
3190
+ * post-aggregation filters (status / metadata / tags / endedAt range).
3191
+ *
3192
+ * When there are no post-aggregation filters, ordering + pagination happen
3193
+ * inside the prefilter so reconstruction runs on at most `perPage` rows.
3194
+ */
3195
+ async function listBranches(db, args) {
3196
+ const { mode, filters, pagination, orderBy, after, limit } = listBranchesArgsSchema.parse(args);
3197
+ const filterRecord = filters ?? {};
3198
+ const page = Number(pagination.page);
3199
+ const perPage = Number(pagination.perPage);
3200
+ const userSpanType = filterRecord.spanType;
3201
+ if (typeof userSpanType === "string" && !BRANCH_SPAN_TYPES.includes(userSpanType)) {
3202
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getBranchDeltaCursor(db, filters) : void 0;
3203
+ if (mode === "delta") {
3204
+ assertDeltaPollingEnabled();
3205
+ return {
3206
+ branches: [],
3207
+ delta: {
3208
+ limit,
3209
+ hasMore: false
3210
+ },
3211
+ deltaCursor: currentDeltaCursor
3212
+ };
3213
+ }
3214
+ return {
3215
+ pagination: {
3216
+ total: 0,
3217
+ page,
3218
+ perPage,
3219
+ hasMore: false
3220
+ },
3221
+ branches: [],
3222
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
3223
+ };
3224
+ }
3225
+ if (mode === "delta") {
3226
+ assertDeltaPollingEnabled();
3227
+ const streamHeadCursor = await getBranchStreamHeadCursor(db, typeof userSpanType === "string" ? userSpanType : null);
3228
+ if (after === void 0) return {
3229
+ branches: [],
3230
+ delta: {
3231
+ limit,
3232
+ hasMore: false
3233
+ },
3234
+ deltaCursor: streamHeadCursor
3235
+ };
3236
+ const afterCursorId = validateCursorId(after);
3237
+ const { spanType: _spanType, ...rest } = filterRecord;
3238
+ const { prefilter, postAgg, hasChildError: _hasChildError } = partitionAnchorFilters(rest);
3239
+ const { clause: prefilterClause, params: prefilterFilterParams } = buildWhereClause(prefilter);
3240
+ const prefilterParts = [
3241
+ `eventType = 'start'`,
3242
+ `cursorId IS NOT NULL`,
3243
+ `cursorId > CAST(? AS BIGINT)`
3244
+ ];
3245
+ let spanTypeParams;
3246
+ if (typeof userSpanType === "string") {
3247
+ prefilterParts.push(`spanType = ?`);
3248
+ spanTypeParams = [userSpanType];
3249
+ } else {
3250
+ prefilterParts.push(`spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS})`);
3251
+ spanTypeParams = [...BRANCH_SPAN_TYPES];
3252
+ }
3253
+ if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
3254
+ const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
3255
+ const prefilterParams = [
3256
+ afterCursorId,
3257
+ ...spanTypeParams,
3258
+ ...prefilterFilterParams
3259
+ ];
3260
+ const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
3261
+ const postAggWhere = postAggClause ? postAggClause : "";
3262
+ const outerAlias = "outer_anchor";
3263
+ const minTs = (await db.query(`SELECT min(timestamp) as minTs FROM span_events AS ${outerAlias} ${prefilterWhere}`, prefilterParams))[0]?.minTs ?? null;
3264
+ if (minTs === null) return {
3265
+ branches: [],
3266
+ delta: {
3267
+ limit,
3268
+ hasMore: false
3269
+ },
3270
+ deltaCursor: streamHeadCursor
3271
+ };
3272
+ const dataSql = `
3273
+ WITH candidate_anchors AS (
3274
+ SELECT traceId, spanId, cursorId
3275
+ FROM span_events AS ${outerAlias}
3276
+ ${prefilterWhere}
3277
+ ),
3278
+ branch_anchors AS (
3279
+ SELECT reconstructed.*, candidate_anchors.cursorId AS anchorCursorId
3280
+ FROM (
3281
+ ${reconstructForAnchorsWithBoundParam(SPAN_RECONSTRUCT_SELECT, "candidate_anchors")}
3282
+ ) AS reconstructed
3283
+ INNER JOIN candidate_anchors USING (traceId, spanId)
3284
+ )
3285
+ SELECT * FROM branch_anchors ${postAggWhere} ORDER BY anchorCursorId ASC LIMIT ?
3286
+ `;
3287
+ const rows = await db.query(dataSql, [
3288
+ ...prefilterParams,
3289
+ minTs,
3290
+ ...postAggParams,
3291
+ limit + 1
3292
+ ]);
3293
+ const visibleRows = rows.slice(0, limit).map((row) => ({
3294
+ cursorId: row.anchorCursorId,
3295
+ branch: rowToSpanRecord(row)
3296
+ }));
3297
+ return {
3298
+ branches: toTraceSpans(visibleRows.map((row) => row.branch)),
3299
+ delta: {
3300
+ limit,
3301
+ hasMore: rows.length > limit
3302
+ },
3303
+ deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
3304
+ };
3305
+ }
3306
+ const { spanType: _spanType, ...rest } = filterRecord;
3307
+ const { prefilter, postAgg, hasChildError: _hasChildError } = partitionAnchorFilters(rest);
3308
+ const { clause: prefilterClause, params: prefilterFilterParams } = buildWhereClause(prefilter);
3309
+ const prefilterParts = [`eventType = 'start'`];
3310
+ let spanTypeParams;
3311
+ if (typeof userSpanType === "string") {
3312
+ prefilterParts.push(`spanType = ?`);
3313
+ spanTypeParams = [userSpanType];
3314
+ } else {
3315
+ prefilterParts.push(`spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS})`);
3316
+ spanTypeParams = [...BRANCH_SPAN_TYPES];
3317
+ }
3318
+ if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
3319
+ const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
3320
+ const prefilterParams = [...spanTypeParams, ...prefilterFilterParams];
3321
+ const outerAlias = "outer_anchor";
3322
+ const orderDir = orderBy.direction.toUpperCase();
3323
+ if (orderDir !== "ASC" && orderDir !== "DESC") throw new Error(`Invalid sort direction: ${orderBy.direction}`);
3324
+ const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getBranchDeltaCursor(db, filters) : void 0;
3325
+ const canOrderInPrefilter = SAFE_PREFILTER_ORDER_FIELDS.has(orderBy.field);
3326
+ if (!(Object.keys(postAgg).length > 0 || !canOrderInPrefilter)) {
3327
+ const prefilterOrderBy = `ORDER BY timestamp ${orderDir}`;
3328
+ const offset = page * perPage;
3329
+ const countSql = `
3330
+ SELECT COUNT(*) as total
3331
+ FROM span_events AS ${outerAlias}
3332
+ ${prefilterWhere}
3333
+ `;
3334
+ const countResult = await db.query(countSql, prefilterParams);
3335
+ const total = Number(countResult[0]?.total ?? 0);
3336
+ if (total === 0) return {
3337
+ pagination: {
3338
+ total: 0,
3339
+ page,
3340
+ perPage,
3341
+ hasMore: false
3342
+ },
3343
+ branches: [],
3344
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
3345
+ };
3346
+ const pageSql = `
3347
+ WITH page_anchors AS (
3348
+ SELECT traceId, spanId, timestamp AS anchorStartedAt
3349
+ FROM span_events AS ${outerAlias}
3350
+ ${prefilterWhere}
3351
+ ${prefilterOrderBy}
3352
+ LIMIT ? OFFSET ?
3353
+ )
3354
+ ${reconstructForAnchors(SPAN_RECONSTRUCT_SELECT, "page_anchors")}
3355
+ ${buildOrderByClause(orderBy)}
3356
+ `;
3357
+ const spans = (await db.query(pageSql, [
3358
+ ...prefilterParams,
3359
+ perPage,
3360
+ offset
3361
+ ])).map((row) => rowToSpanRecord(row));
3362
+ return {
3363
+ pagination: {
3364
+ total,
3365
+ page,
3366
+ perPage,
3367
+ hasMore: (page + 1) * perPage < total
3368
+ },
3369
+ branches: toTraceSpans(spans),
3370
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
3371
+ };
3372
+ }
3373
+ const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
3374
+ const postAggWhere = postAggClause ? postAggClause : "";
3375
+ const orderByClause = buildOrderByClause(orderBy);
3376
+ const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
3377
+ page,
3378
+ perPage
3379
+ });
3380
+ const cteSql = `
3381
+ WITH candidate_anchors AS (
3382
+ SELECT traceId, spanId
3383
+ FROM span_events AS ${outerAlias}
3384
+ ${prefilterWhere}
3385
+ ),
3386
+ branch_anchors AS (
3387
+ ${buildPostAggReconstructSelect(postAgg, orderBy.field)}
3388
+ WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_anchors)
3389
+ GROUP BY traceId, spanId
3390
+ )
3391
+ `;
3392
+ const countSql = `
3393
+ ${cteSql}
3394
+ SELECT COUNT(*) as total FROM branch_anchors ${postAggWhere}
3395
+ `;
3396
+ const countResult = await db.query(countSql, [...prefilterParams, ...postAggParams]);
3397
+ const total = Number(countResult[0]?.total ?? 0);
3398
+ if (total === 0) return {
3399
+ pagination: {
3400
+ total: 0,
3401
+ page,
3402
+ perPage,
3403
+ hasMore: false
3404
+ },
3405
+ branches: [],
3406
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
3407
+ };
3408
+ const dataSql = `
3409
+ ${cteSql},
3410
+ page_anchors AS (
3411
+ SELECT traceId, spanId, startedAt AS anchorStartedAt
3412
+ FROM branch_anchors ${postAggWhere} ${orderByClause} ${paginationClause}
3413
+ )
3414
+ ${reconstructForAnchors(SPAN_RECONSTRUCT_SELECT, "page_anchors")}
3415
+ ${orderByClause}
3416
+ `;
3417
+ const spans = (await db.query(dataSql, [
3418
+ ...prefilterParams,
3419
+ ...postAggParams,
3420
+ ...paginationParams
3421
+ ])).map((row) => rowToSpanRecord(row));
3422
+ return {
3423
+ pagination: {
3424
+ total,
3425
+ page,
3426
+ perPage,
3427
+ hasMore: (page + 1) * perPage < total
3428
+ },
3429
+ branches: toTraceSpans(spans),
3430
+ ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
3431
+ };
3432
+ }
3433
+ async function getTraceDeltaCursor(db, filters) {
3434
+ const { prefilter, postAgg, hasChildError } = partitionAnchorFilters(filters ?? {});
3435
+ const { clause: prefilterClause, params: prefilterParams } = buildWhereClause(prefilter);
3436
+ const prefilterParts = [
3437
+ `eventType = 'start'`,
3438
+ `parentSpanId IS NULL`,
3439
+ `cursorId IS NOT NULL`
3440
+ ];
3441
+ if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
3442
+ const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
3443
+ const outerAlias = "outer_root";
3444
+ const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
3445
+ const postAggParts = [];
3446
+ if (postAggClause) postAggParts.push(postAggClause.replace(/^WHERE\s+/i, ""));
3447
+ const childErrorClause = buildHasChildErrorClause(hasChildError, "root_spans");
3448
+ if (childErrorClause) postAggParts.push(childErrorClause);
3449
+ const postAggWhere = postAggParts.length > 0 ? `WHERE ${postAggParts.join(" AND ")}` : "";
3450
+ if (postAggWhere === "") {
3451
+ const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM span_events AS ${outerAlias} ${prefilterWhere}`, prefilterParams))[0]?.cursorId;
3452
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
3453
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND parentSpanId IS NULL AND cursorId IS NOT NULL`))[0]?.cursorId);
3454
+ }
3455
+ const cteSql = `
3456
+ WITH candidate_roots AS (
3457
+ SELECT traceId, spanId, cursorId
3458
+ FROM span_events AS ${outerAlias}
3459
+ ${prefilterWhere}
3460
+ ),
3461
+ root_spans AS (
3462
+ SELECT reconstructed.*, candidate_roots.cursorId AS anchorCursorId
3463
+ FROM (
3464
+ ${SPAN_RECONSTRUCT_SELECT}
3465
+ WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_roots)
3466
+ GROUP BY traceId, spanId
3467
+ ) AS reconstructed
3468
+ INNER JOIN candidate_roots USING (traceId, spanId)
3469
+ )
3470
+ `;
3471
+ const cursorId = (await db.query(`${cteSql} SELECT max(anchorCursorId) AS cursorId FROM root_spans ${postAggWhere}`, [...prefilterParams, ...postAggParams]))[0]?.cursorId;
3472
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
3473
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND parentSpanId IS NULL AND cursorId IS NOT NULL`))[0]?.cursorId);
3474
+ }
3475
+ async function getTraceStreamHeadCursor(db) {
3476
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND parentSpanId IS NULL AND cursorId IS NOT NULL`))[0]?.cursorId);
3477
+ }
3478
+ async function getBranchDeltaCursor(db, filters) {
3479
+ const filterRecord = filters ?? {};
3480
+ const userSpanType = filterRecord.spanType;
3481
+ const { spanType: _spanType, ...rest } = filterRecord;
3482
+ const { prefilter, postAgg } = partitionAnchorFilters(rest);
3483
+ const { clause: prefilterClause, params: prefilterFilterParams } = buildWhereClause(prefilter);
3484
+ const prefilterParts = [`eventType = 'start'`, `cursorId IS NOT NULL`];
3485
+ let spanTypeParams;
3486
+ if (typeof userSpanType === "string") {
3487
+ prefilterParts.push(`spanType = ?`);
3488
+ spanTypeParams = [userSpanType];
3489
+ } else {
3490
+ prefilterParts.push(`spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS})`);
3491
+ spanTypeParams = [...BRANCH_SPAN_TYPES];
3492
+ }
3493
+ if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
3494
+ const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
3495
+ const prefilterParams = [...spanTypeParams, ...prefilterFilterParams];
3496
+ const outerAlias = "outer_anchor";
3497
+ const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
3498
+ if (!postAggClause) {
3499
+ const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM span_events AS ${outerAlias} ${prefilterWhere}`, prefilterParams))[0]?.cursorId;
3500
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
3501
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS}) AND cursorId IS NOT NULL`, [...BRANCH_SPAN_TYPES]))[0]?.cursorId);
3502
+ }
3503
+ const cteSql = `
3504
+ WITH candidate_anchors AS (
3505
+ SELECT traceId, spanId, cursorId
3506
+ FROM span_events AS ${outerAlias}
3507
+ ${prefilterWhere}
3508
+ ),
3509
+ branch_anchors AS (
3510
+ SELECT reconstructed.*, candidate_anchors.cursorId AS anchorCursorId
3511
+ FROM (
3512
+ ${SPAN_RECONSTRUCT_SELECT}
3513
+ WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_anchors)
3514
+ GROUP BY traceId, spanId
3515
+ ) AS reconstructed
3516
+ INNER JOIN candidate_anchors USING (traceId, spanId)
3517
+ )
3518
+ `;
3519
+ const cursorId = (await db.query(`${cteSql} SELECT max(anchorCursorId) AS cursorId FROM branch_anchors ${postAggClause}`, [...prefilterParams, ...postAggParams]))[0]?.cursorId;
3520
+ if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
3521
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS}) AND cursorId IS NOT NULL`, [...BRANCH_SPAN_TYPES]))[0]?.cursorId);
3522
+ }
3523
+ async function getBranchStreamHeadCursor(db, userSpanType) {
3524
+ if (userSpanType) return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType = ? AND cursorId IS NOT NULL`, [userSpanType]))[0]?.cursorId);
3525
+ return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS}) AND cursorId IS NOT NULL`, [...BRANCH_SPAN_TYPES]))[0]?.cursorId);
3526
+ }
3527
+ //#endregion
3528
+ //#region src/storage/domains/observability/index.ts
3529
+ function buildSignalMigrationRequiredMessage(args) {
3530
+ return `
3531
+ ===========================================================================
3532
+ MIGRATION REQUIRED: DuckDB observability signal tables need signal IDs
3533
+ ===========================================================================
3534
+
3535
+ The following signal tables still use the legacy schema and must be migrated
3536
+ before observability storage can initialize:
3537
+
3538
+ ${args.tables.map((table) => ` - ${table.table}`).join("\n")}\n\nTo fix this, run the manual migration command:\n\n npx mastra migrate\n\nThis command will:\n 1. Create replacement signal tables with signal-ID primary keys\n 2. Backfill missing signal IDs for legacy rows\n 3. Swap the migrated tables into place\n\nWARNING: This migration recreates the signal tables and may take significant\ntime for large databases. Please ensure you have a backup before proceeding.\n===========================================================================\n`;
3539
+ }
3540
+ /**
3541
+ * DuckDB-backed observability storage for traces, metrics, logs, scores, and feedback.
3542
+ * Uses an append-only event-sourced model with SQL-based reconstruction for spans.
3543
+ */
3544
+ var ObservabilityStorageDuckDB = class extends ObservabilityStorage {
3545
+ db;
3546
+ constructor(config) {
3547
+ super();
3548
+ this.db = config.db;
3549
+ }
3550
+ /** Create all observability tables if they don't exist. */
3551
+ async init() {
3552
+ const migrationStatus = await checkSignalTablesMigrationStatus(this.db);
3553
+ if (migrationStatus.needsMigration) throw new MastraError({
3554
+ id: createStorageErrorId("DUCKDB", "MIGRATION_REQUIRED", "SIGNAL_TABLES"),
3555
+ domain: ErrorDomain.STORAGE,
3556
+ category: ErrorCategory.USER,
3557
+ text: buildSignalMigrationRequiredMessage({ tables: migrationStatus.tables.map(({ table }) => ({ table })) })
3558
+ });
3559
+ await this.db.executeBatch([...ALL_DDL, ...ALL_MIGRATIONS]);
3560
+ await dropLegacyCursorIdDefaults(this.db);
3561
+ }
3562
+ /**
3563
+ * Manually migrate legacy signal tables to the signal-ID primary-key schema.
3564
+ * The public method name is historical; the CLI still calls `migrateSpans()`
3565
+ * for observability migrations even though this now also migrates signal tables.
3566
+ */
3567
+ async migrateSpans() {
3568
+ const migrationStatus = await checkSignalTablesMigrationStatus(this.db);
3569
+ if (!migrationStatus.needsMigration) return {
3570
+ success: true,
3571
+ alreadyMigrated: true,
3572
+ duplicatesRemoved: 0,
3573
+ message: "Migration already complete. Signal tables already use signal-ID primary keys."
3574
+ };
3575
+ await migrateSignalTables(this.db, this.logger);
3576
+ return {
3577
+ success: true,
3578
+ alreadyMigrated: false,
3579
+ duplicatesRemoved: 0,
3580
+ message: `Migration complete. Migrated signal tables: ${migrationStatus.tables.map((t) => t.table).join(", ")}.`
3581
+ };
3582
+ }
3583
+ /** Delete all rows from every observability table. Use with caution. */
3584
+ async dangerouslyClearAll() {
3585
+ for (const table of [
3586
+ "span_events",
3587
+ "metric_events",
3588
+ "log_events",
3589
+ "score_events",
3590
+ "feedback_events"
3591
+ ]) await this.db.execute(`TRUNCATE TABLE ${table}`);
3592
+ }
3593
+ get observabilityStrategy() {
3594
+ return {
3595
+ preferred: "event-sourced",
3596
+ supported: ["event-sourced"]
3597
+ };
3598
+ }
3599
+ getFeatures() {
3600
+ if (!deltaPollingFeatureEnabled()) return;
3601
+ return ["delta-polling"];
3602
+ }
3603
+ async createSpan(args) {
3604
+ return createSpan(this.db, args);
3605
+ }
3606
+ async batchCreateSpans(args) {
3607
+ return batchCreateSpans(this.db, args);
3608
+ }
3609
+ async batchDeleteTraces(args) {
3610
+ return batchDeleteTraces(this.db, args);
3611
+ }
3612
+ async getSpan(args) {
3613
+ return getSpan(this.db, args);
3614
+ }
3615
+ async getSpans(args) {
3616
+ return getSpans(this.db, args);
3617
+ }
3618
+ async getRootSpan(args) {
3619
+ return getRootSpan(this.db, args);
3620
+ }
3621
+ async getTrace(args) {
3622
+ return getTrace(this.db, args);
3623
+ }
3624
+ async getTraceLight(args) {
3625
+ return getTraceLight(this.db, args);
3626
+ }
3627
+ async listTraces(args) {
3628
+ return listTraces(this.db, args);
3629
+ }
3630
+ async listTracesLight(args) {
3631
+ return listTracesLight(this.db, args);
3632
+ }
3633
+ async listBranches(args) {
3634
+ return listBranches(this.db, args);
3635
+ }
3636
+ async batchCreateLogs(args) {
3637
+ return batchCreateLogs(this.db, args);
3638
+ }
3639
+ async listLogs(args) {
3640
+ return listLogs(this.db, args);
3641
+ }
3642
+ async batchCreateMetrics(args) {
3643
+ return batchCreateMetrics(this.db, args);
3644
+ }
3645
+ async listMetrics(args) {
3646
+ return listMetrics(this.db, args);
3647
+ }
3648
+ async getMetricAggregate(args) {
3649
+ return getMetricAggregate(this.db, args);
3650
+ }
3651
+ async getMetricBreakdown(args) {
3652
+ return getMetricBreakdown(this.db, args);
3653
+ }
3654
+ async getMetricTimeSeries(args) {
3655
+ return getMetricTimeSeries(this.db, args);
3656
+ }
3657
+ async getMetricPercentiles(args) {
3658
+ return getMetricPercentiles(this.db, args);
3659
+ }
3660
+ async getMetricNames(args) {
3661
+ return getMetricNames(this.db, args);
3662
+ }
3663
+ async getMetricLabelKeys(args) {
3664
+ return getMetricLabelKeys(this.db, args);
3665
+ }
3666
+ async getMetricLabelValues(args) {
3667
+ return getMetricLabelValues(this.db, args);
3668
+ }
3669
+ async getEntityTypes(args) {
3670
+ return getEntityTypes(this.db, args);
3671
+ }
3672
+ async getEntityNames(args) {
3673
+ return getEntityNames(this.db, args);
3674
+ }
3675
+ async getServiceNames(args) {
3676
+ return getServiceNames(this.db, args);
3677
+ }
3678
+ async getEnvironments(args) {
3679
+ return getEnvironments(this.db, args);
3680
+ }
3681
+ async getTags(args) {
3682
+ return getTags(this.db, args);
3683
+ }
3684
+ async createScore(args) {
3685
+ return createScore(this.db, args);
3686
+ }
3687
+ async batchCreateScores(args) {
3688
+ return batchCreateScores(this.db, args);
3689
+ }
3690
+ async listScores(args) {
3691
+ return listScores(this.db, args);
3692
+ }
3693
+ async getScoreById(scoreId) {
3694
+ return getScoreById(this.db, scoreId);
3695
+ }
3696
+ async getScoreAggregate(args) {
3697
+ return getScoreAggregate(this.db, args);
3698
+ }
3699
+ async getScoreBreakdown(args) {
3700
+ return getScoreBreakdown(this.db, args);
3701
+ }
3702
+ async getScoreTimeSeries(args) {
3703
+ return getScoreTimeSeries(this.db, args);
3704
+ }
3705
+ async getScorePercentiles(args) {
3706
+ return getScorePercentiles(this.db, args);
3707
+ }
3708
+ async createFeedback(args) {
3709
+ return createFeedback(this.db, args);
3710
+ }
3711
+ async batchCreateFeedback(args) {
3712
+ return batchCreateFeedback(this.db, args);
3713
+ }
3714
+ async listFeedback(args) {
3715
+ return listFeedback(this.db, args);
3716
+ }
3717
+ async getFeedbackAggregate(args) {
3718
+ return getFeedbackAggregate(this.db, args);
3719
+ }
3720
+ async getFeedbackBreakdown(args) {
3721
+ return getFeedbackBreakdown(this.db, args);
3722
+ }
3723
+ async getFeedbackTimeSeries(args) {
3724
+ return getFeedbackTimeSeries(this.db, args);
3725
+ }
3726
+ async getFeedbackPercentiles(args) {
3727
+ return getFeedbackPercentiles(this.db, args);
3728
+ }
3729
+ };
3730
+ //#endregion
3731
+ export { ObservabilityStorageDuckDB };
3732
+
3733
+ //# sourceMappingURL=observability-5ro3rotr.js.map