@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.
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import * as coreStorage from "@mastra/core/storage";
4
4
  import { BRANCH_SPAN_TYPES, EntityType, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, ObservabilityStorage, SCORERS_SCHEMA, SPAN_SCHEMA, ScoresStorage, TABLE_AGENT_VERSIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_CURSORS, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_TRACES, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowsStorage, buildInputPreview, calculatePagination, computeTraceStatus, createStorageErrorId, getDefaultValue, getSqlType, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, normalizePerPage, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
5
5
  import { MessageList } from "@mastra/core/agent";
6
6
  import { MastraBase } from "@mastra/core/base";
7
+ import { randomUUID } from "crypto";
7
8
  import { parseFieldKey } from "@mastra/core/utils";
8
9
  import { coreFeatures } from "@mastra/core/features";
9
- import { randomUUID } from "crypto";
10
10
  import { saveScorePayloadSchema } from "@mastra/core/evals";
11
11
  //#region src/storage/db/replication.ts
12
12
  const DEFAULT_ZOOKEEPER_PATH = "/clickhouse/tables/{shard}/{database}/{table}";
@@ -3183,6 +3183,7 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
3183
3183
 
3184
3184
  -- IDs
3185
3185
  feedbackId String,
3186
+ writeVersion UInt64 DEFAULT 0,
3186
3187
  traceId Nullable(String),
3187
3188
  spanId Nullable(String),
3188
3189
  experimentId Nullable(String),
@@ -3470,6 +3471,7 @@ const ALL_MIGRATIONS = [
3470
3471
  addColumn(TABLE_SCORE_EVENTS, "entityVersionId", "Nullable(String)"),
3471
3472
  addColumn(TABLE_SCORE_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3472
3473
  addColumn(TABLE_SCORE_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3474
+ addColumn(TABLE_FEEDBACK_EVENTS, "writeVersion", "UInt64 DEFAULT 0"),
3473
3475
  addColumn(TABLE_FEEDBACK_EVENTS, "entityVersionId", "Nullable(String)"),
3474
3476
  addColumn(TABLE_FEEDBACK_EVENTS, "reviewStatus", "LowCardinality(String) DEFAULT 'needs-review'"),
3475
3477
  addColumn(TABLE_FEEDBACK_EVENTS, "parentEntityVersionId", "Nullable(String)"),
@@ -4633,6 +4635,26 @@ async function queryJson$2(client, query, params) {
4633
4635
  clickhouse_settings: CH_SETTINGS
4634
4636
  })).json();
4635
4637
  }
4638
+ async function feedbackRowsWithWriteVersions(client, feedbacks) {
4639
+ const identifiedFeedback = feedbacks.map((feedback) => ({
4640
+ ...feedback,
4641
+ feedbackId: feedback.feedbackId ?? ""
4642
+ }));
4643
+ const feedbackIds = [...new Set(identifiedFeedback.map((feedback) => feedback.feedbackId))];
4644
+ const existingVersions = await queryJson$2(client, `SELECT feedbackId, toString(max(writeVersion)) AS writeVersion
4645
+ FROM ${TABLE_FEEDBACK_EVENTS}
4646
+ WHERE feedbackId IN ({feedbackIds:Array(String)})
4647
+ GROUP BY feedbackId`, { feedbackIds });
4648
+ const versions = new Map(existingVersions.map((row) => [row.feedbackId, BigInt(row.writeVersion)]));
4649
+ return identifiedFeedback.map((feedback) => {
4650
+ const writeVersion = (versions.get(feedback.feedbackId) ?? 0n) + 1n;
4651
+ versions.set(feedback.feedbackId, writeVersion);
4652
+ return {
4653
+ ...feedbackRecordToRow(feedback),
4654
+ writeVersion: writeVersion.toString()
4655
+ };
4656
+ });
4657
+ }
4636
4658
  async function createFeedback(client, args) {
4637
4659
  await batchCreateFeedback(client, { feedbacks: [args.feedback] });
4638
4660
  }
@@ -4640,31 +4662,98 @@ async function batchCreateFeedback(client, args) {
4640
4662
  if (args.feedbacks.length === 0) return;
4641
4663
  await client.insert({
4642
4664
  table: TABLE_FEEDBACK_EVENTS,
4643
- values: args.feedbacks.map(feedbackRecordToRow),
4665
+ values: await feedbackRowsWithWriteVersions(client, args.feedbacks),
4644
4666
  format: "JSONEachRow",
4645
4667
  clickhouse_settings: CH_INSERT_SETTINGS
4646
4668
  });
4647
4669
  }
4648
- async function updateFeedbackReviewStatus(client, args) {
4649
- const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4650
- const existing = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE feedbackId = {feedbackId:String} LIMIT 1`, { feedbackId });
4651
- if (!existing[0]) throw new MastraError({
4670
+ /**
4671
+ * Delete feedback events by feedbackId via lightweight DELETE. Optional
4672
+ * `organizationId` and `resourceId` values are ANDed into the predicate to
4673
+ * restrict deletion to records with matching scope fields.
4674
+ *
4675
+ * A durable deletion request is recorded before the lightweight delete. The
4676
+ * delete is immediately visible to subsequent reads; physical purge depends on
4677
+ * the table's configured retention TTL. The delta table is intentionally not
4678
+ * touched and expires through its fixed two-day TTL.
4679
+ */
4680
+ async function deleteFeedback(client, args, replication) {
4681
+ if (args.feedbackIds.length === 0) return;
4682
+ await recordDeletionRequest(client, {
4683
+ requestId: randomUUID(),
4684
+ organizationId: args.organizationId,
4685
+ resourceId: args.resourceId,
4686
+ signal: "feedback",
4687
+ predicateType: "itemIds",
4688
+ predicateValues: [...args.feedbackIds],
4689
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
4690
+ replication
4691
+ });
4692
+ const params = {};
4693
+ const idPlaceholders = [];
4694
+ for (let i = 0; i < args.feedbackIds.length; i++) {
4695
+ const name = `fid_${i}`;
4696
+ params[name] = args.feedbackIds[i];
4697
+ idPlaceholders.push(`{${name}:String}`);
4698
+ }
4699
+ const conditions = [`feedbackId IN (${idPlaceholders.join(", ")})`];
4700
+ if (args.organizationId !== void 0) {
4701
+ conditions.push("organizationId = {delOrganizationId:String}");
4702
+ params.delOrganizationId = args.organizationId;
4703
+ }
4704
+ if (args.resourceId !== void 0) {
4705
+ conditions.push("resourceId = {delResourceId:String}");
4706
+ params.delResourceId = args.resourceId;
4707
+ }
4708
+ await client.command({
4709
+ query: `DELETE FROM ${TABLE_FEEDBACK_EVENTS} WHERE ${conditions.join(" AND ")}`,
4710
+ query_params: params,
4711
+ clickhouse_settings: { lightweight_deletes_sync: isReplicationConfigured(replication) ? "2" : "1" }
4712
+ });
4713
+ }
4714
+ function feedbackNotFoundError(feedbackId) {
4715
+ return new MastraError({
4652
4716
  id: "OBSERVABILITY_UPDATE_FEEDBACK_REVIEW_STATUS_NOT_FOUND",
4653
4717
  domain: ErrorDomain.MASTRA_OBSERVABILITY,
4654
4718
  category: ErrorCategory.USER,
4655
4719
  text: "Feedback record not found",
4656
4720
  details: { feedbackId }
4657
4721
  });
4722
+ }
4723
+ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId) {
4724
+ return (await queryJson$2(client, `SELECT 1 AS found FROM ${TABLE_DELETION_REQUESTS} FINAL
4725
+ WHERE signal = 'feedback'
4726
+ AND predicateType = 'itemIds'
4727
+ AND has(predicateValues, {feedbackId:String})
4728
+ AND (organizationId = '' OR organizationId = {organizationId:String})
4729
+ AND (resourceId = '' OR resourceId = {resourceId:String})
4730
+ LIMIT 1`, {
4731
+ feedbackId,
4732
+ organizationId: organizationId ?? "",
4733
+ resourceId: resourceId ?? ""
4734
+ })).length > 0;
4735
+ }
4736
+ async function updateFeedbackReviewStatus(client, args, replication) {
4737
+ const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4738
+ const existingRow = (await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4739
+ WHERE feedbackId = {feedbackId:String}
4740
+ ORDER BY writeVersion DESC, timestamp DESC
4741
+ LIMIT 1`, { feedbackId }))[0];
4742
+ if (!existingRow) throw feedbackNotFoundError(feedbackId);
4743
+ if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
4658
4744
  const updated = rowToFeedbackRecord({
4659
- ...existing[0],
4745
+ ...existingRow,
4660
4746
  reviewStatus
4661
4747
  });
