@mastra/duckdb 1.12.1 → 1.13.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/docs/SKILL.md +1 -1
  2. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  3. package/dist/{db-zp2htaur.cjs → features-BzhPmFg2.cjs} +63 -2
  4. package/dist/features-BzhPmFg2.cjs.map +1 -0
  5. package/dist/{db-BWgrbB_u.js → features-D3sUOsCL.js} +23 -3
  6. package/dist/features-D3sUOsCL.js.map +1 -0
  7. package/dist/index.cjs +16 -6
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +12 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/{observability-BmLO_YTO.js → observability-Bk_y3hof.js} +460 -43
  12. package/dist/observability-Bk_y3hof.js.map +1 -0
  13. package/dist/{observability-DlmDRPW3.cjs → observability-DNuNWZFX.cjs} +463 -68
  14. package/dist/observability-DNuNWZFX.cjs.map +1 -0
  15. package/dist/storage/db/index.d.ts +5 -1
  16. package/dist/storage/db/index.d.ts.map +1 -1
  17. package/dist/storage/domains/observability/index.d.ts +4 -2
  18. package/dist/storage/domains/observability/index.d.ts.map +1 -1
  19. package/dist/storage/domains/observability/span-query.d.ts +43 -0
  20. package/dist/storage/domains/observability/span-query.d.ts.map +1 -0
  21. package/dist/storage/domains/observability/trace-aggregate.d.ts +17 -0
  22. package/dist/storage/domains/observability/trace-aggregate.d.ts.map +1 -0
  23. package/dist/storage/domains/observability/trace-query.d.ts +26 -1
  24. package/dist/storage/domains/observability/trace-query.d.ts.map +1 -1
  25. package/dist/storage/features.d.ts +2 -0
  26. package/dist/storage/features.d.ts.map +1 -0
  27. package/dist/storage/index.d.ts +2 -0
  28. package/dist/storage/index.d.ts.map +1 -1
  29. package/package.json +5 -5
  30. package/dist/db-BWgrbB_u.js.map +0 -1
  31. package/dist/db-zp2htaur.cjs.map +0 -1
  32. package/dist/observability-BmLO_YTO.js.map +0 -1
  33. package/dist/observability-DlmDRPW3.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { t as DuckDBConnection } from "./db-BWgrbB_u.js";
1
+ import { n as DuckDBConnection, r as DuckDBQueryTimeoutError, t as spanQueryFeatures } from "./features-D3sUOsCL.js";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
3
  import * as coreStorage from "@mastra/core/storage";
4
4
  import { BRANCH_SPAN_TYPES, METRIC_DISTINCT_COLUMNS, ObservabilityStorage, buildInputPreview, computeTraceStatus, createStorageErrorId, executeRetentionPrune, feedbackRecordSchema, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, resolveRetentionTargets, retentionCutoffMs, toTraceSpans } from "@mastra/core/storage";
