@mastra/clickhouse 1.23.0-alpha.1 → 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 (28) 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 +607 -388
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +607 -388
  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 +1 -1
  18. package/dist/storage/domains/observability/v-next/metrics.d.ts.map +1 -1
  19. package/dist/storage/domains/observability/v-next/polling.d.ts +2 -0
  20. package/dist/storage/domains/observability/v-next/polling.d.ts.map +1 -1
  21. package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
  22. package/dist/storage/domains/observability/v-next/trace-query.d.ts +13 -0
  23. package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
  24. package/dist/storage/domains/observability/v-next/trace-roots.d.ts.map +1 -1
  25. package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
  26. package/dist/storage/domains/scores/index.d.ts.map +1 -1
  27. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  28. 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,
@@ -7887,18 +8067,16 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
7887
8067
  const page = pagination?.page ?? 0;
7888
8068
  const perPage = pagination?.perPage ?? 10;
7889
8069
  const { conditions, params } = buildTraceFilterConditions(filters, "r");
7890
- if (filters?.hasChildError != null) if (filters.hasChildError) conditions.push(`EXISTS (
7891
- SELECT 1 FROM ${TABLE_SPAN_EVENTS} c
7892
- WHERE c.traceId = r.traceId
7893
- AND c.parentSpanId IS NOT NULL
7894
- AND c.error IS NOT NULL
7895
- )`);
7896
- else conditions.push(`NOT EXISTS (
7897
- SELECT 1 FROM ${TABLE_SPAN_EVENTS} c
7898
- WHERE c.traceId = r.traceId
7899
- 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
7900
8077
  AND c.error IS NOT NULL
7901
8078
  )`);
8079
+ }
7902
8080
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7903
8081
  const deltaCursorEnabled = deltaPollingSupported(strategy);
7904
8082
  if (mode === "delta") {
@@ -7954,15 +8132,27 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
7954
8132
  const rows = await (await client.query({
7955
8133
  query: `
7956
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.
7957
8141
  SELECT ${projection.innerSelect}
7958
8142
  FROM ${TABLE_TRACE_ROOTS} r
7959
- ${whereClause}
7960
- 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
7961
8153
  LIMIT 1 BY dedupeKey
7962
8154
  )
7963
- ORDER BY ${orderClause}
7964
- LIMIT {limit:UInt32}
7965
- OFFSET {offset:UInt32}
8155
+ ORDER BY ${orderClause}, dedupeKey ASC
7966
8156
  `,
7967
8157
  query_params: {
7968
8158
  ...params,
@@ -7996,6 +8186,7 @@ async function listTracesLight(client, args, strategy) {
7996
8186
  return listTraceRows(client, args, strategy, LIGHT_PROJECTION, (rows) => rows.map(rowToLightSpanRecord));
7997
8187
  }
7998
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}`;
7999
8190
  return await (await client.query({
8000
8191
  query: `
8001
8192
  SELECT
@@ -8004,12 +8195,16 @@ async function queryTracesAfterCursor(client, deltaSelect, whereClause, params,
8004
8195
  r.traceId AS traceId,
8005
8196
  r.dedupeKey AS dedupeKey,
8006
8197
  toString(d.cursorId) AS cursorId
8007
- FROM ${TABLE_TRACE_ROOTS_DELTA} d
8008
- 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
8009
8204
  ON r.startedAt = d.startedAt
8010
8205
  AND r.traceId = d.traceId
8011
8206
  AND r.dedupeKey = d.dedupeKey
8012
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
8207
+ ${appendWhere(whereClause, `(r.startedAt, r.traceId, r.dedupeKey) IN (${deltaKeys})`)}
8013
8208
  ORDER BY d.cursorId ASC
8014
8209
  LIMIT {fetchLimit:UInt32}
8015
8210
  `,
@@ -8022,27 +8217,30 @@ async function queryTracesAfterCursor(client, deltaSelect, whereClause, params,
8022
8217
  clickhouse_settings: CH_SETTINGS
8023
8218
  })).json();
8024
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
+ */
8025
8226
  async function getDeltaCursor$1(client, whereClause, params) {
8227
+ if (!whereClause) return getStreamHeadCursor$1(client);
8026
8228
  const cursorId = (await (await client.query({
8027
8229
  query: `
8028
8230
  SELECT toString(max(d.cursorId)) AS cursorId
8029
8231
  FROM mastra_trace_roots_delta d
8030
- INNER JOIN mastra_trace_roots r
8031
- ON r.startedAt = d.startedAt
8032
- AND r.traceId = d.traceId
8033
- AND r.dedupeKey = d.dedupeKey
8034
- ${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
+ )
8035
8237
  `,
8036
8238
  query_params: params,
8037
8239
  format: "JSONEachRow",
8038
8240
  clickhouse_settings: CH_SETTINGS
8039
8241
  })).json())[0]?.cursorId ?? null;
8040
8242
  if (cursorId) return cursorId;
8041
- return (await (await client.query({
8042
- query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_trace_roots_delta`,
8043
- format: "JSONEachRow",
8044
- clickhouse_settings: CH_SETTINGS
8045
- })).json())[0]?.cursorId ?? "0";
8243
+ return getStreamHeadCursor$1(client);
8046
8244
  }