4662
- await client.insert({
4663
- table: TABLE_FEEDBACK_EVENTS,
4664
- values: [feedbackRecordToRow(updated)],
4665
- format: "JSONEachRow",
4666
- clickhouse_settings: CH_INSERT_SETTINGS
4667
- });
4748
+ await batchCreateFeedback(client, { feedbacks: [updated] });
4749
+ if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) {
4750
+ await deleteFeedback(client, {
4751
+ feedbackIds: [feedbackId],
4752
+ organizationId: existingRow.organizationId ?? void 0,
4753
+ resourceId: existingRow.resourceId ?? void 0
4754
+ }, replication);
4755
+ throw feedbackNotFoundError(feedbackId);
4756
+ }
4668
4757
  return updated;
4669
4758
  }
4670
4759
  async function listFeedback(client, args, strategy) {
@@ -5954,6 +6043,50 @@ async function batchCreateScores(client, args) {
5954
6043
  clickhouse_settings: CH_INSERT_SETTINGS
5955
6044
  });
5956
6045
  }
6046
+ /**
6047
+ * Delete score events by scoreId via lightweight DELETE. Optional
6048
+ * `organizationId` and `resourceId` values are ANDed into the predicate to
6049
+ * restrict deletion to records with matching scope fields.
6050
+ *
6051
+ * A durable deletion request is recorded before the lightweight delete. The
6052
+ * delete is immediately visible to subsequent reads; physical purge depends on
6053
+ * the table's configured retention TTL. The delta table is intentionally not
6054
+ * touched and expires through its fixed two-day TTL.
6055
+ */
6056
+ async function deleteScores(client, args, replication) {
6057
+ if (args.scoreIds.length === 0) return;
6058
+ await recordDeletionRequest(client, {
6059
+ requestId: randomUUID(),
6060
+ organizationId: args.organizationId,
6061
+ resourceId: args.resourceId,
6062
+ signal: "scores",
6063
+ predicateType: "itemIds",
6064
+ predicateValues: [...args.scoreIds],
6065
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
6066
+ replication
6067
+ });
6068
+ const params = {};
6069
+ const idPlaceholders = [];
6070
+ for (let i = 0; i < args.scoreIds.length; i++) {
6071
+ const name = `sid_${i}`;
6072
+ params[name] = args.scoreIds[i];
6073
+ idPlaceholders.push(`{${name}:String}`);
6074
+ }
6075
+ const conditions = [`scoreId IN (${idPlaceholders.join(", ")})`];
6076
+ if (args.organizationId !== void 0) {
6077
+ conditions.push("organizationId = {delOrganizationId:String}");
6078
+ params.delOrganizationId = args.organizationId;
6079
+ }
6080
+ if (args.resourceId !== void 0) {
6081
+ conditions.push("resourceId = {delResourceId:String}");
6082
+ params.delResourceId = args.resourceId;
6083
+ }
6084
+ await client.command({
6085
+ query: `DELETE FROM ${TABLE_SCORE_EVENTS} WHERE ${conditions.join(" AND ")}`,
6086
+ query_params: params,
6087
+ clickhouse_settings: { lightweight_deletes_sync: isReplicationConfigured(replication) ? "2" : "1" }
6088
+ });
6089
+ }
5957
6090
  async function listScores(client, args, strategy) {
5958
6091
  const parsed = listScoresArgsSchema.parse(args);
5959
6092
  const deltaCursorEnabled = deltaPollingSupported(strategy);
@@ -6324,6 +6457,44 @@ const SCORE_FIELDS = {
6324
6457
  parameterType: "String"
6325
6458
  }
6326
6459
  };
