@mastra/clickhouse 1.18.0-alpha.0 → 1.18.0-alpha.2

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.18.0-alpha.0"
6
+ version: "1.18.0-alpha.2"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.18.0-alpha.0",
2
+ "version": "1.18.0-alpha.2",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -27,9 +27,9 @@ let _mastra_core_storage = require("@mastra/core/storage");
27
27
  _mastra_core_storage = __toESM(_mastra_core_storage, 1);
28
28
  let _mastra_core_agent = require("@mastra/core/agent");
29
29
  let _mastra_core_base = require("@mastra/core/base");
30
+ let crypto$1 = require("crypto");
30
31
  let _mastra_core_utils = require("@mastra/core/utils");
31
32
  let _mastra_core_features = require("@mastra/core/features");
32
- let crypto$1 = require("crypto");
33
33
  let _mastra_core_evals = require("@mastra/core/evals");
34
34
  //#region src/storage/db/replication.ts
35
35
  const DEFAULT_ZOOKEEPER_PATH = "/clickhouse/tables/{shard}/{database}/{table}";
@@ -3206,6 +3206,7 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
3206
3206
 
3207
3207
  -- IDs
3208
3208
  feedbackId String,
3209
+ writeVersion UInt64 DEFAULT 0,
3209
3210
  traceId Nullable(String),
3210
3211
  spanId Nullable(String),
3211
3212
  experimentId Nullable(String),
@@ -3493,6 +3494,7 @@ const ALL_MIGRATIONS = [
3493
3494
  addColumn(TABLE_SCORE_EVENTS, "entityVersionId", "Nullable(String)"),
3494
3495
  addColumn(TABLE_SCORE_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3495
3496
  addColumn(TABLE_SCORE_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3497
+ addColumn(TABLE_FEEDBACK_EVENTS, "writeVersion", "UInt64 DEFAULT 0"),
3496
3498
  addColumn(TABLE_FEEDBACK_EVENTS, "entityVersionId", "Nullable(String)"),
3497
3499
  addColumn(TABLE_FEEDBACK_EVENTS, "reviewStatus", "LowCardinality(String) DEFAULT 'needs-review'"),
3498
3500
  addColumn(TABLE_FEEDBACK_EVENTS, "parentEntityVersionId", "Nullable(String)"),
@@ -4656,6 +4658,26 @@ async function queryJson$2(client, query, params) {
4656
4658
  clickhouse_settings: CH_SETTINGS
4657
4659
  })).json();
4658
4660
  }
4661
+ async function feedbackRowsWithWriteVersions(client, feedbacks) {
4662
+ const identifiedFeedback = feedbacks.map((feedback) => ({
4663
+ ...feedback,
4664
+ feedbackId: feedback.feedbackId ?? ""
4665
+ }));
4666
+ const feedbackIds = [...new Set(identifiedFeedback.map((feedback) => feedback.feedbackId))];
4667
+ const existingVersions = await queryJson$2(client, `SELECT feedbackId, toString(max(writeVersion)) AS writeVersion
4668
+ FROM ${TABLE_FEEDBACK_EVENTS}
4669
+ WHERE feedbackId IN ({feedbackIds:Array(String)})
4670
+ GROUP BY feedbackId`, { feedbackIds });
4671
+ const versions = new Map(existingVersions.map((row) => [row.feedbackId, BigInt(row.writeVersion)]));
4672
+ return identifiedFeedback.map((feedback) => {
4673
+ const writeVersion = (versions.get(feedback.feedbackId) ?? 0n) + 1n;
4674
+ versions.set(feedback.feedbackId, writeVersion);
4675
+ return {
4676
+ ...feedbackRecordToRow(feedback),
4677
+ writeVersion: writeVersion.toString()
4678
+ };
4679
+ });
4680
+ }
4659
4681
  async function createFeedback(client, args) {
4660
4682
  await batchCreateFeedback(client, { feedbacks: [args.feedback] });
4661
4683
  }
