@mastra/duckdb 1.13.0-alpha.1 → 1.13.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.
@@ -3,7 +3,7 @@ name: mastra-duckdb
3
3
  description: Documentation for @mastra/duckdb. Use when working with @mastra/duckdb APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/duckdb"
6
- version: "1.13.0-alpha.1"
6
+ version: "1.13.0-alpha.2"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.13.0-alpha.1",
2
+ "version": "1.13.0-alpha.2",
3
3
  "package": "@mastra/duckdb",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -556,7 +556,7 @@ var ObservabilityStorageDuckDB = class extends _mastra_core_storage.Observabilit
556
556
  async loadDelegate() {
557
557
  if (this.delegate) return this.delegate;
558
558
  if (this.unavailableError) return null;
559
- if (!this.loadPromise) this.loadPromise = Promise.resolve().then(() => require("./observability-DNuNWZFX.cjs")).then(({ ObservabilityStorageDuckDB }) => {
559
+ if (!this.loadPromise) this.loadPromise = Promise.resolve().then(() => require("./observability-D4eFjsKJ.cjs")).then(({ ObservabilityStorageDuckDB }) => {
560
560
  const delegate = new ObservabilityStorageDuckDB({ db: this.db });
561
561
  this.delegate = delegate;
562
562
  return delegate;
package/dist/index.js CHANGED
@@ -555,7 +555,7 @@ var ObservabilityStorageDuckDB = class extends ObservabilityStorage {
555
555
  async loadDelegate() {
556
556
  if (this.delegate) return this.delegate;
557
557
  if (this.unavailableError) return null;
558
- if (!this.loadPromise) this.loadPromise = import("./observability-Bk_y3hof.js").then(({ ObservabilityStorageDuckDB }) => {
558
+ if (!this.loadPromise) this.loadPromise = import("./observability-DINJlR_a.js").then(({ ObservabilityStorageDuckDB }) => {
559
559
  const delegate = new ObservabilityStorageDuckDB({ db: this.db });
560
560
  this.delegate = delegate;
561
561
  return delegate;
@@ -688,6 +688,33 @@ function jsonV(val) {
688
688
  return require_features.DuckDBConnection.sqlValue(JSON.stringify(val));
689
689
  }
690
690
  /**
691
+ * `span_events` JSON columns that can hold large payloads. Missing values are
692
+ * stored as the JSON literal `null` instead of SQL NULL.
693
+ *
694
+ * DuckDB only reads the selected rows of a large-string column when the
695
+ * column's validity mask is constant (no NULLs in the row group). Any SQL NULL
696
+ * makes it fall back to reading the whole 2,048-row vector, which loads every
697
+ * large string in it even when the query needs one row. Keeping these columns
698
+ * NULL-free lets point lookups and page reads load only what they return.
699
+ */
700
+ const SPAN_PAYLOAD_COLUMNS = [
701
+ "attributes",
702
+ "input",
703
+ "output",
704
+ "requestContext"
705
+ ];
706
+ /** Stored in {@link SPAN_PAYLOAD_COLUMNS} in place of SQL NULL; read back as missing. */
707
+ const JSON_NULL_PLACEHOLDER = "null";
708
+ /** Like {@link jsonV}, but writes the JSON `null` placeholder instead of SQL NULL. */
709
+ function payloadJsonV(val) {
710
+ if (val === null || val === void 0) return `'${JSON_NULL_PLACEHOLDER}'`;
711
+ return jsonV(val);
712
+ }
713
+ /** SQL for a payload column that maps the JSON `null` placeholder back to SQL NULL. */
714
+ function payloadColumnSql(column) {
715
+ return `NULLIF(${column}, '${JSON_NULL_PLACEHOLDER}')`;
716
+ }
717
+ /**
691
718
  * Trim, drop blank, and dedupe tags before they are written, matching the
692
719
  * PostgreSQL and ClickHouse stores so tag predicates and discovery agree.
693
720
  */
@@ -2935,7 +2962,7 @@ const TRACE_SELECT = `
2935
2962
  r.entityId AS entityId,
2936
2963
  r.parentSpanId AS parentSpanId,
2937
2964
  r.metadata AS metadata,
2938
- r.input AS input,
2965
+ ${payloadColumnSql("r.input")} AS input,
2939
2966
  r.threadId AS threadId,
2940
2967
  r.resourceId AS resourceId,
2941
2968
  r.startedAt AS startedAt,
@@ -3218,6 +3245,11 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
3218
3245
  GROUP BY r.traceId
3219
3246
  HAVING bool_or(r.timestamp >= CAST(? AS TIMESTAMP) AND r.timestamp < CAST(? AS TIMESTAMP))
3220
3247
  AND bool_or(${rowConditions.join("\n AND ")})
3248
+ )`,
3249
+ `root_bounds AS (
3250
+ SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
3251
+ FROM span_events
3252
+ WHERE parentSpanId IS NULL AND traceId IN (SELECT traceId FROM root_trace_ids)
3221
3253
  )`,
3222
3254
  `root_events AS (
3223
3255
  SELECT
@@ -3228,6 +3260,8 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
3228
3260
  END AS startedAt
3229
3261
  FROM span_events
3230
3262
  WHERE parentSpanId IS NULL
3263
+ AND timestamp >= (SELECT minTs FROM root_bounds)
3264
+ AND timestamp <= (SELECT maxTs FROM root_bounds)
3231
3265
  AND traceId IN (SELECT traceId FROM root_trace_ids)
3232
3266
  )`,
3233
3267
  `current_roots AS (
@@ -3250,7 +3284,15 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
3250
3284
  )`
3251
3285
  ];
3252
3286
  if (relatedCollections.has("spans")) {
3253
- ctes.push(`current_span_rows AS (
3287
+ ctes.push(`span_bounds AS (
3288
+ -- Exact event-time range of the in-scope traces, from a narrow
3289
+ -- (traceId, timestamp) pass. The join below cannot be pushed into the
3290
+ -- scan, so the range keeps it from decompressing every span row.
3291
+ SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
3292
+ FROM span_events
3293
+ WHERE traceId IN (SELECT traceId FROM root_scope)
3294
+ ),
3295
+ current_span_rows AS (
3254
3296
  SELECT
3255
3297
  e.*,
3256
3298
  coalesce(
@@ -3262,7 +3304,9 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
3262
3304
  ORDER BY CASE WHEN e.endedAt IS NULL THEN 1 ELSE 0 END ASC, e.cursorId DESC
3263
3305
  ) AS currentRank
3264
3306
  FROM span_events e
3265
- INNER JOIN root_scope roots ON roots.traceId = e.traceId${tenantWhere("e")}
3307
+ INNER JOIN root_scope roots ON roots.traceId = e.traceId
3308
+ WHERE e.timestamp >= (SELECT minTs FROM span_bounds)
3309
+ AND e.timestamp <= (SELECT maxTs FROM span_bounds)${tenantConditions("e").map((condition) => `\n AND ${condition}`).join("")}
3266
3310
  ),
3267
3311
  current_spans AS (
3268
3312
  SELECT
@@ -3812,7 +3856,7 @@ async function querySpans(db, plan) {
3812
3856
  return `(${key} AND cursorId IS NOT DISTINCT FROM ? AND endedAt = ?)`;
3813
3857
  }).join(" OR ");
3814
3858
  const payloads = await query(`SELECT DISTINCT ${identities},
3815
- substring(CAST(input AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS inputPreview, substring(CAST(output AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS outputPreview
3859
+ substring(CAST(${payloadColumnSql("input")} AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS inputPreview, substring(CAST(${payloadColumnSql("output")} AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS outputPreview
3816
3860
  FROM span_events WHERE ${payloadWhere}`, payloadValues);
3817
3861
  const metricValues = [];
3818
3862
  const metricWhere = keys(page, metricValues);
@@ -4067,13 +4111,15 @@ const COLUMNS_SQL = [
4067
4111
  "error",
4068
4112
  "requestContext"
4069
4113
  ].join(", ");
4114
+ const PAYLOAD_COLUMN_SET = new Set(SPAN_PAYLOAD_COLUMNS);
4070
4115
  /**
4071
4116
  * Reconstruction query uses `arg_max(field, timestamp) FILTER (WHERE field IS NOT NULL)`
4072
4117
  * so that the final end event supplies the terminal span fields without wiping
4073
- * stable values emitted on the start event.
4118
+ * stable values emitted on the start event. Payload columns also skip the JSON
4119
+ * `null` placeholder, which stands in for a missing value.
4074
4120
  */
4075
4121
  function argMaxNonNull(col) {
4076
- return `arg_max(${col}, timestamp) FILTER (WHERE ${col} IS NOT NULL) as ${col}`;
4122
+ return `arg_max(${col}, timestamp) FILTER (WHERE ${col} IS NOT NULL${PAYLOAD_COLUMN_SET.has(col) ? ` AND ${col} <> '${JSON_NULL_PLACEHOLDER}'` : ""}) as ${col}`;
4077
4123
  }
4078
4124
  const SPAN_RECONSTRUCT_SELECT = `
4079
4125
  SELECT
@@ -4184,28 +4230,55 @@ function buildPostAggReconstructSelect(postAgg, orderByField) {
4184
4230
  `;
4185
4231
  }
4186
4232
  /**
4187
- * Reconstruct spans for the `(traceId, spanId)` pairs selected by `anchorCte`,
4188
- * scanning only events at/after the CTE's earliest `anchorStartedAt`.
4233
+ * Reconstruct the spans of one list page, reading payload columns only for
4234
+ * the page's own events.
4235
+ *
4236
+ * Two queries:
4237
+ * 1. `anchorSql` (a `WITH ...` prefix defining `anchorCte`) picks the page's
4238
+ * `(traceId, spanId)` anchors, and a narrow pass over `traceId`, `spanId`
4239
+ * and `timestamp` finds the exact time range of their events.
4240
+ * 2. Reconstruction scans only that range for exactly those anchors.
4189
4241
  *
4190
- * The `(traceId, spanId) IN (subquery)` semi-join alone cannot be pushed into
4191
- * the table scan, so without a bound DuckDB decompresses every column of the
4192
- * entire table just to emit one page of spans. The time bound is a plain range
4193
- * predicate that zone maps can prune on (insertion order tracks event time).
4242
+ * The `(traceId, spanId) IN (...)` semi-join cannot be pushed into the table
4243
+ * scan, so without a range DuckDB decompresses every payload column of every
4244
+ * row it reads. A lower bound alone (the earliest anchor) prunes nothing for
4245
+ * the oldest pages, which then read the whole table and can run out of memory.
4246
+ * Both bounds are passed as literal parameters so they always reach the scan
4247
+ * as plain range filters that zone maps can prune on.
4194
4248
  *
4195
- * Correctness: span events are only 'start' (timestamp = startedAt) and 'end'
4196
- * (timestamp = endedAt >= startedAt), so every event of an anchored span has
4197
- * timestamp >= its start-row timestamp >= min(anchorStartedAt). An empty
4198
- * anchor set makes the bound NULL, which matches the empty IN-list result.
4249
+ * Anchors and bounds are frozen after the first query, so spans written in
4250
+ * between cannot change which rows are on the page. An event a page span
4251
+ * receives in between is left for the next read, as if the page had been
4252
+ * read a moment earlier.
4199
4253
  */
4200
- function reconstructForAnchors(reconstructSelect, anchorCte) {
4201
- return `
4202
- ${reconstructSelect}
4203
- WHERE timestamp >= (SELECT min(anchorStartedAt) FROM ${anchorCte})
4204
- AND (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
4205
- GROUP BY traceId, spanId`;
4254
+ async function reconstructAnchoredPage(db, anchorSql, anchorCte, params, reconstructSelect, orderBySql) {
4255
+ const anchors = await db.query(`
4256
+ ${anchorSql}
4257
+ SELECT anchors.traceId, anchors.spanId, bounds.minTs, bounds.maxTs
4258
+ FROM ${anchorCte} AS anchors,
4259
+ (
4260
+ SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
4261
+ FROM span_events
4262
+ WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
4263
+ ) AS bounds
4264
+ `, params);
4265
+ if (anchors.length === 0) return [];
4266
+ const { minTs, maxTs } = anchors[0];
4267
+ const anchorValues = anchors.map(() => "(?, ?)").join(", ");
4268
+ return db.query(`
4269
+ ${reconstructSelect}
4270
+ WHERE timestamp >= ? AND timestamp <= ?
4271
+ AND (traceId, spanId) IN (SELECT * FROM (VALUES ${anchorValues}))
4272
+ GROUP BY traceId, spanId
4273
+ ${orderBySql}
4274
+ `, [
4275
+ minTs,
4276
+ maxTs,
4277
+ ...anchors.flatMap((anchor) => [anchor.traceId, anchor.spanId])
4278
+ ]);
4206
4279
  }
4207
4280
  /**
4208
- * Same time-bound trick as {@link reconstructForAnchors}, but with the bound
4281
+ * Same time-bound trick as {@link reconstructAnchoredPage}, but with the bound
4209
4282
  * precomputed in JS and passed as a `?` parameter. The delta-poll query shape
4210
4283
  * references its candidate CTE multiple times, which makes DuckDB materialize
4211
4284
  * it and lose the dynamic-filter pushdown a scalar subquery bound relies on —
@@ -4439,15 +4512,15 @@ function toValuesTuple(row) {
4439
4512
  v(row.environment),
4440
4513
  v(row.source),
4441
4514
  v(row.serviceName),
4442
- jsonV(row.attributes),
4515
+ payloadJsonV(row.attributes),
4443
4516
  jsonV(row.metadata),
4444
4517
  jsonV(row.tags),
4445
4518
  jsonV(row.scope),
4446
4519
  jsonV(row.links),
4447
- jsonV(row.input),
4448
- jsonV(row.output),
4520
+ payloadJsonV(row.input),
4521
+ payloadJsonV(row.output),
4449
4522
  jsonV(row.error),
4450
- jsonV(row.requestContext)
4523
+ payloadJsonV(row.requestContext)
4451
4524
  ].join(", ");
4452
4525
  }
4453
4526
  async function insertSpanEvents(db, rows) {
@@ -4661,7 +4734,7 @@ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
4661
4734
  },
4662
4735
  spans: []
4663
4736
  };
4664
- const pageSql = `
4737
+ const spans = (await reconstructAnchoredPage(db, `
4665
4738
  WITH page_roots AS (
4666
4739
  SELECT traceId, spanId, min(timestamp) AS anchorStartedAt
4667
4740
  FROM span_events AS ${outerAlias}
@@ -4670,14 +4743,11 @@ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
4670
4743
  ${prefilterOrderBy}
4671
4744
  LIMIT ? OFFSET ?
4672
4745
  )
4673
- ${reconstructForAnchors(reconstructSelect, "page_roots")}
4674
- ${buildOrderByClause(orderBy)}, traceId, spanId
4675
- `;
4676
- const spans = (await db.query(pageSql, [
4746
+ `, "page_roots", [
4677
4747
  ...prefilterParams,
4678
4748
  perPage,
4679
4749
  offset
4680
- ])).map((row) => mapRow(row));
4750
+ ], reconstructSelect, `${buildOrderByClause(orderBy)}, traceId, spanId`)).map((row) => mapRow(row));
4681
4751
  return {
4682
4752
  pagination: {
4683
4753
  total,
@@ -4726,20 +4796,17 @@ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
4726
4796
  },
4727
4797
  spans: []
4728
4798
  };
4729
- const dataSql = `
4799
+ const spans = (await reconstructAnchoredPage(db, `
4730
4800
  ${cteSql},
4731
4801
  page_roots AS (
4732
4802
  SELECT traceId, spanId, startedAt AS anchorStartedAt
4733
4803
  FROM root_spans ${postAggWhere} ${orderByClause} ${paginationClause}
4734
4804
  )
4735
- ${reconstructForAnchors(reconstructSelect, "page_roots")}
4736
- ${orderByClause}
4737
- `;
4738
- const spans = (await db.query(dataSql, [
4805
+ `, "page_roots", [
4739
4806
  ...prefilterParams,
4740
4807
  ...postAggParams,
4741
4808
  ...paginationParams
4742
- ])).map((row) => mapRow(row));
4809
+ ], reconstructSelect, orderByClause)).map((row) => mapRow(row));
4743
4810
  return {
4744
4811
  pagination: {
4745
4812
  total,
@@ -5043,7 +5110,7 @@ async function listBranches(db, args) {
5043
5110
  branches: [],
5044
5111
  ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
5045
5112
  };
5046
- const pageSql = `
5113
+ const spans = (await reconstructAnchoredPage(db, `
5047
5114
  WITH page_anchors AS (
5048
5115
  SELECT traceId, spanId, min(timestamp) AS anchorStartedAt
5049
5116
  FROM span_events AS ${outerAlias}
@@ -5052,14 +5119,11 @@ async function listBranches(db, args) {
5052
5119
  ${prefilterOrderBy}
5053
5120
  LIMIT ? OFFSET ?
5054
5121
  )
5055
- ${reconstructForAnchors(SPAN_RECONSTRUCT_SELECT, "page_anchors")}
5056
- ${buildOrderByClause(orderBy)}, traceId, spanId
5057
- `;
5058
- const spans = (await db.query(pageSql, [
5122
+ `, "page_anchors", [
5059
5123
  ...prefilterParams,
5060
5124
  perPage,
5061
5125
  offset
5062
- ])).map((row) => rowToSpanRecord(row));
5126
+ ], SPAN_RECONSTRUCT_SELECT, `${buildOrderByClause(orderBy)}, traceId, spanId`)).map((row) => rowToSpanRecord(row));
5063
5127
  return {
5064
5128
  pagination: {
5065
5129
  total,
@@ -5106,20 +5170,17 @@ async function listBranches(db, args) {
5106
5170
  branches: [],
5107
5171
  ...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
5108
5172
  };
5109
- const dataSql = `
5173
+ const spans = (await reconstructAnchoredPage(db, `
5110
5174
  ${cteSql},
5111
5175
  page_anchors AS (
5112
5176
  SELECT traceId, spanId, startedAt AS anchorStartedAt
5113
5177
  FROM branch_anchors ${postAggWhere} ${orderByClause} ${paginationClause}
5114
5178
  )
5115
- ${reconstructForAnchors(SPAN_RECONSTRUCT_SELECT, "page_anchors")}
5116
- ${orderByClause}
5117
- `;
5118
- const spans = (await db.query(dataSql, [
5179
+ `, "page_anchors", [
5119
5180
  ...prefilterParams,
5120
5181
  ...postAggParams,
5121
5182
  ...paginationParams
5122
- ])).map((row) => rowToSpanRecord(row));
5183
+ ], SPAN_RECONSTRUCT_SELECT, orderByClause)).map((row) => rowToSpanRecord(row));
5123
5184
  return {
5124
5185
  pagination: {
5125
5186
  total,
@@ -5543,4 +5604,4 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends _mastr
5543
5604
  //#endregion
5544
5605
  exports.ObservabilityStorageDuckDB = ObservabilityStorageDuckDB;
5545
5606
 
5546
- //# sourceMappingURL=observability-DNuNWZFX.cjs.map
5607
+ //# sourceMappingURL=observability-D4eFjsKJ.cjs.map