6460
+ const FEEDBACK_FIELDS = {
6461
+ feedbackType: {
6462
+ sql: "s.feedbackType",
6463
+ parameterType: "String"
6464
+ },
6465
+ feedbackSource: {
6466
+ sql: "s.feedbackSource",
6467
+ parameterType: "String"
6468
+ },
6469
+ feedbackUserId: {
6470
+ sql: "s.feedbackUserId",
6471
+ parameterType: "String"
6472
+ },
6473
+ sourceId: {
6474
+ sql: "s.sourceId",
6475
+ parameterType: "String"
6476
+ },
6477
+ entityVersionId: {
6478
+ sql: "s.entityVersionId",
6479
+ parameterType: "String"
6480
+ },
6481
+ parentEntityVersionId: {
6482
+ sql: "s.parentEntityVersionId",
6483
+ parameterType: "String"
6484
+ },
6485
+ rootEntityVersionId: {
6486
+ sql: "s.rootEntityVersionId",
6487
+ parameterType: "String"
6488
+ },
6489
+ timestamp: {
6490
+ sql: "s.timestamp",
6491
+ parameterType: "DateTime64(3, 'UTC')"
6492
+ },
6493
+ comment: {
6494
+ sql: "s.comment",
6495
+ parameterType: "String"
6496
+ }
6497
+ };
6327
6498
  const TRACE_SELECT = `
6328
6499
  r.traceId AS traceId,
6329
6500
  r.spanId AS rootSpanId,
@@ -6384,6 +6555,22 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
6384
6555
  if (operator === void 0) throw new Error(`Unsupported trusted trace-query operator: ${predicate.operator}`);
6385
6556
  return `ifNull(${field.sql} ${operator} ${parameter}, ${predicate.operator === "ne" ? "1" : "0"})`;
6386
6557
  }
6558
+ function compileFeedbackScalarPredicate(predicate, parameters) {
6559
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileFeedbackScalarPredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
6560
+ if (predicate.type === "not") return `NOT (${compileFeedbackScalarPredicate(predicate.arg, parameters)})`;
6561
+ if (predicate.field !== "value") return compileScalarPredicate(predicate, FEEDBACK_FIELDS, parameters);
6562
+ if (predicate.type === "presence") {
6563
+ const present = `(isNotNull(s.valueString) OR isNotNull(s.valueNumber))`;
6564
+ return predicate.operator === "exists" ? present : `NOT ${present}`;
6565
+ }
6566
+ return compileScalarPredicate(predicate, typeof (predicate.type === "membership" ? predicate.values[0] : predicate.value) === "number" ? { value: {
6567
+ sql: "s.valueNumber",
6568
+ parameterType: "Float64"
6569
+ } } : { value: {
6570
+ sql: "s.valueString",
6571
+ parameterType: "String"
6572
+ } }, parameters);
6573
+ }
6387
6574
  function collectRelationCollections(predicate, collections = /* @__PURE__ */ new Set()) {
6388
6575
  if (!predicate) return collections;
6389
6576
  if (predicate.type === "relation") collections.add(predicate.collection);
@@ -6393,11 +6580,11 @@ function collectRelationCollections(predicate, collections = /* @__PURE__ */ new
6393
6580
  }
6394
6581
  function compilePredicate(predicate, parameters) {
6395
6582
  if (predicate.type === "relation") {
6396
- const registry = predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS;
6397
6583
  const existence = `EXISTS (
6398
- SELECT 1 FROM ${predicate.collection === "spans" ? "current_spans" : "current_scores"} s
6399
- WHERE s.traceId = r.traceId
6400
- AND (${compileScalarPredicate(predicate.predicate, registry, parameters)})
6584
+ SELECT 1 FROM ${predicate.collection === "spans" ? "current_spans" : predicate.collection === "scores" ? "current_scores" : "current_feedback"} s
6585
+ WHERE isNotNull(s.traceId)
6586
+ AND s.traceId = r.traceId
6587
+ AND (${predicate.collection === "feedback" ? compileFeedbackScalarPredicate(predicate.predicate, parameters) : compileScalarPredicate(predicate.predicate, predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS, parameters)})
6401
6588
  )`;
6402
6589
  return predicate.quantifier === "some" ? existence : `NOT ${existence}`;
6403
6590
  }
@@ -6466,6 +6653,29 @@ function compileClickHouseTraceQuery(plan) {
6466
6653
  AND traceId IN (SELECT traceId FROM root_scope)
6467
6654
  ORDER BY scoreId, timestamp DESC
6468
6655
  LIMIT 1 BY scoreId
6656
+ )`);
6657
+ if (relationCollections.has("feedback")) ctes.push(`current_feedback AS (
6658
+ SELECT
6659
+ traceId,
6660
+ feedbackType,
6661
+ feedbackSource,
6662
+ feedbackUserId,
6663
+ sourceId,
6664
+ valueString,
6665
+ valueNumber,
6666
+ comment,
6667
+ timestamp,
6668
+ entityVersionId,
6669
+ parentEntityVersionId,
6670
+ rootEntityVersionId
6671
+ FROM (
6672
+ SELECT *
6673
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL
6674
+ ORDER BY feedbackId, writeVersion DESC, timestamp DESC
6675
+ LIMIT 1 BY feedbackId
6676
+ ) AS current
6677
+ WHERE isNotNull(traceId)
6678
+ AND traceId IN (SELECT traceId FROM root_scope)
6469
6679
  )`);
