@mastra/clickhouse 1.11.1 → 1.12.0-alpha.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
@@ -5947,6 +5947,11 @@ async function getTableColumns(client, table) {
5947
5947
  const rows = await result.json();
5948
5948
  return rows.map((r) => r.name);
5949
5949
  }
5950
+ async function getTableColumnTypes(client, table) {
5951
+ const result = await client.query({ query: `DESCRIBE TABLE ${table}`, format: "JSONEachRow" });
5952
+ const rows = await result.json();
5953
+ return new Map(rows.map((r) => [r.name, r.type]));
5954
+ }
5950
5955
  function buildTemporaryTableDDL(createDDL, table, tempTable) {
5951
5956
  return createDDL.replace(`CREATE TABLE IF NOT EXISTS ${table}`, `CREATE TABLE ${tempTable}`);
5952
5957
  }
@@ -5980,6 +5985,218 @@ async function checkSignalTablesMigrationStatus(client) {
5980
5985
  tables
5981
5986
  };
5982
5987
  }
5988
+ var TABLE_LEGACY_SPANS = "mastra_ai_spans";
5989
+ var PROMOTED_METADATA_KEYS = [
5990
+ "experimentId",
5991
+ "entityType",
5992
+ "entityId",
5993
+ "entityName",
5994
+ "entityVersionId",
5995
+ "parentEntityVersionId",
5996
+ "rootEntityVersionId",
5997
+ "userId",
5998
+ "organizationId",
5999
+ "resourceId",
6000
+ "runId",
6001
+ "sessionId",
6002
+ "threadId",
6003
+ "requestId",
6004
+ "environment",
6005
+ "executionSource",
6006
+ "serviceName"
6007
+ ];
6008
+ var TABLE_LEGACY_MIGRATION_DONE = "mastra_legacy_span_migration_done";
6009
+ async function hasMarkerRow(client) {
6010
+ const engine = await getTableEngine(client, TABLE_LEGACY_MIGRATION_DONE);
6011
+ if (!engine) return false;
6012
+ const result = await client.query({
6013
+ query: `SELECT count() as cnt FROM ${TABLE_LEGACY_MIGRATION_DONE}`,
6014
+ format: "JSONEachRow"
6015
+ });
6016
+ const rows = await result.json();
6017
+ return Number(rows[0]?.cnt ?? 0) > 0;
6018
+ }
6019
+ async function checkLegacySpanMigrationStatus(client) {
6020
+ if (await hasMarkerRow(client)) {
6021
+ return { needsMigration: false };
6022
+ }
6023
+ const engine = await getTableEngine(client, TABLE_LEGACY_SPANS);
6024
+ if (!engine) {
6025
+ return { needsMigration: false };
6026
+ }
6027
+ const countResult = await client.query({
6028
+ query: `SELECT count() as cnt FROM ${TABLE_LEGACY_SPANS}`,
6029
+ format: "JSONEachRow"
6030
+ });
6031
+ const countRows = await countResult.json();
6032
+ const legacyRowCount = Number(countRows[0]?.cnt ?? 0);
6033
+ if (legacyRowCount === 0) {
6034
+ return { needsMigration: false };
6035
+ }
6036
+ return { needsMigration: true };
6037
+ }
6038
+ var VNEXT_SPAN_COLUMNS = [
6039
+ "dedupeKey",
6040
+ "traceId",
6041
+ "spanId",
6042
+ "parentSpanId",
6043
+ "experimentId",
6044
+ "entityType",
6045
+ "entityId",
6046
+ "entityName",
6047
+ "entityVersionId",
6048
+ "parentEntityVersionId",
6049
+ "parentEntityType",
6050
+ "parentEntityId",
6051
+ "parentEntityName",
6052
+ "rootEntityVersionId",
6053
+ "rootEntityType",
6054
+ "rootEntityId",
6055
+ "rootEntityName",
6056
+ "userId",
6057
+ "organizationId",
6058
+ "resourceId",
6059
+ "runId",
6060
+ "sessionId",
6061
+ "threadId",
6062
+ "requestId",
6063
+ "environment",
6064
+ "executionSource",
6065
+ "serviceName",
6066
+ "name",
6067
+ "spanType",
6068
+ "isEvent",
6069
+ "startedAt",
6070
+ "endedAt",
6071
+ "tags",
6072
+ "metadataSearch",
6073
+ "attributes",
6074
+ "scope",
6075
+ "links",
6076
+ "input",
6077
+ "output",
6078
+ "error",
6079
+ "metadataRaw",
6080
+ "requestContext"
6081
+ ];
6082
+ function buildLegacySpanSelectExprs(legacyColumnTypes) {
6083
+ const has = (col) => legacyColumnTypes.has(col);
6084
+ const colOrNull = (legacy) => has(legacy) ? `"${legacy}"` : "NULL";
6085
+ const promotedIn = PROMOTED_METADATA_KEYS.map((k) => `'${k}'`).join(", ");
6086
+ const metadataSearchExpr = has("metadata") ? `CAST(arrayFilter(x -> x.1 NOT IN (${promotedIn}) AND trim(BOTH ' ' FROM x.2) != '', arrayMap(x -> (x.1, trim(BOTH ' ' FROM x.2)), JSONExtractKeysAndValues(COALESCE("metadata", '{}'), 'String'))), 'Map(String, String)')` : `CAST(map(), 'Map(String, String)')`;
6087
+ let tagsExpr = "[]";
6088
+ if (has("tags")) {
6089
+ const tagsType = legacyColumnTypes.get("tags");
6090
+ if (tagsType.includes("Array")) {
6091
+ tagsExpr = `arrayFilter(x -> x != '', arrayDistinct(arrayMap(x -> trim(BOTH ' ' FROM x), "tags")))`;
6092
+ } else {
6093
+ tagsExpr = `arrayFilter(x -> x != '', arrayDistinct(arrayMap(x -> trim(BOTH ' ' FROM x), JSONExtract(COALESCE("tags", '[]'), 'Array(String)'))))`;
6094
+ }
6095
+ }
6096
+ const exprs = {
6097
+ dedupeKey: `concat("traceId", ':', "spanId")`,
6098
+ traceId: `"traceId"`,
6099
+ spanId: `"spanId"`,
6100
+ // Legacy stores '' for root spans; VNext MV filters on IS NULL.
6101
+ parentSpanId: has("parentSpanId") ? `nullIf("parentSpanId", '')` : "NULL",
6102
+ experimentId: colOrNull("experimentId"),
6103
+ entityType: colOrNull("entityType"),
6104
+ entityId: colOrNull("entityId"),
6105
+ entityName: colOrNull("entityName"),
6106
+ entityVersionId: colOrNull("entityVersionId"),
6107
+ parentEntityVersionId: colOrNull("parentEntityVersionId"),
6108
+ parentEntityType: colOrNull("parentEntityType"),
6109
+ parentEntityId: colOrNull("parentEntityId"),
6110
+ parentEntityName: colOrNull("parentEntityName"),
6111
+ rootEntityVersionId: colOrNull("rootEntityVersionId"),
6112
+ rootEntityType: colOrNull("rootEntityType"),
6113
+ rootEntityId: colOrNull("rootEntityId"),
6114
+ rootEntityName: colOrNull("rootEntityName"),
6115
+ userId: colOrNull("userId"),
6116
+ organizationId: colOrNull("organizationId"),
6117
+ resourceId: colOrNull("resourceId"),
6118
+ runId: colOrNull("runId"),
6119
+ sessionId: colOrNull("sessionId"),
6120
+ threadId: colOrNull("threadId"),
6121
+ requestId: colOrNull("requestId"),
6122
+ environment: colOrNull("environment"),
6123
+ executionSource: has("source") ? `"source"` : "NULL",
6124
+ serviceName: colOrNull("serviceName"),
6125
+ name: `"name"`,
6126
+ spanType: `"spanType"`,
6127
+ isEvent: `"isEvent"`,
6128
+ startedAt: `"startedAt"`,
6129
+ // VNext requires non-null endedAt; events always use startedAt.
6130
+ endedAt: `if("isEvent", "startedAt", COALESCE("endedAt", "startedAt"))`,
6131
+ tags: tagsExpr,
6132
+ metadataSearch: metadataSearchExpr,
6133
+ attributes: colOrNull("attributes"),
6134
+ scope: colOrNull("scope"),
6135
+ links: colOrNull("links"),
6136
+ input: colOrNull("input"),
6137
+ output: colOrNull("output"),
6138
+ error: colOrNull("error"),
6139
+ metadataRaw: has("metadata") ? `"metadata"` : "NULL",
6140
+ requestContext: colOrNull("requestContext")
6141
+ };
6142
+ return VNEXT_SPAN_COLUMNS.map((c) => `${exprs[c]} AS "${c}"`).join(",\n ");
6143
+ }
6144
+ async function migrateLegacySpans(client, logger) {
6145
+ if (await hasMarkerRow(client)) {
6146
+ return { migratedRows: 0, batches: 0 };
6147
+ }
6148
+ const engine = await getTableEngine(client, TABLE_LEGACY_SPANS);
6149
+ if (!engine) {
6150
+ return { migratedRows: 0, batches: 0 };
6151
+ }
6152
+ const legacyColumnTypes = await getTableColumnTypes(client, TABLE_LEGACY_SPANS);
6153
+ const hasUpdatedAt = legacyColumnTypes.has("updatedAt");
6154
+ await client.command({ query: SPAN_EVENTS_DDL });
6155
+ await client.command({ query: TRACE_ROOTS_DDL });
6156
+ await client.command({ query: TRACE_BRANCHES_DDL });
6157
+ await client.command({ query: TRACE_ROOTS_MV_DDL });
6158
+ await client.command({ query: TRACE_BRANCHES_MV_DDL });
6159
+ const selectExprs = buildLegacySpanSelectExprs(legacyColumnTypes);
6160
+ const columnList = VNEXT_SPAN_COLUMNS.map((c) => `"${c}"`).join(", ");
6161
+ const dedupOrder = hasUpdatedAt ? `ORDER BY "traceId", "spanId", COALESCE("updatedAt", "createdAt") DESC` : `ORDER BY "traceId", "spanId", "createdAt" DESC`;
6162
+ const daysResult = await client.query({
6163
+ query: `SELECT
6164
+ toString(toDate(COALESCE(endedAt, startedAt))) as day,
6165
+ count() as cnt
6166
+ FROM ${TABLE_LEGACY_SPANS}
6167
+ GROUP BY day
6168
+ ORDER BY day`,
6169
+ format: "JSONEachRow"
6170
+ });
6171
+ const days = await daysResult.json();
6172
+ let migratedRows = 0;
6173
+ let batches = 0;
6174
+ for (const { day, cnt } of days) {
6175
+ const dayCount = Number(cnt);
6176
+ await client.command({
6177
+ query: `INSERT INTO ${TABLE_SPAN_EVENTS} (${columnList})
6178
+ SELECT ${selectExprs}
6179
+ FROM ${TABLE_LEGACY_SPANS}
6180
+ WHERE toDate(COALESCE(endedAt, startedAt)) = {batchDate:Date}
6181
+ ${dedupOrder}
6182
+ LIMIT 1 BY "traceId", "spanId"`,
6183
+ query_params: { batchDate: day }
6184
+ });
6185
+ migratedRows += dayCount;
6186
+ batches++;
6187
+ logger?.info?.(`Migrated batch ${day}: ${dayCount} rows`);
6188
+ }
6189
+ await client.command({
6190
+ query: `CREATE TABLE IF NOT EXISTS ${TABLE_LEGACY_MIGRATION_DONE} (completedAt DateTime64(3, 'UTC')) ENGINE = MergeTree ORDER BY completedAt`
6191
+ });
6192
+ await client.command({
6193
+ query: `INSERT INTO ${TABLE_LEGACY_MIGRATION_DONE} VALUES (now64(3))`
6194
+ });
6195
+ logger?.info?.(
6196
+ `Legacy span migration complete: ${migratedRows} rows in ${batches} batches. The legacy table '${TABLE_LEGACY_SPANS}' has been preserved. Drop it manually after verifying the migration.`
6197
+ );
6198
+ return { migratedRows, batches };
6199
+ }
5983
6200
  async function migrateSignalTables(client, logger) {
5984
6201
  for (const { table, createDDL, idColumn } of SIGNAL_MIGRATIONS) {
5985
6202
  const engine = await getTableEngine(client, table);
@@ -7294,6 +7511,15 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
7294
7511
  })
