@mastra/clickhouse 0.10.1 → 0.10.2-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.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/clickhouse@0.10.1-alpha.2 build /home/runner/work/mastra/mastra/stores/clickhouse
2
+ > @mastra/clickhouse@0.10.2-alpha.0 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
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.4.0
7
+ CLI tsup v8.5.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 8613ms
9
+ TSC ⚡️ Build success in 9329ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 11358ms
16
+ DTS ⚡️ Build success in 11005ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- ESM dist/index.js 28.23 KB
21
- ESM ⚡️ Build success in 942ms
22
- CJS dist/index.cjs 28.49 KB
23
- CJS ⚡️ Build success in 943ms
20
+ ESM dist/index.js 30.08 KB
21
+ ESM ⚡️ Build success in 1046ms
22
+ CJS dist/index.cjs 30.33 KB
23
+ CJS ⚡️ Build success in 1040ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @mastra/clickhouse
2
2
 
3
+ ## 0.10.2-alpha.0
4
+
5
+ ### Patch Changes
6
+
7
+ - dffb67b: updated stores to add alter table and change tests
8
+ - Updated dependencies [f6fd25f]
9
+ - Updated dependencies [dffb67b]
10
+ - Updated dependencies [f1309d3]
11
+ - Updated dependencies [f7f8293]
12
+ - @mastra/core@0.10.4-alpha.1
13
+
3
14
  ## 0.10.1
4
15
 
5
16
  ### Patch Changes
@@ -66,6 +66,18 @@ declare class ClickhouseStore extends MastraStorage {
66
66
  tableName: TABLE_NAMES;
67
67
  schema: Record<string, StorageColumn>;
68
68
  }): Promise<void>;
69
+ protected getSqlType(type: StorageColumn['type']): string;
70
+ /**
71
+ * Alters table schema to add columns if they don't exist
72
+ * @param tableName Name of the table
73
+ * @param schema Schema of the table
74
+ * @param ifNotExists Array of column names to add if they don't exist
75
+ */
76
+ alterTable({ tableName, schema, ifNotExists, }: {
77
+ tableName: TABLE_NAMES;
78
+ schema: Record<string, StorageColumn>;
79
+ ifNotExists: string[];
80
+ }): Promise<void>;
69
81
  clearTable({ tableName }: {
70
82
  tableName: TABLE_NAMES;
71
83
  }): Promise<void>;
@@ -66,6 +66,18 @@ declare class ClickhouseStore extends MastraStorage {
66
66
  tableName: TABLE_NAMES;
67
67
  schema: Record<string, StorageColumn>;
68
68
  }): Promise<void>;
69
+ protected getSqlType(type: StorageColumn['type']): string;
70
+ /**
71
+ * Alters table schema to add columns if they don't exist
72
+ * @param tableName Name of the table
73
+ * @param schema Schema of the table
74
+ * @param ifNotExists Array of column names to add if they don't exist
75
+ */
76
+ alterTable({ tableName, schema, ifNotExists, }: {
77
+ tableName: TABLE_NAMES;
78
+ schema: Record<string, StorageColumn>;
79
+ ifNotExists: string[];
80
+ }): Promise<void>;
69
81
  clearTable({ tableName }: {
70
82
  tableName: TABLE_NAMES;
71
83
  }): Promise<void>;
package/dist/index.cjs CHANGED
@@ -267,6 +267,61 @@ var ClickhouseStore = class extends storage.MastraStorage {
267
267
  throw error;
268
268
  }
269
269
  }