@@ -4663,31 +4685,98 @@ async function batchCreateFeedback(client, args) {
4663
4685
  if (args.feedbacks.length === 0) return;
4664
4686
  await client.insert({
4665
4687
  table: TABLE_FEEDBACK_EVENTS,
4666
- values: args.feedbacks.map(feedbackRecordToRow),
4688
+ values: await feedbackRowsWithWriteVersions(client, args.feedbacks),
4667
4689
  format: "JSONEachRow",
4668
4690
  clickhouse_settings: CH_INSERT_SETTINGS
4669
4691
  });
4670
4692
  }
4671
- async function updateFeedbackReviewStatus(client, args) {
4672
- const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4673
- const existing = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE feedbackId = {feedbackId:String} LIMIT 1`, { feedbackId });
4674
- if (!existing[0]) throw new _mastra_core_error.MastraError({
4693
+ /**
4694
+ * Delete feedback events by feedbackId via lightweight DELETE. Optional
4695
+ * `organizationId` and `resourceId` values are ANDed into the predicate to
4696
+ * restrict deletion to records with matching scope fields.
4697
+ *
4698
+ * A durable deletion request is recorded before the lightweight delete. The
4699
+ * delete is immediately visible to subsequent reads; physical purge depends on
4700
+ * the table's configured retention TTL. The delta table is intentionally not
4701
+ * touched and expires through its fixed two-day TTL.
4702
+ */
4703
+ async function deleteFeedback(client, args, replication) {
4704
+ if (args.feedbackIds.length === 0) return;
4705
+ await recordDeletionRequest(client, {
4706
+ requestId: (0, crypto$1.randomUUID)(),
4707
+ organizationId: args.organizationId,
4708
+ resourceId: args.resourceId,
4709
+ signal: "feedback",
4710
+ predicateType: "itemIds",
4711
+ predicateValues: [...args.feedbackIds],
4712
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
4713
+ replication
4714
+ });
4715
+ const params = {};
4716
+ const idPlaceholders = [];
4717
+ for (let i = 0; i < args.feedbackIds.length; i++) {
4718
+ const name = `fid_${i}`;
4719
+ params[name] = args.feedbackIds[i];
4720
+ idPlaceholders.push(`{${name}:String}`);
4721
+ }
4722
+ const conditions = [`feedbackId IN (${idPlaceholders.join(", ")})`];
4723
+ if (args.organizationId !== void 0) {
4724
+ conditions.push("organizationId = {delOrganizationId:String}");
4725
+ params.delOrganizationId = args.organizationId;
4726
+ }
4727
+ if (args.resourceId !== void 0) {
4728
+ conditions.push("resourceId = {delResourceId:String}");
4729
+ params.delResourceId = args.resourceId;
4730
+ }
4731
+ await client.command({
4732
+ query: `DELETE FROM ${TABLE_FEEDBACK_EVENTS} WHERE ${conditions.join(" AND ")}`,
4733
+ query_params: params,
4734
+ clickhouse_settings: { lightweight_deletes_sync: isReplicationConfigured(replication) ? "2" : "1" }
4735
+ });
4736
+ }
4737
+ function feedbackNotFoundError(feedbackId) {
4738
+ return new _mastra_core_error.MastraError({
4675
4739
  id: "OBSERVABILITY_UPDATE_FEEDBACK_REVIEW_STATUS_NOT_FOUND",
4676
4740
  domain: _mastra_core_error.ErrorDomain.MASTRA_OBSERVABILITY,
4677
4741
  category: _mastra_core_error.ErrorCategory.USER,
4678
4742
  text: "Feedback record not found",
4679
4743
  details: { feedbackId }
4680
4744
  });
4745
+ }
4746
+ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId) {
4747
+ return (await queryJson$2(client, `SELECT 1 AS found FROM ${TABLE_DELETION_REQUESTS} FINAL
4748
+ WHERE signal = 'feedback'
4749
+ AND predicateType = 'itemIds'
4750
+ AND has(predicateValues, {feedbackId:String})
4751
+ AND (organizationId = '' OR organizationId = {organizationId:String})
4752
+ AND (resourceId = '' OR resourceId = {resourceId:String})
4753
+ LIMIT 1`, {
4754
+ feedbackId,
4755
+ organizationId: organizationId ?? "",
4756
+ resourceId: resourceId ?? ""
4757
+ })).length > 0;
4758
+ }
4759
+ async function updateFeedbackReviewStatus(client, args, replication) {
4760
+ const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4761
+ const existingRow = (await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4762
+ WHERE feedbackId = {feedbackId:String}
4763
+ ORDER BY writeVersion DESC, timestamp DESC
4764
+ LIMIT 1`, { feedbackId }))[0];
4765
+ if (!existingRow) throw feedbackNotFoundError(feedbackId);
4766
+ if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
4681
4767
  const updated = rowToFeedbackRecord({
4682
- ...existing[0],
4768
+ ...existingRow,
4683
4769
  reviewStatus
4684
4770
  });