7295
7512
  });
7296
7513
  }
7514
+ try {
7515
+ const legacyStatus = await checkLegacySpanMigrationStatus(this.#client);
7516
+ if (legacyStatus.needsMigration) {
7517
+ this.logger?.warn?.(
7518
+ `Legacy span table 'mastra_ai_spans' detected. Run 'npx mastra migrate' to migrate historical spans to the v-next schema.`
7519
+ );
7520
+ }
7521
+ } catch {
7522
+ }
7297
7523
  try {
7298
7524
  await assertExistingTablesCompatibleWithReplication(this.#client, this.#replication);
7299
7525
  const existingStrategy = await detectExistingDeltaCursorStrategy(this.#client);
@@ -7368,34 +7594,44 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
7368
7594
  }
7369
7595
  }
7370
7596
  /**
7371
- * Manually migrate legacy signal tables to the signal-ID ReplacingMergeTree schema.
7372
- * The public method name is historical; the CLI still calls `migrateSpans()`
7373
- * for observability migrations even though this now also migrates signal tables.
7597
+ * Manually migrate legacy tables to the v-next schema.
7598
+ * Handles both signal table migrations (MergeTree ReplacingMergeTree)
7599
+ * and legacy span migration (mastra_ai_spans mastra_span_events).
7374
7600
  */
7375
7601
  async migrateSpans() {
7376
- const migrationStatus = await checkSignalTablesMigrationStatus(this.#client);
7377
- if (!migrationStatus.needsMigration) {
7378
- return {
7379
- success: true,
7380
- alreadyMigrated: true,
7381
- duplicatesRemoved: 0,
7382
- message: "Migration already complete. Signal tables already use signal-ID dedupe keys."
7383
- };
7602
+ const messages = [];
7603
+ const signalStatus = await checkSignalTablesMigrationStatus(this.#client);
7604
+ if (signalStatus.needsMigration) {
7605
+ if (isReplicationConfigured(this.#replication)) {
7606
+ throw new MastraError({
7607
+ id: createStorageErrorId("CLICKHOUSE", "REPLICATION", "SIGNAL_TABLES_MIGRATION_UNSUPPORTED"),
7608
+ domain: ErrorDomain.STORAGE,
7609
+ category: ErrorCategory.USER,
7610
+ text: "ClickHouse replication is enabled, so Mastra will not run copy-and-swap signal table migrations automatically. Migrate existing local signal tables manually before enabling replication."
7611
+ });
7612
+ }
7613
+ await migrateSignalTables(this.#client, this.logger);
7614
+ messages.push(`Migrated signal tables: ${signalStatus.tables.map((t) => t.table).join(", ")}.`);
7384
7615
  }
7385
- if (isReplicationConfigured(this.#replication)) {
7386
- throw new MastraError({
7387
- id: createStorageErrorId("CLICKHOUSE", "REPLICATION", "SIGNAL_TABLES_MIGRATION_UNSUPPORTED"),
7388
- domain: ErrorDomain.STORAGE,
7389
- category: ErrorCategory.USER,
7390
- text: "ClickHouse replication is enabled, so Mastra will not run copy-and-swap signal table migrations automatically. Migrate existing local signal tables manually before enabling replication."
7391
- });
7616
+ const legacyStatus = await checkLegacySpanMigrationStatus(this.#client);
7617
+ if (legacyStatus.needsMigration) {
7618
+ if (isReplicationConfigured(this.#replication)) {
7619
+ throw new MastraError({
7620
+ id: createStorageErrorId("CLICKHOUSE", "REPLICATION", "LEGACY_SPAN_MIGRATION_UNSUPPORTED"),
7621
+ domain: ErrorDomain.STORAGE,
7622
+ category: ErrorCategory.USER,
7623
+ text: "ClickHouse replication is enabled. Migrate legacy mastra_ai_spans manually before enabling replication."
7624
+ });
7625
+ }
7626
+ const result = await migrateLegacySpans(this.#client, this.logger);
7627
+ messages.push(`Migrated ${result.migratedRows} legacy spans in ${result.batches} batches.`);
7392
7628
  }
7393
- await migrateSignalTables(this.#client, this.logger);
7629
+ const alreadyMigrated = !signalStatus.needsMigration && !legacyStatus.needsMigration;
7394
7630
  return {
7395
7631
  success: true,
7396
- alreadyMigrated: false,
7632
+ alreadyMigrated,
7397
7633
  duplicatesRemoved: 0,
7398
- message: `Migration complete. Migrated signal tables: ${migrationStatus.tables.map((t) => t.table).join(", ")}.`
7634
+ message: alreadyMigrated ? "Migration already complete." : `Migration complete. ${messages.join(" ")}`
7399
7635
  };
7400
7636
  }
7401
7637
  // -------------------------------------------------------------------------