@mastra/pg 0.2.10-alpha.3 → 0.2.10-alpha.5
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 +13 -0
- package/dist/_tsup-dts-rollup.d.cts +17 -1
- package/dist/_tsup-dts-rollup.d.ts +17 -1
- package/dist/index.cjs +154 -39
- package/dist/index.js +154 -39
- package/package.json +2 -2
- package/src/storage/index.test.ts +277 -12
- package/src/storage/index.ts +90 -21
- package/src/vector/index.test.ts +483 -0
- package/src/vector/index.ts +101 -21
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import type { MetricResult } from '@mastra/core/eval';
|
|
3
|
+
import type { MessageType } from '@mastra/core/memory';
|
|
3
4
|
import { TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, TABLE_THREADS, TABLE_EVALS } from '@mastra/core/storage';
|
|
4
5
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
5
|
-
import
|
|
6
|
+
import pgPromise from 'pg-promise';
|
|
7
|
+
import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
|
|
6
8
|
|
|
7
9
|
import { PostgresStore } from '.';
|
|
8
10
|
import type { PostgresConfig } from '.';
|
|
@@ -15,6 +17,8 @@ const TEST_CONFIG: PostgresConfig = {
|
|
|
15
17
|
password: process.env.POSTGRES_PASSWORD || 'postgres',
|
|
16
18
|
};
|
|
17
19
|
|
|
20
|
+
const connectionString = `postgresql://${TEST_CONFIG.user}:${TEST_CONFIG.password}@${TEST_CONFIG.host}:${TEST_CONFIG.port}/${TEST_CONFIG.database}`;
|
|
21
|
+
|
|
18
22
|
// Sample test data factory functions
|
|
19
23
|
const createSampleThread = () => ({
|
|
20
24
|
id: `thread-${randomUUID()}`,
|
|
@@ -87,11 +91,17 @@ describe('PostgresStore', () => {
|
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
beforeEach(async () => {
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
// Only clear tables if store is initialized
|
|
95
|
+
try {
|
|
96
|
+
// Clear tables before each test
|
|
97
|
+
await store.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
|
|
98
|
+
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
99
|
+
await store.clearTable({ tableName: TABLE_THREADS });
|
|
100
|
+
await store.clearTable({ tableName: TABLE_EVALS });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Ignore errors during table clearing
|
|
103
|
+
console.warn('Error clearing tables:', error);
|
|
104
|
+
}
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
describe('Thread Operations', () => {
|
|
@@ -192,9 +202,9 @@ describe('PostgresStore', () => {
|
|
|
192
202
|
await store.__saveThread({ thread });
|
|
193
203
|
|
|
194
204
|
const messages = [
|
|
195
|
-
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'First' }] },
|
|
196
|
-
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'Second' }] },
|
|
197
|
-
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'Third' }] },
|
|
205
|
+
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'First' }] as MessageType['content'] },
|
|
206
|
+
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'Second' }] as MessageType['content'] },
|
|
207
|
+
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'Third' }] as MessageType['content'] },
|
|
198
208
|
];
|
|
199
209
|
|
|
200
210
|
await store.__saveMessages({ messages });
|
|
@@ -204,7 +214,7 @@ describe('PostgresStore', () => {
|
|
|
204
214
|
|
|
205
215
|
// Verify order is maintained
|
|
206
216
|
retrievedMessages.forEach((msg, idx) => {
|
|
207
|
-
expect(msg.content[0].text).toBe(messages[idx].content[0].text);
|
|
217
|
+
expect((msg.content[0] as any).text).toBe((messages[idx].content[0] as any).text);
|
|
208
218
|
});
|
|
209
219
|
});
|
|
210
220
|
|
|
@@ -214,7 +224,7 @@ describe('PostgresStore', () => {
|
|
|
214
224
|
|
|
215
225
|
const messages = [
|
|
216
226
|
createSampleMessage(thread.id),
|
|
217
|
-
{ ...createSampleMessage(thread.id), id: null }, // This will cause an error
|
|
227
|
+
{ ...createSampleMessage(thread.id), id: null } as any, // This will cause an error
|
|
218
228
|
];
|
|
219
229
|
|
|
220
230
|
await expect(store.__saveMessages({ messages })).rejects.toThrow();
|
|
@@ -645,7 +655,262 @@ describe('PostgresStore', () => {
|
|
|
645
655
|
});
|
|
646
656
|
});
|
|
647
657
|
|
|
658
|
+
describe('Schema Support', () => {
|
|
659
|
+
const customSchema = 'mastra_test';
|
|
660
|
+
let customSchemaStore: PostgresStore;
|
|
661
|
+
|
|
662
|
+
beforeAll(async () => {
|
|
663
|
+
customSchemaStore = new PostgresStore({
|
|
664
|
+
...TEST_CONFIG,
|
|
665
|
+
schema: customSchema,
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
await customSchemaStore.init();
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
afterAll(async () => {
|
|
672
|
+
await customSchemaStore.close();
|
|
673
|
+
// Re-initialize the main store for subsequent tests
|
|
674
|
+
store = new PostgresStore(TEST_CONFIG);
|
|
675
|
+
await store.init();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
describe('Constructor and Initialization', () => {
|
|
679
|
+
it('should accept connectionString directly', () => {
|
|
680
|
+
// Use existing store instead of creating new one
|
|
681
|
+
expect(store).toBeInstanceOf(PostgresStore);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should accept config object with schema', () => {
|
|
685
|
+
// Use existing custom schema store
|
|
686
|
+
expect(customSchemaStore).toBeInstanceOf(PostgresStore);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe('Schema Operations', () => {
|
|
691
|
+
it('should create and query tables in custom schema', async () => {
|
|
692
|
+
// Create thread in custom schema
|
|
693
|
+
const thread = createSampleThread();
|
|
694
|
+
await customSchemaStore.__saveThread({ thread });
|
|
695
|
+
|
|
696
|
+
// Verify thread exists in custom schema
|
|
697
|
+
const retrieved = await customSchemaStore.__getThreadById({ threadId: thread.id });
|
|
698
|
+
expect(retrieved?.title).toBe(thread.title);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('should allow same table names in different schemas', async () => {
|
|
702
|
+
// Create threads in both schemas
|
|
703
|
+
const defaultThread = createSampleThread();
|
|
704
|
+
const customThread = createSampleThread();
|
|
705
|
+
|
|
706
|
+
await store.__saveThread({ thread: defaultThread });
|
|
707
|
+
await customSchemaStore.__saveThread({ thread: customThread });
|
|
708
|
+
|
|
709
|
+
// Verify threads exist in respective schemas
|
|
710
|
+
const defaultResult = await store.__getThreadById({ threadId: defaultThread.id });
|
|
711
|
+
const customResult = await customSchemaStore.__getThreadById({ threadId: customThread.id });
|
|
712
|
+
|
|
713
|
+
expect(defaultResult?.id).toBe(defaultThread.id);
|
|
714
|
+
expect(customResult?.id).toBe(customThread.id);
|
|
715
|
+
|
|
716
|
+
// Verify cross-schema isolation
|
|
717
|
+
const defaultInCustom = await customSchemaStore.__getThreadById({ threadId: defaultThread.id });
|
|
718
|
+
const customInDefault = await store.__getThreadById({ threadId: customThread.id });
|
|
719
|
+
|
|
720
|
+
expect(defaultInCustom).toBeNull();
|
|
721
|
+
expect(customInDefault).toBeNull();
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe('Permission Handling', () => {
|
|
727
|
+
const schemaRestrictedUser = 'mastra_schema_restricted_storage';
|
|
728
|
+
const restrictedPassword = 'test123';
|
|
729
|
+
const testSchema = 'test_schema';
|
|
730
|
+
let adminDb: pgPromise.IDatabase<{}>;
|
|
731
|
+
let pgpAdmin: pgPromise.IMain;
|
|
732
|
+
|
|
733
|
+
beforeAll(async () => {
|
|
734
|
+
// Create a separate pg-promise instance for admin operations
|
|
735
|
+
pgpAdmin = pgPromise();
|
|
736
|
+
adminDb = pgpAdmin(connectionString);
|
|
737
|
+
try {
|
|
738
|
+
await adminDb.tx(async t => {
|
|
739
|
+
// Drop the test schema if it exists from previous runs
|
|
740
|
+
await t.none(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
|
|
741
|
+
|
|
742
|
+
// Create schema restricted user with minimal permissions
|
|
743
|
+
await t.none(`
|
|
744
|
+
DO $$
|
|
745
|
+
BEGIN
|
|
746
|
+
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${schemaRestrictedUser}') THEN
|
|
747
|
+
CREATE USER ${schemaRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
|
|
748
|
+
END IF;
|
|
749
|
+
END
|
|
750
|
+
$$;`);
|
|
751
|
+
|
|
752
|
+
// Grant only connect and usage to schema restricted user
|
|
753
|
+
await t.none(`
|
|
754
|
+
REVOKE ALL ON DATABASE ${TEST_CONFIG.database} FROM ${schemaRestrictedUser};
|
|
755
|
+
GRANT CONNECT ON DATABASE ${TEST_CONFIG.database} TO ${schemaRestrictedUser};
|
|
756
|
+
REVOKE ALL ON SCHEMA public FROM ${schemaRestrictedUser};
|
|
757
|
+
GRANT USAGE ON SCHEMA public TO ${schemaRestrictedUser};
|
|
758
|
+
`);
|
|
759
|
+
});
|
|
760
|
+
} catch (error) {
|
|
761
|
+
// Clean up the database connection on error
|
|
762
|
+
pgpAdmin.end();
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
afterAll(async () => {
|
|
768
|
+
try {
|
|
769
|
+
// First close any store connections
|
|
770
|
+
if (store) {
|
|
771
|
+
await store.close();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Then clean up test user in admin connection
|
|
775
|
+
await adminDb.tx(async t => {
|
|
776
|
+
await t.none(`
|
|
777
|
+
REASSIGN OWNED BY ${schemaRestrictedUser} TO postgres;
|
|
778
|
+
DROP OWNED BY ${schemaRestrictedUser};
|
|
779
|
+
DROP USER IF EXISTS ${schemaRestrictedUser};
|
|
780
|
+
`);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Finally clean up admin connection
|
|
784
|
+
if (pgpAdmin) {
|
|
785
|
+
pgpAdmin.end();
|
|
786
|
+
}
|
|
787
|
+
} catch (error) {
|
|
788
|
+
console.error('Error cleaning up test user:', error);
|
|
789
|
+
// Still try to clean up connections even if user cleanup fails
|
|
790
|
+
if (store) await store.close();
|
|
791
|
+
if (pgpAdmin) pgpAdmin.end();
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
describe('Schema Creation', () => {
|
|
796
|
+
beforeEach(async () => {
|
|
797
|
+
// Create a fresh connection for each test
|
|
798
|
+
const tempPgp = pgPromise();
|
|
799
|
+
const tempDb = tempPgp(connectionString);
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
// Ensure schema doesn't exist before each test
|
|
803
|
+
await tempDb.none(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
|
|
804
|
+
|
|
805
|
+
// Ensure no active connections from restricted user
|
|
806
|
+
await tempDb.none(`
|
|
807
|
+
SELECT pg_terminate_backend(pid)
|
|
808
|
+
FROM pg_stat_activity
|
|
809
|
+
WHERE usename = '${schemaRestrictedUser}'
|
|
810
|
+
`);
|
|
811
|
+
} finally {
|
|
812
|
+
tempPgp.end(); // Always clean up the connection
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
afterEach(async () => {
|
|
817
|
+
// Create a fresh connection for cleanup
|
|
818
|
+
const tempPgp = pgPromise();
|
|
819
|
+
const tempDb = tempPgp(connectionString);
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
// Clean up any connections from the restricted user and drop schema
|
|
823
|
+
await tempDb.none(`
|
|
824
|
+
DO $$
|
|
825
|
+
BEGIN
|
|
826
|
+
-- Terminate connections
|
|
827
|
+
PERFORM pg_terminate_backend(pid)
|
|
828
|
+
FROM pg_stat_activity
|
|
829
|
+
WHERE usename = '${schemaRestrictedUser}';
|
|
830
|
+
|
|
831
|
+
-- Drop schema
|
|
832
|
+
DROP SCHEMA IF EXISTS ${testSchema} CASCADE;
|
|
833
|
+
END $$;
|
|
834
|
+
`);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
console.error('Error in afterEach cleanup:', error);
|
|
837
|
+
} finally {
|
|
838
|
+
tempPgp.end(); // Always clean up the connection
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should fail when user lacks CREATE privilege', async () => {
|
|
843
|
+
const restrictedDB = new PostgresStore({
|
|
844
|
+
...TEST_CONFIG,
|
|
845
|
+
user: schemaRestrictedUser,
|
|
846
|
+
password: restrictedPassword,
|
|
847
|
+
schema: testSchema,
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// Create a fresh connection for verification
|
|
851
|
+
const tempPgp = pgPromise();
|
|
852
|
+
const tempDb = tempPgp(connectionString);
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
// Test schema creation by initializing the store
|
|
856
|
+
await expect(async () => {
|
|
857
|
+
await restrictedDB.init();
|
|
858
|
+
}).rejects.toThrow(
|
|
859
|
+
`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`,
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
// Verify schema was not created
|
|
863
|
+
const exists = await tempDb.oneOrNone(
|
|
864
|
+
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
|
|
865
|
+
[testSchema],
|
|
866
|
+
);
|
|
867
|
+
expect(exists?.exists).toBe(false);
|
|
868
|
+
} finally {
|
|
869
|
+
await restrictedDB.close();
|
|
870
|
+
tempPgp.end(); // Clean up the verification connection
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('should fail with schema creation error when saving thread', async () => {
|
|
875
|
+
const restrictedDB = new PostgresStore({
|
|
876
|
+
...TEST_CONFIG,
|
|
877
|
+
user: schemaRestrictedUser,
|
|
878
|
+
password: restrictedPassword,
|
|
879
|
+
schema: testSchema,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// Create a fresh connection for verification
|
|
883
|
+
const tempPgp = pgPromise();
|
|
884
|
+
const tempDb = tempPgp(connectionString);
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
await expect(async () => {
|
|
888
|
+
await restrictedDB.init();
|
|
889
|
+
const thread = createSampleThread();
|
|
890
|
+
await restrictedDB.__saveThread({ thread });
|
|
891
|
+
}).rejects.toThrow(
|
|
892
|
+
`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`,
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// Verify schema was not created
|
|
896
|
+
const exists = await tempDb.oneOrNone(
|
|
897
|
+
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
|
|
898
|
+
[testSchema],
|
|
899
|
+
);
|
|
900
|
+
expect(exists?.exists).toBe(false);
|
|
901
|
+
} finally {
|
|
902
|
+
await restrictedDB.close();
|
|
903
|
+
tempPgp.end(); // Clean up the verification connection
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
|
|
648
909
|
afterAll(async () => {
|
|
649
|
-
|
|
910
|
+
try {
|
|
911
|
+
await store.close();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
console.warn('Error closing store:', error);
|
|
914
|
+
}
|
|
650
915
|
});
|
|
651
916
|
});
|
package/src/storage/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
|
13
13
|
import pgPromise from 'pg-promise';
|
|
14
14
|
import type { ISSLConfig } from 'pg-promise/typescript/pg-subset';
|
|
15
15
|
|
|
16
|
-
export type PostgresConfig =
|
|
16
|
+
export type PostgresConfig = { schema?: string } & (
|
|
17
17
|
| {
|
|
18
18
|
host: string;
|
|
19
19
|
port: number;
|
|
@@ -24,15 +24,20 @@ export type PostgresConfig =
|
|
|
24
24
|
}
|
|
25
25
|
| {
|
|
26
26
|
connectionString: string;
|
|
27
|
-
}
|
|
27
|
+
}
|
|
28
|
+
);
|
|
28
29
|
|
|
29
30
|
export class PostgresStore extends MastraStorage {
|
|
30
31
|
private db: pgPromise.IDatabase<{}>;
|
|
31
32
|
private pgp: pgPromise.IMain;
|
|
33
|
+
private schema?: string;
|
|
34
|
+
private setupSchemaPromise: Promise<void> | null = null;
|
|
35
|
+
private schemaSetupComplete: boolean | undefined = undefined;
|
|
32
36
|
|
|
33
37
|
constructor(config: PostgresConfig) {
|
|
34
38
|
super({ name: 'PostgresStore' });
|
|
35
39
|
this.pgp = pgPromise();
|
|
40
|
+
this.schema = config.schema;
|
|
36
41
|
this.db = this.pgp(
|
|
37
42
|
`connectionString` in config
|
|
38
43
|
? { connectionString: config.connectionString }
|
|
@@ -47,9 +52,13 @@ export class PostgresStore extends MastraStorage {
|
|
|
47
52
|
);
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
private getTableName(indexName: string) {
|
|
56
|
+
return this.schema ? `${this.schema}."${indexName}"` : `"${indexName}"`;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
51
60
|
try {
|
|
52
|
-
const baseQuery = `SELECT * FROM ${TABLE_EVALS} WHERE agent_name = $1`;
|
|
61
|
+
const baseQuery = `SELECT * FROM ${this.getTableName(TABLE_EVALS)} WHERE agent_name = $1`;
|
|
53
62
|
const typeCondition =
|
|
54
63
|
type === 'test'
|
|
55
64
|
? " AND test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL"
|
|
@@ -186,7 +195,10 @@ export class PostgresStore extends MastraStorage {
|
|
|
186
195
|
endTime: string;
|
|
187
196
|
other: any;
|
|
188
197
|
createdAt: string;
|
|
189
|
-
}>(
|
|
198
|
+
}>(
|
|
199
|
+
`SELECT * FROM ${this.getTableName(TABLE_TRACES)} ${whereClause} ORDER BY "createdAt" DESC LIMIT ${limit} OFFSET ${offset}`,
|
|
200
|
+
args,
|
|
201
|
+
);
|
|
190
202
|
|
|
191
203
|
if (!result) {
|
|
192
204
|
return [];
|
|
@@ -210,6 +222,55 @@ export class PostgresStore extends MastraStorage {
|
|
|
210
222
|
})) as any;
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
private async setupSchema() {
|
|
226
|
+
if (!this.schema || this.schemaSetupComplete) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!this.setupSchemaPromise) {
|
|
231
|
+
this.setupSchemaPromise = (async () => {
|
|
232
|
+
try {
|
|
233
|
+
// First check if schema exists and we have usage permission
|
|
234
|
+
const schemaExists = await this.db.oneOrNone(
|
|
235
|
+
`
|
|
236
|
+
SELECT EXISTS (
|
|
237
|
+
SELECT 1 FROM information_schema.schemata
|
|
238
|
+
WHERE schema_name = $1
|
|
239
|
+
)
|
|
240
|
+
`,
|
|
241
|
+
[this.schema],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (!schemaExists?.exists) {
|
|
245
|
+
try {
|
|
246
|
+
await this.db.none(`CREATE SCHEMA IF NOT EXISTS ${this.schema}`);
|
|
247
|
+
this.logger.info(`Schema "${this.schema}" created successfully`);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.logger.error(`Failed to create schema "${this.schema}"`, { error });
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Unable to create schema "${this.schema}". This requires CREATE privilege on the database. ` +
|
|
252
|
+
`Either create the schema manually or grant CREATE privilege to the user.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// If we got here, schema exists and we can use it
|
|
258
|
+
this.schemaSetupComplete = true;
|
|
259
|
+
this.logger.debug(`Schema "${this.schema}" is ready for use`);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
// Reset flags so we can retry
|
|
262
|
+
this.schemaSetupComplete = undefined;
|
|
263
|
+
this.setupSchemaPromise = null;
|
|
264
|
+
throw error;
|
|
265
|
+
} finally {
|
|
266
|
+
this.setupSchemaPromise = null;
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await this.setupSchemaPromise;
|
|
272
|
+
}
|
|
273
|
+
|
|
213
274
|
async createTable({
|
|
214
275
|
tableName,
|
|
215
276
|
schema,
|
|
@@ -227,8 +288,13 @@ export class PostgresStore extends MastraStorage {
|
|
|
227
288
|
})
|
|
228
289
|
.join(',\n');
|
|
229
290
|
|
|
291
|
+
// Create schema if it doesn't exist
|
|
292
|
+
if (this.schema) {
|
|
293
|
+
await this.setupSchema();
|
|
294
|
+
}
|
|
295
|
+
|
|
230
296
|
const sql = `
|
|
231
|
-
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
297
|
+
CREATE TABLE IF NOT EXISTS ${this.getTableName(tableName)} (
|
|
232
298
|
${columns}
|
|
233
299
|
);
|
|
234
300
|
${
|
|
@@ -238,7 +304,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
238
304
|
IF NOT EXISTS (
|
|
239
305
|
SELECT 1 FROM pg_constraint WHERE conname = 'mastra_workflow_snapshot_workflow_name_run_id_key'
|
|
240
306
|
) THEN
|
|
241
|
-
ALTER TABLE ${tableName}
|
|
307
|
+
ALTER TABLE ${this.getTableName(tableName)}
|
|
242
308
|
ADD CONSTRAINT mastra_workflow_snapshot_workflow_name_run_id_key
|
|
243
309
|
UNIQUE (workflow_name, run_id);
|
|
244
310
|
END IF;
|
|
@@ -257,7 +323,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
257
323
|
|
|
258
324
|
async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
259
325
|
try {
|
|
260
|
-
await this.db.none(`TRUNCATE TABLE ${tableName} CASCADE`);
|
|
326
|
+
await this.db.none(`TRUNCATE TABLE ${this.getTableName(tableName)} CASCADE`);
|
|
261
327
|
} catch (error) {
|
|
262
328
|
console.error(`Error clearing table ${tableName}:`, error);
|
|
263
329
|
throw error;
|
|
@@ -271,7 +337,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
271
337
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
|
272
338
|
|
|
273
339
|
await this.db.none(
|
|
274
|
-
`INSERT INTO ${tableName} (${columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`,
|
|
340
|
+
`INSERT INTO ${this.getTableName(tableName)} (${columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`,
|
|
275
341
|
values,
|
|
276
342
|
);
|
|
277
343
|
} catch (error) {
|
|
@@ -286,7 +352,10 @@ export class PostgresStore extends MastraStorage {
|
|
|
286
352
|
const conditions = keyEntries.map(([key], index) => `"${key}" = $${index + 1}`).join(' AND ');
|
|
287
353
|
const values = keyEntries.map(([_, value]) => value);
|
|
288
354
|
|
|
289
|
-
const result = await this.db.oneOrNone<R>(
|
|
355
|
+
const result = await this.db.oneOrNone<R>(
|
|
356
|
+
`SELECT * FROM ${this.getTableName(tableName)} WHERE ${conditions}`,
|
|
357
|
+
values,
|
|
358
|
+
);
|
|
290
359
|
|
|
291
360
|
if (!result) {
|
|
292
361
|
return null;
|
|
@@ -318,7 +387,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
318
387
|
metadata,
|
|
319
388
|
"createdAt",
|
|
320
389
|
"updatedAt"
|
|
321
|
-
FROM
|
|
390
|
+
FROM ${this.getTableName(TABLE_THREADS)}
|
|
322
391
|
WHERE id = $1`,
|
|
323
392
|
[threadId],
|
|
324
393
|
);
|
|
@@ -349,7 +418,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
349
418
|
metadata,
|
|
350
419
|
"createdAt",
|
|
351
420
|
"updatedAt"
|
|
352
|
-
FROM
|
|
421
|
+
FROM ${this.getTableName(TABLE_THREADS)}
|
|
353
422
|
WHERE "resourceId" = $1`,
|
|
354
423
|
[resourceId],
|
|
355
424
|
);
|
|
@@ -369,7 +438,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
369
438
|
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
370
439
|
try {
|
|
371
440
|
await this.db.none(
|
|
372
|
-
`INSERT INTO
|
|
441
|
+
`INSERT INTO ${this.getTableName(TABLE_THREADS)} (
|
|
373
442
|
id,
|
|
374
443
|
"resourceId",
|
|
375
444
|
title,
|
|
@@ -423,7 +492,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
423
492
|
};
|
|
424
493
|
|
|
425
494
|
const thread = await this.db.one<StorageThreadType>(
|
|
426
|
-
`UPDATE
|
|
495
|
+
`UPDATE ${this.getTableName(TABLE_THREADS)}
|
|
427
496
|
SET title = $1,
|
|
428
497
|
metadata = $2,
|
|
429
498
|
"updatedAt" = $3
|
|
@@ -448,10 +517,10 @@ export class PostgresStore extends MastraStorage {
|
|
|
448
517
|
try {
|
|
449
518
|
await this.db.tx(async t => {
|
|
450
519
|
// First delete all messages associated with this thread
|
|
451
|
-
await t.none(`DELETE FROM
|
|
520
|
+
await t.none(`DELETE FROM ${this.getTableName(TABLE_MESSAGES)} WHERE thread_id = $1`, [threadId]);
|
|
452
521
|
|
|
453
522
|
// Then delete the thread
|
|
454
|
-
await t.none(`DELETE FROM
|
|
523
|
+
await t.none(`DELETE FROM ${this.getTableName(TABLE_THREADS)} WHERE id = $1`, [threadId]);
|
|
455
524
|
});
|
|
456
525
|
} catch (error) {
|
|
457
526
|
console.error('Error deleting thread:', error);
|
|
@@ -472,7 +541,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
472
541
|
SELECT
|
|
473
542
|
*,
|
|
474
543
|
ROW_NUMBER() OVER (ORDER BY "createdAt" DESC) as row_num
|
|
475
|
-
FROM
|
|
544
|
+
FROM ${this.getTableName(TABLE_MESSAGES)}
|
|
476
545
|
WHERE thread_id = $1
|
|
477
546
|
)
|
|
478
547
|
SELECT
|
|
@@ -518,7 +587,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
518
587
|
type,
|
|
519
588
|
"createdAt",
|
|
520
589
|
thread_id AS "threadId"
|
|
521
|
-
FROM
|
|
590
|
+
FROM ${this.getTableName(TABLE_MESSAGES)}
|
|
522
591
|
WHERE thread_id = $1
|
|
523
592
|
AND id != ALL($2)
|
|
524
593
|
ORDER BY "createdAt" DESC
|
|
@@ -568,7 +637,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
568
637
|
await this.db.tx(async t => {
|
|
569
638
|
for (const message of messages) {
|
|
570
639
|
await t.none(
|
|
571
|
-
`INSERT INTO
|
|
640
|
+
`INSERT INTO ${this.getTableName(TABLE_MESSAGES)} (id, thread_id, content, "createdAt", role, type)
|
|
572
641
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
573
642
|
[
|
|
574
643
|
message.id,
|
|
@@ -601,7 +670,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
601
670
|
try {
|
|
602
671
|
const now = new Date().toISOString();
|
|
603
672
|
await this.db.none(
|
|
604
|
-
`INSERT INTO
|
|
673
|
+
`INSERT INTO ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)} (
|
|
605
674
|
workflow_name,
|
|
606
675
|
run_id,
|
|
607
676
|
snapshot,
|
|
@@ -696,7 +765,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
696
765
|
// Only get total count when using pagination
|
|
697
766
|
if (limit !== undefined && offset !== undefined) {
|
|
698
767
|
const countResult = await this.db.one(
|
|
699
|
-
`SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
|
|
768
|
+
`SELECT COUNT(*) as count FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)} ${whereClause}`,
|
|
700
769
|
values,
|
|
701
770
|
);
|
|
702
771
|
total = Number(countResult.count);
|
|
@@ -704,7 +773,7 @@ export class PostgresStore extends MastraStorage {
|
|
|
704
773
|
|
|
705
774
|
// Get results
|
|
706
775
|
const query = `
|
|
707
|
-
SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT}
|
|
776
|
+
SELECT * FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)}
|
|
708
777
|
${whereClause}
|
|
709
778
|
ORDER BY "createdAt" DESC
|
|
710
779
|
${limit !== undefined && offset !== undefined ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ''}
|