@mastra/clickhouse 1.20.0 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3200,7 +3200,8 @@ CREATE TABLE IF NOT EXISTS ${TABLE_SCORE_EVENTS_DELTA} (
3200
3200
  ingestedAt ${DELTA_INGESTED_AT_TYPE},
3201
3201
  traceId Nullable(String),
3202
3202
  timestamp DateTime64(3, 'UTC'),
3203
- scoreId String
3203
+ scoreId String,
3204
+ INDEX idx_scoreId scoreId TYPE bloom_filter(0.01) GRANULARITY 1
3204
3205
  )
3205
3206
  ENGINE = MergeTree
3206
3207
  PARTITION BY toDate(ingestedAt)
@@ -3209,12 +3210,8 @@ TTL ingestedAt + toIntervalDay(2)
3209
3210
  SETTINGS allow_nullable_key = 1
3210
3211
  `;
3211
3212
  }
3212
- function buildScoreEventsDeltaMvDDL(strategy) {
3213
- return `
3214
- CREATE MATERIALIZED VIEW IF NOT EXISTS ${MV_SCORE_EVENTS_DELTA}
3215
- TO ${TABLE_SCORE_EVENTS_DELTA}
3216
- AS
3217
- SELECT
3213
+ function buildScoreEventsDeltaMvQuery(strategy) {
3214
+ return `SELECT
3218
3215
  ${buildDeltaCursorExpr(strategy, "mastra_score_events_delta_cursor", "scoreId")} AS cursorId,
3219
3216
  ingestedAt,
3220
3217
  traceId,
@@ -3227,7 +3224,19 @@ FROM (
3227
3224
  timestamp,
3228
3225
  scoreId
3229
3226
  FROM ${TABLE_SCORE_EVENTS}
3230
- )
3227
+ WHERE scoreId NOT IN (
3228
+ SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA}
3229
+ WHERE scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS})
3230
+ )
3231
+ LIMIT 1 BY scoreId
3232
+ )`;
3233
+ }
3234
+ function buildScoreEventsDeltaMvDDL(strategy) {
3235
+ return `
3236
+ CREATE MATERIALIZED VIEW IF NOT EXISTS ${MV_SCORE_EVENTS_DELTA}
3237
+ TO ${TABLE_SCORE_EVENTS_DELTA}
3238
+ AS
3239
+ ${buildScoreEventsDeltaMvQuery(strategy)}
3231
3240
  `;
3232
3241
  }
3233
3242
  const FEEDBACK_EVENTS_DDL = `
@@ -3536,6 +3545,7 @@ const ALL_MIGRATIONS = [
3536
3545
  addColumn(TABLE_SCORE_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3537
3546
  addColumn(TABLE_SCORE_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3538
3547
  addBloomIndex(TABLE_SCORE_EVENTS, "idx_scoreId", "scoreId", 1),
3548
+ addBloomIndex(TABLE_SCORE_EVENTS_DELTA, "idx_scoreId", "scoreId", 1),
3539
3549
  addColumn(TABLE_FEEDBACK_EVENTS, "writeVersion", "UInt64 DEFAULT 0"),
3540
3550
  addColumn(TABLE_FEEDBACK_EVENTS, "entityVersionId", "Nullable(String)"),
3541
3551
  addColumn(TABLE_FEEDBACK_EVENTS, "reviewStatus", "LowCardinality(String) DEFAULT 'needs-review'"),
@@ -6285,6 +6295,17 @@ async function listScores(client, args, strategy) {
6285
6295
  ...deltaCursorEnabled ? { deltaCursor: currentDeltaCursor } : {}
6286
6296
  };
6287
6297
  }
6298
+ /**
6299
+ * Delta reads join the delta stream to the current-state table by scoreId, so
6300
+ * a poll or replay always returns the latest write of a score (a rewrite may
6301
+ * change traceId or timestamp, which the delta row does not track).
6302
+ *
6303
+ * A score's first delta row is its only cursor: rows after `afterCursor` are
6304
+ * dropped when the same scoreId already has a row at or before it, and
6305
+ * `LIMIT 1 BY` keeps the lowest cursor within the page. Duplicate delta rows
6306
+ * (a concurrent-insert race past the MV check) therefore never surface, in
6307
+ * this poll or a later one.
6308
+ */
6288
6309
  async function queryScoresAfterCursor(client, whereClause, params, limit, cursorId) {
6289
6310
  return await queryJson(client, `
6290
6311
  SELECT
@@ -6294,12 +6315,16 @@ async function queryScoresAfterCursor(client, whereClause, params, limit, cursor
6294
6315
  s.scoreId AS scoreId,
6295
6316
  toString(d.cursorId) AS cursorId
6296
6317
  FROM ${TABLE_SCORE_EVENTS_DELTA} d
6297
- INNER JOIN ${TABLE_SCORE_EVENTS} s
6298
- ON ((s.traceId = d.traceId) OR (s.traceId IS NULL AND d.traceId IS NULL))
6299
- AND s.timestamp = d.timestamp
6300
- AND s.scoreId = d.scoreId
6301
- ${whereClause ? `${whereClause} AND d.cursorId > {afterCursor:UInt64}` : "WHERE d.cursorId > {afterCursor:UInt64}"}
6318
+ INNER JOIN ${TABLE_SCORE_EVENTS_CURRENT} s FINAL
6319
+ ON s.scoreId = d.scoreId
6320
+ ${whereClause ? `${whereClause} AND` : "WHERE"} d.cursorId > {afterCursor:UInt64}
6321
+ AND d.scoreId NOT IN (
6322
+ SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA}
6323
+ WHERE cursorId <= {afterCursor:UInt64}
6324
+ AND scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})
6325
+ )
6302
6326
  ORDER BY d.cursorId ASC