6470
6680
  const predicate = plan.where ? compilePredicate(plan.where, parameters) : "1";
6471
6681
  ctes.push(`candidates AS (
@@ -7925,6 +8135,19 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
7925
8135
  }, error);
7926
8136
  }
7927
8137
  }
8138
+ async deleteScores(args) {
8139
+ try {
8140
+ await deleteScores(this.#client, args, this.#replication);
8141
+ } catch (error) {
8142
+ if (error instanceof MastraError) throw error;
8143
+ throw new MastraError({
8144
+ id: createStorageErrorId("CLICKHOUSE", "DELETE_SCORES", "FAILED"),
8145
+ domain: ErrorDomain.STORAGE,
8146
+ category: ErrorCategory.THIRD_PARTY,
8147
+ details: { count: args.scoreIds.length }
8148
+ }, error);
8149
+ }
8150
+ }
7928
8151
  async getScoreById(scoreId) {
7929
8152
  try {
7930
8153
  return await getScoreById(this.#client, scoreId);
@@ -7963,9 +8186,22 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
7963
8186
  }, error);
7964
8187
  }
7965
8188
  }
8189
+ async deleteFeedback(args) {
8190
+ try {
8191
+ await deleteFeedback(this.#client, args, this.#replication);
8192
+ } catch (error) {
8193
+ if (error instanceof MastraError) throw error;
8194
+ throw new MastraError({
8195
+ id: createStorageErrorId("CLICKHOUSE", "DELETE_FEEDBACK", "FAILED"),
8196
+ domain: ErrorDomain.STORAGE,
8197
+ category: ErrorCategory.THIRD_PARTY,
8198
+ details: { count: args.feedbackIds.length }
8199
+ }, error);
8200
+ }
8201
+ }
7966
8202
  async updateFeedbackReviewStatus(args) {
7967
8203
  try {
7968
- return await updateFeedbackReviewStatus(this.#client, args);
8204
+ return await updateFeedbackReviewStatus(this.#client, args, this.#replication);
7969
8205
  } catch (error) {
7970
8206
  if (error instanceof MastraError) throw error;
7971
8207
  throw new MastraError({