4685
- await client.insert({
4686
- table: TABLE_FEEDBACK_EVENTS,
4687
- values: [feedbackRecordToRow(updated)],
4688
- format: "JSONEachRow",
4689
- clickhouse_settings: CH_INSERT_SETTINGS
4690
- });
4771
+ await batchCreateFeedback(client, { feedbacks: [updated] });
4772
+ if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) {
4773
+ await deleteFeedback(client, {
4774
+ feedbackIds: [feedbackId],
4775
+ organizationId: existingRow.organizationId ?? void 0,
4776
+ resourceId: existingRow.resourceId ?? void 0
4777
+ }, replication);
4778
+ throw feedbackNotFoundError(feedbackId);
4779
+ }
4691
4780
  return updated;
4692
4781
  }
4693
4782
  async function listFeedback(client, args, strategy) {
@@ -5977,6 +6066,50 @@ async function batchCreateScores(client, args) {
5977
6066
  clickhouse_settings: CH_INSERT_SETTINGS
5978
6067
  });
5979
6068
  }
6069
+ /**
6070
+ * Delete score events by scoreId via lightweight DELETE. Optional
6071
+ * `organizationId` and `resourceId` values are ANDed into the predicate to
6072
+ * restrict deletion to records with matching scope fields.
6073
+ *
6074
+ * A durable deletion request is recorded before the lightweight delete. The
6075
+ * delete is immediately visible to subsequent reads; physical purge depends on
6076
+ * the table's configured retention TTL. The delta table is intentionally not
6077
+ * touched and expires through its fixed two-day TTL.
6078
+ */
6079
+ async function deleteScores(client, args, replication) {
6080
+ if (args.scoreIds.length === 0) return;
6081
+ await recordDeletionRequest(client, {
6082
+ requestId: (0, crypto$1.randomUUID)(),
6083
+ organizationId: args.organizationId,
6084
+ resourceId: args.resourceId,
6085
+ signal: "scores",
6086
+ predicateType: "itemIds",
6087
+ predicateValues: [...args.scoreIds],
6088
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
6089
+ replication
6090
+ });
6091
+ const params = {};
6092
+ const idPlaceholders = [];
6093
+ for (let i = 0; i < args.scoreIds.length; i++) {
6094
+ const name = `sid_${i}`;
6095
+ params[name] = args.scoreIds[i];
6096
+ idPlaceholders.push(`{${name}:String}`);
6097
+ }
6098
+ const conditions = [`scoreId IN (${idPlaceholders.join(", ")})`];
6099
+ if (args.organizationId !== void 0) {
6100
+ conditions.push("organizationId = {delOrganizationId:String}");
6101
+ params.delOrganizationId = args.organizationId;
6102
+ }
6103
+ if (args.resourceId !== void 0) {
6104
+ conditions.push("resourceId = {delResourceId:String}");
6105
+ params.delResourceId = args.resourceId;
6106
+ }
6107
+ await client.command({
6108
+ query: `DELETE FROM ${TABLE_SCORE_EVENTS} WHERE ${conditions.join(" AND ")}`,
6109
+ query_params: params,
6110
+ clickhouse_settings: { lightweight_deletes_sync: isReplicationConfigured(replication) ? "2" : "1" }
6111
+ });
6112
+ }
5980
6113
  async function listScores(client, args, strategy) {
5981
6114
  const parsed = _mastra_core_storage.listScoresArgsSchema.parse(args);
5982
6115
  const deltaCursorEnabled = deltaPollingSupported(strategy);
@@ -6347,6 +6480,44 @@ const SCORE_FIELDS = {
6347
6480
  parameterType: "String"
6348
6481
  }
6349
6482
  };