6327
+ LIMIT 1 BY s.scoreId
6303
6328
  LIMIT {fetchLimit:UInt32}
6304
6329
  `, {
6305
6330
  ...params,
@@ -6311,10 +6336,8 @@ async function getDeltaCursor$2(client, whereClause, params) {
6311
6336
  const cursorId = (await queryJson(client, `
6312
6337
  SELECT toString(max(d.cursorId)) AS cursorId
6313
6338
  FROM mastra_score_events_delta d
6314
- INNER JOIN mastra_score_events s
6315
- ON ((s.traceId = d.traceId) OR (s.traceId IS NULL AND d.traceId IS NULL))
6316
- AND s.timestamp = d.timestamp
6317
- AND s.scoreId = d.scoreId
6339
+ INNER JOIN mastra_score_events_current s FINAL
6340
+ ON s.scoreId = d.scoreId
6318
6341
  ${whereClause}
6319
6342
  `, params))[0]?.cursorId ?? null;
6320
6343
  if (cursorId) return cursorId;
@@ -8284,6 +8307,37 @@ async function queryNamesByTable(client, query, tables) {
8284
8307
  }
8285
8308
  return out;
8286
8309
  }
8310
+ /**
8311
+ * Upgrades a score delta MV created before per-scoreId deduplication with
8312
+ * `ALTER TABLE ... MODIFY QUERY`. The view is changed in place, so there is no
8313
+ * window without a view in which score writes would miss their delta row (a
8314
+ * drop-and-recreate would leave such writes out of delta polling for good).
8315
+ *
8316
+ * With a cluster configured the definition is read from every replica via
8317
+ * `clusterAllReplicas`, since each host stores its own copy of the view; a
8318
+ * single legacy copy is enough to alter `ON CLUSTER`.
8319
+ *
8320
+ * Introspection failures are logged and skipped, like the other schema checks
8321
+ * in init(): the existing view keeps working and the next boot retries.
8322
+ */
8323
+ async function reconcileScoreDeltaMv(client, replication, strategy, logger) {
8324
+ const cluster = replication?.cluster?.trim();
8325
+ let createQueries;
8326
+ try {
8327
+ createQueries = (await (await client.query({
8328
+ query: cluster ? `SELECT create_table_query FROM clusterAllReplicas({cluster:String}, system.tables) WHERE database = currentDatabase() AND name = {name:String}` : `SELECT create_table_query FROM system.tables WHERE database = currentDatabase() AND name = {name:String}`,
8329
+ query_params: cluster ? {
8330
+ cluster,
8331
+ name: MV_SCORE_EVENTS_DELTA
8332
+ } : { name: MV_SCORE_EVENTS_DELTA },
8333
+ format: "JSONEachRow"
8334
+ })).json()).map((row) => row.create_table_query ?? "");
8335
+ } catch (error) {
8336
+ logger?.warn?.(`Could not verify the ${MV_SCORE_EVENTS_DELTA} definition; leaving the existing view in place: ${error instanceof Error ? error.message : String(error)}`);
8337
+ return;
8338
+ }
8339
+ if (createQueries.some((createQuery) => createQuery.length > 0 && !/NOT IN/i.test(createQuery))) await client.command({ query: addOnClusterToDDL(`ALTER TABLE ${MV_SCORE_EVENTS_DELTA} MODIFY QUERY ${buildScoreEventsDeltaMvQuery(strategy)}`, replication) });
8340
+ }
8287
8341
  async function detectDeltaCursorStrategy(client, override, existingStrategy) {
8288
8342
  if (override) return override;
8289
8343
  if (existingStrategy && existingStrategy !== "mixed") return existingStrategy;
@@ -8393,6 +8447,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
8393
8447
  for (const migration of pendingMigrations) await this.#client.command({ query: addOnClusterToDDL(migration.sql, this.#replication) });
8394
8448
  const coreMvDdl = this.#deltaCursorStrategy === null ? BASE_MV_DDL : buildAllMvDDL(this.#deltaCursorStrategy);
8395
8449
  for (const ddl of coreMvDdl) await this.#client.command({ query: applyReplicationToDDL(ddl, this.#replication) });
8450
+ if (this.#deltaCursorStrategy !== null) await reconcileScoreDeltaMv(this.#client, this.#replication, this.#deltaCursorStrategy, this.logger);
8396
8451
  await backfillCurrentScores(this.#client);
8397
8452
  if (this.#retention) await this.applyRetention();
8398
8453
  if (this.#deltaCursorStrategy === "serial") for (const counterName of DELTA_CURSOR_COUNTER_NAMES) await this.#client.query({