@mastra/pg 0.14.5 → 0.14.6-alpha.1
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/CHANGELOG.md +18 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/operations/index.d.ts.map +1 -1
- package/package.json +18 -5
- package/.turbo/turbo-build.log +0 -4
- package/docker-compose.perf.yaml +0 -21
- package/docker-compose.yaml +0 -14
- package/eslint.config.js +0 -6
- package/src/index.ts +0 -3
- package/src/storage/domains/legacy-evals/index.ts +0 -151
- package/src/storage/domains/memory/index.ts +0 -1028
- package/src/storage/domains/operations/index.ts +0 -368
- package/src/storage/domains/scores/index.ts +0 -297
- package/src/storage/domains/traces/index.ts +0 -160
- package/src/storage/domains/utils.ts +0 -12
- package/src/storage/domains/workflows/index.ts +0 -291
- package/src/storage/index.test.ts +0 -11
- package/src/storage/index.ts +0 -514
- package/src/storage/test-utils.ts +0 -377
- package/src/vector/filter.test.ts +0 -967
- package/src/vector/filter.ts +0 -136
- package/src/vector/index.test.ts +0 -2729
- package/src/vector/index.ts +0 -926
- package/src/vector/performance.helpers.ts +0 -286
- package/src/vector/prompt.ts +0 -101
- package/src/vector/sql-builder.ts +0 -358
- package/src/vector/types.ts +0 -16
- package/src/vector/vector.performance.test.ts +0 -367
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -17
- package/vitest.config.ts +0 -12
- package/vitest.perf.config.ts +0 -8
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import { createSampleThread } from '@internal/storage-test-utils';
|
|
2
|
-
import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
|
|
3
|
-
import pgPromise from 'pg-promise';
|
|
4
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { PostgresStore } from '.';
|
|
6
|
-
import type { PostgresConfig } from '.';
|
|
7
|
-
|
|
8
|
-
export const TEST_CONFIG: PostgresConfig = {
|
|
9
|
-
host: process.env.POSTGRES_HOST || 'localhost',
|
|
10
|
-
port: Number(process.env.POSTGRES_PORT) || 5434,
|
|
11
|
-
database: process.env.POSTGRES_DB || 'postgres',
|
|
12
|
-
user: process.env.POSTGRES_USER || 'postgres',
|
|
13
|
-
password: process.env.POSTGRES_PASSWORD || 'postgres',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const connectionString = `postgresql://${TEST_CONFIG.user}:${TEST_CONFIG.password}@${TEST_CONFIG.host}:${TEST_CONFIG.port}/${TEST_CONFIG.database}`;
|
|
17
|
-
|
|
18
|
-
export function pgTests() {
|
|
19
|
-
let store: PostgresStore;
|
|
20
|
-
|
|
21
|
-
describe('PG specific tests', () => {
|
|
22
|
-
beforeAll(async () => {
|
|
23
|
-
store = new PostgresStore(TEST_CONFIG);
|
|
24
|
-
await store.init();
|
|
25
|
-
});
|
|
26
|
-
afterAll(async () => {
|
|
27
|
-
try {
|
|
28
|
-
await store.close();
|
|
29
|
-
} catch {}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('Public Fields Access', () => {
|
|
33
|
-
it('should expose db field as public', () => {
|
|
34
|
-
expect(store.db).toBeDefined();
|
|
35
|
-
expect(typeof store.db).toBe('object');
|
|
36
|
-
expect(store.db.query).toBeDefined();
|
|
37
|
-
expect(typeof store.db.query).toBe('function');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should expose pgp field as public', () => {
|
|
41
|
-
expect(store.pgp).toBeDefined();
|
|
42
|
-
expect(typeof store.pgp).toBe('function');
|
|
43
|
-
expect(store.pgp.end).toBeDefined();
|
|
44
|
-
expect(typeof store.pgp.end).toBe('function');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should allow direct database queries via public db field', async () => {
|
|
48
|
-
const result = await store.db.one('SELECT 1 as test');
|
|
49
|
-
expect(result.test).toBe(1);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should allow access to pgp utilities via public pgp field', () => {
|
|
53
|
-
const helpers = store.pgp.helpers;
|
|
54
|
-
expect(helpers).toBeDefined();
|
|
55
|
-
expect(helpers.insert).toBeDefined();
|
|
56
|
-
expect(helpers.update).toBeDefined();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should maintain connection state through public db field', async () => {
|
|
60
|
-
// Test multiple queries to ensure connection state
|
|
61
|
-
const result1 = await store.db.one('SELECT NOW() as timestamp1');
|
|
62
|
-
const result2 = await store.db.one('SELECT NOW() as timestamp2');
|
|
63
|
-
|
|
64
|
-
expect(result1.timestamp1).toBeDefined();
|
|
65
|
-
expect(result2.timestamp2).toBeDefined();
|
|
66
|
-
expect(new Date(result2.timestamp2).getTime()).toBeGreaterThanOrEqual(new Date(result1.timestamp1).getTime());
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should throw error when pool is used after disconnect', async () => {
|
|
70
|
-
await store.close();
|
|
71
|
-
await expect(store.db.connect()).rejects.toThrow();
|
|
72
|
-
store = new PostgresStore(TEST_CONFIG);
|
|
73
|
-
await store.init();
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('PgStorage Table Name Quoting', () => {
|
|
78
|
-
const camelCaseTable = 'TestCamelCaseTable';
|
|
79
|
-
const snakeCaseTable = 'test_snake_case_table';
|
|
80
|
-
const BASE_SCHEMA = {
|
|
81
|
-
id: { type: 'integer', primaryKey: true, nullable: false },
|
|
82
|
-
name: { type: 'text', nullable: true },
|
|
83
|
-
createdAt: { type: 'timestamp', nullable: false },
|
|
84
|
-
updatedAt: { type: 'timestamp', nullable: false },
|
|
85
|
-
} as Record<string, StorageColumn>;
|
|
86
|
-
|
|
87
|
-
beforeEach(async () => {
|
|
88
|
-
// Only clear tables if store is initialized
|
|
89
|
-
try {
|
|
90
|
-
// Clear tables before each test
|
|
91
|
-
await store.clearTable({ tableName: camelCaseTable as TABLE_NAMES });
|
|
92
|
-
await store.clearTable({ tableName: snakeCaseTable as TABLE_NAMES });
|
|
93
|
-
} catch (error) {
|
|
94
|
-
// Ignore errors during table clearing
|
|
95
|
-
console.warn('Error clearing tables:', error);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
afterEach(async () => {
|
|
100
|
-
// Only clear tables if store is initialized
|
|
101
|
-
try {
|
|
102
|
-
// Clear tables before each test
|
|
103
|
-
await store.clearTable({ tableName: camelCaseTable as TABLE_NAMES });
|
|
104
|
-
await store.clearTable({ tableName: snakeCaseTable as TABLE_NAMES });
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// Ignore errors during table clearing
|
|
107
|
-
console.warn('Error clearing tables:', error);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should create and upsert to a camelCase table without quoting errors', async () => {
|
|
112
|
-
await expect(
|
|
113
|
-
store.createTable({
|
|
114
|
-
tableName: camelCaseTable as TABLE_NAMES,
|
|
115
|
-
schema: BASE_SCHEMA,
|
|
116
|
-
}),
|
|
117
|
-
).resolves.not.toThrow();
|
|
118
|
-
|
|
119
|
-
await store.insert({
|
|
120
|
-
tableName: camelCaseTable as TABLE_NAMES,
|
|
121
|
-
record: { id: '1', name: 'Alice', createdAt: new Date(), updatedAt: new Date() },
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const row: any = await store.load({
|
|
125
|
-
tableName: camelCaseTable as TABLE_NAMES,
|
|
126
|
-
keys: { id: '1' },
|
|
127
|
-
});
|
|
128
|
-
expect(row?.name).toBe('Alice');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should create and upsert to a snake_case table without quoting errors', async () => {
|
|
132
|
-
await expect(
|
|
133
|
-
store.createTable({
|
|
134
|
-
tableName: snakeCaseTable as TABLE_NAMES,
|
|
135
|
-
schema: BASE_SCHEMA,
|
|
136
|
-
}),
|
|
137
|
-
).resolves.not.toThrow();
|
|
138
|
-
|
|
139
|
-
await store.insert({
|
|
140
|
-
tableName: snakeCaseTable as TABLE_NAMES,
|
|
141
|
-
record: { id: '2', name: 'Bob', createdAt: new Date(), updatedAt: new Date() },
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const row: any = await store.load({
|
|
145
|
-
tableName: snakeCaseTable as TABLE_NAMES,
|
|
146
|
-
keys: { id: '2' },
|
|
147
|
-
});
|
|
148
|
-
expect(row?.name).toBe('Bob');
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('Permission Handling', () => {
|
|
153
|
-
const schemaRestrictedUser = 'mastra_schema_restricted_storage';
|
|
154
|
-
const restrictedPassword = 'test123';
|
|
155
|
-
const testSchema = 'testSchema';
|
|
156
|
-
let adminDb: pgPromise.IDatabase<{}>;
|
|
157
|
-
let pgpAdmin: pgPromise.IMain;
|
|
158
|
-
|
|
159
|
-
beforeAll(async () => {
|
|
160
|
-
// Re-initialize the main store for subsequent tests
|
|
161
|
-
|
|
162
|
-
await store.init();
|
|
163
|
-
|
|
164
|
-
// Create a separate pg-promise instance for admin operations
|
|
165
|
-
pgpAdmin = pgPromise();
|
|
166
|
-
adminDb = pgpAdmin(connectionString);
|
|
167
|
-
try {
|
|
168
|
-
await adminDb.tx(async t => {
|
|
169
|
-
// Drop the test schema if it exists from previous runs
|
|
170
|
-
await t.none(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
|
|
171
|
-
|
|
172
|
-
// Create schema restricted user with minimal permissions
|
|
173
|
-
await t.none(`
|
|
174
|
-
DO $$
|
|
175
|
-
BEGIN
|
|
176
|
-
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${schemaRestrictedUser}') THEN
|
|
177
|
-
CREATE USER ${schemaRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
|
|
178
|
-
END IF;
|
|
179
|
-
END
|
|
180
|
-
$$;`);
|
|
181
|
-
|
|
182
|
-
// Grant only connect and usage to schema restricted user
|
|
183
|
-
await t.none(`
|
|
184
|
-
REVOKE ALL ON DATABASE ${(TEST_CONFIG as any).database} FROM ${schemaRestrictedUser};
|
|
185
|
-
GRANT CONNECT ON DATABASE ${(TEST_CONFIG as any).database} TO ${schemaRestrictedUser};
|
|
186
|
-
REVOKE ALL ON SCHEMA public FROM ${schemaRestrictedUser};
|
|
187
|
-
GRANT USAGE ON SCHEMA public TO ${schemaRestrictedUser};
|
|
188
|
-
`);
|
|
189
|
-
});
|
|
190
|
-
} catch (error) {
|
|
191
|
-
// Clean up the database connection on error
|
|
192
|
-
pgpAdmin.end();
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
afterAll(async () => {
|
|
198
|
-
try {
|
|
199
|
-
// Then clean up test user in admin connection
|
|
200
|
-
await adminDb.tx(async t => {
|
|
201
|
-
await t.none(`
|
|
202
|
-
REASSIGN OWNED BY ${schemaRestrictedUser} TO postgres;
|
|
203
|
-
DROP OWNED BY ${schemaRestrictedUser};
|
|
204
|
-
DROP USER IF EXISTS ${schemaRestrictedUser};
|
|
205
|
-
`);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Finally clean up admin connection
|
|
209
|
-
if (pgpAdmin) {
|
|
210
|
-
pgpAdmin.end();
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
console.error('Error cleaning up test user:', error);
|
|
214
|
-
if (pgpAdmin) pgpAdmin.end();
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Schema Creation', () => {
|
|
219
|
-
beforeEach(async () => {
|
|
220
|
-
// Create a fresh connection for each test
|
|
221
|
-
const tempPgp = pgPromise();
|
|
222
|
-
const tempDb = tempPgp(connectionString);
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
// Ensure schema doesn't exist before each test
|
|
226
|
-
await tempDb.none(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
|
|
227
|
-
|
|
228
|
-
// Ensure no active connections from restricted user
|
|
229
|
-
await tempDb.none(`
|
|
230
|
-
SELECT pg_terminate_backend(pid)
|
|
231
|
-
FROM pg_stat_activity
|
|
232
|
-
WHERE usename = '${schemaRestrictedUser}'
|
|
233
|
-
`);
|
|
234
|
-
} finally {
|
|
235
|
-
tempPgp.end(); // Always clean up the connection
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
afterEach(async () => {
|
|
240
|
-
// Create a fresh connection for cleanup
|
|
241
|
-
const tempPgp = pgPromise();
|
|
242
|
-
const tempDb = tempPgp(connectionString);
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
// Clean up any connections from the restricted user and drop schema
|
|
246
|
-
await tempDb.none(`
|
|
247
|
-
DO $$
|
|
248
|
-
BEGIN
|
|
249
|
-
-- Terminate connections
|
|
250
|
-
PERFORM pg_terminate_backend(pid)
|
|
251
|
-
FROM pg_stat_activity
|
|
252
|
-
WHERE usename = '${schemaRestrictedUser}';
|
|
253
|
-
|
|
254
|
-
-- Drop schema
|
|
255
|
-
DROP SCHEMA IF EXISTS ${testSchema} CASCADE;
|
|
256
|
-
END $$;
|
|
257
|
-
`);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
console.error('Error in afterEach cleanup:', error);
|
|
260
|
-
} finally {
|
|
261
|
-
tempPgp.end(); // Always clean up the connection
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('should fail when user lacks CREATE privilege', async () => {
|
|
266
|
-
const restrictedDB = new PostgresStore({
|
|
267
|
-
...TEST_CONFIG,
|
|
268
|
-
user: schemaRestrictedUser,
|
|
269
|
-
password: restrictedPassword,
|
|
270
|
-
schemaName: testSchema,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Create a fresh connection for verification
|
|
274
|
-
const tempPgp = pgPromise();
|
|
275
|
-
const tempDb = tempPgp(connectionString);
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
// Test schema creation by initializing the store
|
|
279
|
-
await expect(async () => {
|
|
280
|
-
await restrictedDB.init();
|
|
281
|
-
}).rejects.toThrow(
|
|
282
|
-
`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`,
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// Verify schema was not created
|
|
286
|
-
const exists = await tempDb.oneOrNone(
|
|
287
|
-
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
|
|
288
|
-
[testSchema],
|
|
289
|
-
);
|
|
290
|
-
expect(exists?.exists).toBe(false);
|
|
291
|
-
} finally {
|
|
292
|
-
await restrictedDB.close();
|
|
293
|
-
tempPgp.end(); // Clean up the verification connection
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('should fail with schema creation error when saving thread', async () => {
|
|
298
|
-
const restrictedDB = new PostgresStore({
|
|
299
|
-
...TEST_CONFIG,
|
|
300
|
-
user: schemaRestrictedUser,
|
|
301
|
-
password: restrictedPassword,
|
|
302
|
-
schemaName: testSchema,
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Create a fresh connection for verification
|
|
306
|
-
const tempPgp = pgPromise();
|
|
307
|
-
const tempDb = tempPgp(connectionString);
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
await expect(async () => {
|
|
311
|
-
await restrictedDB.init();
|
|
312
|
-
const thread = createSampleThread();
|
|
313
|
-
await restrictedDB.saveThread({ thread });
|
|
314
|
-
}).rejects.toThrow(
|
|
315
|
-
`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`,
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
// Verify schema was not created
|
|
319
|
-
const exists = await tempDb.oneOrNone(
|
|
320
|
-
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
|
|
321
|
-
[testSchema],
|
|
322
|
-
);
|
|
323
|
-
expect(exists?.exists).toBe(false);
|
|
324
|
-
} finally {
|
|
325
|
-
await restrictedDB.close();
|
|
326
|
-
tempPgp.end(); // Clean up the verification connection
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
describe('Validation', () => {
|
|
333
|
-
const validConfig = TEST_CONFIG as any;
|
|
334
|
-
it('throws if connectionString is empty', () => {
|
|
335
|
-
expect(() => new PostgresStore({ connectionString: '' })).toThrow();
|
|
336
|
-
expect(() => new PostgresStore({ ...validConfig, connectionString: '' })).toThrow();
|
|
337
|
-
});
|
|
338
|
-
it('throws if host is missing or empty', () => {
|
|
339
|
-
expect(() => new PostgresStore({ ...validConfig, host: '' })).toThrow();
|
|
340
|
-
const { host, ...rest } = validConfig;
|
|
341
|
-
expect(() => new PostgresStore(rest as any)).toThrow();
|
|
342
|
-
});
|
|
343
|
-
it('throws if connectionString is empty', () => {
|
|
344
|
-
expect(() => new PostgresStore({ connectionString: '' })).toThrow();
|
|
345
|
-
const { database, ...rest } = validConfig;
|
|
346
|
-
expect(() => new PostgresStore(rest as any)).toThrow();
|
|
347
|
-
});
|
|
348
|
-
it('throws if user is missing or empty', () => {
|
|
349
|
-
expect(() => new PostgresStore({ ...validConfig, user: '' })).toThrow();
|
|
350
|
-
const { user, ...rest } = validConfig;
|
|
351
|
-
expect(() => new PostgresStore(rest as any)).toThrow();
|
|
352
|
-
});
|
|
353
|
-
it('throws if database is missing or empty', () => {
|
|
354
|
-
expect(() => new PostgresStore({ ...validConfig, database: '' })).toThrow();
|
|
355
|
-
const { database, ...rest } = validConfig;
|
|
356
|
-
expect(() => new PostgresStore(rest as any)).toThrow();
|
|
357
|
-
});
|
|
358
|
-
it('throws if password is missing or empty', () => {
|
|
359
|
-
expect(() => new PostgresStore({ ...validConfig, password: '' })).toThrow();
|
|
360
|
-
const { password, ...rest } = validConfig;
|
|
361
|
-
expect(() => new PostgresStore(rest as any)).toThrow();
|
|
362
|
-
});
|
|
363
|
-
it('does not throw on valid config (host-based)', () => {
|
|
364
|
-
expect(() => new PostgresStore(validConfig)).not.toThrow();
|
|
365
|
-
});
|
|
366
|
-
it('does not throw on non-empty connection string', () => {
|
|
367
|
-
expect(() => new PostgresStore({ connectionString })).not.toThrow();
|
|
368
|
-
});
|
|
369
|
-
it('throws if store is not initialized', () => {
|
|
370
|
-
expect(() => new PostgresStore(validConfig).db.any('SELECT 1')).toThrow(
|
|
371
|
-
/PostgresStore: Store is not initialized/,
|
|
372
|
-
);
|
|
373
|
-
expect(() => new PostgresStore(validConfig).pgp).toThrow(/PostgresStore: Store is not initialized/);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
}
|