@@ -2703,6 +2703,15 @@ const TRACE_STATUS_SQL = `CASE WHEN r.error IS NOT NULL THEN 'error' ELSE 'succe
2703
2703
  function durationMsSql(startedAt, endedAt) {
2704
2704
  return `date_diff('millisecond', ${startedAt}, ${endedAt})`;
2705
2705
  }
2706
+ /**
2707
+ * Top-level string metadata value of the trace root (alias `r`). The SQL binds the JSON path
2708
+ * twice; use `traceMetadataPathValues` for the matching parameters.
2709
+ */
2710
+ const TRACE_METADATA_VALUE_SQL = `NULLIF(trim(CASE WHEN json_type(r.metadata, ?) = 'VARCHAR' THEN json_extract_string(r.metadata, ?) END), '')`;
2711
+ function traceMetadataPathValues(key) {
2712
+ const path = `$.${JSON.stringify(key)}`;
2713
+ return [path, path];
2714
+ }
2706
2715
  const TRACE_FIELDS = {
2707
2716
  traceId: {
2708
2717
  sql: "r.traceId",
@@ -2969,13 +2978,11 @@ function compileScalarPredicate(predicate, registry, allowMetadata = false) {
2969
2978
  let fieldValues = [];
2970
2979
  if (isMetadataField(predicate.field)) {
2971
2980
  if (!allowMetadata) throw new Error(`Unsupported trusted trace-query field: ${predicate.field}`);
2972
- const key = predicate.field.slice(9);
2973
- const path = `$.${JSON.stringify(key)}`;
2974
2981
  field = {
2975
- sql: `NULLIF(trim(CASE WHEN json_type(r.metadata, ?) = 'VARCHAR' THEN json_extract_string(r.metadata, ?) END), '')`,
2982
+ sql: TRACE_METADATA_VALUE_SQL,
2976
2983
  parameterType: "scalar"
2977
2984
  };
2978
- fieldValues = [path, path];
2985
+ fieldValues = traceMetadataPathValues(predicate.field.slice(9));
2979
2986
  } else field = fieldDefinition(registry, predicate.field);
2980
2987
  if (predicate.type === "presence") return {
2981
2988
  sql: `${field.sql} IS ${predicate.operator === "exists" ? "NOT " : ""}NULL`,
@@ -3152,15 +3159,66 @@ function collectThreadRelatedCollections(predicate, collections) {
3152
3159
  else collectThreadRelatedCollections(predicate.arg, collections);
3153
3160
  return collections;
3154
3161
  }
3155
- function compileDuckDBTraceScope(relatedCollections, scope) {
3162
+ const ROOT_PUSHDOWN_FIELDS = /* @__PURE__ */ new Set([
3163
+ "traceId",
3164
+ "threadId",
3165
+ "resourceId",
3166
+ "runId",
3167
+ "sessionId",
3168
+ "userId",
3169
+ "organizationId",
3170
+ "endedAt",
3171
+ "entityName",
3172
+ "entityType",
3173
+ "environment"
3174
+ ]);
3175
+ function isRootPushdownPredicate(predicate) {
3176
+ if (predicate.type === "relation") return false;
3177
+ if (predicate.type === "boolean") return predicate.args.every(isRootPushdownPredicate);
3178
+ if (predicate.type === "not") return isRootPushdownPredicate(predicate.arg);
3179
+ return ROOT_PUSHDOWN_FIELDS.has(predicate.field);
3180
+ }
3181
+ /** Top-level AND conjuncts of `where` that only read root-row columns. */
3182
+ function rootPushdownPredicates(predicate) {
3183
+ if (!predicate) return [];
3184
+ return (predicate.type === "boolean" && predicate.operator === "and" ? predicate.args : [predicate]).filter(isRootPushdownPredicate);
3185
+ }
3186
+ /**
3187
+ * Builds the selection CTEs. `root_trace_ids` narrows the root scan to traces that could have a
3188
+ * qualifying current root before the window functions run: some root event carries the current
3189
+ * root's own conditions (ended, tenant, pushed `where` conjuncts) and some root event has a
3190
+ * timestamp in the window (the current root's `startedAt` is a root event's timestamp in its
3191
+ * trace). This only removes traces that cannot qualify; `root_events` / `current_roots` still see
3192
+ * every root event of the remaining traces, and the full `where` is applied in `candidates`.
3193
+ */
3194
+ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredicates = []) {
3156
3195
  const tenantConditions = (alias) => scope ? [`${alias}.organizationId = ?`, ...scope.resourceId === void 0 ? [] : [`${alias}.resourceId = ?`]] : [];
3157
3196
  const tenantValues = scope ? scope.resourceId === void 0 ? [scope.organizationId] : [scope.organizationId, scope.resourceId] : [];
3158
3197
  const tenantWhere = (alias) => {
3159
3198
  const conditions = tenantConditions(alias);
3160
3199
  return conditions.length ? `\n WHERE ${conditions.join(" AND ")}` : "";
3161
3200
  };
3162
- const values = [...tenantValues];
3201
+ const rowConditions = ["r.endedAt IS NOT NULL", ...tenantConditions("r")];
3202
+ const values = [
3203
+ timeRange.from,
3204
+ timeRange.to,
3205
+ ...tenantValues
3206
+ ];
3207
+ for (const predicate of rootPredicates) {
3208
+ const compiled = compilePredicate(predicate);
3209
+ rowConditions.push(`(${compiled.sql})`);
3210
+ values.push(...compiled.values);
3211
+ }
3212
+ values.push(timeRange.from, timeRange.to, ...tenantValues);
3163
3213
  const ctes = [
3214
+ `root_trace_ids AS (
3215
+ SELECT r.traceId
3216
+ FROM span_events r
3217
+ WHERE r.parentSpanId IS NULL
3218
+ GROUP BY r.traceId
3219
+ HAVING bool_or(r.timestamp >= CAST(? AS TIMESTAMP) AND r.timestamp < CAST(? AS TIMESTAMP))
3220
+ AND bool_or(${rowConditions.join("\n AND ")})
3221
+ )`,
3164
3222
  `root_events AS (
3165
3223
  SELECT
3166
3224
  *,
@@ -3170,6 +3228,7 @@ function compileDuckDBTraceScope(relatedCollections, scope) {
3170
3228
  END AS startedAt
3171
3229
  FROM span_events
3172
3230
  WHERE parentSpanId IS NULL
3231
+ AND traceId IN (SELECT traceId FROM root_trace_ids)
3173
3232
  )`,
3174
3233
  `current_roots AS (
3175
3234
  SELECT * EXCLUDE (rootRank)
@@ -3257,28 +3316,30 @@ function compileDuckDBTraceScope(relatedCollections, scope) {
3257
3316
  values
3258
3317
  };
3259
3318
  }
3260
- function compileDuckDBTraceQuery(plan) {
3261
- const { ctes, values: scopeValues } = compileDuckDBTraceScope(collectRelatedCollections(plan.where), plan.scope);
3262
- const values = [
3263
- plan.timeRange.from,
3264
- plan.timeRange.to,
3265
- ...scopeValues
3266
- ];
3267
- const conditions = [
3268
- `r.endedAt IS NOT NULL`,
3269
- `r.startedAt >= CAST(? AS TIMESTAMP)`,
3270
- `r.startedAt < CAST(? AS TIMESTAMP)`
3271
- ];
3319
+ /**
3320
+ * Builds the trace selection stages shared by trace queries and trace aggregates: root scope,
3321
+ * related-collection scopes, and a `candidates` CTE projecting `columns` from matching roots.
3322
+ */
3323
+ function compileDuckDBTraceCandidates(plan, columns) {
3324
+ const { ctes, values } = compileDuckDBTraceScope(collectRelatedCollections(plan.where), plan.scope, plan.timeRange, rootPushdownPredicates(plan.where));
3325
+ let predicateSql = "TRUE";
3272
3326
  if (plan.where) {
3273
3327
  const predicate = compilePredicate(plan.where);
3274
- conditions.push(`(${predicate.sql})`);
3328
+ predicateSql = `(${predicate.sql})`;
3275
3329
  values.push(...predicate.values);
3276
3330
  }
3277
3331
  ctes.push(`candidates AS (
3278
- SELECT ${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.cursorId AS deltaWatermark" : ""}
3332
+ SELECT ${columns}
3279
3333
  FROM root_scope r
3280
- WHERE ${conditions.slice(3).join("\n AND ") || "TRUE"}
3334
+ WHERE ${predicateSql}
3281
3335
  )`);
3336
+ return {
3337
+ ctes,
3338
+ values
3339
+ };
3340
+ }
3341
+ function compileDuckDBTraceQuery(plan) {
3342
+ const { ctes, values } = compileDuckDBTraceCandidates(plan, `${TRACE_SELECT}${plan.paginationMode === "delta" ? ", r.cursorId AS deltaWatermark" : ""}`);
3282
3343
  const candidates = `WITH ${ctes.join(",\n ")}`;
3283
3344
  if (plan.result === "groups") {
3284
3345
  const pageCondition = plan.cursor ? `AND threadId > ?` : "";
@@ -3301,7 +3362,7 @@ LIMIT ?`,
3301
3362
  values.push(watermark ?? "0", plan.limit + 1);
3302
3363
  return {
3303
3364
  sql: `${candidates},
3304
- delta_head AS (SELECT coalesce(max(cursorId), 0) AS streamHead FROM root_events),
3365
+ delta_head AS (SELECT coalesce(max(cursorId), 0) AS streamHead FROM span_events WHERE parentSpanId IS NULL),
3305
3366
  delta_rows AS (
3306
3367
  SELECT * FROM candidates
3307
3368
  WHERE deltaWatermark > CAST(? AS BIGINT) ${watermark === void 0 ? "AND FALSE" : ""}
@@ -3331,7 +3392,7 @@ ORDER BY delta_rows.deltaWatermark ASC NULLS LAST, delta_rows.traceId ASC`,
3331
3392
  SELECT COUNT(*) AS total
3332
3393
  FROM candidates
3333
3394
  )
3334
- SELECT page_rows.*, page_total.total${deltaPollingFeatureEnabled() ? ", (SELECT coalesce(max(cursorId), 0) FROM root_events) AS streamHead" : ""}
3395
+ SELECT page_rows.*, page_total.total${deltaPollingFeatureEnabled() ? ", (SELECT coalesce(max(cursorId), 0) FROM span_events WHERE parentSpanId IS NULL) AS streamHead" : ""}
3335
3396
  FROM page_total
3336
3397
  LEFT JOIN page_rows ON TRUE
3337
3398
  ORDER BY page_rows.__row_position ASC NULLS LAST`,
@@ -3357,12 +3418,7 @@ LIMIT ?`,
3357
3418
  function compileDuckDBThreadQuery(plan) {
3358
3419
  const relatedCollections = collectRelatedCollections(plan.traces.where);
3359
3420
  collectThreadRelatedCollections(plan.where, relatedCollections);
3360
- const { ctes, values: scopeValues } = compileDuckDBTraceScope(relatedCollections, plan.scope);
3361
- const values = [
3362
- plan.traces.timeRange.from,
3363
- plan.traces.timeRange.to,
3364
- ...scopeValues
3365
- ];
3421
+ const { ctes, values } = compileDuckDBTraceScope(relatedCollections, plan.scope, plan.traces.timeRange);
3366
3422
  let eligibilitySql = "TRUE";
3367
3423
  if (plan.traces.where) {
3368
3424
  const eligibility = compilePredicate(plan.traces.where);
@@ -3420,12 +3476,7 @@ function discoveryCollections(scope) {
3420
3476
  return scope === "trace" ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([scope]);
3421
3477
  }
3422
3478
  function compileDuckDBTraceQueryObservedFields(plan) {
3423
- const { ctes, values: scopeValues } = compileDuckDBTraceScope(/* @__PURE__ */ new Set(), plan.scope);
3424
- const values = [
3425
- plan.timeRange.from,
3426
- plan.timeRange.to,
3427
- ...scopeValues
3428
- ];
3479
+ const { ctes, values } = compileDuckDBTraceScope(/* @__PURE__ */ new Set(), plan.scope, plan.timeRange);
3429
3480
  if (plan.search) values.push(plan.search);
3430
3481
  values.push(plan.limit + 1);
3431
3482
  const search = plan.search ? `AND strpos(lower('metadata.' || entry.key), lower(?)) > 0` : "";
@@ -3447,12 +3498,7 @@ LIMIT ?`,
3447
3498
  };
3448
3499
  }
3449
3500
  function compileDuckDBTraceQueryValues(plan) {
3450
- const { ctes, values: scopeValues } = compileDuckDBTraceScope(discoveryCollections(plan.predicateScope), plan.scope);
3451
- const values = [
3452
- plan.timeRange.from,
3453
- plan.timeRange.to,
3454
- ...scopeValues
3455
- ];
3501
+ const { ctes, values } = compileDuckDBTraceScope(discoveryCollections(plan.predicateScope), plan.scope, plan.timeRange);
3456
3502
  let fieldSql;
3457
3503
  let source = discoverySource(plan.predicateScope);
3458
3504
  if (plan.predicateScope === "trace" && plan.path === "tags") {
@@ -3620,6 +3666,367 @@ async function queryThreads(db, plan) {
3620
3666
  }) : null }
3621
3667
  });
3622
3668
  }