6483
+ const FEEDBACK_FIELDS = {
6484
+ feedbackType: {
6485
+ sql: "s.feedbackType",
6486
+ parameterType: "String"
6487
+ },
6488
+ feedbackSource: {
6489
+ sql: "s.feedbackSource",
6490
+ parameterType: "String"
6491
+ },
6492
+ feedbackUserId: {
6493
+ sql: "s.feedbackUserId",
6494
+ parameterType: "String"
6495
+ },
6496
+ sourceId: {
6497
+ sql: "s.sourceId",
6498
+ parameterType: "String"
6499
+ },
6500
+ entityVersionId: {
6501
+ sql: "s.entityVersionId",
6502
+ parameterType: "String"
6503
+ },
6504
+ parentEntityVersionId: {
6505
+ sql: "s.parentEntityVersionId",
6506
+ parameterType: "String"
6507
+ },
6508
+ rootEntityVersionId: {
6509
+ sql: "s.rootEntityVersionId",
6510
+ parameterType: "String"
6511
+ },
6512
+ timestamp: {
6513
+ sql: "s.timestamp",
6514
+ parameterType: "DateTime64(3, 'UTC')"
6515
+ },
6516
+ comment: {
6517
+ sql: "s.comment",
6518
+ parameterType: "String"
6519
+ }
6520
+ };
6350
6521
  const TRACE_SELECT = `
6351
6522
  r.traceId AS traceId,
6352
6523
  r.spanId AS rootSpanId,
@@ -6407,6 +6578,22 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
6407
6578
  if (operator === void 0) throw new Error(`Unsupported trusted trace-query operator: ${predicate.operator}`);
6408
6579
  return `ifNull(${field.sql} ${operator} ${parameter}, ${predicate.operator === "ne" ? "1" : "0"})`;
6409
6580
  }
6581
+ function compileFeedbackScalarPredicate(predicate, parameters) {
6582
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileFeedbackScalarPredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
6583
+ if (predicate.type === "not") return `NOT (${compileFeedbackScalarPredicate(predicate.arg, parameters)})`;
6584
+ if (predicate.field !== "value") return compileScalarPredicate(predicate, FEEDBACK_FIELDS, parameters);
6585
+ if (predicate.type === "presence") {
6586
+ const present = `(isNotNull(s.valueString) OR isNotNull(s.valueNumber))`;
6587
+ return predicate.operator === "exists" ? present : `NOT ${present}`;
6588
+ }
6589
+ return compileScalarPredicate(predicate, typeof (predicate.type === "membership" ? predicate.values[0] : predicate.value) === "number" ? { value: {
6590
+ sql: "s.valueNumber",
6591
+ parameterType: "Float64"
6592
+ } } : { value: {
6593
+ sql: "s.valueString",
6594
+ parameterType: "String"
6595
+ } }, parameters);
6596
+ }
6410
6597
  function collectRelationCollections(predicate, collections = /* @__PURE__ */ new Set()) {
6411
6598
  if (!predicate) return collections;
6412
6599
  if (predicate.type === "relation") collections.add(predicate.collection);
@@ -6416,11 +6603,11 @@ function collectRelationCollections(predicate, collections = /* @__PURE__ */ new
6416
6603
  }
6417
6604
  function compilePredicate(predicate, parameters) {
6418
6605
  if (predicate.type === "relation") {
6419
- const registry = predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS;
6420
6606
  const existence = `EXISTS (
6421
- SELECT 1 FROM ${predicate.collection === "spans" ? "current_spans" : "current_scores"} s
6422
- WHERE s.traceId = r.traceId
6423
- AND (${compileScalarPredicate(predicate.predicate, registry, parameters)})
6607
+ SELECT 1 FROM ${predicate.collection === "spans" ? "current_spans" : predicate.collection === "scores" ? "current_scores" : "current_feedback"} s
6608
+ WHERE isNotNull(s.traceId)
6609
+ AND s.traceId = r.traceId
6610
+ AND (${predicate.collection === "feedback" ? compileFeedbackScalarPredicate(predicate.predicate, parameters) : compileScalarPredicate(predicate.predicate, predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS, parameters)})
6424
6611
  )`;
6425
6612
  return predicate.quantifier === "some" ? existence : `NOT ${existence}`;
6426
6613
  }
@@ -6489,6 +6676,29 @@ function compileClickHouseTraceQuery(plan) {
6489
6676
  AND traceId IN (SELECT traceId FROM root_scope)
6490
6677
  ORDER BY scoreId, timestamp DESC
6491
6678
  LIMIT 1 BY scoreId
6679
+ )`);
6680
+ if (relationCollections.has("feedback")) ctes.push(`current_feedback AS (
6681
+ SELECT
6682
+ traceId,
6683
+ feedbackType,
6684
+ feedbackSource,
6685
+ feedbackUserId,
6686
+ sourceId,
6687
+ valueString,
6688
+ valueNumber,
6689
+ comment,
6690
+ timestamp,
6691
+ entityVersionId,
6692
+ parentEntityVersionId,
6693
+ rootEntityVersionId
6694
+ FROM (
6695
+ SELECT *
6696
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL
6697
+ ORDER BY feedbackId, writeVersion DESC, timestamp DESC
6698
+ LIMIT 1 BY feedbackId
6699
+ ) AS current
6700
+ WHERE isNotNull(traceId)
6701
+ AND traceId IN (SELECT traceId FROM root_scope)
6492
6702
  )`);
