@mastra/duckdb 1.19.0-alpha.0 → 1.19.0-alpha.2

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.
@@ -733,10 +733,6 @@ function buildPaginationClause(pagination) {
733
733
  // src/storage/domains/observability/preview-compat.ts
734
734
  const generator = coreStorage.buildStoredPreview;
735
735
  const canGeneratePreviews = typeof generator === "function";
736
- /** SQL detection only; preview formatting stays in TypeScript. Empty strings are intentional. */
737
- function missingPreviewSql(column) {
738
- return canGeneratePreviews ? `${column} IS NULL` : "FALSE";
739
- }
740
736
  function needsPreview(value) {
741
737
  return canGeneratePreviews && value == null;
742
738
  }
@@ -823,6 +819,17 @@ function payloadColumnSql(column) {
823
819
  return `NULLIF(${column}, '${JSON_NULL_PLACEHOLDER}')`;
824
820
  }
825
821
  /**
822
+ * The inclusive range of `dates`, used to bound lookups that find a page's rows by their keys.
823
+ *
824
+ * DuckDB cannot push an OR of key tuples (or a tuple semi-join) into the table scan, so without a
825
+ * range it decompresses the payload columns of the whole table. A range reaches the scan as a plain
826
+ * filter that zone maps can prune on, and it cannot change the result: it covers every looked-up row.
827
+ */
828
+ function timeRangeOf(dates) {
829
+ const times = dates.map((date) => date.getTime());
830
+ return [new Date(Math.min(...times)), new Date(Math.max(...times))];
831
+ }
832
+ /**
826
833
  * Trim, drop blank, and dedupe tags before they are written, matching the
827
834
  * PostgreSQL and ClickHouse stores so tag predicates and discovery agree.
828
835
  */
@@ -1276,6 +1283,11 @@ async function deleteFeedback(db, args) {
1276
1283
  }
1277
1284
  await db.execute(`DELETE FROM feedback_events WHERE ${conditions.join(" AND ")}`, params);
1278
1285
  }
1286
+ /** Retrieves the latest feedback record with this ID, or null when missing. */
1287
+ async function getFeedbackById(db, feedbackId) {
1288
+ const rows = await db.query(`SELECT * FROM feedback_events WHERE feedbackId = ? ORDER BY timestamp DESC LIMIT 1`, [feedbackId]);
1289
+ return rows[0] ? rowToFeedbackRecord(rows[0]) : null;
1290
+ }
1279
1291
  /** Update the review workflow status of a single feedback event. */