3669
+ /** Compile a span-row filter with exactly the same rules as trace span predicates. */
3670
+ function compileSpanQueryPredicate(predicate) {
3671
+ return compileScalarPredicate(predicate, SPAN_FIELDS);
3672
+ }
3673
+ //#endregion
3674
+ //#region src/storage/domains/observability/span-query.ts
3675
+ const identityFields = [
3676
+ "organizationId",
3677
+ "resourceId",
3678
+ "traceId",
3679
+ "spanId"
3680
+ ];
3681
+ const identities = identityFields.join(", ");
3682
+ const fields = [
3683
+ ...identityFields,
3684
+ "parentSpanId",
3685
+ "name",
3686
+ "spanType",
3687
+ "entityType",
3688
+ "entityId",
3689
+ "entityName",
3690
+ "entityVersionId",
3691
+ "parentEntityVersionId",
3692
+ "rootEntityVersionId",
3693
+ "runId",
3694
+ "sessionId",
3695
+ "userId"
3696
+ ];
3697
+ function keys(rows, values, alias = "") {
3698
+ return rows.map((row) => `(${identityFields.map((field) => {
3699
+ values.push(row[field]);
3700
+ return `${alias}${field} IS NOT DISTINCT FROM ?`;
3701
+ }).join(" AND ")})`).join(" OR ");
3702
+ }
3703
+ function afterCursor(plan) {
3704
+ if (!plan.cursor) return {
3705
+ sql: "TRUE",
3706
+ values: []
3707
+ };
3708
+ const cursor = plan.cursor;
3709
+ const comparisons = [`s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? ">" : "<"} CAST(? AS TIMESTAMP)`];
3710
+ const values = [cursor.sortValue];
3711
+ for (let i = 0; i < identityFields.length; i++) {
3712
+ const field = identityFields[i];
3713
+ if (cursor[field] === null) continue;
3714
+ const equal = [`s.${plan.orderBy.field} = CAST(? AS TIMESTAMP)`];
3715
+ values.push(cursor.sortValue);
3716
+ for (const prior of identityFields.slice(0, i)) {
3717
+ equal.push(`s.${prior} IS NOT DISTINCT FROM ?`);
3718
+ values.push(cursor[prior]);
3719
+ }
3720
+ values.push(cursor[field]);
3721
+ comparisons.push(`(${equal.join(" AND ")} AND (s.${field} > ? OR s.${field} IS NULL))`);
3722
+ }
3723
+ return {
3724
+ sql: `(${comparisons.join(" OR ")})`,
3725
+ values
3726
+ };
3727
+ }
3728
+ function compileDuckDBSpanQuery(plan) {
3729
+ const values = [plan.timeRange.from, plan.timeRange.to];
3730
+ const scope = [];
3731
+ if (plan.scope) {
3732
+ scope.push("organizationId = ?");
3733
+ values.push(plan.scope.organizationId);
3734
+ if (plan.scope.resourceId !== void 0) {
3735
+ scope.push("resourceId = ?");
3736
+ values.push(plan.scope.resourceId);
3737
+ }
3738
+ }
3739
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where) : {
3740
+ sql: "TRUE",
3741
+ values: []
3742
+ };
3743
+ const after = afterCursor(plan);
3744
+ values.push(plan.timeRange.from, plan.timeRange.to, ...predicate.values, ...after.values, plan.limit + 1);
3745
+ const join = identityFields.map((field) => `e.${field} IS NOT DISTINCT FROM c.${field}`).join(" AND ");
3746
+ return {
3747
+ sql: `WITH candidates AS MATERIALIZED (
3748
+ SELECT ${identities} FROM span_events
3749
+ WHERE eventType = 'start' AND timestamp >= CAST(? AS TIMESTAMP) AND timestamp < CAST(? AS TIMESTAMP)
3750
+ ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
3751
+ GROUP BY ${identities}
3752
+ ), ranked AS (
3753
+ SELECT ${fields.map((field) => `e.${field}`).join(", ")},
3754
+ min(e.timestamp) FILTER (WHERE e.eventType = 'start') OVER (PARTITION BY ${identityFields.map((field) => `e.${field}`).join(", ")}) AS startedAt, e.endedAt, e.cursorId,
3755
+ CASE WHEN json_type(e.attributes, '$.model') = 'VARCHAR' THEN json_extract_string(e.attributes, '$.model') END AS model,
3756
+ CASE WHEN json_type(e.attributes, '$.provider') = 'VARCHAR' THEN json_extract_string(e.attributes, '$.provider') END AS provider,
3757
+ date_diff('millisecond', min(e.timestamp) FILTER (WHERE e.eventType = 'start') OVER (PARTITION BY ${identityFields.map((field) => `e.${field}`).join(", ")}), e.endedAt) AS durationMs,
3758
+ CASE WHEN e.error IS NULL THEN 'success' ELSE 'error' END AS status,
3759
+ CASE WHEN e.error IS NULL THEN NULL ELSE 'error' END AS error,
3760
+ row_number() OVER (PARTITION BY ${identityFields.map((field) => `e.${field}`).join(", ")}
3761
+ ORDER BY CASE WHEN e.endedAt IS NULL THEN 1 ELSE 0 END, e.cursorId DESC NULLS LAST, e.endedAt DESC) AS rank
3762
+ FROM span_events e INNER JOIN candidates c ON ${join}
3763
+ )
3764
+ SELECT ${[
3765
+ ...identityFields,
3766
+ "parentSpanId",
3767
+ "name",
3768
+ "spanType",
3769
+ "entityType",
3770
+ "entityId",
3771
+ "entityName",
3772
+ "model",
3773
+ "provider",
3774
+ "startedAt",
3775
+ "endedAt",
3776
+ "durationMs",
3777
+ "status",
3778
+ "cursorId"
3779
+ ].map((field) => `s.${field}`).join(", ")}
3780
+ FROM ranked s WHERE rank = 1 AND endedAt IS NOT NULL AND startedAt >= CAST(? AS TIMESTAMP) AND startedAt < CAST(? AS TIMESTAMP) AND (${predicate.sql}) AND ${after.sql}
3781
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
3782
+ LIMIT ?`,
3783
+ values
3784
+ };
3785
+ }
3786
+ async function querySpans(db, plan) {
3787
+ const deadline = performance.now() + coreStorage.TRACE_QUERY_DEFAULT_TIMEOUT_MS;
3788
+ const query = async (sql, values) => {
3789
+ const remaining = Math.floor(deadline - performance.now());
3790
+ if (remaining <= 0) throw new coreStorage.TraceQueryExecutionError();
3791
+ try {
3792
+ return await db.query(sql, values, remaining);
3793
+ } catch (error) {
3794
+ if (error instanceof DuckDBQueryTimeoutError) throw new coreStorage.TraceQueryExecutionError();
3795
+ if (error instanceof Error && /out of memory/i.test(error.message)) throw new coreStorage.TraceQueryResourceLimitError();
3796
+ throw error;
3797
+ }
3798
+ };
3799
+ const compiled = compileDuckDBSpanQuery(plan);
3800
+ const rows = await query(compiled.sql, compiled.values);
3801
+ const selected = rows.map(({ cursorId: _cursorId, startedAt, endedAt, ...row }) => ({
3802
+ ...row,
3803
+ startedAt: startedAt.toISOString(),
3804
+ endedAt: endedAt.toISOString()
3805
+ }));
3806
+ const page = rows.slice(0, plan.limit);
3807
+ if (!page.length) return coreStorage.buildSpanQueryResponse(plan, selected, [], []);
3808
+ const payloadValues = [];
3809
+ const payloadWhere = page.map((row) => {
3810
+ const key = keys([row], payloadValues);
3811
+ payloadValues.push(row.cursorId, row.endedAt);
3812
+ return `(${key} AND cursorId IS NOT DISTINCT FROM ? AND endedAt = ?)`;
3813
+ }).join(" OR ");
3814
+ const payloads = await query(`SELECT DISTINCT ${identities},
3815
+ substring(CAST(input AS VARCHAR), 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS inputPreview, substring(CAST(output AS VARCHAR), 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS outputPreview
3816
+ FROM span_events WHERE ${payloadWhere}`, payloadValues);
3817
+ const metricValues = [];
3818
+ const metricWhere = keys(page, metricValues);
3819
+ const metrics = await query(`SELECT ${identities}, name, estimatedCost, costUnit,
3820
+ json_extract_string(costMetadata, '$.allocation') AS allocation,
3821
+ COALESCE(json_exists(costMetadata, '$.error'), false) AS costError
3822
+ FROM metric_events WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
3823
+ LIMIT ${coreStorage.SPAN_QUERY_MAX_COST_METRICS + 1}`, metricValues);
3824
+ if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
3825
+ return coreStorage.buildSpanQueryResponse(plan, selected, payloads, metrics);
3826
+ }
3827
+ //#endregion
3828
+ //#region src/storage/domains/observability/trace-aggregate.ts
3829
+ /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
3830
+ const DIMENSION_SQL = {
3831
+ entityType: "r.entityType",
3832
+ entityName: "r.entityName",
3833
+ environment: "r.environment",
3834
+ status: TRACE_STATUS_SQL,
3835
+ serviceName: "r.serviceName",
3836
+ executionSource: "r.executionSource",
3837
+ threadId: "r.threadId",
3838
+ resourceId: "r.resourceId",
3839
+ userId: "r.userId",
3840
+ sessionId: "r.sessionId",
3841
+ organizationId: "r.organizationId",
3842
+ experimentId: "r.experimentId"
3843
+ };
3844
+ const PERCENTILES = {
3845
+ "duration.p50": .5,
3846
+ "duration.p90": .9,
3847
+ "duration.p95": .95,
3848
+ "duration.p99": .99
3849
+ };
3850
+ const COMPARISON_SQL = {
3851
+ eq: "=",
3852
+ ne: "<>",
3853
+ lt: "<",
3854
+ lte: "<=",
3855
+ gt: ">",
3856
+ gte: ">="
3857
+ };
3858
+ /**
3859
+ * Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
3860
+ *
3861
+ * Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
3862
+ * countDistinct inputs, bucket) so grouping, measures, and joins only reference plain columns.
3863
+ * Without `interval`, groups are filtered, ordered, and limited in one grouped select. With
3864
+ * `interval`, a `ranked` CTE ranks groups on whole-window measures, then only the top `limit`
3865
+ * groups are expanded into per-bucket rows.
3866
+ *
3867
+ * Parameters are positional, so `values` is appended in the order placeholders appear in the text.
3868
+ */
3869
+ function compileDuckDBTraceAggregate(plan) {
3870
+ const { ctes, values } = compileDuckDBTraceCandidates(plan, "r.*");
3871
+ const dimensionSql = (field) => {
3872
+ if (field.startsWith("metadata.")) return {
3873
+ sql: TRACE_METADATA_VALUE_SQL,
3874
+ values: traceMetadataPathValues(field.slice(9))
3875
+ };
3876
+ const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
3877
+ if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
3878
+ return {
3879
+ sql,
3880
+ values: []
3881
+ };
3882
+ };
3883
+ const distinctSql = (field) => field === "traceId" ? {
3884
+ sql: "r.traceId",
3885
+ values: []
3886
+ } : dimensionSql(field);
3887
+ const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
3888
+ const distinctColumns = /* @__PURE__ */ new Map();
3889
+ const factColumns = [];
3890
+ plan.dimensions.forEach((dimension, index) => {
3891
+ const compiled = dimensionSql(dimension);
3892
+ factColumns.push(`CAST(${compiled.sql} AS VARCHAR) AS d${index}`);
3893
+ values.push(...compiled.values);
3894
+ });
3895
+ for (const measure of plan.measures) {
3896
+ if (measure.type !== "countDistinct" || distinctColumns.has(measure.field)) continue;
3897
+ const column = `cd${distinctColumns.size}`;
3898
+ distinctColumns.set(measure.field, column);
3899
+ const compiled = distinctSql(measure.field);
3900
+ factColumns.push(`CAST(${compiled.sql} AS VARCHAR) AS ${column}`);
3901
+ values.push(...compiled.values);
3902
+ }
3903
+ factColumns.push(`CAST(${durationMsSql("r.startedAt", "r.endedAt")} AS DOUBLE) AS durationMs`, `r.error IS NOT NULL AS isError`);
3904
+ if (plan.interval !== void 0) {
3905
+ const intervalMs = coreStorage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
3906
+ if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
3907
+ factColumns.push(`epoch_ms((epoch_ms(r.startedAt) // ${intervalMs}) * ${intervalMs}) AS bucket`);
3908
+ }
3909
+ ctes.push(`facts AS (
3910
+ SELECT ${factColumns.join(",\n ")}
3911
+ FROM candidates r
3912
+ )`);
3913
+ const measureSql = (name, alias = "") => {
3914
+ const column = (name) => `${alias}${name}`;
3915
+ if (name === "count") return "CAST(count(*) AS DOUBLE)";
3916
+ if (name === "errorCount") return `CAST(count_if(${column("isError")}) AS DOUBLE)`;
3917
+ if (name === "errorRate") return `CAST(count_if(${column("isError")}) AS DOUBLE) / count(*)`;
3918
+ if (name === "duration.avg") return `avg(${column("durationMs")})`;
3919
+ if (name === "duration.min") return `min(${column("durationMs")})`;
3920
+ if (name === "duration.max") return `max(${column("durationMs")})`;
3921
+ const percentile = PERCENTILES[name];
3922
+ if (percentile !== void 0) return `quantile_cont(${column("durationMs")}, ${percentile})`;
3923
+ const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
3924
+ const distinctColumn = measure && distinctColumns.get(measure.field);
3925
+ if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
3926
+ return `CAST(count(DISTINCT ${alias}${distinctColumn}) AS DOUBLE)`;
3927
+ };
3928
+ const havingValues = [];
3929
+ const compileHaving = (predicate) => {
3930
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileHaving(arg)})`).join(predicate.operator === "and" ? " AND " : " OR ");
3931
+ if (predicate.type === "not") return `NOT (${compileHaving(predicate.arg)})`;
3932
+ const measure = measureSql(predicate.measure);
3933
+ if (predicate.type === "membership") {
3934
+ if (predicate.values.length === 0) return predicate.operator === "in" ? "FALSE" : "TRUE";
3935
+ havingValues.push(...predicate.values);
3936
+ const list = predicate.values.map(() => "CAST(? AS DOUBLE)").join(", ");
3937
+ return `${measure} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${list})`;
3938
+ }
3939
+ const operator = COMPARISON_SQL[predicate.operator];
3940
+ if (operator === void 0) throw new Error(`Unsupported trusted trace-aggregate operator: ${predicate.operator}`);
3941
+ havingValues.push(predicate.value);
3942
+ return `${measure} ${operator} CAST(? AS DOUBLE)`;
3943
+ };
3944
+ const havingSql = ["count(*) > 0", ...plan.having ? [`(${compileHaving(plan.having)})`] : []].join(" AND ");
3945
+ const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
3946
+ let orderTarget;
3947
+ if (plan.orderBy.target === "measure") orderTarget = measureSql(plan.orderBy.measure);
3948
+ else {
3949
+ const index = plan.dimensions.indexOf(plan.orderBy.dimension);
3950
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate order dimension: ${plan.orderBy.dimension}`);
3951
+ orderTarget = dimensionColumns[index];
3952
+ }
3953
+ const orderSql = [`${orderTarget} ${direction} NULLS LAST`, ...dimensionColumns.map((column) => `${column} ASC NULLS LAST`)].join(", ");
3954
+ const groupSql = dimensionColumns.length > 0 ? `\nGROUP BY ${dimensionColumns.join(", ")}` : "";
3955
+ if (plan.interval === void 0) {
3956
+ const projected = [...dimensionColumns, ...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`)];
3957
+ values.push(...havingValues, plan.limit + 1);
3958
+ return {
3959
+ sql: `WITH ${ctes.join(",\n ")}
3960
+ SELECT ${projected.join(", ")}
3961
+ FROM facts${groupSql}
3962
+ HAVING ${havingSql}
3963
+ ORDER BY ${orderSql}
3964
+ LIMIT ?`,
3965
+ values
3966
+ };
3967
+ }
3968
+ ctes.push(`ranked AS (
3969
+ SELECT ${[...dimensionColumns, `row_number() OVER (ORDER BY ${orderSql}) AS rank`].join(", ")}
3970
+ FROM facts${groupSql}
3971
+ HAVING ${havingSql}
3972
+ ORDER BY ${orderSql}
3973
+ LIMIT ?
3974
+ )`);
3975
+ values.push(...havingValues, plan.limit + 1);
3976
+ const joinSql = dimensionColumns.length > 0 ? dimensionColumns.map((column) => `f.${column} IS NOT DISTINCT FROM g.${column}`).join(" AND ") : "TRUE";
3977
+ const projected = [
3978
+ ...dimensionColumns.map((column) => `f.${column}`),
3979
+ "f.bucket",
3980
+ ...plan.measures.map((measure, index) => `${measureSql(measure.name, "f.")} AS m${index}`),
3981
+ `(SELECT count(*) FROM ranked) > ? AS truncated`
3982
+ ];
3983
+ values.push(plan.limit, plan.limit);
3984
+ return {
3985
+ sql: `WITH ${ctes.join(",\n ")}
3986
+ SELECT ${projected.join(", ")}
3987
+ FROM facts f
3988
+ JOIN ranked g ON ${joinSql}
3989
+ WHERE g.rank <= ?
3990
+ GROUP BY ${[
3991
+ "g.rank",
3992
+ ...dimensionColumns.map((column) => `f.${column}`),
3993
+ "f.bucket"
3994
+ ].join(", ")}
3995
+ ORDER BY g.rank ASC, f.bucket ASC`,
3996
+ values
3997
+ };
3998
+ }
3999
+ async function aggregateTraces(db, plan) {
4000
+ const compiled = compileDuckDBTraceAggregate(plan);
4001
+ let result;
4002
+ try {
4003
+ result = await db.query(compiled.sql, compiled.values);
4004
+ } catch (error) {
4005
+ if (isDuckDBResourceLimit(error)) throw new coreStorage.TraceQueryResourceLimitError();
4006
+ throw error;
4007
+ }
4008
+ let truncated;
4009
+ let resultRows;
4010
+ if (plan.interval === void 0) {
4011
+ truncated = result.length > plan.limit;
4012
+ resultRows = result.slice(0, plan.limit);
4013
+ } else {
4014
+ truncated = result[0]?.truncated === true;
4015
+ resultRows = result;
4016
+ }
4017
+ return {
4018
+ rows: resultRows.map((row) => {
4019
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
4020
+ if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => {
4021
+ const value = row[`d${index}`];
4022
+ return [dimension, value == null ? null : String(value)];
4023
+ }));
4024
+ if (plan.interval !== void 0) shaped.bucket = asIsoTimestamp(row.bucket);
4025
+ return shaped;
4026
+ }),
4027
+ truncated
4028
+ };
4029
+ }
3623
4030
  //#endregion
3624
4031
  //#region src/storage/domains/observability/tracing.ts
3625
4032
  const COLUMNS_SQL = [
@@ -4948,6 +5355,8 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends Observ
4948
5355
  "tag-discovery",
4949
5356
  "metric-discovery",
4950
5357
  "trace-query",
5358
+ "trace-aggregate",
5359
+ ...spanQueryFeatures,
4951
5360
  "trace-query-root-duration",
4952
5361
  "trace-query-discovery",
4953
5362
  "thread-query",
@@ -4966,6 +5375,8 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends Observ
4966
5375
  "metric-discovery",
4967
5376
  "delta-polling",
4968
5377
  "trace-query",
5378
+ "trace-aggregate",
5379
+ ...spanQueryFeatures,
4969
5380
  "trace-query-root-duration",
4970
5381
  "trace-query-discovery",
4971
5382
  "thread-query",
@@ -5001,9 +5412,15 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends Observ
5001
5412
  async listTraces(args) {
5002
5413
  return listTraces(this.db, args);
5003
5414
  }
5415
+ async querySpans(plan) {
5416
+ return querySpans(this.db, plan);
5417
+ }
5004
5418
  async queryTraces(plan) {
5005
5419
  return queryTraces(this.db, plan);
5006
5420
  }
5421
+ async aggregateTraces(plan) {
5422
+ return aggregateTraces(this.db, plan);
5423
+ }
5007
5424
  async getTraceQueryObservedFields(plan) {
5008
5425
  return getTraceQueryObservedFields(this.db, plan);
5009
5426
  }
@@ -5126,4 +5543,4 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends Observ
5126
5543
  //#endregion
5127
5544
  export { ObservabilityStorageDuckDB };
5128
5545
 
5129
- //# sourceMappingURL=observability-BmLO_YTO.js.map
5546
+ //# sourceMappingURL=observability-Bk_y3hof.js.map