270
+ getSqlType(type) {
271
+ switch (type) {
272
+ case "text":
273
+ return "String";
274
+ case "timestamp":
275
+ return "DateTime64(3)";
276
+ case "integer":
277
+ case "bigint":
278
+ return "Int64";
279
+ case "jsonb":
280
+ return "String";
281
+ default:
282
+ return super.getSqlType(type);
283
+ }
284
+ }
285
+ /**
286
+ * Alters table schema to add columns if they don't exist
287
+ * @param tableName Name of the table
288
+ * @param schema Schema of the table
289
+ * @param ifNotExists Array of column names to add if they don't exist
290
+ */
291
+ async alterTable({
292
+ tableName,
293
+ schema,
294
+ ifNotExists
295
+ }) {
296
+ try {
297
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
298
+ const result = await this.db.query({
299
+ query: describeSql
300
+ });
301
+ const rows = await result.json();
302
+ const existingColumnNames = new Set(rows.data.map((row) => row.name.toLowerCase()));
303
+ for (const columnName of ifNotExists) {
304
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
305
+ const columnDef = schema[columnName];
306
+ let sqlType = this.getSqlType(columnDef.type);
307
+ if (columnDef.nullable !== false) {
308
+ sqlType = `Nullable(${sqlType})`;
309
+ }
310
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
311
+ const alterSql = `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
312
+ await this.db.query({
313
+ query: alterSql
314
+ });
315
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
316
+ }
317
+ }
318
+ } catch (error) {
319
+ this.logger?.error?.(
320
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
321
+ );
322
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
323
+ }
324
+ }
270
325
  async clearTable({ tableName }) {
271
326
  try {
272
327
  await this.db.query({
package/dist/index.js CHANGED
@@ -265,6 +265,61 @@ var ClickhouseStore = class extends MastraStorage {
265
265
  throw error;
266
266
  }
267
267
  }
268
+ getSqlType(type) {
269
+ switch (type) {
270
+ case "text":
271
+ return "String";
272
+ case "timestamp":
273
+ return "DateTime64(3)";
274
+ case "integer":
275
+ case "bigint":
276
+ return "Int64";
277
+ case "jsonb":
278
+ return "String";
279
+ default:
280
+ return super.getSqlType(type);
281
+ }
282
+ }
283
+ /**
284
+ * Alters table schema to add columns if they don't exist
285
+ * @param tableName Name of the table
286
+ * @param schema Schema of the table
287
+ * @param ifNotExists Array of column names to add if they don't exist
288
+ */
289
+ async alterTable({
290
+ tableName,
291
+ schema,
292
+ ifNotExists
293
+ }) {
294
+ try {
295
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
296
+ const result = await this.db.query({
297
+ query: describeSql
298
+ });
299
+ const rows = await result.json();
300
+ const existingColumnNames = new Set(rows.data.map((row) => row.name.toLowerCase()));
301
+ for (const columnName of ifNotExists) {
302
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
303
+ const columnDef = schema[columnName];
304
+ let sqlType = this.getSqlType(columnDef.type);
305
+ if (columnDef.nullable !== false) {
306
+ sqlType = `Nullable(${sqlType})`;
307
+ }
308
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
309
+ const alterSql = `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
310
+ await this.db.query({
311
+ query: alterSql
312
+ });
313
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
314
+ }
315
+ }
316
+ } catch (error) {
317
+ this.logger?.error?.(
318
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
319
+ );
320
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
321
+ }
322
+ }
268
323
  async clearTable({ tableName }) {
269
324
  try {
270
325
  await this.db.query({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/clickhouse",
3
- "version": "0.10.1",
3
+ "version": "0.10.2-alpha.0",
4
4
  "description": "Clickhouse provider for Mastra - includes db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,14 +23,15 @@
23
23
  "@clickhouse/client": "^1.11.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@microsoft/api-extractor": "^7.52.5",
27
- "@types/node": "^20.17.27",
28
- "eslint": "^9.23.0",
29
- "tsup": "^8.4.0",
26
+ "@microsoft/api-extractor": "^7.52.8",
27
+ "@types/node": "^20.17.57",
28
+ "eslint": "^9.28.0",
29
+ "tsup": "^8.5.0",
30
30
  "typescript": "^5.8.2",
31
- "vitest": "^3.1.2",
32
- "@mastra/core": "0.10.2",
33
- "@internal/lint": "0.0.8"
31
+ "vitest": "^3.2.2",
32
+ "@internal/lint": "0.0.10",
33
+ "@internal/storage-test-utils": "0.0.6",
34
+ "@mastra/core": "0.10.4-alpha.1"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@mastra/core": "^0.10.2-alpha.0"
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from 'crypto';
2
- import type { MastraMessageV1, WorkflowRunState } from '@mastra/core';
2
+ import { createSampleMessageV1, createSampleThread, createSampleWorkflowSnapshot } from '@internal/storage-test-utils';
3
+ import type { MastraMessageV1, StorageColumn, WorkflowRunState } from '@mastra/core';
4
+ import type { TABLE_NAMES } from '@mastra/core/storage';
3
5
  import { TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
4
6
  import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
5
7
 
6
- import { ClickhouseStore } from '.';
8
+ import { ClickhouseStore, TABLE_ENGINES } from '.';
7
9
  import type { ClickhouseConfig } from '.';
8
10
 
9
11
  vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
@@ -24,33 +26,6 @@ const TEST_CONFIG: ClickhouseConfig = {
24
26
  },
25
27
  };
26
28
 
27
- // Sample test data factory functions
28
- const createSampleThread = () => ({
29
- id: `thread-${randomUUID()}`,
30
- resourceId: `clickhouse-test`,
31
- title: 'Test Thread',
32
- createdAt: new Date(),
33
- updatedAt: new Date(),
34
- metadata: { key: 'value' },
35
- });
36
-
37
- let role = `user`;
38
- const getRole = () => {
39
- if (role === `user`) role = `assistant`;
40
- else role = `user`;
41
- return role as 'user' | 'assistant';
42
- };
43
-
44
- const createSampleMessage = (threadId: string, createdAt: Date = new Date()): MastraMessageV1 => ({
45
- id: `msg-${randomUUID()}`,
46
- resourceId: `clickhouse-test`,
47
- role: getRole(),
48
- type: 'text',
49
- threadId,
50
- content: 'Hello',
51
- createdAt,
52
- });
53
-
54
29
  const createSampleTrace = () => ({
55
30
  id: `trace-${randomUUID()}`,
56
31
  name: 'Test Trace',
@@ -66,32 +41,6 @@ const createSampleEval = () => ({
66
41
  createdAt: new Date(),
67
42
  });
68
43
 
69
- const createSampleWorkflowSnapshot = (status: WorkflowRunState['context']['steps']['status'], createdAt?: Date) => {
70
- const runId = `run-${randomUUID()}`;
71
- const stepId = `step-${randomUUID()}`;
72
- const timestamp = createdAt || new Date();
73
- const snapshot = {
74
- result: { success: true },
75
- value: {},
76
- context: {
77
- [stepId]: {
78
- status,
79
- payload: {},
80
- error: undefined,
81
- startedAt: timestamp.getTime(),
82
- endedAt: new Date(timestamp.getTime() + 15000).getTime(),
83
- },
84
- input: {},
85
- },
86
- serializedStepGraph: [],
87
- activePaths: [],
88
- suspendedPaths: {},
89
- runId,
90
- timestamp: timestamp.getTime(),
91
- } as unknown as WorkflowRunState;
92
- return { snapshot, runId, stepId };
93
- };
94
-
95
44
  const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
96
45
  if (typeof snapshot === 'string') {
97
46
  throw new Error('Expected WorkflowRunState, got string');
@@ -171,7 +120,10 @@ describe('ClickhouseStore', () => {
171
120
  await store.saveThread({ thread });
172
121
 
173
122
  // Add some messages
174
- const messages = [createSampleMessage(thread.id), createSampleMessage(thread.id)];
123
+ const messages = [
124
+ createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
125
+ createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
126
+ ];
175
127
  await store.saveMessages({ messages });
176
128
 
177
129
  await store.deleteThread({ threadId: thread.id });
@@ -191,8 +143,12 @@ describe('ClickhouseStore', () => {
191
143
  await store.saveThread({ thread });
192
144
 
193
145
  const messages = [
194
- createSampleMessage(thread.id, new Date(Date.now() - 1000 * 60 * 60 * 24)),
195
- createSampleMessage(thread.id),
146
+ createSampleMessageV1({
147
+ threadId: thread.id,
148
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
149
+ resourceId: 'clickhouse-test',
150
+ }),
151
+ createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
196
152
  ];
197
153
 
198
154
  // Save messages
@@ -220,18 +176,30 @@ describe('ClickhouseStore', () => {
220
176
 
221
177
  const messages: MastraMessageV1[] = [
222
178
  {
223
- ...createSampleMessage(thread.id, new Date(Date.now() - 1000 * 3)),
224
- content: [{ type: 'text', text: 'First' }],
179
+ ...createSampleMessageV1({
180
+ threadId: thread.id,
181
+ createdAt: new Date(Date.now() - 1000 * 3),
182
+ content: 'First',
183
+ resourceId: 'clickhouse-test',
184
+ }),
225
185
  role: 'user',
226
186
  },
227
187
  {
228
- ...createSampleMessage(thread.id, new Date(Date.now() - 1000 * 2)),
229
- content: [{ type: 'text', text: 'Second' }],
188
+ ...createSampleMessageV1({
189
+ threadId: thread.id,
190
+ createdAt: new Date(Date.now() - 1000 * 2),
191
+ content: 'Second',
192
+ resourceId: 'clickhouse-test',
193
+ }),
230
194
  role: 'assistant',
231
195
  },
232
196
  {
233
- ...createSampleMessage(thread.id, new Date(Date.now() - 1000 * 1)),
234
- content: [{ type: 'text', text: 'Third' }],
197
+ ...createSampleMessageV1({
198
+ threadId: thread.id,
199
+ createdAt: new Date(Date.now() - 1000 * 1),
200
+ content: 'Third',
201
+ resourceId: 'clickhouse-test',
202
+ }),
235
203
  role: 'user',
236
204
  },
237
205
  ];
@@ -253,8 +221,8 @@ describe('ClickhouseStore', () => {
253
221
  // await store.saveThread({ thread });
254
222
 
255
223
  // const messages = [
256
- // createSampleMessage(thread.id),
257
- // { ...createSampleMessage(thread.id), id: null }, // This will cause an error
224
+ // createSampleMessageV1({ threadId: thread.id }),
225
+ // { ...createSampleMessageV1({ threadId: thread.id }), id: null }, // This will cause an error
258
226
  // ];
259
227
 
260
228
  // await expect(store.saveMessages({ messages })).rejects.toThrow();
@@ -842,6 +810,100 @@ describe('ClickhouseStore', () => {
842
810
  });
843
811
  });
844
812
 
813
+ describe('alterTable', () => {
814
+ const TEST_TABLE = 'test_alter_table';
815
+ const BASE_SCHEMA = {
816
+ id: { type: 'integer', primaryKey: true, nullable: false },
817
+ name: { type: 'text', nullable: true },
818
+ createdAt: { type: 'timestamp', nullable: false },
819
+ updatedAt: { type: 'timestamp', nullable: false },
820
+ } as Record<string, StorageColumn>;
821
+
822
+ TABLE_ENGINES[TEST_TABLE] = 'MergeTree()';
823
+
824
+ beforeEach(async () => {
825
+ await store.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
826
+ });
827
+
828
+ afterEach(async () => {
829
+ await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
830
+ });
831
+
832
+ it('adds a new column to an existing table', async () => {
833
+ await store.alterTable({
834
+ tableName: TEST_TABLE as TABLE_NAMES,
835
+ schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
836
+ ifNotExists: ['age'],
837
+ });
838
+
839
+ await store.insert({
840
+ tableName: TEST_TABLE as TABLE_NAMES,
841
+ record: { id: 1, name: 'Alice', age: 42, createdAt: new Date(), updatedAt: new Date() },
842
+ });
843
+
844
+ const row = await store.load<{ id: string; name: string; age?: number }>({
845
+ tableName: TEST_TABLE as TABLE_NAMES,
846
+ keys: { id: '1' },
847
+ });
848
+ expect(row?.age).toBe(42);
849
+ });
850
+
851
+ it('is idempotent when adding an existing column', async () => {
852
+ await store.alterTable({
853
+ tableName: TEST_TABLE as TABLE_NAMES,
854
+ schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
855
+ ifNotExists: ['foo'],
856
+ });
857
+ // Add the column again (should not throw)
858
+ await expect(
859
+ store.alterTable({
860
+ tableName: TEST_TABLE as TABLE_NAMES,
861
+ schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
862
+ ifNotExists: ['foo'],
863
+ }),
864
+ ).resolves.not.toThrow();
865
+ });
866
+
867
+ it('should add a default value to a column when using not null', async () => {
868
+ await store.insert({
869
+ tableName: TEST_TABLE as TABLE_NAMES,
870
+ record: { id: 1, name: 'Bob', createdAt: new Date(), updatedAt: new Date() },
871
+ });
872
+
873
+ await expect(
874
+ store.alterTable({
875
+ tableName: TEST_TABLE as TABLE_NAMES,
876
+ schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
877
+ ifNotExists: ['text_column'],
878
+ }),
879
+ ).resolves.not.toThrow();
880
+
881
+ await expect(
882
+ store.alterTable({
883
+ tableName: TEST_TABLE as TABLE_NAMES,
884
+ schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
885
+ ifNotExists: ['timestamp_column'],
886
+ }),
887
+ ).resolves.not.toThrow();
888
+
889
+ await expect(
890
+ store.alterTable({
891
+ tableName: TEST_TABLE as TABLE_NAMES,
892
+ schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
893
+ ifNotExists: ['bigint_column'],
894
+ }),
895
+ ).resolves.not.toThrow();
896
+
897
+ await expect(
898
+ store.alterTable({
899
+ tableName: TEST_TABLE as TABLE_NAMES,
900
+ schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
901
+ ifNotExists: ['jsonb_column'],
902
+ }),
903
+ ).resolves.not.toThrow();
904
+ });
905
+ });
906
+
845
907
  afterAll(async () => {
846
908
  await store.close();
847
909
  });
@@ -371,6 +371,73 @@ export class ClickhouseStore extends MastraStorage {
371
371
  }
372
372
  }
373
373
 
374
+ protected getSqlType(type: StorageColumn['type']): string {
375
+ switch (type) {
376
+ case 'text':
377
+ return 'String';
378
+ case 'timestamp':
379
+ return 'DateTime64(3)';
380
+ case 'integer':
381
+ case 'bigint':
382
+ return 'Int64';
383
+ case 'jsonb':
384
+ return 'String';
385
+ default:
386
+ return super.getSqlType(type); // fallback to base implementation
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Alters table schema to add columns if they don't exist
392
+ * @param tableName Name of the table
393
+ * @param schema Schema of the table
394
+ * @param ifNotExists Array of column names to add if they don't exist
395
+ */
396
+ async alterTable({
397
+ tableName,
398
+ schema,
399
+ ifNotExists,
400
+ }: {
401
+ tableName: TABLE_NAMES;
402
+ schema: Record<string, StorageColumn>;
403
+ ifNotExists: string[];
404
+ }): Promise<void> {
405
+ try {
406
+ // 1. Get existing columns
407
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
408
+ const result = await this.db.query({
409
+ query: describeSql,
410
+ });
411
+ const rows = await result.json();
412
+ const existingColumnNames = new Set(rows.data.map((row: any) => row.name.toLowerCase()));
413
+
414
+ // 2. Add missing columns
415
+ for (const columnName of ifNotExists) {
416
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
417
+ const columnDef = schema[columnName];
418
+ let sqlType = this.getSqlType(columnDef.type);
419
+ if (columnDef.nullable !== false) {
420
+ sqlType = `Nullable(${sqlType})`;
421
+ }
422
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
423
+ // Use backticks or double quotes as needed for identifiers
424
+ const alterSql =
425
+ `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
426
+
427
+ await this.db.query({
428
+ query: alterSql,
429
+ });
430
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
431
+ }
432
+ }
433
+ } catch (error) {
434
+ this.logger?.error?.(
435
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
436
+ );
437
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
438
+ }
439
+ }
440
+
374
441
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
375
442
  try {
376
443
  await this.db.query({