@mastra/clickhouse 1.4.2-alpha.0 → 1.5.0

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.
package/dist/index.js CHANGED
@@ -2428,6 +2428,49 @@ time for large tables. Please ensure you have a backup before proceeding.
2428
2428
  );
2429
2429
  }
2430
2430
  }
2431
+ async getTraceLight(args) {
2432
+ const { traceId } = args;
2433
+ try {
2434
+ const engine = TABLE_ENGINES[TABLE_SPANS] ?? "MergeTree()";
2435
+ const result = await this.client.query({
2436
+ query: `
2437
+ SELECT traceId, spanId, parentSpanId, name,
2438
+ entityType, entityId, entityName,
2439
+ spanType, error, isEvent,
2440
+ startedAt, endedAt, createdAt, updatedAt
2441
+ FROM ${TABLE_SPANS} ${engine.startsWith("ReplacingMergeTree") ? "FINAL" : ""}
2442
+ WHERE traceId = {traceId:String}
2443
+ ORDER BY startedAt ASC
2444
+ `,
2445
+ query_params: { traceId },
2446
+ format: "JSONEachRow",
2447
+ clickhouse_settings: {
2448
+ date_time_input_format: "best_effort",
2449
+ date_time_output_format: "iso",
2450
+ use_client_time_zone: 1,
2451
+ output_format_json_quote_64bit_integers: 0
2452
+ }
2453
+ });
2454
+ const rows = await result.json();
2455
+ if (!rows || rows.length === 0) {
2456
+ return null;
2457
+ }
2458
+ return {
2459
+ traceId,
2460
+ spans: transformRows(rows)
2461
+ };
2462
+ } catch (error) {
2463
+ throw new MastraError(
2464
+ {
2465
+ id: createStorageErrorId("CLICKHOUSE", "GET_TRACE_LIGHT", "FAILED"),
2466
+ domain: ErrorDomain.STORAGE,
2467
+ category: ErrorCategory.THIRD_PARTY,
2468
+ details: { traceId }
2469
+ },
2470
+ error
2471
+ );
2472
+ }
2473
+ }
2431
2474
  async updateSpan(args) {
2432
2475
  const { traceId, spanId, updates } = args;
2433
2476
  try {
@@ -2942,6 +2985,7 @@ CREATE TABLE IF NOT EXISTS ${TABLE_METRIC_EVENTS} (
2942
2985
  timestamp DateTime64(3, 'UTC'),
2943
2986
 
2944
2987
  -- IDs
2988
+ metricId String,
2945
2989
  traceId Nullable(String),
2946
2990
  spanId Nullable(String),
2947
2991
  experimentId Nullable(String),
@@ -2989,9 +3033,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_METRIC_EVENTS} (
2989
3033
  metadata Nullable(String),
2990
3034
  scope Nullable(String)
2991
3035
  )
2992
- ENGINE = MergeTree
3036
+ ENGINE = ReplacingMergeTree
2993
3037
  PARTITION BY toDate(timestamp)
2994
- ORDER BY (name, timestamp)
3038
+ ORDER BY (name, timestamp, metricId)
2995
3039
  `;
2996
3040
  var LOG_EVENTS_DDL = `
2997
3041
  CREATE TABLE IF NOT EXISTS ${TABLE_LOG_EVENTS} (
@@ -2999,6 +3043,7 @@ CREATE TABLE IF NOT EXISTS ${TABLE_LOG_EVENTS} (
2999
3043
  timestamp DateTime64(3, 'UTC'),
3000
3044
 
3001
3045
  -- IDs
3046
+ logId String,
3002
3047
  traceId Nullable(String),
3003
3048
  spanId Nullable(String),
3004
3049
  experimentId Nullable(String),
@@ -3041,10 +3086,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_LOG_EVENTS} (
3041
3086
  metadata Nullable(String),
3042
3087
  scope Nullable(String)
3043
3088
  )
3044
- ENGINE = MergeTree
3089
+ ENGINE = ReplacingMergeTree
3045
3090
  PARTITION BY toDate(timestamp)
3046
- ORDER BY (timestamp, traceId)
3047
- SETTINGS allow_nullable_key = 1
3091
+ ORDER BY (timestamp, logId)
3048
3092
  `;
3049
3093
  var SCORE_EVENTS_DDL = `
3050
3094
  CREATE TABLE IF NOT EXISTS ${TABLE_SCORE_EVENTS} (
@@ -3052,6 +3096,7 @@ CREATE TABLE IF NOT EXISTS ${TABLE_SCORE_EVENTS} (
3052
3096
  timestamp DateTime64(3, 'UTC'),
3053
3097
 
3054
3098
  -- IDs
3099
+ scoreId String,
3055
3100
  traceId Nullable(String),
3056
3101
  spanId Nullable(String),
3057
3102
  experimentId Nullable(String),
@@ -3101,9 +3146,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_SCORE_EVENTS} (
3101
3146
  metadata Nullable(String),
3102
3147
  scope Nullable(String)
3103
3148
  )
3104
- ENGINE = MergeTree
3149
+ ENGINE = ReplacingMergeTree
3105
3150
  PARTITION BY toDate(timestamp)
3106
- ORDER BY (traceId, timestamp)
3151
+ ORDER BY (traceId, timestamp, scoreId)
3107
3152
  SETTINGS allow_nullable_key = 1
3108
3153
  `;
3109
3154
  var FEEDBACK_EVENTS_DDL = `
@@ -3112,6 +3157,7 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
3112
3157
  timestamp DateTime64(3, 'UTC'),
3113
3158
 
3114
3159
  -- IDs
3160
+ feedbackId String,
3115
3161
  traceId Nullable(String),
3116
3162
  spanId Nullable(String),
3117
3163
  experimentId Nullable(String),
@@ -3164,9 +3210,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
3164
3210
  metadata Nullable(String),
3165
3211
  scope Nullable(String)
3166
3212
  )
3167
- ENGINE = MergeTree
3213
+ ENGINE = ReplacingMergeTree
3168
3214
  PARTITION BY toDate(timestamp)
3169
- ORDER BY (traceId, timestamp)
3215
+ ORDER BY (traceId, timestamp, feedbackId)
3170
3216
  SETTINGS allow_nullable_key = 1
3171
3217
  `;
3172
3218
  var DISCOVERY_VALUES_DDL = `
@@ -3521,6 +3567,7 @@ function spanRecordToRow(span) {
3521
3567
  }
3522
3568
  function rowToLogRecord(row) {
3523
3569
  return {
3570
+ logId: row.logId,
3524
3571
  timestamp: toDate(row.timestamp),
3525
3572
  level: row.level,
3526
3573
  message: row.message,
@@ -3557,6 +3604,7 @@ function rowToLogRecord(row) {
3557
3604
  }
3558
3605
  function logRecordToRow(log) {
3559
3606
  return {
3607
+ logId: log.logId,
3560
3608
  timestamp: toISOString(log.timestamp),
3561
3609
  level: log.level,
3562
3610
  message: log.message,
@@ -3593,6 +3641,7 @@ function logRecordToRow(log) {
3593
3641
  }
3594
3642
  function rowToMetricRecord(row) {
3595
3643
  return {
3644
+ metricId: row.metricId,
3596
3645
  timestamp: toDate(row.timestamp),
3597
3646
  name: row.name,
3598
3647
  value: Number(row.value),
@@ -3634,6 +3683,7 @@ function rowToMetricRecord(row) {
3634
3683
  }
3635
3684
  function metricRecordToRow(metric) {
3636
3685
  return {
3686
+ metricId: metric.metricId,
3637
3687
  timestamp: toISOString(metric.timestamp),
3638
3688
  name: metric.name,
3639
3689
  value: metric.value,
@@ -3675,6 +3725,7 @@ function metricRecordToRow(metric) {
3675
3725
  }
3676
3726
  function rowToScoreRecord(row) {
3677
3727
  return {
3728
+ scoreId: row.scoreId,
3678
3729
  timestamp: toDate(row.timestamp),
3679
3730
  // Core score/feedback shapes still type traceId as required for now.
3680
3731
  traceId: nullableString(row.traceId),
@@ -3717,6 +3768,7 @@ function scoreRecordToRow(score) {
3717
3768
  const metadata = score.metadata ?? null;
3718
3769
  const scoreSource = score.scoreSource ?? score.source ?? null;
3719
3770
  return {
3771
+ scoreId: score.scoreId,
3720
3772
  timestamp: toISOString(score.timestamp),
3721
3773
  traceId: score.traceId ?? null,
3722
3774
  spanId: score.spanId ?? null,
@@ -3759,6 +3811,7 @@ function rowToFeedbackRecord(row) {
3759
3811
  const feedbackSource = nullableString(row.feedbackSource);
3760
3812
  const feedbackUserId = nullableString(row.feedbackUserId) ?? nullableString(row.userId);
3761
3813
  return {
3814
+ feedbackId: row.feedbackId,
3762
3815
  timestamp: toDate(row.timestamp),
3763
3816
  // Core score/feedback shapes still type traceId as required for now.
3764
3817
  traceId: nullableString(row.traceId),
@@ -3802,6 +3855,7 @@ function feedbackRecordToRow(feedback) {
3802
3855
  const feedbackSource = feedback.feedbackSource ?? feedback.source ?? "";
3803
3856
  const feedbackUserId = feedback.feedbackUserId ?? feedback.userId ?? null;
3804
3857
  return {
3858
+ feedbackId: feedback.feedbackId,
3805
3859
  timestamp: toISOString(feedback.timestamp),
3806
3860
  traceId: feedback.traceId ?? null,
3807
3861
  spanId: feedback.spanId ?? null,
@@ -4921,6 +4975,93 @@ async function getMetricLabelValues(client, args) {
4921
4975
  );
4922
4976
  return { values: rows.map((r) => r.value) };
4923
4977
  }
4978
+ var SIGNAL_MIGRATIONS = [
4979
+ { table: TABLE_METRIC_EVENTS, createDDL: METRIC_EVENTS_DDL, idColumn: "metricId" },
4980
+ { table: TABLE_LOG_EVENTS, createDDL: LOG_EVENTS_DDL, idColumn: "logId" },
4981
+ { table: TABLE_SCORE_EVENTS, createDDL: SCORE_EVENTS_DDL, idColumn: "scoreId" },
4982
+ { table: TABLE_FEEDBACK_EVENTS, createDDL: FEEDBACK_EVENTS_DDL, idColumn: "feedbackId" }
4983
+ ];
4984
+ async function getTableEngine(client, table) {
4985
+ const result = await client.query({
4986
+ query: `SELECT engine FROM system.tables WHERE database = currentDatabase() AND name = {table:String}`,
4987
+ query_params: { table },
4988
+ format: "JSONEachRow"
4989
+ });
4990
+ const rows = await result.json();
4991
+ return rows[0]?.engine ?? null;
4992
+ }
4993
+ async function getTableColumns(client, table) {
4994
+ const result = await client.query({ query: `DESCRIBE TABLE ${table}`, format: "JSONEachRow" });
4995
+ const rows = await result.json();
4996
+ return rows.map((r) => r.name);
4997
+ }
4998
+ function buildTemporaryTableDDL(createDDL, table, tempTable) {
4999
+ return createDDL.replace(`CREATE TABLE IF NOT EXISTS ${table}`, `CREATE TABLE ${tempTable}`);
5000
+ }
5001
+ async function dropTableIfExists(client, table) {
5002
+ if (await getTableEngine(client, table) !== null) {
5003
+ await client.command({ query: `DROP TABLE ${table}` });
5004
+ }
5005
+ }
5006
+ function createMigrationError(args, error) {
5007
+ return new MastraError(
5008
+ {
5009
+ id: createStorageErrorId("CLICKHOUSE", "MIGRATE_SIGNAL_TABLES", "FAILED"),
5010
+ domain: ErrorDomain.STORAGE,
5011
+ category: ErrorCategory.THIRD_PARTY,
5012
+ details: args
5013
+ },
5014
+ error
5015
+ );
5016
+ }
5017
+ async function checkSignalTablesMigrationStatus(client) {
5018
+ const tables = [];
5019
+ for (const { table, idColumn } of SIGNAL_MIGRATIONS) {
5020
+ const engine = await getTableEngine(client, table);
5021
+ if (!engine || engine === "ReplacingMergeTree") {
5022
+ continue;
5023
+ }
5024
+ tables.push({ table, engine, idColumn });
5025
+ }
5026
+ return {
5027
+ needsMigration: tables.length > 0,
5028
+ tables
5029
+ };
5030
+ }
5031
+ async function migrateSignalTables(client, logger) {
5032
+ for (const { table, createDDL, idColumn } of SIGNAL_MIGRATIONS) {
5033
+ const engine = await getTableEngine(client, table);
5034
+ if (!engine || engine === "ReplacingMergeTree") continue;
5035
+ logger?.info?.(`Migrating ${table} from ${engine} to ReplacingMergeTree with ${idColumn} column`);
5036
+ const temp = `${table}_migrating_${Date.now()}`;
5037
+ try {
5038
+ await client.command({ query: buildTemporaryTableDDL(createDDL, table, temp) });
5039
+ const newColumns = await getTableColumns(client, temp);
5040
+ const currentColumns = new Set(await getTableColumns(client, table));
5041
+ const columnList = newColumns.map((c) => `"${c}"`).join(", ");
5042
+ const selectExprs = newColumns.map((c) => {
5043
+ if (c === idColumn) {
5044
+ return currentColumns.has(c) ? `COALESCE(nullIf("${c}", ''), toString(generateUUIDv4())) AS "${c}"` : `toString(generateUUIDv4()) AS "${c}"`;
5045
+ }
5046
+ return currentColumns.has(c) ? `"${c}"` : `NULL AS "${c}"`;
5047
+ }).join(", ");
5048
+ await client.command({
5049
+ query: `INSERT INTO ${temp} (${columnList}) SELECT ${selectExprs} FROM ${table}`
5050
+ });
5051
+ await client.command({ query: `EXCHANGE TABLES ${temp} AND ${table}` });
5052
+ await client.command({ query: `DROP TABLE ${temp}` });
5053
+ logger?.info?.(`Successfully migrated ${table}`);
5054
+ } catch (error) {
5055
+ logger?.error?.(`Migration of ${table} failed: ${error.message}`);
5056
+ try {
5057
+ await dropTableIfExists(client, temp);
5058
+ } catch (restoreError) {
5059
+ logger?.error?.(`Failed to clean up temporary table ${temp}: ${restoreError.message}`);
5060
+ }
5061
+ throw createMigrationError({ table, idColumn }, error);
5062
+ }
5063
+ }
5064
+ }
4924
5065
  var SCORE_TYPED_COLUMNS = /* @__PURE__ */ new Set([
4925
5066
  "timestamp",
4926
5067
  "traceId",
@@ -5384,6 +5525,31 @@ async function getTrace(client, args) {
5384
5525
  const spans = rows.map(rowToSpanRecord);
5385
5526
  return { traceId: args.traceId, spans };
5386
5527
  }
5528
+ async function getTraceLight(client, args) {
5529
+ const result = await client.query({
5530
+ query: `
5531
+ SELECT traceId, spanId, parentSpanId, name,
5532
+ entityType, entityId, entityName,
5533
+ spanType, error, isEvent,
5534
+ startedAt, endedAt
5535
+ FROM (
5536
+ SELECT *
5537
+ FROM ${TABLE_SPAN_EVENTS}
5538
+ WHERE traceId = {traceId:String}
5539
+ ORDER BY dedupeKey, endedAt DESC
5540
+ LIMIT 1 BY dedupeKey
5541
+ )
5542
+ ORDER BY startedAt ASC
5543
+ `,
5544
+ query_params: { traceId: args.traceId },
5545
+ format: "JSONEachRow",
5546
+ clickhouse_settings: CH_SETTINGS
5547
+ });
5548
+ const rows = await result.json();
5549
+ if (!rows || rows.length === 0) return null;
5550
+ const spans = rows.map(rowToSpanRecord);
5551
+ return { traceId: args.traceId, spans };
5552
+ }
5387
5553
  async function batchDeleteTraces(client, args) {
5388
5554
  if (args.traceIds.length === 0) return;
5389
5555
  const params = {};
@@ -5412,6 +5578,32 @@ async function batchDeleteTraces(client, args) {
5412
5578
  }
5413
5579
 
5414
5580
  // src/storage/domains/observability/v-next/index.ts
5581
+ function buildSignalMigrationRequiredMessage(args) {
5582
+ const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
5583
+ return `
5584
+ ===========================================================================
5585
+ MIGRATION REQUIRED: ${args.store} observability signal tables need signal IDs
5586
+ ===========================================================================
5587
+
5588
+ The following signal tables still use the legacy schema and must be migrated
5589
+ before observability storage can initialize:
5590
+
5591
+ ${tableList}
5592
+
5593
+ To fix this, run the manual migration command:
5594
+
5595
+ npx mastra migrate
5596
+
5597
+ This command will:
5598
+ 1. Create replacement signal tables with signal-ID dedupe keys
5599
+ 2. Backfill missing signal IDs for legacy rows
5600
+ 3. Swap the migrated tables into place
5601
+
5602
+ WARNING: This migration recreates the signal tables and may take significant
5603
+ time for large databases. Please ensure you have a backup before proceeding.
5604
+ ===========================================================================
5605
+ `;
5606
+ }
5415
5607
  var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
5416
5608
  #client;
5417
5609
  #retention;
@@ -5425,6 +5617,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
5425
5617
  // Initialization
5426
5618
  // -------------------------------------------------------------------------
5427
5619
  async init() {
5620
+ const migrationStatus = await checkSignalTablesMigrationStatus(this.#client);
5621
+ if (migrationStatus.needsMigration) {
5622
+ throw new MastraError({
5623
+ id: createStorageErrorId("CLICKHOUSE", "MIGRATION_REQUIRED", "SIGNAL_TABLES"),
5624
+ domain: ErrorDomain.STORAGE,
5625
+ category: ErrorCategory.USER,
5626
+ text: buildSignalMigrationRequiredMessage({
5627
+ store: "ClickHouse",
5628
+ tables: migrationStatus.tables.map(({ table, engine }) => ({ table, engine }))
5629
+ })
5630
+ });
5631
+ }
5428
5632
  try {
5429
5633
  for (const ddl of [...ALL_TABLE_DDL, ...ALL_MV_DDL]) {
5430
5634
  await this.#client.command({ query: ddl });
@@ -5439,6 +5643,9 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
5439
5643
  }
5440
5644
  }
5441
5645
  } catch (error) {
5646
+ if (error instanceof MastraError) {
5647
+ throw error;
5648
+ }
5442
5649
  throw new MastraError(
5443
5650
  {
5444
5651
  id: createStorageErrorId("CLICKHOUSE", "VNEXT_INIT", "FAILED"),
@@ -5460,6 +5667,29 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
5460
5667
  } catch {
5461
5668
  }
5462
5669
  }
5670
+ /**
5671
+ * Manually migrate legacy signal tables to the signal-ID ReplacingMergeTree schema.
5672
+ * The public method name is historical; the CLI still calls `migrateSpans()`
5673
+ * for observability migrations even though this now also migrates signal tables.
5674
+ */
5675
+ async migrateSpans() {
5676
+ const migrationStatus = await checkSignalTablesMigrationStatus(this.#client);
5677
+ if (!migrationStatus.needsMigration) {
5678
+ return {
5679
+ success: true,
5680
+ alreadyMigrated: true,
5681
+ duplicatesRemoved: 0,
5682
+ message: "Migration already complete. Signal tables already use signal-ID dedupe keys."
5683
+ };
5684
+ }
5685
+ await migrateSignalTables(this.#client, this.logger);
5686
+ return {
5687
+ success: true,
5688
+ alreadyMigrated: false,
5689
+ duplicatesRemoved: 0,
5690
+ message: `Migration complete. Migrated signal tables: ${migrationStatus.tables.map((t) => t.table).join(", ")}.`
5691
+ };
5692
+ }
5463
5693
  // -------------------------------------------------------------------------
5464
5694
  // Strategy
5465
5695
  // -------------------------------------------------------------------------
@@ -5555,6 +5785,22 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
5555
5785
  );
5556
5786
  }
5557
5787
  }
5788
+ async getTraceLight(args) {
5789
+ try {
5790
+ return await getTraceLight(this.#client, args);
5791
+ } catch (error) {
5792
+ if (error instanceof MastraError) throw error;
5793
+ throw new MastraError(
5794
+ {
5795
+ id: createStorageErrorId("CLICKHOUSE", "GET_TRACE_LIGHT", "FAILED"),
5796
+ domain: ErrorDomain.STORAGE,
5797
+ category: ErrorCategory.THIRD_PARTY,
5798
+ details: { traceId: args.traceId }
5799
+ },
5800
+ error
5801
+ );
5802
+ }
5803
+ }
5558
5804
  async listTraces(args) {
5559
5805
  try {
5560
5806
  return await listTraces(this.#client, args);