@mastra/clickhouse 1.23.0-alpha.0 → 1.23.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.
Files changed (31) hide show
  1. package/dist/docs/SKILL.md +1 -1
  2. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  3. package/dist/index.cjs +815 -386
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +816 -387
  6. package/dist/index.js.map +1 -1
  7. package/dist/storage/db/index.d.ts +8 -0
  8. package/dist/storage/db/index.d.ts.map +1 -1
  9. package/dist/storage/db/utils.d.ts +11 -0
  10. package/dist/storage/db/utils.d.ts.map +1 -1
  11. package/dist/storage/domains/memory/index.d.ts +2 -0
  12. package/dist/storage/domains/memory/index.d.ts.map +1 -1
  13. package/dist/storage/domains/observability/index.d.ts.map +1 -1
  14. package/dist/storage/domains/observability/v-next/ddl.d.ts +5 -5
  15. package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
  16. package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
  17. package/dist/storage/domains/observability/v-next/index.d.ts +3 -2
  18. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  19. package/dist/storage/domains/observability/v-next/metrics.d.ts.map +1 -1
  20. package/dist/storage/domains/observability/v-next/polling.d.ts +2 -0
  21. package/dist/storage/domains/observability/v-next/polling.d.ts.map +1 -1
  22. package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
  23. package/dist/storage/domains/observability/v-next/span-query.d.ts +45 -0
  24. package/dist/storage/domains/observability/v-next/span-query.d.ts.map +1 -0
  25. package/dist/storage/domains/observability/v-next/trace-query.d.ts +21 -2
  26. package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
  27. package/dist/storage/domains/observability/v-next/trace-roots.d.ts.map +1 -1
  28. package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
  29. package/dist/storage/domains/scores/index.d.ts.map +1 -1
  30. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  31. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -279,6 +279,35 @@ const TABLE_ENGINES = {
279
279
  [_mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY]: `ReplacingMergeTree()`,
280
280
  [_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX]: `ReplacingMergeTree()`
281
281
  };
282
+ /**
283
+ * Bloom-filter skip indexes for lookups on columns outside the sort key.
284
+ *
285
+ * These tables are keyed `(createdAt, id)` (or `(createdAt, run_id, ...)` for
286
+ * workflow snapshots), so every lookup by id, thread, resource or run would
287
+ * otherwise scan every granule. Added idempotently at init; existing parts are
288
+ * covered as merges and retention rewrite them (or via `MATERIALIZE INDEX`).
289
+ */
290
+ const TABLE_SKIP_INDEXES = {
291
+ [_mastra_core_storage.TABLE_MESSAGES]: [
292
+ "id",
293
+ "thread_id",
294
+ "resourceId"
295
+ ],
296
+ [_mastra_core_storage.TABLE_THREADS]: ["id", "resourceId"],
297
+ [_mastra_core_storage.TABLE_RESOURCES]: ["id"],
298
+ [_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT]: ["run_id"],
299
+ [_mastra_core_storage.TABLE_SCORERS]: [
300
+ "id",
301
+ "runId",
302
+ "scorerId",
303
+ "entityId",
304
+ "traceId"
305
+ ]
306
+ };
307
+ /** Name of the skip index {@link TABLE_SKIP_INDEXES} declares for `column`. */
308
+ function skipIndexName(column) {
309
+ return `idx_${column}`;
310
+ }
282
311
  const COLUMN_TYPES = {
283
312
  text: "String",
284
313
  timestamp: "DateTime64(3)",
@@ -391,10 +420,39 @@ var ClickhouseDB = class extends _mastra_core_base.MastraBase {
391
420
  return filtered;
392
421
  }
393
422
  async hasColumn(table, column) {
394
- return (await (await this.client.query({
395
- query: `DESCRIBE TABLE ${table}`,
396
- format: "JSONEachRow"
397
- })).json()).some((c) => c.name === column);
423
+ return (await this.getTableColumns(table)).has(column);
424
+ }
425
+ /**
426
+ * Adds the bloom-filter skip indexes declared in {@link TABLE_SKIP_INDEXES}
427
+ * for `tableName`. Call after createTable/alterTable so optional columns
428
+ * exist. Indexes (or columns) already present are skipped: on Replicated
429
+ * tables every issued ALTER bumps the metadata version even with
430
+ * `IF NOT EXISTS`.
431
+ */
432
+ async ensureSkipIndexes(tableName) {
433
+ const columns = TABLE_SKIP_INDEXES[tableName];
434
+ if (!columns?.length) return;
435
+ try {
436
+ const existingColumns = await this.getTableColumns(tableName);
437
+ const result = await this.client.query({
438
+ query: `SELECT name FROM system.data_skipping_indices WHERE database = currentDatabase() AND table = {table:String}`,
439
+ query_params: { table: tableName },
440
+ format: "JSONEachRow"
441
+ });
442
+ const existingIndexes = new Set((await result.json()).map((row) => row.name));
443
+ for (const column of columns) {
444
+ const name = skipIndexName(column);
445
+ if (existingIndexes.has(name) || !existingColumns.has(column)) continue;
446
+ await this.client.command({ query: addOnClusterToDDL(`ALTER TABLE ${tableName} ADD INDEX IF NOT EXISTS ${name} "${column}" TYPE bloom_filter(0.01) GRANULARITY 1`, this.replication) });
447
+ }
448
+ } catch (error) {
449
+ throw new _mastra_core_error.MastraError({
450
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "ENSURE_SKIP_INDEXES", "FAILED"),
451
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
452
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
453
+ details: { tableName }
454
+ }, error);
455
+ }
398
456
  }
