@mastra/clickhouse 1.21.0-alpha.2 → 1.21.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
@@ -4272,17 +4272,46 @@ async function recordDeletionRequest(client, args) {
4272
4272
  purgeVerifiedAt: EPOCH,
4273
4273
  updatedAt: args.requestedAt
4274
4274
  };
4275
+ await insertDeletionRequest(client, row, args.replication);
4276
+ return row;
4277
+ }
4278
+ /**
4279
+ * Marks a recorded deletion request as applied after its lightweight DELETEs
4280
+ * succeeded. The table is `ReplacingMergeTree(updatedAt)`, so re-inserting the
4281
+ * row with a newer `updatedAt` supersedes the pending version on merge and
4282
+ * under `FINAL`. Requests whose DELETE failed keep `lastAppliedAt` at the
4283
+ * epoch; mutation guards ignore them, and re-invoking the delete API records a
4284
+ * new request and converges.
4285
+ */
4286
+ async function markDeletionRequestApplied(client, row, replication) {
4287
+ const pendingAt = Date.parse(row.updatedAt);
4288
+ const appliedAt = new Date(Number.isFinite(pendingAt) ? Math.max(Date.now(), pendingAt + 1) : Date.now()).toISOString();
4289
+ const applied = {
4290
+ ...row,
4291
+ lastAppliedAt: appliedAt,
4292
+ updatedAt: appliedAt
4293
+ };
4294
+ await insertDeletionRequest(client, applied, replication);
4295
+ return applied;
4296
+ }
4297
+ /**
4298
+ * Replicated clusters insert with parallel quorum, the same as every other
4299
+ * deletion-request write on main, so concurrent deletes never reject each other.
4300
+ * The guard that reads these rows is advisory: review updates mutate feedback
4301
+ * in place and preserve the delete mask, so a replica that has not yet received
4302
+ * a marker can only mis-report a status, never bring deleted feedback back.
4303
+ */
4304
+ async function insertDeletionRequest(client, row, replication) {
4275
4305
  await client.insert({
4276
4306
  table: TABLE_DELETION_REQUESTS,
4277
4307
  values: [row],
4278
4308
  format: "JSONEachRow",
4279
- clickhouse_settings: isReplicationConfigured(args.replication) ? {
4309
+ clickhouse_settings: isReplicationConfigured(replication) ? {
4280
4310
  ...CH_INSERT_SETTINGS,
4281
4311
  insert_quorum: "auto",
4282
4312
  insert_quorum_parallel: 1
4283
4313
  } : CH_INSERT_SETTINGS
4284
4314
  });
4285
- return row;
4286
4315
  }
4287
4316
  //#endregion
4288
4317
  //#region src/storage/domains/observability/v-next/discovery.ts
@@ -4774,14 +4803,18 @@ async function batchCreateFeedback(client, args) {
4774
4803
  * `organizationId` and `resourceId` values are ANDed into the predicate to
4775
4804
  * restrict deletion to records with matching scope fields.
4776
4805
  *
4777
- * A durable deletion request is recorded before the lightweight delete. The
4778
- * delete is immediately visible to subsequent reads; physical purge depends on
4779
- * the table's configured retention TTL. The delta table is intentionally not
4806
+ * A durable deletion request is recorded before the lightweight delete and
4807
+ * marked applied once the delete succeeds. If the delete fails, the request
4808
+ * stays unapplied and does not block updates to the still-visible rows; retry
4809
+ * by calling this function again.
4810
+ *
4811
+ * The delete is immediately visible to subsequent reads; physical purge depends
4812
+ * on the table's configured retention TTL. The delta table is intentionally not
4780
4813
  * touched and expires through its fixed two-day TTL.
4781
4814
  */
4782
4815
  async function deleteFeedback(client, args, replication) {
4783
4816
  if (args.feedbackIds.length === 0) return;
4784
- await recordDeletionRequest(client, {
4817
+ const request = await recordDeletionRequest(client, {
4785
4818
  requestId: randomUUID(),
4786
4819
  organizationId: args.organizationId,
4787
4820
  resourceId: args.resourceId,
@@ -4812,6 +4845,7 @@ async function deleteFeedback(client, args, replication) {
4812
4845
  query_params: params,
4813
4846
  clickhouse_settings: { lightweight_deletes_sync: isReplicationConfigured(replication) ? "2" : "1" }
4814
4847
  });
4848
+ await markDeletionRequestApplied(client, request, replication);
4815
4849
  }
4816
4850
  function feedbackNotFoundError(feedbackId) {
4817
4851
  return new MastraError({
@@ -4822,11 +4856,29 @@ function feedbackNotFoundError(feedbackId) {
4822
4856
  details: { feedbackId }
4823
4857
  });
4824
4858
  }
4859
+ function feedbackConflictError(feedbackId) {
4860
+ return new MastraError({
4861
+ id: "OBSERVABILITY_UPDATE_FEEDBACK_REVIEW_STATUS_CONFLICT",
4862
+ domain: ErrorDomain.MASTRA_OBSERVABILITY,
4863
+ category: ErrorCategory.USER,
4864
+ text: "Feedback record changed while its review status was being updated; retry the update",
4865
+ details: { feedbackId }
4866
+ });
4867
+ }
4868
+ /** Re-reads before giving up when ingestion keeps superseding the observed row. */
4869
+ const REVIEW_UPDATE_ATTEMPTS = 3;
4870
+ /**
4871
+ * Only applied requests block a review update. The guard decides the reply,
4872
+ * not data safety: the update mutates the row in place and preserves the
4873
+ * delete mask, so a replica that has not received a marker yet can only
4874
+ * mis-report a status, never bring deleted feedback back.
4875
+ */
4825
4876
  async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId) {
4826
4877
  return (await queryJson$2(client, `SELECT 1 AS found FROM ${TABLE_DELETION_REQUESTS} FINAL
4827
4878
  WHERE signal = 'feedback'
4828
4879
  AND predicateType = 'itemIds'
4829
4880
  AND has(predicateValues, {feedbackId:String})
4881
+ AND lastAppliedAt > toDateTime64(0, 3)
4830
4882
  AND (organizationId = '' OR organizationId = {organizationId:String})
4831
4883
  AND (resourceId = '' OR resourceId = {resourceId:String})
4832
4884
  LIMIT 1`, {
@@ -4835,28 +4887,63 @@ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, re
4835
4887
  resourceId: resourceId ?? ""
4836
4888
  })).length > 0;
4837
4889
  }
4838
- async function updateFeedbackReviewStatus(client, args, replication) {
4890
+ async function updateFeedbackReviewStatus(client, args, strategy = null) {
4839
4891
  const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4840
- const existingRow = (await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4892
+ for (let attempt = 1;; attempt++) {
4893
+ const updated = await applyReviewStatus(client, feedbackId, reviewStatus, strategy);
4894
+ if (updated) return updated;
4895
+ if (attempt >= REVIEW_UPDATE_ATTEMPTS) throw feedbackConflictError(feedbackId);
4896
+ }
4897
+ }
4898
+ /**
4899
+ * One read-guard-mutate-verify pass. Returns `null` when the read-back does not
4900
+ * show the new status: a newer version superseded the observed row, a delete
4901
+ * hid it after the guard read (the next pass then reports not found), or the
4902
+ * mutation left the row untouched.
4903
+ */
4904
+ async function applyReviewStatus(client, feedbackId, reviewStatus, strategy) {
4905
+ const existingRow = (await queryJson$2(client, `SELECT *, toString(writeVersion) AS reviewWriteVersion FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4841
4906
  WHERE feedbackId = {feedbackId:String}
4842
4907
  ORDER BY writeVersion DESC, timestamp DESC
4843
4908
  LIMIT 1`, { feedbackId }))[0];
4844
4909
  if (!existingRow) throw feedbackNotFoundError(feedbackId);
4845
4910
  if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
4846
- const updated = rowToFeedbackRecord({
4847
- ...existingRow,
4911
+ const identity = `feedbackId = {feedbackId:String}
4912
+ AND timestamp = parseDateTime64BestEffort({timestamp:String}, 3, 'UTC')
4913
+ AND (traceId = {traceId:Nullable(String)} OR (isNull(traceId) AND isNull({traceId:Nullable(String)})))
4914
+ AND writeVersion = {writeVersion:UInt64}`;
4915
+ const params = {
4916
+ feedbackId,
4917
+ timestamp: existingRow.timestamp,
4918
+ traceId: existingRow.traceId,
4919
+ writeVersion: existingRow.reviewWriteVersion,
4848
4920
  reviewStatus
4921
+ };
4922
+ await client.command({
4923
+ query: `ALTER TABLE ${TABLE_FEEDBACK_EVENTS} UPDATE reviewStatus = {reviewStatus:String} WHERE ${identity}`,
4924
+ query_params: params,
4925
+ clickhouse_settings: {
4926
+ ...CH_SETTINGS,
4927
+ mutations_sync: "1"
4928
+ }
4849
4929
  });
4850
- await batchCreateFeedback(client, { feedbacks: [updated] });
4851
- if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) {
4852
- await deleteFeedback(client, {
4853
- feedbackIds: [feedbackId],
4854
- organizationId: existingRow.organizationId ?? void 0,
4855
- resourceId: existingRow.resourceId ?? void 0
4856
- }, replication);
4857
- throw feedbackNotFoundError(feedbackId);
4858
- }
4859
- return updated;
4930
+ if (strategy !== null) await client.command({
4931
+ query: `INSERT INTO ${TABLE_FEEDBACK_EVENTS_DELTA}
4932
+ SELECT ${buildDeltaCursorExpr(strategy, "mastra_feedback_events_delta_cursor", "feedbackId")} AS cursorId,
4933
+ ingestedAt, traceId, timestamp, feedbackId
4934
+ FROM (
4935
+ SELECT now64(9, 'UTC') AS ingestedAt, traceId, timestamp, feedbackId
4936
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE ${identity}
4937
+ )`,
4938
+ query_params: params,
4939
+ clickhouse_settings: {
4940
+ ...CH_INSERT_SETTINGS,
4941
+ async_insert: 0
4942
+ }
4943
+ });
4944
+ if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
4945
+ const current = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE ${identity} LIMIT 1`, params);
4946
+ return current[0]?.reviewStatus === reviewStatus ? rowToFeedbackRecord(current[0]) : null;
4860
4947
  }
4861
4948
  async function listFeedback(client, args, strategy) {
4862
4949
  const parsed = listFeedbackArgsSchema.parse(args);
@@ -6205,14 +6292,16 @@ async function batchCreateScores(client, args) {
6205
6292
  * `organizationId` and `resourceId` values are ANDed into the predicate to
6206
6293
  * restrict deletion to records with matching scope fields.
6207
6294
  *
6208
- * A durable deletion request is recorded before the lightweight delete. The
6209
- * delete is immediately visible to subsequent reads; physical purge depends on
6210
- * the table's configured retention TTL. The delta table is intentionally not
6295
+ * A durable deletion request is recorded before the lightweight delete and
6296
+ * marked applied once the delete succeeds. If the delete fails, the request
6297
+ * stays unapplied; retry by calling this function again. The delete is
6298
+ * immediately visible to subsequent reads; physical purge depends on the
6299
+ * table's configured retention TTL. The delta table is intentionally not
6211
6300
  * touched and expires through its fixed two-day TTL.
6212
6301
  */
6213
6302
  async function deleteScores(client, args, replication) {
6214
6303
  if (args.scoreIds.length === 0) return;
6215
- await recordDeletionRequest(client, {
6304
+ const request = await recordDeletionRequest(client, {
6216
6305
  requestId: randomUUID(),
6217
6306
  organizationId: args.organizationId,
6218
6307
  resourceId: args.resourceId,
@@ -6244,6 +6333,7 @@ async function deleteScores(client, args, replication) {
6244
6333
  query_params: params,
6245
6334
  clickhouse_settings
6246
6335
  });
6336
+ await markDeletionRequestApplied(client, request, replication);
6247
6337
  }
6248
6338
  async function listScores(client, args, strategy) {
6249
6339
  const parsed = listScoresArgsSchema.parse(args);
@@ -7709,7 +7799,9 @@ async function getTraceLight(client, args) {
7709
7799
  * so span deletes never propagate to it. Delta tables self-expire via TTL and
7710
7800
  * discovery tables self-heal, so neither needs explicit deletes.
7711
7801
  *
7712
- * Records the predicate before using lightweight DELETE FROM on every table.
7802
+ * Records the predicate before using lightweight DELETE FROM on every table
7803
+ * and marks the request applied once every delete succeeds. If any delete
7804
+ * fails, the request stays unapplied; retry by calling this function again.
7713
7805
  * Lightweight deletes hide rows through ClickHouse's delete mask; physical
7714
7806
  * removal depends on the deployment's configured retention and merge policy.
7715
7807
  *
@@ -7718,7 +7810,7 @@ async function getTraceLight(client, args) {
7718
7810
  */
7719
7811
  async function batchDeleteTraces(client, args, replication) {
7720
7812
  if (args.traceIds.length === 0) return;
7721
- await recordDeletionRequest(client, {
7813
+ const request = await recordDeletionRequest(client, {
7722
7814
  requestId: randomUUID(),
7723
7815
  organizationId: args.organizationId,
7724
7816
  resourceId: args.resourceId,
@@ -7771,6 +7863,7 @@ async function batchDeleteTraces(client, args, replication) {
7771
7863
  query_params: params,
7772
7864
  clickhouse_settings: { lightweight_deletes_sync: "2" }
7773
7865
  }))]);
7866
+ await markDeletionRequestApplied(client, request, replication);
7774
7867
  }
7775
7868
  /**
7776
7869
  * List trace branches with optional filtering, pagination, and ordering.
@@ -8903,7 +8996,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
8903
8996
  }
8904
8997
  async updateFeedbackReviewStatus(args) {
8905
8998
  try {
8906
- return await updateFeedbackReviewStatus(this.#client, args, this.#replication);
8999
+ return await updateFeedbackReviewStatus(this.#client, args, deltaPollingSupported(this.#deltaCursorStrategy) ? this.#deltaCursorStrategy : null);
8907
9000
  } catch (error) {
8908
9001
  if (error instanceof MastraError) throw error;
8909
9002
  throw new MastraError({
@@ -10042,6 +10135,6 @@ var ClickhouseStoreVNext = class extends ClickhouseStore {
10042
10135
  }
10043
10136
  };
10044
10137
  //#endregion
10045
- export { COLUMN_TYPES, ClickhouseStore, ClickhouseStoreVNext, MemoryStorageClickhouse, ObservabilityStorageClickhouse, ObservabilityStorageClickhouseVNext, ScoresStorageClickhouse, TABLE_DELETION_REQUESTS, TABLE_ENGINES, WorkflowsStorageClickhouse, applyClickHouseRetention, recordDeletionRequest };
10138
+ export { COLUMN_TYPES, ClickhouseStore, ClickhouseStoreVNext, MemoryStorageClickhouse, ObservabilityStorageClickhouse, ObservabilityStorageClickhouseVNext, ScoresStorageClickhouse, TABLE_DELETION_REQUESTS, TABLE_ENGINES, WorkflowsStorageClickhouse, applyClickHouseRetention, markDeletionRequestApplied, recordDeletionRequest };
10046
10139
 
10047
10140
  //# sourceMappingURL=index.js.map