6493
6703
  const predicate = plan.where ? compilePredicate(plan.where, parameters) : "1";
6494
6704
  ctes.push(`candidates AS (
@@ -7948,6 +8158,19 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
7948
8158
  }, error);
7949
8159
  }
7950
8160
  }
8161
+ async deleteScores(args) {
8162
+ try {
8163
+ await deleteScores(this.#client, args, this.#replication);
8164
+ } catch (error) {
8165
+ if (error instanceof _mastra_core_error.MastraError) throw error;
8166
+ throw new _mastra_core_error.MastraError({
8167
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "DELETE_SCORES", "FAILED"),
8168
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
8169
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
8170
+ details: { count: args.scoreIds.length }
8171
+ }, error);
8172
+ }
8173
+ }
7951
8174
  async getScoreById(scoreId) {
7952
8175
  try {
7953
8176
  return await getScoreById(this.#client, scoreId);
@@ -7986,9 +8209,22 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
7986
8209
  }, error);
7987
8210
  }
7988
8211
  }
8212
+ async deleteFeedback(args) {
8213
+ try {
8214
+ await deleteFeedback(this.#client, args, this.#replication);
8215
+ } catch (error) {
8216
+ if (error instanceof _mastra_core_error.MastraError) throw error;
8217
+ throw new _mastra_core_error.MastraError({
8218
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "DELETE_FEEDBACK", "FAILED"),
8219
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
8220
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
8221
+ details: { count: args.feedbackIds.length }
8222
+ }, error);
8223
+ }
8224
+ }
7989
8225
  async updateFeedbackReviewStatus(args) {
7990
8226
  try {
7991
- return await updateFeedbackReviewStatus(this.#client, args);
8227
+ return await updateFeedbackReviewStatus(this.#client, args, this.#replication);
7992
8228
  } catch (error) {
7993
8229
  if (error instanceof _mastra_core_error.MastraError) throw error;
7994
8230
  throw new _mastra_core_error.MastraError({