399
457
  /**
400
458
  * Checks if a table exists in the database.
@@ -787,7 +845,8 @@ var ClickhouseDB = class extends _mastra_core_base.MastraBase {
787
845
  date_time_input_format: "best_effort",
788
846
  date_time_output_format: "iso",
789
847
  use_client_time_zone: 1,
790
- output_format_json_quote_64bit_integers: 0
848
+ output_format_json_quote_64bit_integers: 0,
849
+ ...tableName === _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT ? { use_skip_indexes_if_final: 1 } : {}
791
850
  }
792
851
  });
793
852
  if (!result) return null;
@@ -884,6 +943,9 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
884
943
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_MESSAGES],
885
944
  ifNotExists: ["resourceId"]
886
945
  });
946
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_THREADS);
947
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_MESSAGES);
948
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_RESOURCES);
887
949
  }
888
950
  async dangerouslyClearAll() {
889
951
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_MESSAGES });
@@ -1226,16 +1288,9 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1226
1288
  for (const message of messages) {
1227
1289
  if (!message.resourceId) throw new Error("Resource ID is required");
1228
1290
  if (!message.threadId) throw new Error("Thread ID is required");
1229
- if (!await this.getThreadById({ threadId: message.threadId })) throw new Error(`Thread ${message.threadId} not found`);
1230
- }
1231
- const threadIdSet = /* @__PURE__ */ new Map();
1232
- await Promise.all(messages.map(async (m) => {
1233
- if (!m.resourceId) throw new Error("Resource ID is required");
1234
- if (!m.threadId) throw new Error("Thread ID is required");
1235
- const thread = await this.getThreadById({ threadId: m.threadId });
1236
- if (!thread) throw new Error(`Thread ${m.threadId} not found`);
1237
- threadIdSet.set(m.threadId, thread);
1238
- }));
1291
+ }
1292
+ const threadIdSet = await this.getLatestThreads([...new Set(messages.map((m) => m.threadId))]);
1293
+ for (const message of messages) if (!threadIdSet.has(message.threadId)) throw new Error(`Thread ${message.threadId} not found`);
1239
1294
  try {
1240
1295
  const existingRows = await (await this.client.query({
1241
1296
  query: `SELECT id, thread_id FROM ${_mastra_core_storage.TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
@@ -1338,6 +1393,38 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1338
1393
  }, error);
1339
1394
  }
1340
1395
  }
1396
+ /** Newest version of each requested thread, keyed by id. Missing threads are absent. */
1397
+ async getLatestThreads(threadIds) {
1398
+ const rows = await (await this.client.query({
1399
+ query: `SELECT
1400
+ id,
1401
+ "resourceId",
1402
+ title,
1403
+ metadata,
1404
+ toDateTime64(createdAt, 3) as createdAt,
1405
+ toDateTime64(updatedAt, 3) as updatedAt
1406
+ FROM "${_mastra_core_storage.TABLE_THREADS}"
1407
+ WHERE id IN {threadIds:Array(String)}
1408
+ ORDER BY updatedAt DESC
1409
+ LIMIT 1 BY id`,
1410
+ query_params: { threadIds },
1411
+ clickhouse_settings: {
1412
+ date_time_input_format: "best_effort",
1413
+ date_time_output_format: "iso",
1414
+ use_client_time_zone: 1,
1415
+ output_format_json_quote_64bit_integers: 0
1416
+ }
1417
+ })).json();
1418
+ const threads = /* @__PURE__ */ new Map();
1419
+ for (const row of rows.data) {
1420
+ const thread = transformRow(row);
1421
+ threads.set(thread.id, {
1422
+ ...thread,
1423
+ metadata: parseMetadata(thread.metadata)
1424
+ });
1425
+ }
1426
+ return threads;
1427
+ }
1341
1428
  async getThreadById({ threadId, resourceId }) {
1342
1429
  try {
1343
1430
  const thread = transformRow((await (await this.client.query({
@@ -1512,19 +1599,27 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1512
1599
  metadataIndex++;
1513
1600
  }
1514
1601
  }
1602
+ const latestThreads = `
1603
+ latest_threads AS (
1604
+ SELECT
1605
+ id,
1606
+ resourceId,
1607
+ title,
1608
+ metadata,
1609
+ toDateTime64(createdAt, 3) as createdAt,
1610
+ toDateTime64(updatedAt, 3) as updatedAt
1611
+ FROM ${_mastra_core_storage.TABLE_THREADS}
1612
+ ${filter?.resourceId ? `WHERE id IN (SELECT id FROM ${_mastra_core_storage.TABLE_THREADS} WHERE resourceId = {resourceId:String})` : ""}
1613
+ ORDER BY updatedAt DESC
1614
+ LIMIT 1 BY id
1615
+ )`;
1616
+ const latestWhere = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
1515
1617
  const total = (await (await this.client.query({
1516
1618
  query: `
1517
- WITH ranked_threads AS (
1518
- SELECT
1519
- id,
1520
- resourceId,
1521
- metadata,
1522
- ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
1523
- FROM ${_mastra_core_storage.TABLE_THREADS}
1524
- )
1525
- SELECT count(*) as total
1526
- FROM ranked_threads
1527
- WHERE row_num = 1 ${whereClauses.length > 0 ? `AND ${whereClauses.join(" AND ")}` : ""}
1619
+ WITH ${latestThreads}
1620
+ SELECT count(*) as total
1621
+ FROM latest_threads
1622
+ ${latestWhere}
1528
1623
  `,
1529
1624
  query_params: queryParams,
1530
1625
  clickhouse_settings: {
@@ -1544,17 +1639,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1544
1639
  return {
1545
1640
  threads: transformRows((await (await this.client.query({
1546
1641
  query: `
1547
- WITH ranked_threads AS (
1548
- SELECT
1549
- id,
1550
- resourceId,
1551
- title,
1552
- metadata,
1553
- toDateTime64(createdAt, 3) as createdAt,
1554
- toDateTime64(updatedAt, 3) as updatedAt,
1555
- ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
1556
- FROM ${_mastra_core_storage.TABLE_THREADS}
1557
- )
1642
+ WITH ${latestThreads}
1558
1643
  SELECT
1559
1644
  id,
1560
1645
  resourceId,
@@ -1562,8 +1647,8 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1562
1647
  metadata,
1563
1648
  createdAt,
1564
1649
  updatedAt
1565
- FROM ranked_threads
1566
- WHERE row_num = 1 ${whereClauses.length > 0 ? `AND ${whereClauses.join(" AND ")}` : ""}
1650
+ FROM latest_threads
1651
+ ${latestWhere}
1567
1652
  ORDER BY "${field}" ${direction === "DESC" ? "DESC" : "ASC"}
1568
1653
  LIMIT {perPage:Int64} OFFSET {offset:Int64}
1569
1654
  `,
@@ -1676,37 +1761,32 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1676
1761
  clickhouse_settings: {
1677
1762
  date_time_input_format: "best_effort",
1678
1763
  use_client_time_zone: 1,
1679
- output_format_json_quote_64bit_integers: 0
1764
+ output_format_json_quote_64bit_integers: 0,
1765
+ mutations_sync: "1"
1680
1766
  }
1681
1767
  }));
1682
1768
  }
1683
1769
  }
1684
1770
  if (updatePromises.length > 0) await Promise.all(updatePromises);
1685
- await this.client.command({
1686
- query: `OPTIMIZE TABLE ${_mastra_core_storage.TABLE_MESSAGES} FINAL`,
1771
+ const verifyResult = await this.client.query({
1772
+ query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${_mastra_core_storage.TABLE_MESSAGES} WHERE id IN {messageIds:Array(String)}`,
1773
+ query_params: { messageIds },
1687
1774
  clickhouse_settings: {
1688
1775
  date_time_input_format: "best_effort",
1776
+ date_time_output_format: "iso",
1689
1777
  use_client_time_zone: 1,
1690
1778
  output_format_json_quote_64bit_integers: 0
1691
1779
  }
1692
1780
  });
1781
+ const verifiedById = /* @__PURE__ */ new Map();
1782
+ for (const row of transformRows((await verifyResult.json()).data)) if (!verifiedById.has(row.id)) verifiedById.set(row.id, row);
1693
1783
  for (const existingMessage of parsedExistingMessages) {
1694
1784
  const updatePayload = messages.find((m) => m.id === existingMessage.id);
1695
1785
  if (!updatePayload) continue;
1696
1786
  const { id, ...fieldsToUpdate } = updatePayload;
1697
1787
  if (Object.keys(fieldsToUpdate).length === 0) continue;
1698
- const verifyRows = await (await this.client.query({
1699
- query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${_mastra_core_storage.TABLE_MESSAGES} WHERE id = {messageId:String}`,
1700
- query_params: { messageId: id },
1701
- clickhouse_settings: {
1702
- date_time_input_format: "best_effort",
1703
- date_time_output_format: "iso",
1704
- use_client_time_zone: 1,
1705
- output_format_json_quote_64bit_integers: 0
1706
- }
1707
- })).json();
1708
- if (verifyRows.data.length > 0) {
1709
- const updatedMessage = transformRows(verifyRows.data)[0];
1788
+ if (verifiedById.has(id)) {
1789
+ const updatedMessage = verifiedById.get(id);
1710
1790
  if (updatedMessage) {
1711
1791
  let needsRetry = false;
1712
1792
  for (const [key, value] of Object.entries(fieldsToUpdate)) if (key === "content") {
@@ -1773,68 +1853,42 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1773
1853
  }
1774
1854
  if (threadIdsToUpdate.size > 0) {
1775
1855
  await new Promise((resolve) => setTimeout(resolve, 10));
1776
- const now = (/* @__PURE__ */ new Date()).toISOString().replace("Z", "");
1777
- const threadUpdatePromises = Array.from(threadIdsToUpdate).map(async (threadId) => {
1778
- const threadRows = await (await this.client.query({
1779
- query: `SELECT id, resourceId, title, metadata, createdAt FROM ${_mastra_core_storage.TABLE_THREADS} WHERE id = {threadId:String} ORDER BY updatedAt DESC LIMIT 1`,
1780
- query_params: { threadId },
1781
- clickhouse_settings: {
1782
- date_time_input_format: "best_effort",
1783
- date_time_output_format: "iso",
1784
- use_client_time_zone: 1,
1785
- output_format_json_quote_64bit_integers: 0
1786
- }
1787
- })).json();
1788
- if (threadRows.data.length > 0) {
1789
- const existingThread = threadRows.data[0];
1790
- await this.client.command({
1791
- query: `DELETE FROM ${_mastra_core_storage.TABLE_THREADS} WHERE id = {threadId:String}`,
1792
- query_params: { threadId },
1793
- clickhouse_settings: {
1794
- date_time_input_format: "best_effort",
1795
- use_client_time_zone: 1,
1796
- output_format_json_quote_64bit_integers: 0
1797
- }
1798
- });
1799
- await this.client.insert({
1800
- table: _mastra_core_storage.TABLE_THREADS,
1801
- format: "JSONEachRow",
1802
- values: [{
1803
- id: existingThread.id,
1804
- resourceId: existingThread.resourceId,
1805
- title: existingThread.title,
1806
- metadata: typeof existingThread.metadata === "string" ? existingThread.metadata : serializeMetadata(existingThread.metadata),
1807
- createdAt: existingThread.createdAt,
1808
- updatedAt: now
1809
- }],
1810
- clickhouse_settings: {
1811
- date_time_input_format: "best_effort",
1812
- use_client_time_zone: 1,
1813
- output_format_json_quote_64bit_integers: 0
1814
- }
1815
- });
1816
- }
1817
- });
1818
- await Promise.all(threadUpdatePromises);
1819
- }
1820
- const updatedMessages = [];
1821
- for (const messageId of messageIds) {
1822
- const updatedRows = await (await this.client.query({
1823
- query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${_mastra_core_storage.TABLE_MESSAGES} WHERE id = {messageId:String}`,
1824
- query_params: { messageId },
1856
+ const now = Date.now();
1857
+ const existingThreads = await this.getLatestThreads(Array.from(threadIdsToUpdate));
1858
+ if (existingThreads.size > 0) await this.client.insert({
1859
+ table: _mastra_core_storage.TABLE_THREADS,
1860
+ format: "JSONEachRow",
1861
+ values: Array.from(existingThreads.values()).map((existingThread) => ({
1862
+ id: existingThread.id,
1863
+ resourceId: existingThread.resourceId,
1864
+ title: existingThread.title,
1865
+ metadata: serializeMetadata(existingThread.metadata),
1866
+ createdAt: existingThread.createdAt,
1867
+ updatedAt: new Date(Math.max(now, new Date(existingThread.updatedAt).getTime() + 1)).toISOString().replace("Z", "")
1868
+ })),
1825
1869
  clickhouse_settings: {
1826
1870
  date_time_input_format: "best_effort",
1827
- date_time_output_format: "iso",
1828
1871
  use_client_time_zone: 1,
1829
1872
  output_format_json_quote_64bit_integers: 0
1830
1873
  }
1831
- })).json();
1832
- if (updatedRows.data.length > 0) {
1833
- const message = transformRows(updatedRows.data)[0];
1834
- if (message) updatedMessages.push(message);
1835
- }
1874
+ });
1836
1875
  }
1837
- return updatedMessages.map((message) => {
1876
+ const updatedResult = await this.client.query({
1877
+ query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${_mastra_core_storage.TABLE_MESSAGES} WHERE id IN {messageIds:Array(String)}`,
1878
+ query_params: { messageIds },
1879
+ clickhouse_settings: {
1880
+ date_time_input_format: "best_effort",
1881
+ date_time_output_format: "iso",
1882
+ use_client_time_zone: 1,
1883
+ output_format_json_quote_64bit_integers: 0
1884
+ }
1885
+ });
1886
+ const updatedById = /* @__PURE__ */ new Map();
1887
+ for (const row of transformRows((await updatedResult.json()).data)) if (!updatedById.has(row.id)) updatedById.set(row.id, row);
1888
+ return messageIds.flatMap((messageId) => {
1889
+ const message = updatedById.get(messageId);
1890
+ return message ? [message] : [];
1891
+ }).map((message) => {
1838
1892
  if (typeof message.content === "string") try {
1839
1893
  message.content = JSON.parse(message.content);
1840
1894
  } catch {}
@@ -1930,35 +1984,9 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1930
1984
  ...existingResource.metadata,
1931
1985
  ...metadata
1932
1986
  },
1933
- updatedAt: /* @__PURE__ */ new Date()
1987
+ updatedAt: new Date(Math.max(Date.now(), existingResource.updatedAt.getTime() + 1))
1934
1988
  };
1935
- const updateQuery = `
1936
- ALTER TABLE ${_mastra_core_storage.TABLE_RESOURCES}
1937
- UPDATE workingMemory = {workingMemory:String}, metadata = {metadata:String}, updatedAt = {updatedAt:String}
1938
- WHERE id = {resourceId:String}
1939
- `;
1940
- await this.client.command({
1941
- query: updateQuery,
1942
- query_params: {
1943
- workingMemory: updatedResource.workingMemory,
1944
- metadata: JSON.stringify(updatedResource.metadata),
1945
- updatedAt: updatedResource.updatedAt.toISOString().replace("Z", ""),
1946
- resourceId
1947
- },
1948
- clickhouse_settings: {
1949
- date_time_input_format: "best_effort",
1950
- use_client_time_zone: 1,
1951
- output_format_json_quote_64bit_integers: 0
1952
- }
1953
- });
1954
- await this.client.command({
1955
- query: `OPTIMIZE TABLE ${_mastra_core_storage.TABLE_RESOURCES} FINAL`,
1956
- clickhouse_settings: {
1957
- date_time_input_format: "best_effort",
1958
- use_client_time_zone: 1,
1959
- output_format_json_quote_64bit_integers: 0
1960
- }
1961
- });
1989
+ await this.saveResource({ resource: updatedResource });
1962
1990
  return updatedResource;
1963
1991
  } catch (error) {
1964
1992
  throw new _mastra_core_error.MastraError({
@@ -2406,14 +2434,10 @@ var ObservabilityStorageClickhouse = class extends _mastra_core_storage.Observab
2406
2434
  }
2407
2435
  if (filters.hasChildError !== void 0) {
2408
2436
  const finalClause = (TABLE_ENGINES[_mastra_core_storage.TABLE_SPANS] ?? "MergeTree()").startsWith("ReplacingMergeTree") ? "FINAL" : "";
2409
- if (filters.hasChildError) conditions.push(`EXISTS (
2410
- SELECT 1 FROM ${_mastra_core_storage.TABLE_SPANS} ${finalClause} c
2411
- WHERE c.traceId = ${_mastra_core_storage.TABLE_SPANS}.traceId AND c.error IS NOT NULL AND c.error != ''
2412
- )`);
2413
- else conditions.push(`NOT EXISTS (
2414
- SELECT 1 FROM ${_mastra_core_storage.TABLE_SPANS} ${finalClause} c
2415
- WHERE c.traceId = ${_mastra_core_storage.TABLE_SPANS}.traceId AND c.error IS NOT NULL AND c.error != ''
2416
- )`);
2437
+ conditions.push(`traceId ${filters.hasChildError ? "IN" : "NOT IN"} (
2438
+ SELECT c.traceId FROM ${_mastra_core_storage.TABLE_SPANS} ${finalClause} c
2439
+ WHERE c.error IS NOT NULL AND c.error != ''
2440
+ )`);
2417
2441
  }
2418
2442
  }
2419
2443
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -2505,35 +2529,58 @@ var ObservabilityStorageClickhouse = class extends _mastra_core_storage.Observab
2505
2529
  async batchUpdateSpans(args) {
2506
2530
  try {
2507
2531
  const now = Date.now();
2532
+ if (args.records.length === 0) return;
2533
+ const keyParams = {};
2534
+ const keyTuples = args.records.map((record, i) => {
2535
+ keyParams[`t_${i}`] = record.traceId;
2536
+ keyParams[`s_${i}`] = record.spanId;
2537
+ return `({t_${i}:String}, {s_${i}:String})`;
2538
+ });
2539
+ const existingResult = await this.client.query({
2540
+ query: `SELECT *, toDateTime64(createdAt, 3) as createdAt, toDateTime64(updatedAt, 3) as updatedAt
2541
+ FROM ${_mastra_core_storage.TABLE_SPANS}
2542
+ WHERE (traceId, spanId) IN (${keyTuples.join(", ")})
2543
+ -- Newest version per span, which is what ReplacingMergeTree(updatedAt)
2544
+ -- keeps. FINAL would merge every part the batch's keys touch, which
2545
+ -- costs far more than reading the unmerged versions and sorting them.
2546
+ ORDER BY updatedAt DESC
2547
+ LIMIT 1 BY traceId, spanId`,
2548
+ query_params: keyParams,
2549
+ clickhouse_settings: {
2550
+ date_time_input_format: "best_effort",
2551
+ date_time_output_format: "iso",
2552
+ use_client_time_zone: 1,
2553
+ output_format_json_quote_64bit_integers: 0
2554
+ }
2555
+ });
2556
+ const spanKey = (traceId, spanId) => `${traceId}\u0000${spanId}`;
2557
+ const current = /* @__PURE__ */ new Map();
2558
+ for (const row of transformRows((await existingResult.json()).data)) current.set(spanKey(row.traceId, row.spanId), row);
2508
2559
  for (const record of args.records) {
2509
- const existing = await this.#db.load({
2510
- tableName: _mastra_core_storage.TABLE_SPANS,
2511
- keys: {
2512
- spanId: record.spanId,
2513
- traceId: record.traceId
2514
- }
2560
+ const key = spanKey(record.traceId, record.spanId);
2561
+ const existing = current.get(key);
2562
+ if (!existing) continue;
2563
+ const updates = { ...record.updates };
2564
+ if (updates.startedAt instanceof Date) updates.startedAt = updates.startedAt.getTime();
2565
+ if (updates.endedAt instanceof Date) updates.endedAt = updates.endedAt.getTime();
2566
+ current.set(key, {
2567
+ ...existing,
2568
+ ...updates,
2569
+ updatedAt: now,
2570
+ __updated: true
2515
2571
  });
2516
- if (existing) {
2517
- const updates = { ...record.updates };
2518
- if (updates.startedAt instanceof Date) updates.startedAt = updates.startedAt.getTime();
2519
- if (updates.endedAt instanceof Date) updates.endedAt = updates.endedAt.getTime();
2520
- const updated = {
2521
- ...existing,
2522
- ...updates,
2523
- updatedAt: now
2524
- };
2525
- await this.client.insert({
2526
- table: _mastra_core_storage.TABLE_SPANS,
2527
- values: [updated],
2528
- format: "JSONEachRow",
2529
- clickhouse_settings: {
2530
- date_time_input_format: "best_effort",
2531
- use_client_time_zone: 1,
2532
- output_format_json_quote_64bit_integers: 0
2533
- }
2534
- });
2535
- }
2536
2572
  }
2573
+ const updatedRows = Array.from(current.values()).filter((row) => row.__updated).map(({ __updated: _updated, ...row }) => row);
2574
+ if (updatedRows.length > 0) await this.client.insert({
2575
+ table: _mastra_core_storage.TABLE_SPANS,
2576
+ values: updatedRows,
2577
+ format: "JSONEachRow",
2578
+ clickhouse_settings: {
2579
+ date_time_input_format: "best_effort",
2580
+ use_client_time_zone: 1,
2581
+ output_format_json_quote_64bit_integers: 0
2582
+ }
2583
+ });
2537
2584
  } catch (error) {
2538
2585
  throw new _mastra_core_error.MastraError({
2539
2586
  id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "BATCH_UPDATE_SPANS", "FAILED"),
@@ -2762,7 +2809,12 @@ CREATE TABLE IF NOT EXISTS ${TABLE_TRACE_ROOTS} (
2762
2809
  output Nullable(String),
2763
2810
  error Nullable(String),
2764
2811
  metadataRaw Nullable(String),
2765
- requestContext Nullable(String)
2812
+ requestContext Nullable(String),
2813
+
2814
+ -- traceId is second in the sort key, behind startedAt, so point lookups by
2815
+ -- traceId (getRootSpan, trace deletes, trace-query root dedupe) need a skip
2816
+ -- index to avoid scanning every granule.
2817
+ INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
2766
2818
  )
2767
2819
  ENGINE = ReplacingMergeTree
2768
2820
  PARTITION BY toDate(endedAt)
@@ -2836,7 +2888,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_TRACE_BRANCHES} (
2836
2888
  output Nullable(String),
2837
2889
  error Nullable(String),
2838
2890
  metadataRaw Nullable(String),
2839
- requestContext Nullable(String)
2891
+ requestContext Nullable(String),
2892
+
2893
+ INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
2840
2894
  )
2841
2895
  ENGINE = ReplacingMergeTree
2842
2896
  PARTITION BY toDate(endedAt)
@@ -3065,7 +3119,19 @@ CREATE TABLE IF NOT EXISTS ${TABLE_LOG_EVENTS} (
3065
3119
  -- Information-only JSON payloads
3066
3120
  data Nullable(String),
3067
3121
  metadata Nullable(String),
3068
- scope Nullable(String)
3122
+ scope Nullable(String),
3123
+
3124
+ -- Same ID drilldown indexes as metric_events: these columns are outside
3125
+ -- the sort key, so without them every drilldown scans the time range.
3126
+ INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 2,
3127
+ INDEX idx_threadId threadId TYPE bloom_filter(0.01) GRANULARITY 2,
3128
+ INDEX idx_resourceId resourceId TYPE bloom_filter(0.01) GRANULARITY 2,
3129
+ INDEX idx_userId userId TYPE bloom_filter(0.01) GRANULARITY 2,
3130
+ INDEX idx_organizationId organizationId TYPE bloom_filter(0.01) GRANULARITY 2,
3131
+ INDEX idx_experimentId experimentId TYPE bloom_filter(0.01) GRANULARITY 2,
3132
+ INDEX idx_runId runId TYPE bloom_filter(0.01) GRANULARITY 2,
3133
+ INDEX idx_sessionId sessionId TYPE bloom_filter(0.01) GRANULARITY 2,
3134
+ INDEX idx_requestId requestId TYPE bloom_filter(0.01) GRANULARITY 2
3069
3135
  )
3070
3136
  ENGINE = ReplacingMergeTree
3071
3137
  PARTITION BY toDate(timestamp)
@@ -3194,7 +3260,8 @@ SETTINGS allow_nullable_key = 1
3194
3260
  `;
3195
3261
  const SCORE_EVENTS_CURRENT_DDL = `
3196
3262
  CREATE TABLE IF NOT EXISTS ${TABLE_SCORE_EVENTS_CURRENT} (
3197
- ${SCORE_EVENT_COLUMNS_DDL}
3263
+ ${SCORE_EVENT_COLUMNS_DDL},
3264
+ INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
3198
3265
  )
3199
3266
  ENGINE = ReplacingMergeTree(writeVersion)
3200
3267
  PARTITION BY cityHash64(scoreId) % 64
@@ -3322,7 +3389,11 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
3322
3389
 
3323
3390
  -- Information-only JSON payloads
3324
3391
  metadata Nullable(String),
3325
- scope Nullable(String)
3392
+ scope Nullable(String),
3393
+
3394
+ -- feedbackId is last in the sort key; id lookups (write versions, review
3395
+ -- updates, deletes, trace-query dedupe) need a skip index.
3396
+ INDEX idx_feedbackId feedbackId TYPE bloom_filter(0.01) GRANULARITY 1
3326
3397
  )
3327
3398
  ENGINE = ReplacingMergeTree
3328
3399
  PARTITION BY toDate(timestamp)
@@ -3568,11 +3639,15 @@ const ALL_MIGRATIONS = [
3568
3639
  addColumn(TABLE_SCORE_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3569
3640
  addBloomIndex(TABLE_SCORE_EVENTS, "idx_scoreId", "scoreId", 1),
3570
3641
  addBloomIndex(TABLE_SCORE_EVENTS_DELTA, "idx_scoreId", "scoreId", 1),
3642
+ addBloomIndex(TABLE_SCORE_EVENTS_CURRENT, "idx_traceId", "traceId", 1),
3571
3643
  addColumn(TABLE_FEEDBACK_EVENTS, "writeVersion", "UInt64 DEFAULT 0"),
3572
3644
  addColumn(TABLE_FEEDBACK_EVENTS, "entityVersionId", "Nullable(String)"),
3573
3645
  addColumn(TABLE_FEEDBACK_EVENTS, "reviewStatus", "LowCardinality(String) DEFAULT 'needs-review'"),
3574
3646
  addColumn(TABLE_FEEDBACK_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3575
3647
  addColumn(TABLE_FEEDBACK_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3648
+ addBloomIndex(TABLE_FEEDBACK_EVENTS, "idx_feedbackId", "feedbackId", 1),
3649
+ addBloomIndex(TABLE_TRACE_ROOTS, "idx_traceId", "traceId", 1),
3650
+ addBloomIndex(TABLE_TRACE_BRANCHES, "idx_traceId", "traceId", 1),
3576
3651
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_traceId", "traceId"),
3577
3652
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_threadId", "threadId"),
3578
3653
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_resourceId", "resourceId"),
@@ -3582,6 +3657,17 @@ const ALL_MIGRATIONS = [
3582
3657
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_runId", "runId"),
3583
3658
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_sessionId", "sessionId"),
3584
3659
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_requestId", "requestId"),
3660
+ ...[
3661
+ "traceId",
3662
+ "threadId",
3663
+ "resourceId",
3664
+ "userId",
3665
+ "organizationId",
3666
+ "experimentId",
3667
+ "runId",
3668
+ "sessionId",
3669
+ "requestId"
3670
+ ].map((column) => addBloomIndex(TABLE_LOG_EVENTS, `idx_${column}`, column)),
3585
3671
  addBloomIndex(TABLE_DELETION_REQUESTS, "idx_predicateValues", "predicateValues")
3586
3672
  ];
3587
3673
  const ALL_TABLE_NAMES = [
@@ -4677,6 +4763,11 @@ function validateCursorId(cursor) {
4677
4763
  text: "Invalid observability delta cursor"
4678
4764
  });
4679
4765
  }
4766
+ /** ANDs `conditions` onto a `WHERE ...` clause, or starts one when `whereClause` is empty. */
4767
+ function appendWhere(whereClause, ...conditions) {
4768
+ const parts = [whereClause.replace(/^\s*WHERE\s+/i, ""), ...conditions].filter(Boolean);
4769
+ return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
4770
+ }
4680
4771
  //#endregion
4681
4772
  //#region src/storage/domains/observability/v-next/feedback.ts
4682
4773
  const FEEDBACK_TYPED_COLUMNS = /* @__PURE__ */ new Set([
@@ -4783,12 +4874,21 @@ function resolveFeedbackGroupBy(groupBy) {
4783
4874
  function toSeriesName$1(values) {
4784
4875
  return values.map((v) => v == null ? "" : String(v)).join("|");
4785
4876
  }
4786
- async function queryJson$2(client, query, params) {
4877
+ /**
4878
+ * Lets `FINAL` reads use skip indexes on older servers, where it defaults off.
4879
+ * Only for lookups on a column shared by every version of a sort key (so a
4880
+ * skipped granule can never hold a newer version of a matching row).
4881
+ */
4882
+ const FINAL_SKIP_INDEX_SETTINGS = { use_skip_indexes_if_final: 1 };
4883
+ async function queryJson$2(client, query, params, settings = {}) {
4787
4884
  return await (await client.query({
4788
4885
  query,
4789
4886
  query_params: params,
4790
4887
  format: "JSONEachRow",
4791
- clickhouse_settings: CH_SETTINGS
4888
+ clickhouse_settings: {
4889
+ ...CH_SETTINGS,
4890
+ ...settings
4891
+ }
4792
4892
  })).json();
4793
4893
  }
4794
4894
  async function feedbackRowsWithWriteVersions(client, feedbacks) {
@@ -4897,7 +4997,7 @@ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, re
4897
4997
  feedbackId,
4898
4998
  organizationId: organizationId ?? "",
4899
4999
  resourceId: resourceId ?? ""
4900
- })).length > 0;
5000
+ }, FINAL_SKIP_INDEX_SETTINGS)).length > 0;
4901
5001
  }
4902
5002
  /**
4903
5003
  * Review updates insert a replacement row, so they need only `INSERT` and the
@@ -4918,7 +5018,7 @@ async function updateFeedbackReviewStatus(client, args, replication) {
4918
5018
  const existingRow = (await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4919
5019
  WHERE feedbackId = {feedbackId:String}
4920
5020
  ORDER BY writeVersion DESC, timestamp DESC
4921
- LIMIT 1`, { feedbackId }))[0];
5021
+ LIMIT 1`, { feedbackId }, FINAL_SKIP_INDEX_SETTINGS))[0];
4922
5022
  if (!existingRow) throw feedbackNotFoundError(feedbackId);
4923
5023
  const { organizationId, resourceId } = existingRow;
4924
5024
  const redelete = async () => {
@@ -4995,12 +5095,16 @@ async function queryFeedbackAfterCursor(client, whereClause, params, limit, curs
4995
5095
  f.timestamp AS timestamp,
4996
5096
  f.feedbackId AS feedbackId,
4997
5097
  toString(d.cursorId) AS cursorId
4998
- FROM ${TABLE_FEEDBACK_EVENTS_DELTA} d
4999
- INNER JOIN ${TABLE_FEEDBACK_EVENTS} f FINAL
5000
- ON ((f.traceId = d.traceId) OR (f.traceId IS NULL AND d.traceId IS NULL))
5098
+ FROM ${TABLE_FEEDBACK_EVENTS} f FINAL
5099
+ INNER JOIN (
5100
+ SELECT cursorId, traceId, timestamp, feedbackId
5101
+ FROM ${TABLE_FEEDBACK_EVENTS_DELTA}
5102
+ WHERE cursorId > {afterCursor:UInt64}
5103
+ ) d
5104
+ ON isNotDistinctFrom(f.traceId, d.traceId)
5001
5105
  AND f.timestamp = d.timestamp
5002
5106
  AND f.feedbackId = d.feedbackId
5003
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
5107
+ ${appendWhere(whereClause, `f.timestamp >= (SELECT min(timestamp) FROM ${TABLE_FEEDBACK_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})`, `(f.timestamp, f.feedbackId) IN (${`SELECT timestamp, feedbackId FROM ${TABLE_FEEDBACK_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`})`)}
5004
5108
  ORDER BY d.cursorId ASC
5005
5109
  LIMIT {fetchLimit:UInt32}
5006
5110
  `, {
@@ -5009,18 +5113,24 @@ async function queryFeedbackAfterCursor(client, whereClause, params, limit, curs
5009
5113
  fetchLimit: limit + 1
5010
5114
  });
5011
5115
  }
5116
+ /**
5117
+ * Newest delta cursor whose feedback matches the filters. Without filters
5118
+ * this is the stream head; with filters, the feedback scan is bounded below by
5119
+ * the oldest `timestamp` still in the delta table.
5120
+ */
5012
5121
  async function getDeltaCursor$5(client, whereClause, params) {
5122
+ if (!whereClause) return getStreamHeadCursor$5(client);
5013
5123
  const cursorId = (await queryJson$2(client, `
5014
5124
  SELECT toString(max(d.cursorId)) AS cursorId
5015
5125
  FROM mastra_feedback_events_delta d
5016
- INNER JOIN mastra_feedback_events f FINAL
5017
- ON ((f.traceId = d.traceId) OR (f.traceId IS NULL AND d.traceId IS NULL))
5018
- AND f.timestamp = d.timestamp
5019
- AND f.feedbackId = d.feedbackId
5020
- ${whereClause}
5126
+ WHERE (d.timestamp, d.feedbackId) IN (
5127
+ SELECT f.timestamp, f.feedbackId
5128
+ FROM mastra_feedback_events f FINAL
5129
+ ${appendWhere(whereClause, `f.timestamp >= (SELECT min(timestamp) FROM mastra_feedback_events_delta)`)}
5130
+ )
5021
5131
  `, params))[0]?.cursorId ?? null;
5022
5132
  if (cursorId) return cursorId;
5023
- return (await queryJson$2(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_feedback_events_delta`, {}))[0]?.cursorId ?? "0";
5133
+ return getStreamHeadCursor$5(client);
5024
5134
  }
5025
5135
  async function getStreamHeadCursor$5(client) {
5026
5136
  return (await queryJson$2(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_feedback_events_delta`, {}))[0]?.cursorId ?? "0";
@@ -5142,26 +5252,22 @@ async function getFeedbackPercentiles(client, args) {
5142
5252
  const combined = mergeFilters$2(buildFeedbackIdentityFilter(args), buildFeedbackFilterConditions(args.filters));
5143
5253
  const whereClause = toWhereClause$2(combined);
5144
5254
  if (!Array.isArray(args.percentiles) || args.percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
5145
- const series = [];
5146
- for (const p of args.percentiles) {
5147
- if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
5148
- const rows = await queryJson$2(client, `
5149
- SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
5150
- quantile(${p})(valueNumber) AS pvalue
5151
- FROM ${TABLE_FEEDBACK_EVENTS} FINAL
5152
- ${whereClause}
5153
- GROUP BY bucket
5154
- ORDER BY bucket
5155
- `, combined.params);
5156
- series.push({
5157
- percentile: p,
5158
- points: rows.map((row) => ({
5159
- timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
5160
- value: Number(row.pvalue ?? 0)
5161
- }))
5162
- });
5163
- }
5164
- return { series };
5255
+ for (const p of args.percentiles) if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
5256
+ const rows = await queryJson$2(client, `
5257
+ SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
5258
+ quantiles(${args.percentiles.join(", ")})(valueNumber) AS pvalues
5259
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL
5260
+ ${whereClause}
5261
+ GROUP BY bucket
5262
+ ORDER BY bucket
5263
+ `, combined.params);
5264
+ return { series: args.percentiles.map((percentile, index) => ({
5265
+ percentile,
5266
+ points: rows.map((row) => ({
5267
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
5268
+ value: Number(row.pvalues?.[index] ?? 0)
5269
+ }))
5270
+ })) };
5165
5271
  }
5166
5272
  //#endregion
5167
5273
  //#region src/storage/domains/observability/v-next/logs.ts
@@ -5240,6 +5346,7 @@ async function listLogs(client, args, strategy) {
5240
5346
  };
5241
5347
  }
5242
5348
  async function queryLogsAfterCursor(client, whereClause, params, limit, cursorId) {
5349
+ const deltaKeys = `SELECT timestamp, logId FROM ${TABLE_LOG_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`;
5243
5350
  return await (await client.query({
5244
5351
  query: `
5245
5352
  SELECT
@@ -5247,11 +5354,15 @@ async function queryLogsAfterCursor(client, whereClause, params, limit, cursorId
5247
5354
  l.timestamp AS timestamp,
5248
5355
  l.logId AS logId,
5249
5356
  toString(d.cursorId) AS cursorId
5250
- FROM ${TABLE_LOG_EVENTS_DELTA} d
5251
- INNER JOIN ${TABLE_LOG_EVENTS} l
5357
+ FROM ${TABLE_LOG_EVENTS} l
5358
+ INNER JOIN (
5359
+ SELECT cursorId, timestamp, logId
5360
+ FROM ${TABLE_LOG_EVENTS_DELTA}
5361
+ WHERE cursorId > {afterCursor:UInt64}
5362
+ ) d
5252
5363
  ON l.timestamp = d.timestamp
5253
5364
  AND l.logId = d.logId
5254
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
5365
+ ${appendWhere(whereClause, `(l.timestamp, l.logId) IN (${deltaKeys})`)}
5255
5366
  ORDER BY d.cursorId ASC
5256
5367
  LIMIT {fetchLimit:UInt32}
5257
5368
  `,
@@ -5264,26 +5375,29 @@ async function queryLogsAfterCursor(client, whereClause, params, limit, cursorId
5264
5375
  clickhouse_settings: CH_SETTINGS
5265
5376
  })).json();
5266
5377
  }
5378
+ /**
5379
+ * Newest delta cursor whose log matches the filters. Without filters this is
5380
+ * the stream head; with filters, the log scan is bounded below by the oldest
5381
+ * `timestamp` still in the delta table.
5382
+ */
5267
5383
  async function getDeltaCursor$4(client, whereClause, params) {
5384
+ if (!whereClause) return getStreamHeadCursor$4(client);
5268
5385
  const cursorId = (await (await client.query({
5269
5386
  query: `
5270
5387
  SELECT toString(max(d.cursorId)) AS cursorId
5271
5388
  FROM mastra_log_events_delta d
5272
- INNER JOIN mastra_log_events l
5273
- ON l.timestamp = d.timestamp
5274
- AND l.logId = d.logId
5275
- ${whereClause}
5389
+ WHERE (d.timestamp, d.logId) IN (
5390
+ SELECT l.timestamp, l.logId
5391
+ FROM mastra_log_events l
5392
+ ${appendWhere(whereClause, `l.timestamp >= (SELECT min(timestamp) FROM mastra_log_events_delta)`)}
5393
+ )
5276
5394
  `,
5277
5395
  query_params: params,
5278
5396
  format: "JSONEachRow",
5279
5397
  clickhouse_settings: CH_SETTINGS
5280
5398
  })).json())[0]?.cursorId ?? null;
5281
5399
  if (cursorId) return cursorId;
5282
- return (await (await client.query({
5283
- query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_log_events_delta`,
5284
- format: "JSONEachRow",
5285
- clickhouse_settings: CH_SETTINGS
5286
- })).json())[0]?.cursorId ?? "0";
5400
+ return getStreamHeadCursor$4(client);
5287
5401
  }
5288
5402
  async function getStreamHeadCursor$4(client) {
5289
5403
  return (await (await client.query({
@@ -5509,12 +5623,16 @@ async function queryMetricsAfterCursor(client, whereClause, params, limit, curso
5509
5623
  m.timestamp AS timestamp,
5510
5624
  m.metricId AS metricId,
5511
5625
  toString(d.cursorId) AS cursorId
5512
- FROM ${TABLE_METRIC_EVENTS_DELTA} d
5513
- INNER JOIN ${TABLE_METRIC_EVENTS} m
5626
+ FROM ${TABLE_METRIC_EVENTS} m
5627
+ INNER JOIN (
5628
+ SELECT cursorId, name, timestamp, metricId
5629
+ FROM ${TABLE_METRIC_EVENTS_DELTA}
5630
+ WHERE cursorId > {afterCursor:UInt64}
5631
+ ) d
5514
5632
  ON m.name = d.name
5515
5633
  AND m.timestamp = d.timestamp
5516
5634
  AND m.metricId = d.metricId
5517
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
5635
+ ${appendWhere(whereClause, `(m.name, m.timestamp, m.metricId) IN (${`SELECT name, timestamp, metricId FROM ${TABLE_METRIC_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`})`)}
5518
5636
  ORDER BY d.cursorId ASC
5519
5637
  LIMIT {fetchLimit:UInt32}
5520
5638
  `, {
@@ -5523,18 +5641,24 @@ async function queryMetricsAfterCursor(client, whereClause, params, limit, curso
5523
5641
  fetchLimit: limit + 1
5524
5642
  });
5525
5643
  }
5644
+ /**
5645
+ * Newest delta cursor whose metric matches the filters. Without filters this
5646
+ * is the stream head; with filters, the metric scan is bounded below by the
5647
+ * oldest `timestamp` still in the delta table.
5648
+ */
5526
5649
  async function getDeltaCursor$3(client, whereClause, params) {
5650
+ if (!whereClause) return getStreamHeadCursor$3(client);
5527
5651
  const cursorId = (await queryJson$1(client, `
5528
5652
  SELECT toString(max(d.cursorId)) AS cursorId
5529
5653
  FROM mastra_metric_events_delta d
5530
- INNER JOIN mastra_metric_events m
5531
- ON m.name = d.name
5532
- AND m.timestamp = d.timestamp
5533
- AND m.metricId = d.metricId
5534
- ${whereClause}
5654
+ WHERE (d.name, d.timestamp, d.metricId) IN (
5655
+ SELECT m.name, m.timestamp, m.metricId
5656
+ FROM mastra_metric_events m
5657
+ ${appendWhere(whereClause, `m.timestamp >= (SELECT min(timestamp) FROM mastra_metric_events_delta)`)}
5658
+ )
5535
5659
  `, params))[0]?.cursorId ?? null;
5536
5660
  if (cursorId) return cursorId;
5537
- return (await queryJson$1(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_metric_events_delta`, {}))[0]?.cursorId ?? "0";
5661
+ return getStreamHeadCursor$3(client);
5538
5662
  }
5539
5663
  async function getStreamHeadCursor$3(client) {
5540
5664
  return (await queryJson$1(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_metric_events_delta`, {}))[0]?.cursorId ?? "0";
@@ -5733,29 +5857,26 @@ async function getMetricPercentiles(client, args) {
5733
5857
  params: { percentileName: args.name }
5734
5858
  }, signalFilter);
5735
5859
  const whereClause = toWhereClause$1(combined);
5736
- const series = [];
5737
- for (const p of args.percentiles) {
5738
- if (p < 0 || p > 1) throw new Error(`Percentile value must be between 0 and 1, got ${p}`);
5739
- const rows = await queryJson$1(client, `
5740
- SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
5741
- quantile(${p})(value) AS pvalue
5742
- FROM ${TABLE_METRIC_EVENTS}
5743
- ${whereClause}
5744
- GROUP BY bucket
5745
- ORDER BY bucket
5746
- `, combined.params);
5747
- series.push({
5748
- percentile: p,
5749
- points: rows.map((row) => ({
5750
- timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
5751
- value: Number(row.pvalue ?? 0)
5752
- }))
5753
- });
5754
- }
5755
- return { series };
5860
+ for (const p of args.percentiles) if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
5861
+ if (args.percentiles.length === 0) return { series: [] };
5862
+ const rows = await queryJson$1(client, `
5863
+ SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
5864
+ quantiles(${args.percentiles.join(", ")})(value) AS pvalues
5865
+ FROM ${TABLE_METRIC_EVENTS}
5866
+ ${whereClause}
5867
+ GROUP BY bucket
5868
+ ORDER BY bucket
5869
+ `, combined.params);
5870
+ return { series: args.percentiles.map((percentile, index) => ({
5871
+ percentile,
5872
+ points: rows.map((row) => ({
5873
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
5874
+ value: Number(row.pvalues?.[index] ?? 0)
5875
+ }))
5876
+ })) };
5756
5877
  }
5757
5878
  async function getMetricNames(client, args) {
5758
- const conditions = [`kind = 'metricName'`];
5879
+ const conditions = [`kind = 'metricName'`, `key1 = ''`];
5759
5880
  const params = {};
5760
5881
  if (args.prefix) {
5761
5882
  conditions.push(`value LIKE {namePrefix:String}`);
@@ -6216,6 +6337,22 @@ function currentScoresRelation(sourcePredicate) {
6216
6337
  ${sourcePredicate ? `WHERE ${sourcePredicate}` : ""}
6217
6338
  )`;
6218
6339
  }
6340
+ /**
6341
+ * Current-state scores narrowed before FINAL merges. `score_events_current` is
6342
+ * keyed by scoreId, so a traceId filter only applies after FINAL has merged
6343
+ * the whole table. When the (unaliased) filter pins traceId, the matching
6344
+ * scoreIds are first read from the history table, whose sort key starts with
6345
+ * traceId. Every current row was written to history, so the candidate set is a
6346
+ * superset; callers still apply the same filter to the current version.
6347
+ *
6348
+ * Time ranges alone are not narrowed this way: scoreIds are hash-distributed,
6349
+ * so the ids from any real time window touch nearly every granule of the
6350
+ * current table and the extra history scan costs more than it saves.
6351
+ */
6352
+ function prunedCurrentScoresRelation(historyFilter) {
6353
+ if (!historyFilter.conditions.some((condition) => condition.startsWith("traceId "))) return currentScoresRelation();
6354
+ return currentScoresRelation(`scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS} WHERE ${historyFilter.conditions.join(" AND ")})`);
6355
+ }
6219
6356
  function buildScoreIdentityFilter(args) {
6220
6357
  const conditions = ["scorerId = {olapScorerId:String}"];
6221
6358
  const params = { olapScorerId: args.scorerId };
@@ -6360,7 +6497,7 @@ async function listScores(client, args, strategy) {
6360
6497
  };
6361
6498
  }
6362
6499
  const currentDeltaCursor = deltaCursorEnabled ? await getDeltaCursor$2(client, whereClause, filter.params) : void 0;
6363
- const scoresRelation = currentScoresRelation();
6500
+ const scoresRelation = prunedCurrentScoresRelation(buildScoresFilterConditions(parsed.filters));
6364
6501
  const countResult = await queryJson(client, `SELECT count() AS total FROM ${scoresRelation} AS s ${whereClause}`, filter.params);
6365
6502
  const rows = await queryJson(client, `SELECT * FROM ${scoresRelation} AS s ${whereClause} ORDER BY ${orderBy} LIMIT {limit:UInt32} OFFSET {offset:UInt32}`, {
6366
6503
  ...filter.params,
@@ -6398,15 +6535,19 @@ async function queryScoresAfterCursor(client, whereClause, params, limit, cursor
6398
6535
  s.timestamp AS timestamp,
6399
6536
  s.scoreId AS scoreId,
6400
6537
  toString(d.cursorId) AS cursorId
6401
- FROM ${TABLE_SCORE_EVENTS_DELTA} d
6402
- INNER JOIN ${TABLE_SCORE_EVENTS_CURRENT} s FINAL
6538
+ FROM ${TABLE_SCORE_EVENTS_CURRENT} s FINAL
6539
+ INNER JOIN (
6540
+ SELECT cursorId, scoreId
6541
+ FROM ${TABLE_SCORE_EVENTS_DELTA}
6542
+ WHERE cursorId > {afterCursor:UInt64}
6543
+ AND scoreId NOT IN (
6544
+ SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA}
6545
+ WHERE cursorId <= {afterCursor:UInt64}
6546
+ AND scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})
6547
+ )
6548
+ ) d
6403
6549
  ON s.scoreId = d.scoreId
6404
- ${whereClause ? `${whereClause} AND` : "WHERE"} d.cursorId > {afterCursor:UInt64}
6405
- AND d.scoreId NOT IN (
6406
- SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA}
6407
- WHERE cursorId <= {afterCursor:UInt64}
6408
- AND scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})
6409
- )
6550
+ ${appendWhere(whereClause, `s.scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})`)}
6410
6551
  ORDER BY d.cursorId ASC
6411
6552
  LIMIT 1 BY s.scoreId
6412
6553
  LIMIT {fetchLimit:UInt32}
@@ -6416,16 +6557,24 @@ async function queryScoresAfterCursor(client, whereClause, params, limit, cursor
6416
6557
  fetchLimit: limit + 1
6417
6558
  });
6418
6559
  }
6560
+ /**
6561
+ * Newest delta cursor whose current score matches the filters. Without
6562
+ * filters this is the stream head; with filters, FINAL only merges the
6563
+ * scoreIds still in the delta table.
6564
+ */
6419
6565
  async function getDeltaCursor$2(client, whereClause, params) {
6566
+ if (!whereClause) return getStreamHeadCursor$2(client);
6420
6567
  const cursorId = (await queryJson(client, `
6421
6568
  SELECT toString(max(d.cursorId)) AS cursorId
6422
6569
  FROM mastra_score_events_delta d
6423
- INNER JOIN mastra_score_events_current s FINAL
6424
- ON s.scoreId = d.scoreId
6425
- ${whereClause}
6570
+ WHERE d.scoreId IN (
6571
+ SELECT s.scoreId
6572
+ FROM mastra_score_events_current s FINAL
6573
+ ${appendWhere(whereClause, `s.scoreId IN (SELECT scoreId FROM mastra_score_events_delta)`)}
6574
+ )
6426
6575
  `, params))[0]?.cursorId ?? null;
6427
6576
  if (cursorId) return cursorId;
6428
- return (await queryJson(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_score_events_delta`, {}))[0]?.cursorId ?? "0";
6577
+ return getStreamHeadCursor$2(client);
6429
6578
  }
6430
6579
  async function getStreamHeadCursor$2(client) {
6431
6580
  return (await queryJson(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_score_events_delta`, {}))[0]?.cursorId ?? "0";
@@ -6442,7 +6591,7 @@ async function getScoreAggregate(client, args) {
6442
6591
  const identity = buildScoreIdentityFilter(args);
6443
6592
  const combined = mergeFilters(identity, buildScoresFilterConditions(args.filters));
6444
6593
  const whereClause = toWhereClause(combined);
6445
- const result = await queryJson(client, `SELECT ${aggSql} AS value FROM ${currentScoresRelation()} ${whereClause}`, combined.params);
6594
+ const result = await queryJson(client, `SELECT ${aggSql} AS value FROM ${prunedCurrentScoresRelation(combined)} ${whereClause}`, combined.params);
6446
6595
  const value = result[0]?.value == null ? null : Number(result[0]?.value);
6447
6596
  if (args.comparePeriod && args.filters?.timestamp) {
6448
6597
  const ts = args.filters.timestamp;
@@ -6477,7 +6626,7 @@ async function getScoreAggregate(client, args) {
6477
6626
  }
6478
6627
  }));
6479
6628
  const prevWhereClause = toWhereClause(prevCombined);
6480
- const prevResult = await queryJson(client, `SELECT ${aggSql} AS value FROM ${currentScoresRelation()} ${prevWhereClause}`, prevCombined.params);
6629
+ const prevResult = await queryJson(client, `SELECT ${aggSql} AS value FROM ${prunedCurrentScoresRelation(prevCombined)} ${prevWhereClause}`, prevCombined.params);
6481
6630
  const previousValue = prevResult[0]?.value == null ? null : Number(prevResult[0]?.value);
6482
6631
  let changePercent = null;
6483
6632
  if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
@@ -6495,7 +6644,7 @@ async function getScoreBreakdown(client, args) {
6495
6644
  const combined = mergeFilters(buildScoreIdentityFilter(args), buildScoresFilterConditions(args.filters));
6496
6645
  const whereClause = toWhereClause(combined);
6497
6646
  const resolved = resolveScoreGroupBy(args.groupBy);
6498
- return { groups: (await queryJson(client, `SELECT ${resolved.map((e) => e.selectSql).join(", ")}, ${aggSql} AS value FROM ${currentScoresRelation()} ${whereClause} GROUP BY ${resolved.map((e) => e.groupSql).join(", ")} ORDER BY value DESC`, combined.params)).map((row) => ({
6647
+ return { groups: (await queryJson(client, `SELECT ${resolved.map((e) => e.selectSql).join(", ")}, ${aggSql} AS value FROM ${prunedCurrentScoresRelation(combined)} ${whereClause} GROUP BY ${resolved.map((e) => e.groupSql).join(", ")} ORDER BY value DESC`, combined.params)).map((row) => ({
6499
6648
  dimensions: Object.fromEntries(resolved.map((entry, index) => {
6500
6649
  const v = row[`group_by_${index}`];
6501
6650
  return [entry.key, v == null ? null : String(v)];
@@ -6514,7 +6663,7 @@ async function getScoreTimeSeries(client, args) {
6514
6663
  SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
6515
6664
  ${resolved.map((e) => e.selectSql).join(", ")},
6516
6665
  ${aggSql} AS value
6517
- FROM ${currentScoresRelation()} ${whereClause}
6666
+ FROM ${prunedCurrentScoresRelation(combined)} ${whereClause}
6518
6667
  GROUP BY bucket, ${resolved.map((e) => e.groupSql).join(", ")}
6519
6668
  ORDER BY bucket
6520
6669
  `, combined.params);
@@ -6536,7 +6685,7 @@ async function getScoreTimeSeries(client, args) {
6536
6685
  const rows = await queryJson(client, `
6537
6686
  SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
6538
6687
  ${aggSql} AS value
6539
- FROM ${currentScoresRelation()} ${whereClause}
6688
+ FROM ${prunedCurrentScoresRelation(combined)} ${whereClause}
6540
6689
  GROUP BY bucket
6541
6690
  ORDER BY bucket
6542
6691
  `, combined.params);
@@ -6553,26 +6702,22 @@ async function getScorePercentiles(client, args) {
6553
6702
  const combined = mergeFilters(buildScoreIdentityFilter(args), buildScoresFilterConditions(args.filters));
6554
6703
  const whereClause = toWhereClause(combined);
6555
6704
  if (!Array.isArray(args.percentiles) || args.percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
6556
- const series = [];
6557
- for (const p of args.percentiles) {
6558
- if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
6559
- const rows = await queryJson(client, `
6560
- SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
6561
- quantile(${p})(score) AS pvalue
6562
- FROM ${currentScoresRelation()}
6563
- ${whereClause}
6564
- GROUP BY bucket
6565
- ORDER BY bucket
6566
- `, combined.params);
6567
- series.push({
6568
- percentile: p,
6569
- points: rows.map((row) => ({
6570
- timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
6571
- value: Number(row.pvalue ?? 0)
6572
- }))
6573
- });
6574
- }
6575
- return { series };
6705
+ for (const p of args.percentiles) if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
6706
+ const rows = await queryJson(client, `
6707
+ SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
6708
+ quantiles(${args.percentiles.join(", ")})(score) AS pvalues
6709
+ FROM ${prunedCurrentScoresRelation(combined)}
6710
+ ${whereClause}
6711
+ GROUP BY bucket
6712
+ ORDER BY bucket
6713
+ `, combined.params);
6714
+ return { series: args.percentiles.map((percentile, index) => ({
6715
+ percentile,
6716
+ points: rows.map((row) => ({
6717
+ timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
6718
+ value: Number(row.pvalues?.[index] ?? 0)
6719
+ }))
6720
+ })) };
6576
6721
  }
6577
6722
  //#endregion
6578
6723
  //#region src/storage/domains/observability/v-next/trace-query.ts
@@ -6816,6 +6961,22 @@ const TRACE_SELECT = `
6816
6961
  r.entityType AS entityType,
6817
6962
  r.environment AS environment,
6818
6963
  ${TRACE_STATUS_SQL} AS status`;
6964
+ /** Candidate columns carried through the page-mode window sort (no payload blobs). */
6965
+ const TRACE_PAGE_COLUMNS = [
6966
+ "traceId",
6967
+ "rootSpanId",
6968
+ "name",
6969
+ "entityId",
6970
+ "parentSpanId",
6971
+ "threadId",
6972
+ "resourceId",
6973
+ "startedAt",
6974
+ "endedAt",
6975
+ "entityName",
6976
+ "entityType",
6977
+ "environment",
6978
+ "status"
6979
+ ];
6819
6980
  var ParameterBuilder = class {
6820
6981
  params = {};
6821
6982
  #next = 1;
@@ -6902,13 +7063,12 @@ function collectThreadRelationCollections(predicate, collections) {
6902
7063
  }
6903
7064
  function compilePredicate(predicate, parameters) {
6904
7065
  if (predicate.type === "relation") {
6905
- const existence = `EXISTS (
6906
- SELECT 1 FROM ${predicate.collection === "spans" ? "current_spans" : predicate.collection === "scores" ? "current_scores" : "current_feedback"} s
7066
+ const matching = `(
7067
+ SELECT s.traceId FROM ${predicate.collection === "spans" ? "current_spans" : predicate.collection === "scores" ? "current_scores" : "current_feedback"} s
6907
7068
  WHERE isNotNull(s.traceId)
6908
- AND s.traceId = r.traceId
6909
7069
  AND (${predicate.collection === "feedback" ? compileFeedbackScalarPredicate(predicate.predicate, parameters) : compileScalarPredicate(predicate.predicate, predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS, parameters)})
6910
7070
  )`;
6911
- return predicate.quantifier === "some" ? existence : `NOT ${existence}`;
7071
+ return `r.traceId ${predicate.quantifier === "some" ? "IN" : "NOT IN"} ${matching}`;
6912
7072
  }
6913
7073
  if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compilePredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
6914
7074
  if (predicate.type === "not") return `NOT (${compilePredicate(predicate.arg, parameters)})`;
@@ -6916,12 +7076,12 @@ function compilePredicate(predicate, parameters) {
6916
7076
  }
6917
7077
  function compileThreadPredicate(predicate, parameters) {
6918
7078
  if (predicate.type === "relation") {
6919
- const existence = `EXISTS (
6920
- SELECT 1 FROM eligible_roots r
6921
- WHERE r.threadId = t.threadId
7079
+ const matching = `(
7080
+ SELECT r.threadId FROM eligible_roots r
7081
+ WHERE isNotNull(r.threadId)
6922
7082
  AND (${compilePredicate(predicate.predicate, parameters)})
6923
7083
  )`;
6924
- return predicate.quantifier === "some" ? existence : `NOT ${existence}`;
7084
+ return `t.threadId ${predicate.quantifier === "some" ? "IN" : "NOT IN"} ${matching}`;
6925
7085
  }
6926
7086
  if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileThreadPredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
6927
7087
  return `NOT (${compileThreadPredicate(predicate.arg, parameters)})`;
@@ -7022,6 +7182,15 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7022
7182
  FROM (
7023
7183
  SELECT *
7024
7184
  FROM ${TABLE_FEEDBACK_EVENTS} FINAL
7185
+ -- ClickHouse cannot push the scope filter through LIMIT 1 BY, so keep
7186
+ -- every version of each feedbackId that ever pointed into the scope
7187
+ -- (traceId leads the sort key). A rewrite may move a feedback to another
7188
+ -- trace, so the current version is still filtered below.
7189
+ WHERE feedbackId IN (
7190
+ SELECT feedbackId
7191
+ FROM ${TABLE_FEEDBACK_EVENTS}
7192
+ WHERE traceId IN (SELECT traceId FROM root_scope)
7193
+ )
7025
7194
  ORDER BY feedbackId, writeVersion DESC, timestamp DESC
7026
7195
  LIMIT 1 BY feedbackId
7027
7196
  ) AS current
@@ -7076,11 +7245,12 @@ LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
7076
7245
  cursorId: "0",
7077
7246
  traceId: ""
7078
7247
  };
7248
+ const lowerCursor = parameters.add(after.cursorId, "UInt64");
7079
7249
  return {
7080
7250
  query: `${candidates}, delta_candidates AS (
7081
7251
  SELECT traceId, max(cursorId) AS latestCursorId
7082
7252
  FROM ${TABLE_TRACE_ROOTS_DELTA}
7083
- WHERE tuple(cursorId, traceId) > ${`tuple(${parameters.add(after.cursorId, "UInt64")}, ${parameters.add(after.traceId, "String")})`} ${deltaHead ? `AND tuple(cursorId, traceId) <= tuple(${parameters.add(deltaHead.cursorId, "UInt64")}, ${parameters.add(deltaHead.traceId, "String")})` : ""}
7253
+ WHERE cursorId >= ${lowerCursor} AND tuple(cursorId, traceId) > ${`tuple(${lowerCursor}, ${parameters.add(after.traceId, "String")})`} ${deltaHead ? `AND tuple(cursorId, traceId) <= tuple(${parameters.add(deltaHead.cursorId, "UInt64")}, ${parameters.add(deltaHead.traceId, "String")})` : ""}
7084
7254
  GROUP BY traceId
7085
7255
  )
7086
7256
  SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
@@ -7094,46 +7264,25 @@ LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
7094
7264
  }
7095
7265
  const orderField = resolveOrderField(plan.orderBy.field);
7096
7266
  const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
7097
- if (plan.paginationMode === "page") return {
7098
- query: `${candidates},
7267
+ if (plan.paginationMode === "page") {
7268
+ const onPage = `__row_position > ${parameters.add(plan.page * plan.perPage, "UInt64")} AND __row_position <= ${parameters.add((plan.page + 1) * plan.perPage, "UInt64")}`;
7269
+ return {
7270
+ query: `${candidates},
7099
7271
  page_rows AS (
7100
- SELECT *, row_number() OVER (ORDER BY ${orderField} ${direction}, traceId ASC) AS __row_position
7101
- FROM candidates
7102
- ORDER BY ${orderField} ${direction}, traceId ASC
7103
- LIMIT ${parameters.add(plan.perPage, "UInt64")} OFFSET ${parameters.add(plan.page * plan.perPage, "UInt64")}
7104
- ),
7105
- page_total AS (
7106
- SELECT count() AS total
7272
+ SELECT
7273
+ ${TRACE_PAGE_COLUMNS.join(",\n ")},
7274
+ row_number() OVER (ORDER BY ${orderField} ${direction}, traceId ASC) AS __row_position,
7275
+ count() OVER () AS total
7107
7276
  FROM candidates
7108
7277
  )
7109
- SELECT page_rows.*, page_total.total, 0 AS __metadata
7278
+ SELECT *, if(${onPage}, 0, 1) AS __metadata
7110
7279
  FROM page_rows
7111
- CROSS JOIN page_total
7112
- UNION ALL
7113
- SELECT
7114
- '' AS traceId,
7115
- '' AS rootSpanId,
7116
- '' AS name,
7117
- CAST(NULL, 'Nullable(String)') AS entityId,
7118
- CAST(NULL, 'Nullable(String)') AS parentSpanId,
7119
- CAST(NULL, 'Nullable(String)') AS metadata,
7120
- CAST(NULL, 'Nullable(String)') AS input,
7121
- CAST(NULL, 'Nullable(String)') AS threadId,
7122
- CAST(NULL, 'Nullable(String)') AS resourceId,
7123
- toDateTime64(0, 3, 'UTC') AS startedAt,
7124
- toDateTime64(0, 3, 'UTC') AS endedAt,
7125
- CAST(NULL, 'Nullable(String)') AS entityName,
7126
- CAST(NULL, 'Nullable(String)') AS entityType,
7127
- CAST(NULL, 'Nullable(String)') AS environment,
7128
- '' AS status,
7129
- 0 AS __row_position,
7130
- page_total.total AS total,
7131
- 1 AS __metadata
7132
- FROM page_total
7280
+ WHERE (${onPage}) OR __row_position = 1
7133
7281
  ORDER BY __metadata ASC, __row_position ASC`,
7134
- query_params: parameters.params,
7135
- sharedSnapshot: true
7136
- };
7282
+ query_params: parameters.params,
7283
+ sharedSnapshot: true
7284
+ };
7285
+ }
7137
7286
  let pageCondition = "";
7138
7287
  if (plan.cursor) {
7139
7288
  const comparison = plan.orderBy.direction === "asc" ? ">" : "<";
@@ -7151,6 +7300,23 @@ LIMIT ${limit}`,
7151
7300
  query_params: parameters.params
7152
7301
  };
7153
7302
  }
7303
+ /**
7304
+ * Fetches the metadata/input payloads for page-mode rows. Looks rows up by the
7305
+ * trace_roots sort-key prefix `(startedAt, traceId)`, so only the page's
7306
+ * granules are read. A root can have unmerged versions in different `endedAt`
7307
+ * partitions, so `endedAt` is part of the key: the payload comes from the same
7308
+ * version as the candidate row.
7309
+ */
7310
+ function compileClickHouseTraceRootPayloads(keys) {
7311
+ const parameters = new ParameterBuilder();
7312
+ return {
7313
+ query: `SELECT traceId, spanId AS rootSpanId, metadataRaw AS metadata, input
7314
+ FROM ${TABLE_TRACE_ROOTS}
7315
+ WHERE (startedAt, traceId, spanId, endedAt) IN (${keys.map((key) => `(${parameters.add(key.startedAt, "DateTime64(3, 'UTC')")}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.rootSpanId, "String")}, ${parameters.add(key.endedAt, "DateTime64(3, 'UTC')")})`).join(", ")})
7316
+ LIMIT 1 BY traceId, spanId`,
7317
+ query_params: parameters.params
7318
+ };
7319
+ }
7154
7320
  function compileClickHouseThreadQuery(plan) {
7155
7321
  const parameters = new ParameterBuilder();
7156
7322
  const relationCollections = collectRelationCollections(plan.traces.where);
@@ -7344,7 +7510,21 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7344
7510
  if (plan.paginationMode === "page") {
7345
7511
  const rows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: deltaHead ? remaining() : timeoutMs }, compileClickHouseTraceQuery(plan, deltaHead));
7346
7512
  const total = Number(rows.at(-1)?.total ?? 0);
7347
- const traces = rows.filter((row) => Number(row.__metadata) === 0).map((row) => ({
7513
+ const pageRows = rows.filter((row) => Number(row.__metadata) === 0);
7514
+ const payloads = /* @__PURE__ */ new Map();
7515
+ if (pageRows.length > 0) {
7516
+ const payloadRows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: remaining() }, compileClickHouseTraceRootPayloads(pageRows.map((row) => ({
7517
+ traceId: String(row.traceId),
7518
+ rootSpanId: String(row.rootSpanId),
7519
+ startedAt: asIsoTimestamp(row.startedAt),
7520
+ endedAt: asIsoTimestamp(row.endedAt)
7521
+ }))));
7522
+ for (const payload of payloadRows) payloads.set(`${payload.traceId}\u0000${payload.rootSpanId}`, payload);
7523
+ }
7524
+ const traces = pageRows.map((row) => ({
7525
+ ...row,
7526
+ ...payloads.get(`${row.traceId}\u0000${row.rootSpanId}`)
7527
+ })).map((row) => ({
7348
7528
  traceId: String(row.traceId),
7349
7529
  rootSpanId: String(row.rootSpanId),
7350
7530
  name: row.name,
@@ -7443,6 +7623,201 @@ async function queryThreads(client, plan, timeoutMs) {
7443
7623
  }) : null }
7444
7624
  });
7445
7625
  }
7626
+ /** Compile a span-row filter with exactly the same rules as trace span predicates. */
7627
+ function compileSpanQueryPredicate(predicate) {
7628
+ const parameters = new ParameterBuilder();
7629
+ return {
7630
+ sql: compileScalarPredicate(predicate, SPAN_FIELDS, parameters),
7631
+ params: parameters.params
7632
+ };
7633
+ }
7634
+ //#endregion
7635
+ //#region src/storage/domains/observability/v-next/span-query.ts
7636
+ const identityFields = [
7637
+ "organizationId",
7638
+ "resourceId",
7639
+ "traceId",
7640
+ "spanId"
7641
+ ];
7642
+ const identities = identityFields.join(", ");
7643
+ const identitySetColumns = "isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''), traceId, spanId";
7644
+ const identitySetKey = `tuple(${identitySetColumns})`;
7645
+ const displayFields = [
7646
+ "parentSpanId",
7647
+ "name",
7648
+ "spanType",
7649
+ "entityType",
7650
+ "entityId",
7651
+ "entityName",
7652
+ "startedAt",
7653
+ "endedAt",
7654
+ "model",
7655
+ "provider",
7656
+ "durationMs",
7657
+ "status"
7658
+ ];
7659
+ var Parameters = class {
7660
+ values = {};
7661
+ add(value, timestamp = false) {
7662
+ const name = `span_${Object.keys(this.values).length}`;
7663
+ this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
7664
+ return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
7665
+ }
7666
+ };
7667
+ function identityValues(row) {
7668
+ return [
7669
+ row.organizationId === null ? 1 : 0,
7670
+ row.organizationId ?? "",
7671
+ row.resourceId === null ? 1 : 0,
7672
+ row.resourceId ?? "",
7673
+ row.traceId,
7674
+ row.spanId
7675
+ ];
7676
+ }
7677
+ function afterCursor(plan, parameters) {
7678
+ if (!plan.cursor) return "1";
7679
+ const cursor = plan.cursor;
7680
+ const timestamp = parameters.add(cursor.sortValue, true);
7681
+ const field = `s.${plan.orderBy.field}`;
7682
+ const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
7683
+ const equal = [`${field} = ${timestamp}`];
7684
+ for (const key of identityFields) {
7685
+ const value = cursor[key];
7686
+ if (value === null) equal.push(`isNull(s.${key})`);
7687
+ else {
7688
+ const parameter = parameters.add(value);
7689
+ terms.push(`(${equal.join(" AND ")} AND (s.${key} > ${parameter} OR isNull(s.${key})))`);
7690
+ equal.push(`s.${key} = ${parameter}`);
7691
+ }
7692
+ }
7693
+ return `(${terms.join(" OR ")})`;
7694
+ }
7695
+ function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
7696
+ if (!predicate) return fields;
7697
+ if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
7698
+ else if (predicate.type === "not") predicateFields(predicate.arg, fields);
7699
+ else fields.add(predicate.field);
7700
+ return fields;
7701
+ }
7702
+ function fieldExpression(field) {
7703
+ if (field === "model" || field === "provider") return `if(JSONType(e.attributes, '${field}') = 'String', JSONExtractString(e.attributes, '${field}'), NULL)`;
7704
+ if (field === "durationMs") return "dateDiff('millisecond', e.startedAt, e.endedAt)";
7705
+ if (field === "status") return "if(isNull(e.error), 'success', 'error')";
7706
+ if (field === "error") return "if(isNull(e.error), NULL, 'error')";
7707
+ return `e.${field}`;
7708
+ }
7709
+ function compileClickHouseSpanQuery(plan) {
7710
+ const parameters = new Parameters();
7711
+ const from = parameters.add(plan.timeRange.from, true);
7712
+ const to = parameters.add(plan.timeRange.to, true);
7713
+ const scope = [];
7714
+ if (plan.scope) {
7715
+ scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
7716
+ if (plan.scope.resourceId !== void 0) scope.push(`resourceId = ${parameters.add(plan.scope.resourceId)}`);
7717
+ }
7718
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where) : {
7719
+ sql: "1",
7720
+ params: {}
7721
+ };
7722
+ const selectionFields = [.../* @__PURE__ */ new Set([
7723
+ "startedAt",
7724
+ "endedAt",
7725
+ ...predicateFields(plan.where)
7726
+ ])].filter((field) => !new Set(identityFields).has(field));
7727
+ const expressions = selectionFields.map(fieldExpression);
7728
+ const after = afterCursor(plan, parameters);
7729
+ return {
7730
+ query: `WITH candidate_ids AS (
7731
+ SELECT ${identitySetKey} AS identity FROM ${TABLE_SPAN_EVENTS}
7732
+ PREWHERE endedAt >= ${from}
7733
+ WHERE startedAt >= ${from} AND startedAt < ${to} ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
7734
+ ), candidates AS (
7735
+ SELECT ${identities}, ${selectionFields.map((field, index) => `${expressions[index]} AS ${field}`).join(", ")}
7736
+ FROM ${TABLE_SPAN_EVENTS} e
7737
+ PREWHERE e.endedAt >= ${from}
7738
+ WHERE ${identitySetKey} IN (SELECT identity FROM candidate_ids) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
7739
+ ORDER BY ${identities}, e.endedAt DESC
7740
+ LIMIT 1 BY ${identities}
7741
+ )
7742
+ SELECT ${[
7743
+ ...identityFields,
7744
+ "startedAt",
7745
+ "endedAt"
7746
+ ].map((field) => `s.${field}`).join(", ")}
7747
+ FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
7748
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
7749
+ LIMIT ${parameters.add(plan.limit + 1)}`,
7750
+ query_params: {
7751
+ ...parameters.values,
7752
+ ...predicate.params
7753
+ }
7754
+ };
7755
+ }
7756
+ function iso(value) {
7757
+ return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
7758
+ }
7759
+ async function querySpans(client, plan, timeoutMs) {
7760
+ const deadline = performance.now() + _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
7761
+ const run = async (compiled) => {
7762
+ const remaining = Math.floor(deadline - performance.now());
7763
+ if (remaining <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
7764
+ return await runWithClickHouseTraceQueryTimeout(client, {
7765
+ timeoutMs: remaining,
7766
+ memoryLimitBytes: 512 * 1024 * 1024
7767
+ }, compiled);
7768
+ };
7769
+ const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
7770
+ ...row,
7771
+ startedAt: iso(row.startedAt),
7772
+ endedAt: iso(row.endedAt)
7773
+ }));
7774
+ const page = selected.slice(0, plan.limit);
7775
+ if (!page.length) return {
7776
+ spans: [],
7777
+ page: { next: null }
7778
+ };
7779
+ const payloadWhere = `tuple(${identitySetColumns}, endedAt) IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC')))}`;
7780
+ const payloads = await run({
7781
+ query: `SELECT ${identities}, ${displayFields.map((field, index) => `data.${index + 1} AS ${field}`).join(", ")},
7782
+ data.${displayFields.length + 1} AS inputPreview, data.${displayFields.length + 2} AS outputPreview
7783
+ FROM (SELECT ${identities}, any(tuple(${displayFields.map(fieldExpression).join(", ")}, substringUTF8(input, 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}), substringUTF8(output, 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}))) AS data
7784
+ FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
7785
+ query_params: { page: page.map((row) => new _clickhouse_client.TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")])) }
7786
+ });
7787
+ const metricWhere = `${identitySetKey} IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String))}`;
7788
+ const metrics = await run({
7789
+ query: `SELECT ${identities}, data.1 AS name, data.2 AS estimatedCost,
7790
+ data.3 AS costUnit, data.4 AS allocation, data.5 AS costError
7791
+ FROM (SELECT ${identities}, metricId, argMax(tuple(name, estimatedCost, costUnit,
7792
+ JSONExtractString(costMetadata, 'allocation'), JSONHas(costMetadata, 'error')), timestamp) AS data
7793
+ FROM ${TABLE_METRIC_EVENTS} WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
7794
+ GROUP BY ${identities}, metricId)
7795
+ LIMIT ${_mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS + 1}`,
7796
+ query_params: { page: page.map((row) => new _clickhouse_client.TupleParam(identityValues(row))) }
7797
+ });
7798
+ if (metrics.length > _mastra_core_storage.SPAN_QUERY_MAX_COST_METRICS) throw new _mastra_core_storage.TraceQueryResourceLimitError();
7799
+ const byIdentity = new Map(payloads.map((row) => [_mastra_core_storage.spanQueryIdentityKey(row), row]));
7800
+ const hydrated = page.flatMap((identity) => {
7801
+ const row = byIdentity.get(_mastra_core_storage.spanQueryIdentityKey(identity));
7802
+ if (!row) return [];
7803
+ const { inputPreview: _input, outputPreview: _output, ...metadata } = row;
7804
+ return [{
7805
+ ...metadata,
7806
+ ...identity,
7807
+ durationMs: Number(row.durationMs)
7808
+ }];
7809
+ });
7810
+ const result = _mastra_core_storage.buildSpanQueryResponse(plan, hydrated, payloads, metrics);
7811
+ const last = page.at(-1);
7812
+ result.page.next = selected.length > plan.limit ? _mastra_core_storage.encodeSpanQueryCursor(plan, {
7813
+ organizationId: last.organizationId,
7814
+ resourceId: last.resourceId,
7815
+ traceId: last.traceId,
7816
+ spanId: last.spanId,
7817
+ sortValue: last[plan.orderBy.field]
7818
+ }) : null;
7819
+ return result;
7820
+ }
7446
7821
  //#endregion
7447
7822
  //#region src/storage/domains/observability/v-next/trace-aggregate.ts
7448
7823
  /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
@@ -7692,18 +8067,16 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
7692
8067
  const page = pagination?.page ?? 0;
7693
8068
  const perPage = pagination?.perPage ?? 10;
7694
8069
  const { conditions, params } = buildTraceFilterConditions(filters, "r");
7695
- if (filters?.hasChildError != null) if (filters.hasChildError) conditions.push(`EXISTS (
7696
- SELECT 1 FROM ${TABLE_SPAN_EVENTS} c
7697
- WHERE c.traceId = r.traceId
7698
- AND c.parentSpanId IS NOT NULL
7699
- AND c.error IS NOT NULL
7700
- )`);
7701
- else conditions.push(`NOT EXISTS (
7702
- SELECT 1 FROM ${TABLE_SPAN_EVENTS} c
7703
- WHERE c.traceId = r.traceId
7704
- AND c.parentSpanId IS NOT NULL
8070
+ if (filters?.hasChildError != null) {
8071
+ const scope = buildTraceFilterConditions(filters, "scope_r");
8072
+ const traceScope = scope.conditions.length ? `c.traceId IN (SELECT scope_r.traceId FROM ${TABLE_TRACE_ROOTS} scope_r WHERE ${scope.conditions.join(" AND ")})
8073
+ AND ` : "";
8074
+ conditions.push(`r.traceId ${filters.hasChildError ? "IN" : "NOT IN"} (
8075
+ SELECT c.traceId FROM ${TABLE_SPAN_EVENTS} c
8076
+ WHERE ${traceScope}c.parentSpanId IS NOT NULL
7705
8077
  AND c.error IS NOT NULL
7706
8078
  )`);
8079
+ }
7707
8080
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7708
8081
  const deltaCursorEnabled = deltaPollingSupported(strategy);
7709
8082
  if (mode === "delta") {
@@ -7759,15 +8132,27 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
7759
8132
  const rows = await (await client.query({
7760
8133
  query: `
7761
8134
  SELECT ${projection.outerSelect} FROM (
8135
+ -- Deferred join: pick the page's sort keys from a narrow sort, then
8136
+ -- read full rows only for those keys. LIMIT 1 BY disables ClickHouse's
8137
+ -- own lazy materialization, so sorting SELECT * directly would carry
8138
+ -- every matching row's payload columns through the sort. The filter is
8139
+ -- applied again on the re-read: a dedupeKey can have unmerged versions
8140
+ -- (in different endedAt partitions) and only a matching one may win.
7762
8141
  SELECT ${projection.innerSelect}
7763
8142
  FROM ${TABLE_TRACE_ROOTS} r
7764
- ${whereClause}
7765
- ORDER BY dedupeKey
8143
+ ${appendWhere(whereClause, `(r.startedAt, r.traceId, r.dedupeKey) IN (
8144
+ SELECT startedAt, traceId, dedupeKey
8145
+ FROM ${TABLE_TRACE_ROOTS} r
8146
+ ${whereClause}
8147
+ ORDER BY ${orderClause}, dedupeKey ASC
8148
+ LIMIT 1 BY dedupeKey
8149
+ LIMIT {limit:UInt32}
8150
+ OFFSET {offset:UInt32}
8151
+ )`)}
8152
+ ORDER BY ${orderClause}, dedupeKey ASC
7766
8153
  LIMIT 1 BY dedupeKey
7767
8154
  )
7768
- ORDER BY ${orderClause}
7769
- LIMIT {limit:UInt32}
7770
- OFFSET {offset:UInt32}
8155
+ ORDER BY ${orderClause}, dedupeKey ASC
7771
8156
  `,
7772
8157
  query_params: {
7773
8158
  ...params,
@@ -7801,6 +8186,7 @@ async function listTracesLight(client, args, strategy) {
7801
8186
  return listTraceRows(client, args, strategy, LIGHT_PROJECTION, (rows) => rows.map(rowToLightSpanRecord));
7802
8187
  }
7803
8188
  async function queryTracesAfterCursor(client, deltaSelect, whereClause, params, limit, cursorId) {
8189
+ const deltaKeys = `SELECT startedAt, traceId, dedupeKey FROM ${TABLE_TRACE_ROOTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`;
7804
8190
  return await (await client.query({
7805
8191
  query: `
7806
8192
  SELECT
@@ -7809,12 +8195,16 @@ async function queryTracesAfterCursor(client, deltaSelect, whereClause, params,
7809
8195
  r.traceId AS traceId,
7810
8196
  r.dedupeKey AS dedupeKey,
7811
8197
  toString(d.cursorId) AS cursorId
7812
- FROM ${TABLE_TRACE_ROOTS_DELTA} d
7813
- INNER JOIN ${TABLE_TRACE_ROOTS} r
8198
+ FROM ${TABLE_TRACE_ROOTS} r
8199
+ INNER JOIN (
8200
+ SELECT cursorId, startedAt, traceId, dedupeKey
8201
+ FROM ${TABLE_TRACE_ROOTS_DELTA}
8202
+ WHERE cursorId > {afterCursor:UInt64}
8203
+ ) d
7814
8204
  ON r.startedAt = d.startedAt
7815
8205
  AND r.traceId = d.traceId
7816
8206
  AND r.dedupeKey = d.dedupeKey
7817
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
8207
+ ${appendWhere(whereClause, `(r.startedAt, r.traceId, r.dedupeKey) IN (${deltaKeys})`)}
7818
8208
  ORDER BY d.cursorId ASC
7819
8209
  LIMIT {fetchLimit:UInt32}
7820
8210
  `,
@@ -7827,27 +8217,30 @@ async function queryTracesAfterCursor(client, deltaSelect, whereClause, params,
7827
8217
  clickhouse_settings: CH_SETTINGS
7828
8218
  })).json();
7829
8219
  }
8220
+ /**
8221
+ * Newest delta cursor whose trace root matches the filters. Without filters
8222
+ * every delta row qualifies, so this is the stream head. With filters, the
8223
+ * root scan is bounded below by the oldest `startedAt` still in the delta
8224
+ * table (a 2-day TTL window) instead of reading all of trace_roots.
8225
+ */
7830
8226
  async function getDeltaCursor$1(client, whereClause, params) {
8227
+ if (!whereClause) return getStreamHeadCursor$1(client);
7831
8228
  const cursorId = (await (await client.query({
7832
8229
  query: `
7833
8230
  SELECT toString(max(d.cursorId)) AS cursorId
7834
8231
  FROM mastra_trace_roots_delta d
7835
- INNER JOIN mastra_trace_roots r
7836
- ON r.startedAt = d.startedAt
7837
- AND r.traceId = d.traceId
7838
- AND r.dedupeKey = d.dedupeKey
7839
- ${whereClause}
8232
+ WHERE (d.startedAt, d.traceId, d.dedupeKey) IN (
8233
+ SELECT r.startedAt, r.traceId, r.dedupeKey
8234
+ FROM mastra_trace_roots r
8235
+ ${appendWhere(whereClause, `r.startedAt >= (SELECT min(startedAt) FROM mastra_trace_roots_delta)`)}
8236
+ )
7840
8237
  `,
7841
8238
  query_params: params,
7842
8239
  format: "JSONEachRow",
7843
8240
  clickhouse_settings: CH_SETTINGS
7844
8241
  })).json())[0]?.cursorId ?? null;
7845
8242
  if (cursorId) return cursorId;
7846
- return (await (await client.query({
7847
- query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_trace_roots_delta`,
7848
- format: "JSONEachRow",
7849
- clickhouse_settings: CH_SETTINGS
7850
- })).json())[0]?.cursorId ?? "0";
8243
+ return getStreamHeadCursor$1(client);
7851
8244
  }
7852
8245
  async function getStreamHeadCursor$1(client) {
7853
8246
  return (await (await client.query({
@@ -8338,15 +8731,28 @@ async function listBranches(client, args, strategy) {
8338
8731
  const spans = (await (await client.query({
8339
8732
  query: `
8340
8733
  SELECT * FROM (
8734
+ -- Deferred join: pick the page's sort keys from a narrow sort, then
8735
+ -- read full rows only for those keys. LIMIT 1 BY disables ClickHouse's
8736
+ -- own lazy materialization, so sorting SELECT * directly would carry
8737
+ -- every matching branch's payload columns through the sort. The filter
8738
+ -- is applied again on the re-read: a dedupeKey can have unmerged
8739
+ -- versions (in different endedAt partitions) and only a matching one
8740
+ -- may win.
8341
8741
  SELECT *
8342
8742
  FROM ${TABLE_TRACE_BRANCHES} b
8343
- ${whereClause}
8344
- ORDER BY b.dedupeKey
8743
+ ${appendWhere(whereClause, `(b.spanType, b.startedAt, b.traceId, b.dedupeKey) IN (
8744
+ SELECT b.spanType, b.startedAt, b.traceId, b.dedupeKey
8745
+ FROM ${TABLE_TRACE_BRANCHES} b
8746
+ ${whereClause}
8747
+ ORDER BY b.${sortField} ${sortDirection}, b.dedupeKey ASC
8748
+ LIMIT 1 BY b.dedupeKey
8749
+ LIMIT {limit:UInt32}
8750
+ OFFSET {offset:UInt32}
8751
+ )`)}
8752
+ ORDER BY b.${sortField} ${sortDirection}, b.dedupeKey ASC
8345
8753
  LIMIT 1 BY b.dedupeKey
8346
8754
  )
8347
8755
  ORDER BY ${sortField} ${sortDirection}, dedupeKey ASC
8348
- LIMIT {limit:UInt32}
8349
- OFFSET {offset:UInt32}
8350
8756
  `,
8351
8757
  query_params: {
8352
8758
  ...params,
@@ -8368,6 +8774,7 @@ async function listBranches(client, args, strategy) {
8368
8774
  };
8369
8775
  }
8370
8776
  async function queryBranchesAfterCursor(client, whereClause, params, limit, cursorId) {
8777
+ const deltaKeys = `SELECT spanType, startedAt, traceId, dedupeKey FROM ${TABLE_TRACE_BRANCHES_DELTA} WHERE cursorId > {afterCursor:UInt64}`;
8371
8778
  return await (await client.query({
8372
8779
  query: `
8373
8780
  SELECT
@@ -8378,14 +8785,18 @@ async function queryBranchesAfterCursor(client, whereClause, params, limit, curs
8378
8785
  b.spanId AS spanId,
8379
8786
  b.dedupeKey AS dedupeKey,
8380
8787
  toString(d.cursorId) AS cursorId
8381
- FROM ${TABLE_TRACE_BRANCHES_DELTA} d
8382
- INNER JOIN ${TABLE_TRACE_BRANCHES} b
8788
+ FROM ${TABLE_TRACE_BRANCHES} b
8789
+ INNER JOIN (
8790
+ SELECT cursorId, spanType, startedAt, traceId, spanId, dedupeKey
8791
+ FROM ${TABLE_TRACE_BRANCHES_DELTA}
8792
+ WHERE cursorId > {afterCursor:UInt64}
8793
+ ) d
8383
8794
  ON b.spanType = d.spanType
8384
8795
  AND b.startedAt = d.startedAt
8385
8796
  AND b.traceId = d.traceId
8386
8797
  AND b.spanId = d.spanId
8387
8798
  AND b.dedupeKey = d.dedupeKey
8388
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
8799
+ ${appendWhere(whereClause, `(b.spanType, b.startedAt, b.traceId, b.dedupeKey) IN (${deltaKeys})`)}
8389
8800
  ORDER BY d.cursorId ASC
8390
8801
  LIMIT {fetchLimit:UInt32}
8391
8802
  `,
@@ -8398,29 +8809,29 @@ async function queryBranchesAfterCursor(client, whereClause, params, limit, curs
8398
8809
  clickhouse_settings: CH_SETTINGS
8399
8810
  })).json();
8400
8811
  }
8812
+ /**
8813
+ * Newest delta cursor whose branch matches the filters. Without filters this
8814
+ * is the stream head; with filters, the branch scan is bounded below by the
8815
+ * oldest `startedAt` still in the delta table.
8816
+ */
8401
8817
  async function getDeltaCursor(client, whereClause, params) {
8818
+ if (!whereClause) return getStreamHeadCursor(client);
8402
8819
  const cursorId = (await (await client.query({
8403
8820
  query: `
8404
8821
  SELECT toString(max(d.cursorId)) AS cursorId
8405
8822
  FROM mastra_trace_branches_delta d
8406
- INNER JOIN mastra_trace_branches b
8407
- ON b.spanType = d.spanType
8408
- AND b.startedAt = d.startedAt
8409
- AND b.traceId = d.traceId
8410
- AND b.spanId = d.spanId
8411
- AND b.dedupeKey = d.dedupeKey
8412
- ${whereClause}
8823
+ WHERE (d.spanType, d.startedAt, d.traceId, d.spanId, d.dedupeKey) IN (
8824
+ SELECT b.spanType, b.startedAt, b.traceId, b.spanId, b.dedupeKey
8825
+ FROM mastra_trace_branches b
8826
+ ${appendWhere(whereClause, `b.startedAt >= (SELECT min(startedAt) FROM mastra_trace_branches_delta)`)}
8827
+ )
8413
8828
  `,
8414
8829
  query_params: params,
8415
8830
  format: "JSONEachRow",
8416
8831
  clickhouse_settings: CH_SETTINGS
8417
8832
  })).json())[0]?.cursorId ?? null;
8418
8833
  if (cursorId) return cursorId;
8419
- return (await (await client.query({
8420
- query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_trace_branches_delta`,
8421
- format: "JSONEachRow",
8422
- clickhouse_settings: CH_SETTINGS
8423
- })).json())[0]?.cursorId ?? "0";
8834
+ return getStreamHeadCursor(client);
8424
8835
  }
8425
8836
  async function getStreamHeadCursor(client) {
8426
8837
  return (await (await client.query({
@@ -8434,6 +8845,7 @@ function buildBranchCursor(row) {
8434
8845
  }
8435
8846
  //#endregion
8436
8847
  //#region src/storage/domains/observability/v-next/index.ts
8848
+ const spanQueryFeatures = typeof _mastra_core_storage.planSpanQuery === "function" ? ["span-query"] : [];
8437
8849
  function buildSignalMigrationRequiredMessage(args) {
8438
8850
  const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
8439
8851
  return `
@@ -8871,7 +9283,8 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8871
9283
  "trace-query-tenant-scope",
8872
9284
  "feedback",
8873
9285
  "trace-query-context-ids",
8874
- "trace-aggregate"
9286
+ "trace-aggregate",
9287
+ ...spanQueryFeatures
8875
9288
  ];
8876
9289
  return [
8877
9290
  "metrics",
@@ -8890,7 +9303,8 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8890
9303
  "trace-query-tenant-scope",
8891
9304
  "feedback",
8892
9305
  "trace-query-context-ids",
8893
- "trace-aggregate"
9306
+ "trace-aggregate",
9307
+ ...spanQueryFeatures
8894
9308
  ];
8895
9309
  }
8896
9310
  async createSpan(args) {
@@ -9005,6 +9419,18 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
9005
9419
  }, error);
9006
9420
  }
9007
9421
  }
9422
+ async querySpans(plan) {
9423
+ try {
9424
+ return await querySpans(this.#client, plan, this.#traceQueryTimeoutMs);
9425
+ } catch (error) {
9426
+ if (error instanceof _mastra_core_error.MastraError || error instanceof _mastra_core_storage.TraceQueryExecutionError || error instanceof _mastra_core_storage.TraceQueryCursorError || error instanceof _mastra_core_storage.TraceQueryResourceLimitError) throw error;
9427
+ throw new _mastra_core_error.MastraError({
9428
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "QUERY_SPANS", "FAILED"),
9429
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
9430
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY
9431
+ }, error);
9432
+ }
9433
+ }
9008
9434
  async queryTraces(plan) {
9009
9435
  try {
9010
9436
  return await queryTraces(this.#client, plan, this.#traceQueryTimeoutMs, this.#deltaCursorStrategy);
@@ -9571,6 +9997,7 @@ var ScoresStorageClickhouse = class extends _mastra_core_storage.ScoresStorage {
9571
9997
  tableName: _mastra_core_storage.TABLE_SCORERS,
9572
9998
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_SCORERS]
9573
9999
  });
10000
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_SCORERS);
9574
10001
  }
9575
10002
  async dangerouslyClearAll() {
9576
10003
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_SCORERS });
@@ -9981,6 +10408,7 @@ var WorkflowsStorageClickhouse = class extends _mastra_core_storage.WorkflowsSto
9981
10408
  schema,
9982
10409
  ifNotExists: ["resourceId"]
9983
10410
  });
10411
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
9984
10412
  }
9985
10413
  async dangerouslyClearAll() {
9986
10414
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
@@ -10177,7 +10605,8 @@ var WorkflowsStorageClickhouse = class extends _mastra_core_storage.WorkflowsSto
10177
10605
  ORDER BY createdAt DESC LIMIT 1
10178
10606
  `,
10179
10607
  query_params: values,
10180
- format: "JSONEachRow"
10608
+ format: "JSONEachRow",
10609
+ clickhouse_settings: { use_skip_indexes_if_final: 1 }
10181
10610
  })).json();
10182
10611
  if (!Array.isArray(resultJson) || resultJson.length === 0) return null;
10183
10612
  return this.parseWorkflowRun(resultJson[0]);