@mastra/clickhouse 1.21.0 → 1.21.1-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.
@@ -3,7 +3,7 @@ name: mastra-clickhouse
3
3
  description: Documentation for @mastra/clickhouse. Use when working with @mastra/clickhouse APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/clickhouse"
6
- version: "1.21.0"
6
+ version: "1.21.1-alpha.0"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.21.0",
2
+ "version": "1.21.1-alpha.0",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -93,9 +93,7 @@ Lightweight deletion is a hide-only operation that marks rows with ClickHouse's
93
93
 
94
94
  When all five observability signals have finite retention, Mastra also applies a TTL to deletion requests so they outlive the signal rows they protect. If any signal is unbounded, deletion requests remain unbounded. See [storage retention](https://mastra.ai/reference/storage/retention) for how the deletion-request TTL is calculated.
95
95
 
96
- Feedback review-status updates require `ALTER UPDATE` permission on `mastra_feedback_events` and, when delta polling is enabled, `INSERT` permission on `mastra_feedback_events_delta`. These updates wait for the ClickHouse mutation to finish on the server that receives the write, so latency depends on that server's mutation queue. Other replicas apply the mutation through the replication log, so a reader on a lagging replica can briefly see the previous status, and an inactive replica doesn't block the update. They modify the existing feedback row and preserve deletion masks: a concurrent review update cannot recreate deleted feedback. Successful updates remain available through delta polling. If a newer version of the same feedback event is ingested while a review update is in flight, the update is re-applied to that newer version; after repeated conflicts it fails with a conflict error (HTTP 409) and the caller retries.
97
-
98
- The status mutation and the delta insert are separate operations. If the delta insert fails, the API returns an error even though the status may have changed, and continued delta polling doesn't recover that notification: later polls from the same cursor never return it, and starting delta mode without a cursor subscribes at the current head. To recover, retry the review update after resolving the error, or reread the feedback with a regular list query.
96
+ Feedback review-status updates read the current row and insert a replacement, so they need `SELECT` and `INSERT` rather than `ALTER UPDATE`, and the insert materialized view publishes each update to delta polling. An update that re-runs a delete also needs the delete permissions listed in [initialization](#initialization). A review update that lands while a delete of the same feedback is in progress re-runs that delete and returns not found, so it can't recreate deleted feedback. A failed delete doesn't block review updates, but the next update retries it: if the retry succeeds, the update returns not found; if it fails again, the new status is saved but the update returns the delete's error. If a network error or a lagging replica lets a review row outlive a successful delete, the next review update of that feedback deletes it again and returns not found.
99
97
 
100
98
  ### Observability with the legacy domain
101
99
 
@@ -372,16 +370,10 @@ const observability = new ObservabilityStorageClickhouseVNext({
372
370
  await observability.init()
373
371
  ```
374
372
 
375
- In CI/CD pipelines, set `disableInit: true` on `ClickhouseStore` and run `init()` from a deployment step that uses elevated credentials. Runtime application credentials still need more than read and insert:
373
+ In CI/CD pipelines, set `disableInit: true` on `ClickhouseStore` and run `init()` from a deployment step that uses elevated credentials. Runtime application credentials need:
376
374
 
377
375
  - `SELECT` and `INSERT` on the Mastra tables.
378
376
  - `ALTER DELETE` on the observability tables you delete from. On ClickHouse 26.6 and earlier, lightweight deletes also require `ALTER UPDATE` on those tables; ClickHouse 26.7 removed that requirement.
379
- - For feedback review updates, `ALTER UPDATE(reviewStatus)` on `mastra_feedback_events` and `INSERT` on `mastra_feedback_events_delta`.
380
-
381
- ```sql
382
- GRANT ALTER UPDATE(reviewStatus) ON <database>.mastra_feedback_events TO <runtime_user>;
383
- GRANT INSERT ON <database>.mastra_feedback_events_delta TO <runtime_user>;
384
- ```
385
377
 
386
378
  ## Observability
387
379
 
package/dist/index.cjs CHANGED
@@ -4879,29 +4879,16 @@ function feedbackNotFoundError(feedbackId) {
4879
4879
  details: { feedbackId }
4880
4880
  });
4881
4881
  }
4882
- function feedbackConflictError(feedbackId) {
4883
- return new _mastra_core_error.MastraError({
4884
- id: "OBSERVABILITY_UPDATE_FEEDBACK_REVIEW_STATUS_CONFLICT",
4885
- domain: _mastra_core_error.ErrorDomain.MASTRA_OBSERVABILITY,
4886
- category: _mastra_core_error.ErrorCategory.USER,
4887
- text: "Feedback record changed while its review status was being updated; retry the update",
4888
- details: { feedbackId }
4889
- });
4890
- }
4891
- /** Re-reads before giving up when ingestion keeps superseding the observed row. */
4892
- const REVIEW_UPDATE_ATTEMPTS = 3;
4893
4882
  /**
4894
- * Only applied requests block a review update. The guard decides the reply,
4895
- * not data safety: the update mutates the row in place and preserves the
4896
- * delete mask, so a replica that has not received a marker yet can only
4897
- * mis-report a status, never bring deleted feedback back.
4883
+ * Finds a deletion request covering this feedback. With `appliedOnly`, only
4884
+ * requests whose DELETE succeeded count; otherwise pending requests count too,
4885
+ * including a delete that is still running or one that failed.
4898
4886
  */
4899
- async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId) {
4887
+ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId, { appliedOnly }) {
4900
4888
  return (await queryJson$2(client, `SELECT 1 AS found FROM ${TABLE_DELETION_REQUESTS} FINAL
4901
4889
  WHERE signal = 'feedback'
4902
4890
  AND predicateType = 'itemIds'
4903
- AND has(predicateValues, {feedbackId:String})
4904
- AND lastAppliedAt > toDateTime64(0, 3)
4891
+ AND has(predicateValues, {feedbackId:String})${appliedOnly ? "\n AND lastAppliedAt > toDateTime64(0, 3)" : ""}
4905
4892
  AND (organizationId = '' OR organizationId = {organizationId:String})
4906
4893
  AND (resourceId = '' OR resourceId = {resourceId:String})
4907
4894
  LIMIT 1`, {
@@ -4910,63 +4897,44 @@ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, re
4910
4897
  resourceId: resourceId ?? ""
4911
4898
  })).length > 0;
4912
4899
  }
4913
- async function updateFeedbackReviewStatus(client, args, strategy = null) {
4914
- const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4915
- for (let attempt = 1;; attempt++) {
4916
- const updated = await applyReviewStatus(client, feedbackId, reviewStatus, strategy);
4917
- if (updated) return updated;
4918
- if (attempt >= REVIEW_UPDATE_ATTEMPTS) throw feedbackConflictError(feedbackId);
4919
- }
4920
- }
4921
4900
  /**
4922
- * One read-guard-mutate-verify pass. Returns `null` when the read-back does not
4923
- * show the new status: a newer version superseded the observed row, a delete
4924
- * hid it after the guard read (the next pass then reports not found), or the
4925
- * mutation left the row untouched.
4901
+ * Review updates insert a replacement row, so they need only `INSERT` and the
4902
+ * insert materialized view publishes the delta cursor.
4903
+ *
4904
+ * A replacement written after a concurrent DELETE is not covered by that
4905
+ * DELETE. The post-write check therefore looks for any request, pending or
4906
+ * applied, and re-runs the delete when it finds one. Only applied requests
4907
+ * block the update up front, so feedback whose delete failed stays editable.
4908
+ *
4909
+ * If the post-write check reads a replica that has not received the request
4910
+ * yet, or the check or its delete fails, the replacement stays visible. A row
4911
+ * that is still visible under an applied request can only be such a leftover,
4912
+ * so the next update deletes it again.
4926
4913
  */
4927
- async function applyReviewStatus(client, feedbackId, reviewStatus, strategy) {
4928
- const existingRow = (await queryJson$2(client, `SELECT *, toString(writeVersion) AS reviewWriteVersion FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4914
+ async function updateFeedbackReviewStatus(client, args, replication) {
4915
+ const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4916
+ const existingRow = (await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4929
4917
  WHERE feedbackId = {feedbackId:String}
4930
4918
  ORDER BY writeVersion DESC, timestamp DESC
4931
4919
  LIMIT 1`, { feedbackId }))[0];
4932
4920
  if (!existingRow) throw feedbackNotFoundError(feedbackId);
4933
- if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
4934
- const identity = `feedbackId = {feedbackId:String}
4935
- AND timestamp = parseDateTime64BestEffort({timestamp:String}, 3, 'UTC')
4936
- AND (traceId = {traceId:Nullable(String)} OR (isNull(traceId) AND isNull({traceId:Nullable(String)})))
4937
- AND writeVersion = {writeVersion:UInt64}`;
4938
- const params = {
4939
- feedbackId,
4940
- timestamp: existingRow.timestamp,
4941
- traceId: existingRow.traceId,
4942
- writeVersion: existingRow.reviewWriteVersion,
4943
- reviewStatus
4921
+ const { organizationId, resourceId } = existingRow;
4922
+ const redelete = async () => {
4923
+ await deleteFeedback(client, {
4924
+ feedbackIds: [feedbackId],
4925
+ organizationId: organizationId ?? void 0,
4926
+ resourceId: resourceId ?? void 0
4927
+ }, replication);
4928
+ throw feedbackNotFoundError(feedbackId);
4944
4929
  };
4945
- await client.command({
4946
- query: `ALTER TABLE ${TABLE_FEEDBACK_EVENTS} UPDATE reviewStatus = {reviewStatus:String} WHERE ${identity}`,
4947
- query_params: params,
4948
- clickhouse_settings: {
4949
- ...CH_SETTINGS,
4950
- mutations_sync: "1"
4951
- }
4952
- });
4953
- if (strategy !== null) await client.command({
4954
- query: `INSERT INTO ${TABLE_FEEDBACK_EVENTS_DELTA}
4955
- SELECT ${buildDeltaCursorExpr(strategy, "mastra_feedback_events_delta_cursor", "feedbackId")} AS cursorId,
4956
- ingestedAt, traceId, timestamp, feedbackId
4957
- FROM (
4958
- SELECT now64(9, 'UTC') AS ingestedAt, traceId, timestamp, feedbackId
4959
- FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE ${identity}
4960
- )`,
4961
- query_params: params,
4962
- clickhouse_settings: {
4963
- ...CH_INSERT_SETTINGS,
4964
- async_insert: 0
4965
- }
4930
+ if (await hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId, { appliedOnly: true })) return redelete();
4931
+ const updated = rowToFeedbackRecord({
4932
+ ...existingRow,
4933
+ reviewStatus
4966
4934
  });
4967
- if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
4968
- const current = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE ${identity} LIMIT 1`, params);
4969
- return current[0]?.reviewStatus === reviewStatus ? rowToFeedbackRecord(current[0]) : null;
4935
+ await batchCreateFeedback(client, { feedbacks: [updated] });
4936
+ if (await hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId, { appliedOnly: false })) return redelete();
4937
+ return updated;
4970
4938
  }
4971
4939
  async function listFeedback(client, args, strategy) {
4972
4940
  const parsed = _mastra_core_storage.listFeedbackArgsSchema.parse(args);
@@ -8665,21 +8633,35 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8665
8633
  if (!deltaPollingSupported(this.#deltaCursorStrategy)) return [
8666
8634
  "metrics",
8667
8635
  "logs",
8636
+ "entity-type-discovery",
8637
+ "entity-name-discovery",
8638
+ "service-name-discovery",
8639
+ "environment-discovery",
8640
+ "tag-discovery",
8641
+ "metric-discovery",
8668
8642
  "trace-query",
8669
8643
  "trace-query-root-duration",
8670
8644
  "trace-query-discovery",
8671
8645
  "thread-query",
8672
- "trace-query-tenant-scope"
8646
+ "trace-query-tenant-scope",
8647
+ "feedback"
8673
8648
  ];
8674
8649
  return [
8675
8650
  "metrics",
8676
8651
  "logs",
8652
+ "entity-type-discovery",
8653
+ "entity-name-discovery",
8654
+ "service-name-discovery",
8655
+ "environment-discovery",
8656
+ "tag-discovery",
8657
+ "metric-discovery",
8677
8658
  "delta-polling",
8678
8659
  "trace-query",
8679
8660
  "trace-query-root-duration",
8680
8661
  "trace-query-discovery",
8681
8662
  "thread-query",
8682
- "trace-query-tenant-scope"
8663
+ "trace-query-tenant-scope",
8664
+ "feedback"
8683
8665
  ];
8684
8666
  }
8685
8667
  async createSpan(args) {
@@ -9019,7 +9001,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
9019
9001
  }
9020
9002
  async updateFeedbackReviewStatus(args) {
9021
9003
  try {
9022
- return await updateFeedbackReviewStatus(this.#client, args, deltaPollingSupported(this.#deltaCursorStrategy) ? this.#deltaCursorStrategy : null);
9004
+ return await updateFeedbackReviewStatus(this.#client, args, this.#replication);
9023
9005
  } catch (error) {
9024
9006
  if (error instanceof _mastra_core_error.MastraError) throw error;
9025
9007
  throw new _mastra_core_error.MastraError({