8047
8245
  async function getStreamHeadCursor$1(client) {
8048
8246
  return (await (await client.query({
@@ -8533,15 +8731,28 @@ async function listBranches(client, args, strategy) {
8533
8731
  const spans = (await (await client.query({
8534
8732
  query: `
8535
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.
8536
8741
  SELECT *
8537
8742
  FROM ${TABLE_TRACE_BRANCHES} b
8538
- ${whereClause}
8539
- 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
8540
8753
  LIMIT 1 BY b.dedupeKey
8541
8754
  )
8542
8755
  ORDER BY ${sortField} ${sortDirection}, dedupeKey ASC
8543
- LIMIT {limit:UInt32}
8544
- OFFSET {offset:UInt32}
8545
8756
  `,
8546
8757
  query_params: {
8547
8758
  ...params,
@@ -8563,6 +8774,7 @@ async function listBranches(client, args, strategy) {
8563
8774
  };
8564
8775
  }
8565
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}`;
8566
8778
  return await (await client.query({
8567
8779
  query: `
8568
8780
  SELECT
@@ -8573,14 +8785,18 @@ async function queryBranchesAfterCursor(client, whereClause, params, limit, curs
8573
8785
  b.spanId AS spanId,
8574
8786
  b.dedupeKey AS dedupeKey,
8575
8787
  toString(d.cursorId) AS cursorId
8576
- FROM ${TABLE_TRACE_BRANCHES_DELTA} d
8577
- 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
8578
8794
  ON b.spanType = d.spanType
8579
8795
  AND b.startedAt = d.startedAt
8580
8796
  AND b.traceId = d.traceId
8581
8797
  AND b.spanId = d.spanId
8582
8798
  AND b.dedupeKey = d.dedupeKey
8583
- ${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})`)}
8584
8800
  ORDER BY d.cursorId ASC
8585
8801
  LIMIT {fetchLimit:UInt32}
8586
8802
  `,
@@ -8593,29 +8809,29 @@ async function queryBranchesAfterCursor(client, whereClause, params, limit, curs
8593
8809
  clickhouse_settings: CH_SETTINGS
8594
8810
  })).json();
8595
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
+ */
8596
8817
  async function getDeltaCursor(client, whereClause, params) {
8818
+ if (!whereClause) return getStreamHeadCursor(client);
8597
8819
  const cursorId = (await (await client.query({
8598
8820
  query: `
8599
8821
  SELECT toString(max(d.cursorId)) AS cursorId
8600
8822
  FROM mastra_trace_branches_delta d
8601
- INNER JOIN mastra_trace_branches b
8602
- ON b.spanType = d.spanType
8603
- AND b.startedAt = d.startedAt
8604
- AND b.traceId = d.traceId
8605
- AND b.spanId = d.spanId
8606
- AND b.dedupeKey = d.dedupeKey
8607
- ${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
+ )
8608
8828
  `,
8609
8829
  query_params: params,
8610
8830
  format: "JSONEachRow",
8611
8831
  clickhouse_settings: CH_SETTINGS
8612
8832
  })).json())[0]?.cursorId ?? null;
8613
8833
  if (cursorId) return cursorId;
8614
- return (await (await client.query({
8615
- query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_trace_branches_delta`,
8616
- format: "JSONEachRow",
8617
- clickhouse_settings: CH_SETTINGS
8618
- })).json())[0]?.cursorId ?? "0";
8834
+ return getStreamHeadCursor(client);
8619
8835
  }
8620
8836
  async function getStreamHeadCursor(client) {
8621
8837
  return (await (await client.query({
@@ -9061,14 +9277,14 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
9061
9277
  "tag-discovery",
9062
9278
  "metric-discovery",
9063
9279
  "trace-query",
9064
- ...spanQueryFeatures,
9065
9280
  "trace-query-root-duration",
9066
9281
  "trace-query-discovery",
9067
9282
  "thread-query",
9068
9283
  "trace-query-tenant-scope",
9069
9284
  "feedback",
9070
9285
  "trace-query-context-ids",
9071
- "trace-aggregate"
9286
+ "trace-aggregate",
9287
+ ...spanQueryFeatures
9072
9288
  ];
9073
9289
  return [
9074
9290
  "metrics",
@@ -9081,14 +9297,14 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
9081
9297
  "metric-discovery",
9082
9298
  "delta-polling",
9083
9299
  "trace-query",
9084
- ...spanQueryFeatures,
9085
9300
  "trace-query-root-duration",
9086
9301
  "trace-query-discovery",
9087
9302
  "thread-query",
9088
9303
  "trace-query-tenant-scope",
9089
9304
  "feedback",
9090
9305
  "trace-query-context-ids",
9091
- "trace-aggregate"
9306
+ "trace-aggregate",
9307
+ ...spanQueryFeatures
9092
9308
  ];
9093
9309
  }
9094
9310
  async createSpan(args) {
@@ -9781,6 +9997,7 @@ var ScoresStorageClickhouse = class extends _mastra_core_storage.ScoresStorage {
9781
9997
  tableName: _mastra_core_storage.TABLE_SCORERS,
9782
9998
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_SCORERS]
9783
9999
  });
10000
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_SCORERS);
9784
10001
  }
9785
10002
  async dangerouslyClearAll() {
9786
10003
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_SCORERS });
@@ -10191,6 +10408,7 @@ var WorkflowsStorageClickhouse = class extends _mastra_core_storage.WorkflowsSto
10191
10408
  schema,
10192
10409
  ifNotExists: ["resourceId"]
10193
10410
  });
10411
+ await this.#db.ensureSkipIndexes(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
10194
10412
  }
10195
10413
  async dangerouslyClearAll() {
10196
10414
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
@@ -10387,7 +10605,8 @@ var WorkflowsStorageClickhouse = class extends _mastra_core_storage.WorkflowsSto
10387
10605
  ORDER BY createdAt DESC LIMIT 1
10388
10606
  `,
10389
10607
  query_params: values,
10390
- format: "JSONEachRow"
10608
+ format: "JSONEachRow",
10609
+ clickhouse_settings: { use_skip_indexes_if_final: 1 }
10391
10610
  })).json();
10392
10611
  if (!Array.isArray(resultJson) || resultJson.length === 0) return null;
10393
10612
  return this.parseWorkflowRun(resultJson[0]);