@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/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/integrations-databases-clickhouse.md +14 -1
- package/dist/index.cjs +121 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +121 -28
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/deletion-requests.d.ts +9 -0
- package/dist/storage/domains/observability/v-next/deletion-requests.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/feedback.d.ts +8 -4
- package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/scores.d.ts +5 -3
- package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts +3 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/docs/SKILL.md
CHANGED
|
@@ -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.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## When to use
|
|
@@ -93,6 +93,10 @@ 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.
|
|
99
|
+
|
|
96
100
|
### Observability with the legacy domain
|
|
97
101
|
|
|
98
102
|
`ObservabilityStorageClickhouse` is the original observability adapter and remains supported for projects that haven't migrated to the vNext schema. The configuration shape is the same as the vNext class.
|
|
@@ -368,7 +372,16 @@ const observability = new ObservabilityStorageClickhouseVNext({
|
|
|
368
372
|
await observability.init()
|
|
369
373
|
```
|
|
370
374
|
|
|
371
|
-
In CI/CD pipelines, set `disableInit: true` on `ClickhouseStore` and run `init()` from a deployment step that uses elevated credentials. Runtime application credentials
|
|
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:
|
|
376
|
+
|
|
377
|
+
- `SELECT` and `INSERT` on the Mastra tables.
|
|
378
|
+
- `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
|
+
```
|
|
372
385
|
|
|
373
386
|
## Observability
|
|
374
387
|
|
package/dist/index.cjs
CHANGED
|
@@ -4295,17 +4295,46 @@ async function recordDeletionRequest(client, args) {
|
|
|
4295
4295
|
purgeVerifiedAt: EPOCH,
|
|
4296
4296
|
updatedAt: args.requestedAt
|
|
4297
4297
|
};
|
|
4298
|
+
await insertDeletionRequest(client, row, args.replication);
|
|
4299
|
+
return row;
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Marks a recorded deletion request as applied after its lightweight DELETEs
|
|
4303
|
+
* succeeded. The table is `ReplacingMergeTree(updatedAt)`, so re-inserting the
|
|
4304
|
+
* row with a newer `updatedAt` supersedes the pending version on merge and
|
|
4305
|
+
* under `FINAL`. Requests whose DELETE failed keep `lastAppliedAt` at the
|
|
4306
|
+
* epoch; mutation guards ignore them, and re-invoking the delete API records a
|
|
4307
|
+
* new request and converges.
|
|
4308
|
+
*/
|
|
4309
|
+
async function markDeletionRequestApplied(client, row, replication) {
|
|
4310
|
+
const pendingAt = Date.parse(row.updatedAt);
|
|
4311
|
+
const appliedAt = new Date(Number.isFinite(pendingAt) ? Math.max(Date.now(), pendingAt + 1) : Date.now()).toISOString();
|
|
4312
|
+
const applied = {
|
|
4313
|
+
...row,
|
|
4314
|
+
lastAppliedAt: appliedAt,
|
|
4315
|
+
updatedAt: appliedAt
|
|
4316
|
+
};
|
|
4317
|
+
await insertDeletionRequest(client, applied, replication);
|
|
4318
|
+
return applied;
|
|
4319
|
+
}
|
|
4320
|
+
/**
|
|
4321
|
+
* Replicated clusters insert with parallel quorum, the same as every other
|
|
4322
|
+
* deletion-request write on main, so concurrent deletes never reject each other.
|
|
4323
|
+
* The guard that reads these rows is advisory: review updates mutate feedback
|
|
4324
|
+
* in place and preserve the delete mask, so a replica that has not yet received
|
|
4325
|
+
* a marker can only mis-report a status, never bring deleted feedback back.
|
|
4326
|
+
*/
|
|
4327
|
+
async function insertDeletionRequest(client, row, replication) {
|
|
4298
4328
|
await client.insert({
|
|
4299
4329
|
table: TABLE_DELETION_REQUESTS,
|
|
4300
4330
|
values: [row],
|
|
4301
4331
|
format: "JSONEachRow",
|
|
4302
|
-
clickhouse_settings: isReplicationConfigured(
|
|
4332
|
+
clickhouse_settings: isReplicationConfigured(replication) ? {
|
|
4303
4333
|
...CH_INSERT_SETTINGS,
|
|
4304
4334
|
insert_quorum: "auto",
|
|
4305
4335
|
insert_quorum_parallel: 1
|
|
4306
4336
|
} : CH_INSERT_SETTINGS
|
|
4307
4337
|
});
|
|
4308
|
-
return row;
|
|
4309
4338
|
}
|
|
4310
4339
|
//#endregion
|
|
4311
4340
|
//#region src/storage/domains/observability/v-next/discovery.ts
|
|
@@ -4797,14 +4826,18 @@ async function batchCreateFeedback(client, args) {
|
|
|
4797
4826
|
* `organizationId` and `resourceId` values are ANDed into the predicate to
|
|
4798
4827
|
* restrict deletion to records with matching scope fields.
|
|
4799
4828
|
*
|
|
4800
|
-
* A durable deletion request is recorded before the lightweight delete
|
|
4801
|
-
*
|
|
4802
|
-
*
|
|
4829
|
+
* A durable deletion request is recorded before the lightweight delete and
|
|
4830
|
+
* marked applied once the delete succeeds. If the delete fails, the request
|
|
4831
|
+
* stays unapplied and does not block updates to the still-visible rows; retry
|
|
4832
|
+
* by calling this function again.
|
|
4833
|
+
*
|
|
4834
|
+
* The delete is immediately visible to subsequent reads; physical purge depends
|
|
4835
|
+
* on the table's configured retention TTL. The delta table is intentionally not
|
|
4803
4836
|
* touched and expires through its fixed two-day TTL.
|
|
4804
4837
|
*/
|
|
4805
4838
|
async function deleteFeedback(client, args, replication) {
|
|
4806
4839
|
if (args.feedbackIds.length === 0) return;
|
|
4807
|
-
await recordDeletionRequest(client, {
|
|
4840
|
+
const request = await recordDeletionRequest(client, {
|
|
4808
4841
|
requestId: (0, crypto$1.randomUUID)(),
|
|
4809
4842
|
organizationId: args.organizationId,
|
|
4810
4843
|
resourceId: args.resourceId,
|
|
@@ -4835,6 +4868,7 @@ async function deleteFeedback(client, args, replication) {
|
|
|
4835
4868
|
query_params: params,
|
|
4836
4869
|
clickhouse_settings: { lightweight_deletes_sync: isReplicationConfigured(replication) ? "2" : "1" }
|
|
4837
4870
|
});
|
|
4871
|
+
await markDeletionRequestApplied(client, request, replication);
|
|
4838
4872
|
}
|
|
4839
4873
|
function feedbackNotFoundError(feedbackId) {
|
|
4840
4874
|
return new _mastra_core_error.MastraError({
|
|
@@ -4845,11 +4879,29 @@ function feedbackNotFoundError(feedbackId) {
|
|
|
4845
4879
|
details: { feedbackId }
|
|
4846
4880
|
});
|
|
4847
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
|
+
/**
|
|
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.
|
|
4898
|
+
*/
|
|
4848
4899
|
async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, resourceId) {
|
|
4849
4900
|
return (await queryJson$2(client, `SELECT 1 AS found FROM ${TABLE_DELETION_REQUESTS} FINAL
|
|
4850
4901
|
WHERE signal = 'feedback'
|
|
4851
4902
|
AND predicateType = 'itemIds'
|
|
4852
4903
|
AND has(predicateValues, {feedbackId:String})
|
|
4904
|
+
AND lastAppliedAt > toDateTime64(0, 3)
|
|
4853
4905
|
AND (organizationId = '' OR organizationId = {organizationId:String})
|
|
4854
4906
|
AND (resourceId = '' OR resourceId = {resourceId:String})
|
|
4855
4907
|
LIMIT 1`, {
|
|
@@ -4858,28 +4910,63 @@ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, re
|
|
|
4858
4910
|
resourceId: resourceId ?? ""
|
|
4859
4911
|
})).length > 0;
|
|
4860
4912
|
}
|
|
4861
|
-
async function updateFeedbackReviewStatus(client, args,
|
|
4913
|
+
async function updateFeedbackReviewStatus(client, args, strategy = null) {
|
|
4862
4914
|
const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
|
|
4863
|
-
|
|
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
|
+
/**
|
|
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.
|
|
4926
|
+
*/
|
|
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
|
|
4864
4929
|
WHERE feedbackId = {feedbackId:String}
|
|
4865
4930
|
ORDER BY writeVersion DESC, timestamp DESC
|
|
4866
4931
|
LIMIT 1`, { feedbackId }))[0];
|
|
4867
4932
|
if (!existingRow) throw feedbackNotFoundError(feedbackId);
|
|
4868
4933
|
if (await hasFeedbackDeletionRequest(client, feedbackId, existingRow.organizationId, existingRow.resourceId)) throw feedbackNotFoundError(feedbackId);
|
|
4869
|
-
const
|
|
4870
|
-
|
|
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,
|
|
4871
4943
|
reviewStatus
|
|
4944
|
+
};
|
|
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
|
+
}
|
|
4872
4952
|
});
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
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
|
+
}
|
|
4966
|
+
});
|
|
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;
|
|
4883
4970
|
}
|
|
4884
4971
|
async function listFeedback(client, args, strategy) {
|
|
4885
4972
|
const parsed = _mastra_core_storage.listFeedbackArgsSchema.parse(args);
|
|
@@ -6228,14 +6315,16 @@ async function batchCreateScores(client, args) {
|
|
|
6228
6315
|
* `organizationId` and `resourceId` values are ANDed into the predicate to
|
|
6229
6316
|
* restrict deletion to records with matching scope fields.
|
|
6230
6317
|
*
|
|
6231
|
-
* A durable deletion request is recorded before the lightweight delete
|
|
6232
|
-
*
|
|
6233
|
-
*
|
|
6318
|
+
* A durable deletion request is recorded before the lightweight delete and
|
|
6319
|
+
* marked applied once the delete succeeds. If the delete fails, the request
|
|
6320
|
+
* stays unapplied; retry by calling this function again. The delete is
|
|
6321
|
+
* immediately visible to subsequent reads; physical purge depends on the
|
|
6322
|
+
* table's configured retention TTL. The delta table is intentionally not
|
|
6234
6323
|
* touched and expires through its fixed two-day TTL.
|
|
6235
6324
|
*/
|
|
6236
6325
|
async function deleteScores(client, args, replication) {
|
|
6237
6326
|
if (args.scoreIds.length === 0) return;
|
|
6238
|
-
await recordDeletionRequest(client, {
|
|
6327
|
+
const request = await recordDeletionRequest(client, {
|
|
6239
6328
|
requestId: (0, crypto$1.randomUUID)(),
|
|
6240
6329
|
organizationId: args.organizationId,
|
|
6241
6330
|
resourceId: args.resourceId,
|
|
@@ -6267,6 +6356,7 @@ async function deleteScores(client, args, replication) {
|
|
|
6267
6356
|
query_params: params,
|
|
6268
6357
|
clickhouse_settings
|
|
6269
6358
|
});
|
|
6359
|
+
await markDeletionRequestApplied(client, request, replication);
|
|
6270
6360
|
}
|
|
6271
6361
|
async function listScores(client, args, strategy) {
|
|
6272
6362
|
const parsed = _mastra_core_storage.listScoresArgsSchema.parse(args);
|
|
@@ -7732,7 +7822,9 @@ async function getTraceLight(client, args) {
|
|
|
7732
7822
|
* so span deletes never propagate to it. Delta tables self-expire via TTL and
|
|
7733
7823
|
* discovery tables self-heal, so neither needs explicit deletes.
|
|
7734
7824
|
*
|
|
7735
|
-
* Records the predicate before using lightweight DELETE FROM on every table
|
|
7825
|
+
* Records the predicate before using lightweight DELETE FROM on every table
|
|
7826
|
+
* and marks the request applied once every delete succeeds. If any delete
|
|
7827
|
+
* fails, the request stays unapplied; retry by calling this function again.
|
|
7736
7828
|
* Lightweight deletes hide rows through ClickHouse's delete mask; physical
|
|
7737
7829
|
* removal depends on the deployment's configured retention and merge policy.
|
|
7738
7830
|
*
|
|
@@ -7741,7 +7833,7 @@ async function getTraceLight(client, args) {
|
|
|
7741
7833
|
*/
|
|
7742
7834
|
async function batchDeleteTraces(client, args, replication) {
|
|
7743
7835
|
if (args.traceIds.length === 0) return;
|
|
7744
|
-
await recordDeletionRequest(client, {
|
|
7836
|
+
const request = await recordDeletionRequest(client, {
|
|
7745
7837
|
requestId: (0, crypto$1.randomUUID)(),
|
|
7746
7838
|
organizationId: args.organizationId,
|
|
7747
7839
|
resourceId: args.resourceId,
|
|
@@ -7794,6 +7886,7 @@ async function batchDeleteTraces(client, args, replication) {
|
|
|
7794
7886
|
query_params: params,
|
|
7795
7887
|
clickhouse_settings: { lightweight_deletes_sync: "2" }
|
|
7796
7888
|
}))]);
|
|
7889
|
+
await markDeletionRequestApplied(client, request, replication);
|
|
7797
7890
|
}
|
|
7798
7891
|
/**
|
|
7799
7892
|
* List trace branches with optional filtering, pagination, and ordering.
|
|
@@ -8926,7 +9019,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
|
|
|
8926
9019
|
}
|
|
8927
9020
|
async updateFeedbackReviewStatus(args) {
|
|
8928
9021
|
try {
|
|
8929
|
-
return await updateFeedbackReviewStatus(this.#client, args, this.#
|
|
9022
|
+
return await updateFeedbackReviewStatus(this.#client, args, deltaPollingSupported(this.#deltaCursorStrategy) ? this.#deltaCursorStrategy : null);
|
|
8930
9023
|
} catch (error) {
|
|
8931
9024
|
if (error instanceof _mastra_core_error.MastraError) throw error;
|
|
8932
9025
|
throw new _mastra_core_error.MastraError({
|
|
@@ -10076,6 +10169,7 @@ exports.TABLE_DELETION_REQUESTS = TABLE_DELETION_REQUESTS;
|
|
|
10076
10169
|
exports.TABLE_ENGINES = TABLE_ENGINES;
|
|
10077
10170
|
exports.WorkflowsStorageClickhouse = WorkflowsStorageClickhouse;
|
|
10078
10171
|
exports.applyClickHouseRetention = applyClickHouseRetention;
|
|
10172
|
+
exports.markDeletionRequestApplied = markDeletionRequestApplied;
|
|
10079
10173
|
exports.recordDeletionRequest = recordDeletionRequest;
|
|
10080
10174
|
|
|
10081
10175
|
//# sourceMappingURL=index.cjs.map
|