@mastra/clickhouse 0.11.1-alpha.1 → 0.12.0-alpha.3
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +25 -0
- package/dist/_tsup-dts-rollup.d.cts +8 -4
- package/dist/_tsup-dts-rollup.d.ts +8 -4
- package/dist/index.cjs +58 -2
- package/dist/index.js +58 -2
- package/package.json +4 -4
- package/src/storage/index.test.ts +78 -0
- package/src/storage/index.ts +77 -5
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/clickhouse@0.
|
|
2
|
+
> @mastra/clickhouse@0.12.0-alpha.3 build /home/runner/work/mastra/mastra/stores/clickhouse
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.5.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 11027ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
14
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 12403ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
21
|
-
[32mESM[39m ⚡️ Build success in
|
|
22
|
-
[32mCJS[39m [1mdist/index.cjs [22m[
|
|
23
|
-
[32mCJS[39m ⚡️ Build success in
|
|
20
|
+
[32mESM[39m [1mdist/index.js [22m[32m38.52 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 986ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m39.29 KB[39m
|
|
23
|
+
[32mCJS[39m ⚡️ Build success in 988ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @mastra/clickhouse
|
|
2
2
|
|
|
3
|
+
## 0.12.0-alpha.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8a3bfd2: Update peerdeps to latest core
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [792c4c0]
|
|
12
|
+
- Updated dependencies [502fe05]
|
|
13
|
+
- Updated dependencies [4efcfa0]
|
|
14
|
+
- @mastra/core@0.10.7-alpha.3
|
|
15
|
+
|
|
16
|
+
## 0.11.1-alpha.2
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 15e9d26: Added per-resource working memory for LibSQL, Upstash, and PG
|
|
21
|
+
- 0fb9d64: [MASTRA-4018] Update saveMessages in storage adapters to upsert messages
|
|
22
|
+
- Updated dependencies [15e9d26]
|
|
23
|
+
- Updated dependencies [07d6d88]
|
|
24
|
+
- Updated dependencies [5d74aab]
|
|
25
|
+
- Updated dependencies [144eb0b]
|
|
26
|
+
- @mastra/core@0.10.7-alpha.2
|
|
27
|
+
|
|
3
28
|
## 0.11.1-alpha.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -10,6 +10,7 @@ import type { StorageGetMessagesArg } from '@mastra/core/storage';
|
|
|
10
10
|
import type { StorageGetTracesArg } from '@mastra/core/storage';
|
|
11
11
|
import type { StorageThreadType } from '@mastra/core/memory';
|
|
12
12
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
13
|
+
import type { TABLE_RESOURCES } from '@mastra/core/storage';
|
|
13
14
|
import { TABLE_SCHEMAS } from '@mastra/core/storage';
|
|
14
15
|
import type { Trace } from '@mastra/core/telemetry';
|
|
15
16
|
import type { WorkflowRun } from '@mastra/core/storage';
|
|
@@ -45,6 +46,7 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
45
46
|
protected ttl: ClickhouseConfig['ttl'];
|
|
46
47
|
constructor(config: ClickhouseConfig);
|
|
47
48
|
private transformEvalRow;
|
|
49
|
+
private escape;
|
|
48
50
|
getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]>;
|
|
49
51
|
batchInsert({ tableName, records }: {
|
|
50
52
|
tableName: TABLE_NAMES;
|
|
@@ -67,7 +69,7 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
67
69
|
tableName: TABLE_NAMES;
|
|
68
70
|
}): Promise<void>;
|
|
69
71
|
createTable({ tableName, schema, }: {
|
|
70
|
-
tableName:
|
|
72
|
+
tableName: SUPPORTED_TABLE_NAMES;
|
|
71
73
|
schema: Record<string, StorageColumn>;
|
|
72
74
|
}): Promise<void>;
|
|
73
75
|
protected getSqlType(type: StorageColumn['type']): string;
|
|
@@ -89,8 +91,8 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
89
91
|
tableName: TABLE_NAMES;
|
|
90
92
|
record: Record<string, any>;
|
|
91
93
|
}): Promise<void>;
|
|
92
|
-
load<R>({ tableName, keys }: {
|
|
93
|
-
tableName:
|
|
94
|
+
load<R>({ tableName, keys, }: {
|
|
95
|
+
tableName: SUPPORTED_TABLE_NAMES;
|
|
94
96
|
keys: Record<string, string>;
|
|
95
97
|
}): Promise<R | null>;
|
|
96
98
|
getThreadById({ threadId }: {
|
|
@@ -180,7 +182,9 @@ export { COLUMN_TYPES as COLUMN_TYPES_alias_1 }
|
|
|
180
182
|
|
|
181
183
|
declare type IntervalUnit = 'NANOSECOND' | 'MICROSECOND' | 'MILLISECOND' | 'SECOND' | 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR';
|
|
182
184
|
|
|
183
|
-
declare
|
|
185
|
+
declare type SUPPORTED_TABLE_NAMES = Exclude<TABLE_NAMES, typeof TABLE_RESOURCES>;
|
|
186
|
+
|
|
187
|
+
declare const TABLE_ENGINES: Record<SUPPORTED_TABLE_NAMES, string>;
|
|
184
188
|
export { TABLE_ENGINES }
|
|
185
189
|
export { TABLE_ENGINES as TABLE_ENGINES_alias_1 }
|
|
186
190
|
|
|
@@ -10,6 +10,7 @@ import type { StorageGetMessagesArg } from '@mastra/core/storage';
|
|
|
10
10
|
import type { StorageGetTracesArg } from '@mastra/core/storage';
|
|
11
11
|
import type { StorageThreadType } from '@mastra/core/memory';
|
|
12
12
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
13
|
+
import type { TABLE_RESOURCES } from '@mastra/core/storage';
|
|
13
14
|
import { TABLE_SCHEMAS } from '@mastra/core/storage';
|
|
14
15
|
import type { Trace } from '@mastra/core/telemetry';
|
|
15
16
|
import type { WorkflowRun } from '@mastra/core/storage';
|
|
@@ -45,6 +46,7 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
45
46
|
protected ttl: ClickhouseConfig['ttl'];
|
|
46
47
|
constructor(config: ClickhouseConfig);
|
|
47
48
|
private transformEvalRow;
|
|
49
|
+
private escape;
|
|
48
50
|
getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]>;
|
|
49
51
|
batchInsert({ tableName, records }: {
|
|
50
52
|
tableName: TABLE_NAMES;
|
|
@@ -67,7 +69,7 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
67
69
|
tableName: TABLE_NAMES;
|
|
68
70
|
}): Promise<void>;
|
|
69
71
|
createTable({ tableName, schema, }: {
|
|
70
|
-
tableName:
|
|
72
|
+
tableName: SUPPORTED_TABLE_NAMES;
|
|
71
73
|
schema: Record<string, StorageColumn>;
|
|
72
74
|
}): Promise<void>;
|
|
73
75
|
protected getSqlType(type: StorageColumn['type']): string;
|
|
@@ -89,8 +91,8 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
89
91
|
tableName: TABLE_NAMES;
|
|
90
92
|
record: Record<string, any>;
|
|
91
93
|
}): Promise<void>;
|
|
92
|
-
load<R>({ tableName, keys }: {
|
|
93
|
-
tableName:
|
|
94
|
+
load<R>({ tableName, keys, }: {
|
|
95
|
+
tableName: SUPPORTED_TABLE_NAMES;
|
|
94
96
|
keys: Record<string, string>;
|
|
95
97
|
}): Promise<R | null>;
|
|
96
98
|
getThreadById({ threadId }: {
|
|
@@ -180,7 +182,9 @@ export { COLUMN_TYPES as COLUMN_TYPES_alias_1 }
|
|
|
180
182
|
|
|
181
183
|
declare type IntervalUnit = 'NANOSECOND' | 'MICROSECOND' | 'MILLISECOND' | 'SECOND' | 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR';
|
|
182
184
|
|
|
183
|
-
declare
|
|
185
|
+
declare type SUPPORTED_TABLE_NAMES = Exclude<TABLE_NAMES, typeof TABLE_RESOURCES>;
|
|
186
|
+
|
|
187
|
+
declare const TABLE_ENGINES: Record<SUPPORTED_TABLE_NAMES, string>;
|
|
184
188
|
export { TABLE_ENGINES }
|
|
185
189
|
export { TABLE_ENGINES as TABLE_ENGINES_alias_1 }
|
|
186
190
|
|
package/dist/index.cjs
CHANGED
|
@@ -87,6 +87,18 @@ var ClickhouseStore = class extends storage.MastraStorage {
|
|
|
87
87
|
createdAt: row.created_at
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
escape(value) {
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
93
|
+
}
|
|
94
|
+
if (value instanceof Date) {
|
|
95
|
+
return `'${value.toISOString()}'`;
|
|
96
|
+
}
|
|
97
|
+
if (value === null || value === void 0) {
|
|
98
|
+
return "NULL";
|
|
99
|
+
}
|
|
100
|
+
return value.toString();
|
|
101
|
+
}
|
|
90
102
|
async getEvalsByAgentName(agentName, type) {
|
|
91
103
|
try {
|
|
92
104
|
const baseQuery = `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${storage.TABLE_EVALS} WHERE agent_name = {var_agent_name:String}`;
|
|
@@ -457,7 +469,10 @@ var ClickhouseStore = class extends storage.MastraStorage {
|
|
|
457
469
|
);
|
|
458
470
|
}
|
|
459
471
|
}
|
|
460
|
-
async load({
|
|
472
|
+
async load({
|
|
473
|
+
tableName,
|
|
474
|
+
keys
|
|
475
|
+
}) {
|
|
461
476
|
try {
|
|
462
477
|
const keyEntries = Object.entries(keys);
|
|
463
478
|
const conditions = keyEntries.map(
|
|
@@ -832,12 +847,52 @@ var ClickhouseStore = class extends storage.MastraStorage {
|
|
|
832
847
|
if (!thread) {
|
|
833
848
|
throw new Error(`Thread ${threadId} not found`);
|
|
834
849
|
}
|
|
850
|
+
const existingResult = await this.db.query({
|
|
851
|
+
query: `SELECT id, thread_id FROM ${storage.TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
|
|
852
|
+
query_params: {
|
|
853
|
+
ids: messages.map((m) => m.id)
|
|
854
|
+
},
|
|
855
|
+
clickhouse_settings: {
|
|
856
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
857
|
+
date_time_input_format: "best_effort",
|
|
858
|
+
date_time_output_format: "iso",
|
|
859
|
+
use_client_time_zone: 1,
|
|
860
|
+
output_format_json_quote_64bit_integers: 0
|
|
861
|
+
},
|
|
862
|
+
format: "JSONEachRow"
|
|
863
|
+
});
|
|
864
|
+
const existingRows = await existingResult.json();
|
|
865
|
+
const existingSet = new Set(existingRows.map((row) => `${row.id}::${row.thread_id}`));
|
|
866
|
+
const toInsert = messages.filter((m) => !existingSet.has(`${m.id}::${threadId}`));
|
|
867
|
+
const toUpdate = messages.filter((m) => existingSet.has(`${m.id}::${threadId}`));
|
|
868
|
+
const updatePromises = toUpdate.map(
|
|
869
|
+
(message) => this.db.command({
|
|
870
|
+
query: `
|
|
871
|
+
ALTER TABLE ${storage.TABLE_MESSAGES}
|
|
872
|
+
UPDATE content = {var_content:String}, role = {var_role:String}, type = {var_type:String}
|
|
873
|
+
WHERE id = {var_id:String} AND thread_id = {var_thread_id:String}
|
|
874
|
+
`,
|
|
875
|
+
query_params: {
|
|
876
|
+
var_content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
|
|
877
|
+
var_role: message.role,
|
|
878
|
+
var_type: message.type || "v2",
|
|
879
|
+
var_id: message.id,
|
|
880
|
+
var_thread_id: threadId
|
|
881
|
+
},
|
|
882
|
+
clickhouse_settings: {
|
|
883
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
884
|
+
date_time_input_format: "best_effort",
|
|
885
|
+
use_client_time_zone: 1,
|
|
886
|
+
output_format_json_quote_64bit_integers: 0
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
);
|
|
835
890
|
await Promise.all([
|
|
836
891
|
// Insert messages
|
|
837
892
|
this.db.insert({
|
|
838
893
|
table: storage.TABLE_MESSAGES,
|
|
839
894
|
format: "JSONEachRow",
|
|
840
|
-
values:
|
|
895
|
+
values: toInsert.map((message) => ({
|
|
841
896
|
id: message.id,
|
|
842
897
|
thread_id: threadId,
|
|
843
898
|
content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
|
|
@@ -852,6 +907,7 @@ var ClickhouseStore = class extends storage.MastraStorage {
|
|
|
852
907
|
output_format_json_quote_64bit_integers: 0
|
|
853
908
|
}
|
|
854
909
|
}),
|
|
910
|
+
...updatePromises,
|
|
855
911
|
// Update thread's updatedAt timestamp
|
|
856
912
|
this.db.insert({
|
|
857
913
|
table: storage.TABLE_THREADS,
|
package/dist/index.js
CHANGED
|
@@ -85,6 +85,18 @@ var ClickhouseStore = class extends MastraStorage {
|
|
|
85
85
|
createdAt: row.created_at
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
escape(value) {
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
91
|
+
}
|
|
92
|
+
if (value instanceof Date) {
|
|
93
|
+
return `'${value.toISOString()}'`;
|
|
94
|
+
}
|
|
95
|
+
if (value === null || value === void 0) {
|
|
96
|
+
return "NULL";
|
|
97
|
+
}
|
|
98
|
+
return value.toString();
|
|
99
|
+
}
|
|
88
100
|
async getEvalsByAgentName(agentName, type) {
|
|
89
101
|
try {
|
|
90
102
|
const baseQuery = `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_EVALS} WHERE agent_name = {var_agent_name:String}`;
|
|
@@ -455,7 +467,10 @@ var ClickhouseStore = class extends MastraStorage {
|
|
|
455
467
|
);
|
|
456
468
|
}
|
|
457
469
|
}
|
|
458
|
-
async load({
|
|
470
|
+
async load({
|
|
471
|
+
tableName,
|
|
472
|
+
keys
|
|
473
|
+
}) {
|
|
459
474
|
try {
|
|
460
475
|
const keyEntries = Object.entries(keys);
|
|
461
476
|
const conditions = keyEntries.map(
|
|
@@ -830,12 +845,52 @@ var ClickhouseStore = class extends MastraStorage {
|
|
|
830
845
|
if (!thread) {
|
|
831
846
|
throw new Error(`Thread ${threadId} not found`);
|
|
832
847
|
}
|
|
848
|
+
const existingResult = await this.db.query({
|
|
849
|
+
query: `SELECT id, thread_id FROM ${TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
|
|
850
|
+
query_params: {
|
|
851
|
+
ids: messages.map((m) => m.id)
|
|
852
|
+
},
|
|
853
|
+
clickhouse_settings: {
|
|
854
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
855
|
+
date_time_input_format: "best_effort",
|
|
856
|
+
date_time_output_format: "iso",
|
|
857
|
+
use_client_time_zone: 1,
|
|
858
|
+
output_format_json_quote_64bit_integers: 0
|
|
859
|
+
},
|
|
860
|
+
format: "JSONEachRow"
|
|
861
|
+
});
|
|
862
|
+
const existingRows = await existingResult.json();
|
|
863
|
+
const existingSet = new Set(existingRows.map((row) => `${row.id}::${row.thread_id}`));
|
|
864
|
+
const toInsert = messages.filter((m) => !existingSet.has(`${m.id}::${threadId}`));
|
|
865
|
+
const toUpdate = messages.filter((m) => existingSet.has(`${m.id}::${threadId}`));
|
|
866
|
+
const updatePromises = toUpdate.map(
|
|
867
|
+
(message) => this.db.command({
|
|
868
|
+
query: `
|
|
869
|
+
ALTER TABLE ${TABLE_MESSAGES}
|
|
870
|
+
UPDATE content = {var_content:String}, role = {var_role:String}, type = {var_type:String}
|
|
871
|
+
WHERE id = {var_id:String} AND thread_id = {var_thread_id:String}
|
|
872
|
+
`,
|
|
873
|
+
query_params: {
|
|
874
|
+
var_content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
|
|
875
|
+
var_role: message.role,
|
|
876
|
+
var_type: message.type || "v2",
|
|
877
|
+
var_id: message.id,
|
|
878
|
+
var_thread_id: threadId
|
|
879
|
+
},
|
|
880
|
+
clickhouse_settings: {
|
|
881
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
882
|
+
date_time_input_format: "best_effort",
|
|
883
|
+
use_client_time_zone: 1,
|
|
884
|
+
output_format_json_quote_64bit_integers: 0
|
|
885
|
+
}
|
|
886
|
+
})
|
|
887
|
+
);
|
|
833
888
|
await Promise.all([
|
|
834
889
|
// Insert messages
|
|
835
890
|
this.db.insert({
|
|
836
891
|
table: TABLE_MESSAGES,
|
|
837
892
|
format: "JSONEachRow",
|
|
838
|
-
values:
|
|
893
|
+
values: toInsert.map((message) => ({
|
|
839
894
|
id: message.id,
|
|
840
895
|
thread_id: threadId,
|
|
841
896
|
content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
|
|
@@ -850,6 +905,7 @@ var ClickhouseStore = class extends MastraStorage {
|
|
|
850
905
|
output_format_json_quote_64bit_integers: 0
|
|
851
906
|
}
|
|
852
907
|
}),
|
|
908
|
+
...updatePromises,
|
|
853
909
|
// Update thread's updatedAt timestamp
|
|
854
910
|
this.db.insert({
|
|
855
911
|
table: TABLE_THREADS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/clickhouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0-alpha.3",
|
|
4
4
|
"description": "Clickhouse provider for Mastra - includes db storage capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@microsoft/api-extractor": "^7.52.8",
|
|
27
27
|
"@types/node": "^20.19.0",
|
|
28
|
-
"eslint": "^9.
|
|
28
|
+
"eslint": "^9.29.0",
|
|
29
29
|
"tsup": "^8.5.0",
|
|
30
30
|
"typescript": "^5.8.3",
|
|
31
31
|
"vitest": "^3.2.3",
|
|
32
32
|
"@internal/lint": "0.0.13",
|
|
33
33
|
"@internal/storage-test-utils": "0.0.9",
|
|
34
|
-
"@mastra/core": "0.10.7-alpha.
|
|
34
|
+
"@mastra/core": "0.10.7-alpha.3"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@mastra/core": ">=0.10.
|
|
37
|
+
"@mastra/core": ">=0.10.7-0 <0.11.0-0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createSampleThread,
|
|
5
5
|
createSampleWorkflowSnapshot,
|
|
6
6
|
checkWorkflowSnapshot,
|
|
7
|
+
createSampleMessageV2,
|
|
7
8
|
} from '@internal/storage-test-utils';
|
|
8
9
|
import type { MastraMessageV1, StorageColumn, WorkflowRunState } from '@mastra/core';
|
|
9
10
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
@@ -236,6 +237,83 @@ describe('ClickhouseStore', () => {
|
|
|
236
237
|
});
|
|
237
238
|
}, 10e3);
|
|
238
239
|
|
|
240
|
+
it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
|
|
241
|
+
const thread = await createSampleThread({ resourceId: 'clickhouse-test' });
|
|
242
|
+
await store.saveThread({ thread });
|
|
243
|
+
const baseMessage = createSampleMessageV2({
|
|
244
|
+
threadId: thread.id,
|
|
245
|
+
createdAt: new Date(),
|
|
246
|
+
content: { content: 'Original' },
|
|
247
|
+
resourceId: 'clickhouse-test',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Insert the message for the first time
|
|
251
|
+
await store.saveMessages({ messages: [baseMessage], format: 'v2' });
|
|
252
|
+
|
|
253
|
+
// Insert again with the same id and threadId but different content
|
|
254
|
+
const updatedMessage = {
|
|
255
|
+
...createSampleMessageV2({
|
|
256
|
+
threadId: thread.id,
|
|
257
|
+
createdAt: new Date(),
|
|
258
|
+
content: { content: 'Updated' },
|
|
259
|
+
resourceId: 'clickhouse-test',
|
|
260
|
+
}),
|
|
261
|
+
id: baseMessage.id,
|
|
262
|
+
};
|
|
263
|
+
await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
265
|
+
|
|
266
|
+
// Retrieve messages for the thread
|
|
267
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
268
|
+
|
|
269
|
+
// Only one message should exist for that id+threadId
|
|
270
|
+
expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
|
|
271
|
+
|
|
272
|
+
// The content should be the updated one
|
|
273
|
+
expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
|
|
274
|
+
}, 10e3);
|
|
275
|
+
|
|
276
|
+
it('should upsert messages: duplicate id and different threadid', async () => {
|
|
277
|
+
const thread1 = await createSampleThread();
|
|
278
|
+
const thread2 = await createSampleThread();
|
|
279
|
+
await store.saveThread({ thread: thread1 });
|
|
280
|
+
await store.saveThread({ thread: thread2 });
|
|
281
|
+
|
|
282
|
+
const message = createSampleMessageV2({
|
|
283
|
+
threadId: thread1.id,
|
|
284
|
+
createdAt: new Date(),
|
|
285
|
+
content: { content: 'Thread1 Content' },
|
|
286
|
+
resourceId: thread1.resourceId,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Insert message into thread1
|
|
290
|
+
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
291
|
+
|
|
292
|
+
// Attempt to insert a message with the same id but different threadId
|
|
293
|
+
const conflictingMessage = {
|
|
294
|
+
...createSampleMessageV2({
|
|
295
|
+
threadId: thread2.id,
|
|
296
|
+
createdAt: new Date(),
|
|
297
|
+
content: { content: 'Thread2 Content' },
|
|
298
|
+
resourceId: thread2.resourceId,
|
|
299
|
+
}),
|
|
300
|
+
id: message.id,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Save should also save the message to the new thread
|
|
304
|
+
await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
|
|
305
|
+
|
|
306
|
+
// Retrieve messages for both threads
|
|
307
|
+
const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
|
|
308
|
+
const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
|
|
309
|
+
|
|
310
|
+
// Thread 1 should have the message with that id
|
|
311
|
+
expect(thread1Messages.find(m => m.id === message.id)?.content.content).toBe('Thread1 Content');
|
|
312
|
+
|
|
313
|
+
// Thread 2 should have the message with that id
|
|
314
|
+
expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
|
|
315
|
+
}, 10e3);
|
|
316
|
+
|
|
239
317
|
// it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
|
|
240
318
|
// const messages: MastraMessageV2[] = [
|
|
241
319
|
// createSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
|
package/src/storage/index.ts
CHANGED
|
@@ -23,10 +23,13 @@ import type {
|
|
|
23
23
|
WorkflowRun,
|
|
24
24
|
WorkflowRuns,
|
|
25
25
|
StorageGetTracesArg,
|
|
26
|
+
TABLE_RESOURCES,
|
|
26
27
|
} from '@mastra/core/storage';
|
|
27
28
|
import type { Trace } from '@mastra/core/telemetry';
|
|
28
29
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
29
30
|
|
|
31
|
+
type SUPPORTED_TABLE_NAMES = Exclude<TABLE_NAMES, typeof TABLE_RESOURCES>;
|
|
32
|
+
|
|
30
33
|
function safelyParseJSON(jsonString: string): any {
|
|
31
34
|
try {
|
|
32
35
|
return JSON.parse(jsonString);
|
|
@@ -66,7 +69,7 @@ export type ClickhouseConfig = {
|
|
|
66
69
|
};
|
|
67
70
|
};
|
|
68
71
|
|
|
69
|
-
export const TABLE_ENGINES: Record<
|
|
72
|
+
export const TABLE_ENGINES: Record<SUPPORTED_TABLE_NAMES, string> = {
|
|
70
73
|
[TABLE_MESSAGES]: `MergeTree()`,
|
|
71
74
|
[TABLE_WORKFLOW_SNAPSHOT]: `ReplacingMergeTree()`,
|
|
72
75
|
[TABLE_TRACES]: `MergeTree()`,
|
|
@@ -149,6 +152,19 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
149
152
|
};
|
|
150
153
|
}
|
|
151
154
|
|
|
155
|
+
private escape(value: any): string {
|
|
156
|
+
if (typeof value === 'string') {
|
|
157
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
158
|
+
}
|
|
159
|
+
if (value instanceof Date) {
|
|
160
|
+
return `'${value.toISOString()}'`;
|
|
161
|
+
}
|
|
162
|
+
if (value === null || value === undefined) {
|
|
163
|
+
return 'NULL';
|
|
164
|
+
}
|
|
165
|
+
return value.toString();
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
153
169
|
try {
|
|
154
170
|
const baseQuery = `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_EVALS} WHERE agent_name = {var_agent_name:String}`;
|
|
@@ -388,7 +404,7 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
388
404
|
tableName,
|
|
389
405
|
schema,
|
|
390
406
|
}: {
|
|
391
|
-
tableName:
|
|
407
|
+
tableName: SUPPORTED_TABLE_NAMES;
|
|
392
408
|
schema: Record<string, StorageColumn>;
|
|
393
409
|
}): Promise<void> {
|
|
394
410
|
try {
|
|
@@ -577,7 +593,13 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
577
593
|
}
|
|
578
594
|
}
|
|
579
595
|
|
|
580
|
-
async load<R>({
|
|
596
|
+
async load<R>({
|
|
597
|
+
tableName,
|
|
598
|
+
keys,
|
|
599
|
+
}: {
|
|
600
|
+
tableName: SUPPORTED_TABLE_NAMES;
|
|
601
|
+
keys: Record<string, string>;
|
|
602
|
+
}): Promise<R | null> {
|
|
581
603
|
try {
|
|
582
604
|
const keyEntries = Object.entries(keys);
|
|
583
605
|
const conditions = keyEntries
|
|
@@ -591,7 +613,7 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
591
613
|
}, {});
|
|
592
614
|
|
|
593
615
|
const result = await this.db.query({
|
|
594
|
-
query: `SELECT *, toDateTime64(createdAt, 3) as createdAt, toDateTime64(updatedAt, 3) as updatedAt FROM ${tableName} ${TABLE_ENGINES[tableName as
|
|
616
|
+
query: `SELECT *, toDateTime64(createdAt, 3) as createdAt, toDateTime64(updatedAt, 3) as updatedAt FROM ${tableName} ${TABLE_ENGINES[tableName as SUPPORTED_TABLE_NAMES].startsWith('ReplacingMergeTree') ? 'FINAL' : ''} WHERE ${conditions}`,
|
|
595
617
|
query_params: values,
|
|
596
618
|
clickhouse_settings: {
|
|
597
619
|
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
@@ -1007,13 +1029,62 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
1007
1029
|
throw new Error(`Thread ${threadId} not found`);
|
|
1008
1030
|
}
|
|
1009
1031
|
|
|
1032
|
+
// Clickhouse's MergeTree engine does not support native upserts or unique constraints on (id, thread_id).
|
|
1033
|
+
// Note: We cannot switch to ReplacingMergeTree without a schema migration,
|
|
1034
|
+
// as it would require altering the table engine.
|
|
1035
|
+
// To ensure correct upsert behavior, we first fetch existing (id, thread_id) pairs for the incoming messages.
|
|
1036
|
+
const existingResult = await this.db.query({
|
|
1037
|
+
query: `SELECT id, thread_id FROM ${TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
|
|
1038
|
+
query_params: {
|
|
1039
|
+
ids: messages.map(m => m.id),
|
|
1040
|
+
},
|
|
1041
|
+
clickhouse_settings: {
|
|
1042
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
1043
|
+
date_time_input_format: 'best_effort',
|
|
1044
|
+
date_time_output_format: 'iso',
|
|
1045
|
+
use_client_time_zone: 1,
|
|
1046
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1047
|
+
},
|
|
1048
|
+
format: 'JSONEachRow',
|
|
1049
|
+
});
|
|
1050
|
+
const existingRows: Array<{ id: string; thread_id: string }> = await existingResult.json();
|
|
1051
|
+
|
|
1052
|
+
const existingSet = new Set(existingRows.map(row => `${row.id}::${row.thread_id}`));
|
|
1053
|
+
// Partition the batch into new inserts and updates:
|
|
1054
|
+
// New messages are inserted in bulk.
|
|
1055
|
+
const toInsert = messages.filter(m => !existingSet.has(`${m.id}::${threadId}`));
|
|
1056
|
+
// Existing messages are updated via ALTER TABLE ... UPDATE.
|
|
1057
|
+
const toUpdate = messages.filter(m => existingSet.has(`${m.id}::${threadId}`));
|
|
1058
|
+
const updatePromises = toUpdate.map(message =>
|
|
1059
|
+
this.db.command({
|
|
1060
|
+
query: `
|
|
1061
|
+
ALTER TABLE ${TABLE_MESSAGES}
|
|
1062
|
+
UPDATE content = {var_content:String}, role = {var_role:String}, type = {var_type:String}
|
|
1063
|
+
WHERE id = {var_id:String} AND thread_id = {var_thread_id:String}
|
|
1064
|
+
`,
|
|
1065
|
+
query_params: {
|
|
1066
|
+
var_content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
|
|
1067
|
+
var_role: message.role,
|
|
1068
|
+
var_type: message.type || 'v2',
|
|
1069
|
+
var_id: message.id,
|
|
1070
|
+
var_thread_id: threadId,
|
|
1071
|
+
},
|
|
1072
|
+
clickhouse_settings: {
|
|
1073
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
1074
|
+
date_time_input_format: 'best_effort',
|
|
1075
|
+
use_client_time_zone: 1,
|
|
1076
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1077
|
+
},
|
|
1078
|
+
}),
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1010
1081
|
// Execute message inserts and thread update in parallel for better performance
|
|
1011
1082
|
await Promise.all([
|
|
1012
1083
|
// Insert messages
|
|
1013
1084
|
this.db.insert({
|
|
1014
1085
|
table: TABLE_MESSAGES,
|
|
1015
1086
|
format: 'JSONEachRow',
|
|
1016
|
-
values:
|
|
1087
|
+
values: toInsert.map(message => ({
|
|
1017
1088
|
id: message.id,
|
|
1018
1089
|
thread_id: threadId,
|
|
1019
1090
|
content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
|
|
@@ -1028,6 +1099,7 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
1028
1099
|
output_format_json_quote_64bit_integers: 0,
|
|
1029
1100
|
},
|
|
1030
1101
|
}),
|
|
1102
|
+
...updatePromises,
|
|
1031
1103
|
// Update thread's updatedAt timestamp
|
|
1032
1104
|
this.db.insert({
|
|
1033
1105
|
table: TABLE_THREADS,
|