1280
1292
  async function updateFeedbackReviewStatus(db, args) {
1281
1293
  const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
@@ -3897,7 +3909,7 @@ const TRACE_PREVIEW_PAYLOADS = [
3897
3909
  async function fillMissingTracePreviews(db, rows) {
3898
3910
  const flagged = rows.filter((row) => row.traceId != null && TRACE_PREVIEW_PAYLOADS.some(([preview]) => needsPreview(row[preview])));
3899
3911
  if (flagged.length === 0) return;
3900
- const values = [];
3912
+ const values = timeRangeOf(flagged.map((row) => row.rootTimestamp));
3901
3913
  const where = flagged.map((row) => {
3902
3914
  values.push(row.traceId, row.rootSpanId, row.rootEventType, row.rootTimestamp, row.rootCursorId);
3903
3915
  return "(traceId = ? AND spanId = ? AND eventType = ? AND timestamp = ? AND cursorId IS NOT DISTINCT FROM ?)";
@@ -3907,7 +3919,7 @@ async function fillMissingTracePreviews(db, rows) {
3907
3919
  ${payloadColumnSql("output")} AS output,
3908
3920
  error
3909
3921
  FROM span_events
3910
- WHERE parentSpanId IS NULL AND (${where})`, values);
3922
+ WHERE parentSpanId IS NULL AND timestamp BETWEEN ? AND ? AND (${where})`, values);
3911
3923
  const byTrace = new Map(payloads.map((payload) => [String(payload.traceId), payload]));
3912
3924
  for (const row of flagged) {
3913
3925
  const payload = byTrace.get(String(row.traceId));
@@ -4174,6 +4186,17 @@ const previewPayloads = [
4174
4186
  ["outputPreview", payloadColumnSql("output")],
4175
4187
  ["errorPreview", "error"]
4176
4188
  ];
4189
+ function eventRowsFilter(rows) {
4190
+ const values = timeRangeOf(rows.map((row) => row.endedAt));
4191
+ return {
4192
+ sql: `endedAt BETWEEN ? AND ? AND (${rows.map((row) => {
4193
+ const key = keys([row], values);
4194
+ values.push(row.cursorId, row.endedAt);
4195
+ return `(${key} AND cursorId IS NOT DISTINCT FROM ? AND endedAt = ?)`;
4196
+ }).join(" OR ")})`,
4197
+ values
4198
+ };
4199
+ }
4177
4200
  async function querySpans(db, plan) {
4178
4201
  const deadline = performance.now() + coreStorage.TRACE_QUERY_DEFAULT_TIMEOUT_MS;
4179
4202
  const query = async (sql, values) => {
@@ -4202,17 +4225,21 @@ async function querySpans(db, plan) {
4202
4225
  }));
4203
4226
  const page = rows.slice(0, plan.limit);
4204
4227
  if (!page.length) return buildSpanQueryResponse(plan, [], []);
4205
- const payloadValues = [];
4206
- const payloadWhere = page.map((row) => {
4207
- const key = keys([row], payloadValues);
4208
- payloadValues.push(row.cursorId, row.endedAt);
4209
- return `(${key} AND cursorId IS NOT DISTINCT FROM ? AND endedAt = ?)`;
4210
- }).join(" OR ");
4211
- const previewRows = await query(`SELECT DISTINCT ${identities}, CAST(stopReasonContext AS VARCHAR) AS stopReasonContext,
4228
+ const lookupColumns = `${identities}, CAST(stopReasonContext AS VARCHAR) AS stopReasonContext,
4212
4229
  COALESCE(model, CASE WHEN json_type(attributes, '$.model') = 'VARCHAR' THEN json_extract_string(attributes, '$.model') END) AS model,
4213
4230
  COALESCE(provider, CASE WHEN json_type(attributes, '$.provider') = 'VARCHAR' THEN json_extract_string(attributes, '$.provider') END) AS provider,
4214
- ${previewPayloads.map(([preview, payload]) => `${preview}, CASE WHEN ${missingPreviewSql(preview)} THEN CAST(${payload} AS VARCHAR) END AS ${preview}Payload`).join(",\n ")}
4215
- FROM span_events WHERE ${payloadWhere}`, payloadValues);
4231
+ ${previewPayloads.map(([preview]) => preview).join(", ")}`;
4232
+ const pageFilter = eventRowsFilter(page);
4233
+ const storedRows = await query(`SELECT DISTINCT ${lookupColumns} FROM span_events WHERE ${pageFilter.sql}`, pageFilter.values);
4234
+ const withoutPreview = new Set(storedRows.filter((row) => previewPayloads.some(([preview]) => needsPreview(row[preview]))).map((row) => coreStorage.spanQueryIdentityKey(row)));
4235
+ const rowsWithoutPreview = page.filter((row) => withoutPreview.has(coreStorage.spanQueryIdentityKey(row)));
4236
+ let previewRows = storedRows;
4237
+ if (rowsWithoutPreview.length) {
4238
+ const payloadFilter = eventRowsFilter(rowsWithoutPreview);
4239
+ const payloadRows = await query(`SELECT DISTINCT ${lookupColumns}, ${previewPayloads.map(([preview, payload]) => `CAST(${payload} AS VARCHAR) AS ${preview}Payload`).join(", ")}
4240
+ FROM span_events WHERE ${payloadFilter.sql}`, payloadFilter.values);
4241
+ previewRows = [...storedRows.filter((row) => !withoutPreview.has(coreStorage.spanQueryIdentityKey(row))), ...payloadRows];
4242
+ }
4216
4243
  const payloads = previewRows.map((row) => {
4217
4244
  const payload = {
4218
4245
  organizationId: row.organizationId,
@@ -4387,6 +4414,22 @@ function compileDuckDBThreadRowsQuery(plan, strategy = "full") {
4387
4414
  values
4388
4415
  };
4389
4416
  }
4417
+ /**
4418
+ * A chat's first and last traces can be months apart, so the range alone can cover the whole table.
4419
+ * The IN list lets DuckDB also skip the row groups between them.
4420
+ */
4421
+ function rootRowsFilter(roots) {
4422
+ const endedAts = roots.map((root) => root.endedAt);
4423
+ const values = [...timeRangeOf(endedAts), ...endedAts];
4424
+ const keys = roots.map((root) => `(${Object.entries(root).map(([field, value]) => {
4425
+ values.push(value);
4426
+ return `${field} ${field === "organizationId" || field === "resourceId" ? "IS NOT DISTINCT FROM" : "="} ?`;
4427
+ }).join(" AND ")})`);
4428
+ return {
4429
+ sql: `endedAt BETWEEN ? AND ? AND endedAt IN (${endedAts.map(() => "?").join(", ")}) AND (${keys.join(" OR ")})`,
4430
+ values
4431
+ };
4432
+ }
4390
4433
  async function queryThreadRows(db, plan, strategy = "auto") {
4391
4434
  const deadline = performance.now() + coreStorage.TRACE_QUERY_DEFAULT_TIMEOUT_MS;
4392
4435
  const run = async (sql, values) => {
@@ -4416,38 +4459,47 @@ async function queryThreadRows(db, plan, strategy = "auto") {
4416
4459
  threads: [],
4417
4460
  page: { next: null }
4418
4461
  };
4419
- const values = [];
4420
- const keys = page.flatMap((row) => ["first", "last"].map((which) => {
4421
- const entries = {
4422
- organizationId: row.organizationId,
4423
- resourceId: which === "first" ? row.resourceId : row.lastResourceId,
4424
- traceId: row[`${which}TraceId`],
4425
- spanId: row[`${which}SpanId`],
4426
- cursorId: row[`${which}CursorId`],
4427
- endedAt: row[`${which}EndedAt`]
4428
- };
4429
- return `(${Object.entries(entries).map(([field, value]) => {
4430
- values.push(value);
4431
- return `${field} ${field === "organizationId" || field === "resourceId" ? "IS NOT DISTINCT FROM" : "="} ?`;
4432
- }).join(" AND ")})`;
4433
- }));
4434
- const payloads = await run(`SELECT organizationId, traceId, cursorId,
4462
+ const roots = page.flatMap((row) => ["first", "last"].map((which) => ({
4463
+ organizationId: row.organizationId,
4464
+ resourceId: which === "first" ? row.resourceId : row.lastResourceId,
4465
+ traceId: row[`${which}TraceId`],
4466
+ spanId: row[`${which}SpanId`],
4467
+ cursorId: row[`${which}CursorId`],
4468
+ endedAt: row[`${which}EndedAt`]
4469
+ })));
4470
+ const pageFilter = rootRowsFilter(roots);
4471
+ const rootRows = await run(`SELECT organizationId, traceId, cursorId,
4435
4472
  ${contextFields.map((field) => field === "executionSource" ? "source AS executionSource" : field).join(", ")},
4436
4473
  CASE WHEN spanType = 'agent_run' THEN timeToFirstChunkMs END AS timeToFirstChunkMs,
4437
4474
  CASE WHEN spanType = 'agent_run' THEN timeToFirstTokenMs END AS timeToFirstTokenMs,
4438
4475
  CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
4439
- ${[
4440
- "input",
4441
- "output",
4442
- "error"
4443
- ].map((field) => `${field}Preview, CASE WHEN ${missingPreviewSql(`${field}Preview`)} THEN CAST(${field === "error" ? field : payloadColumnSql(field)} AS VARCHAR) END AS ${field}`).join(", ")}
4444
- FROM span_events WHERE ${keys.join(" OR ")}`, values);
4476
+ inputPreview, outputPreview, errorPreview
4477
+ FROM span_events
4478
+ WHERE ${pageFilter.sql}`, pageFilter.values);
4445
4479
  const key = (org, trace, version) => JSON.stringify([
4446
4480
  org,
4447
4481
  trace,
4448
4482
  String(version)
4449
4483
  ]);
4450
- const byRoot = new Map(payloads.map((row) => [key(row.organizationId, row.traceId, row.cursorId), row]));
4484
+ const byRoot = new Map(rootRows.map((row) => [key(row.organizationId, row.traceId, row.cursorId), row]));
4485
+ const rootsWithoutPreview = roots.filter((root) => {
4486
+ const row = byRoot.get(key(root.organizationId, root.traceId, root.cursorId));
4487
+ return row && [
4488
+ row.inputPreview,
4489
+ row.outputPreview,
4490
+ row.errorPreview
4491
+ ].some(needsPreview);
4492
+ });
4493
+ if (rootsWithoutPreview.length) {
4494
+ const payloadFilter = rootRowsFilter(rootsWithoutPreview);
4495
+ const payloads = await run(`SELECT organizationId, traceId, cursorId,
4496
+ CAST(${payloadColumnSql("input")} AS VARCHAR) AS input,
4497
+ CAST(${payloadColumnSql("output")} AS VARCHAR) AS output,
4498
+ CAST(error AS VARCHAR) AS error
4499
+ FROM span_events
4500
+ WHERE ${payloadFilter.sql}`, payloadFilter.values);
4501
+ for (const payload of payloads) Object.assign(byRoot.get(key(payload.organizationId, payload.traceId, payload.cursorId)), payload);
4502
+ }
4451
4503
  const threads = page.flatMap((row) => {
4452
4504
  const first = byRoot.get(key(row.organizationId, row.firstTraceId, row.firstCursorId));
4453
4505
  const last = byRoot.get(key(row.organizationId, row.lastTraceId, row.lastCursorId));
@@ -5071,7 +5123,9 @@ const SPAN_RECONSTRUCT_SELECT_LIGHT_LIST = `
5071
5123
  ${argMaxNonNull("resourceId")},
5072
5124
  ${argMaxNonNull("error")},
5073
5125
  ${argMaxNonNull("metadata")},
5074
- ${RECONSTRUCT_PREVIEWS_SQL}
5126
+ ${RECONSTRUCT_PREVIEWS_SQL},
5127
+ min(timestamp) AS firstEventAt,
5128
+ max(timestamp) AS lastEventAt
5075
5129
  FROM span_events
5076
5130
  `;
5077
5131
  /**
@@ -5202,11 +5256,18 @@ async function fillMissingReconstructedPreviews(db, rows) {
5202
5256
  const flagged = rows.filter((row) => PREVIEW_PAYLOADS.some(([preview]) => needsPreview(row[preview])));
5203
5257
  if (flagged.length === 0) return;
5204
5258
  const traceIds = [...new Set(flagged.map((row) => row.traceId))];
5259
+ const [minTs, maxTs] = timeRangeOf(flagged.flatMap((row) => [row.firstEventAt, row.lastEventAt]));
5205
5260
  const payloads = await db.query(`SELECT traceId, spanId, ${PREVIEW_PAYLOADS.map(([, payload]) => argMaxNonNull(payload)).join(", ")}
5206
5261
  FROM span_events
5207
- WHERE traceId IN (${traceIds.map(() => "?").join(", ")})
5262
+ WHERE timestamp BETWEEN ? AND ?
5263
+ AND traceId IN (${traceIds.map(() => "?").join(", ")})
5208
5264
  AND (traceId, spanId) IN (SELECT * FROM (VALUES ${flagged.map(() => "(?, ?)").join(", ")}))
5209
- GROUP BY traceId, spanId`, [...traceIds, ...flagged.flatMap((row) => [row.traceId, row.spanId])]);
5265
+ GROUP BY traceId, spanId`, [
5266
+ minTs,
5267
+ maxTs,
5268
+ ...traceIds,
5269
+ ...flagged.flatMap((row) => [row.traceId, row.spanId])
5270
+ ]);
5210
5271
  const byKey = new Map(payloads.map((payload) => [`${payload.traceId}\u0000${payload.spanId}`, payload]));
5211
5272
  for (const row of flagged) {
5212
5273
  const payload = byKey.get(`${row.traceId}\u0000${row.spanId}`);
@@ -6311,6 +6372,9 @@ ${args.tables.map((table) => ` - ${table.table}`).join("\n")}\n\nTo fix this, r
6311
6372
  * Uses an append-only event-sourced model with SQL-based reconstruction for spans.
6312
6373
  */
6313
6374
  var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends ObservabilityStorage {
6375
+ get persistenceLocation() {
6376
+ return "local";
6377
+ }
6314
6378
  static retentionTables = {
6315
6379
  spans: {
6316
6380
  table: "span_events",
@@ -6591,6 +6655,9 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends Observ
6591
6655
  async getScoreById(scoreId) {
6592
6656
  return getScoreById(this.db, scoreId);
6593
6657
  }
6658
+ async getFeedbackById(feedbackId) {
6659
+ return getFeedbackById(this.db, feedbackId);
6660
+ }
6594
6661
  async getScoreAggregate(args) {
6595
6662
  return getScoreAggregate(this.db, args);
6596
6663
  }
@@ -6633,4 +6700,4 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends Observ
6633
6700
  };
6634
6701
  export { ObservabilityStorageDuckDB };
6635
6702
 
6636
- //# sourceMappingURL=observability-Bpaorjgj.js.map
6703
+ //# sourceMappingURL=observability-FlY1RbTC.js.map