@mastra/pg 0.11.1-alpha.0 → 0.11.1-alpha.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/pg",
3
- "version": "0.11.1-alpha.0",
3
+ "version": "0.11.1-alpha.2",
4
4
  "description": "Postgres provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,13 +29,13 @@
29
29
  "@microsoft/api-extractor": "^7.52.8",
30
30
  "@types/node": "^20.19.0",
31
31
  "@types/pg": "^8.15.4",
32
- "eslint": "^9.28.0",
32
+ "eslint": "^9.29.0",
33
33
  "tsup": "^8.5.0",
34
34
  "typescript": "^5.8.3",
35
35
  "vitest": "^3.2.3",
36
36
  "@internal/lint": "0.0.13",
37
- "@internal/storage-test-utils": "0.0.9",
38
- "@mastra/core": "0.10.7-alpha.0"
37
+ "@mastra/core": "0.10.7-alpha.2",
38
+ "@internal/storage-test-utils": "0.0.9"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@mastra/core": ">=0.10.4-0 <0.11.0"
@@ -4,11 +4,12 @@ import {
4
4
  createSampleTraceForDB,
5
5
  createSampleThread,
6
6
  createSampleMessageV1,
7
+ createSampleMessageV2,
7
8
  createSampleWorkflowSnapshot,
8
9
  resetRole,
9
10
  checkWorkflowSnapshot,
10
11
  } from '@internal/storage-test-utils';
11
- import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
12
+ import type { MastraMessageV2 } from '@mastra/core/agent';
12
13
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
13
14
  import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
14
15
  import {
@@ -37,37 +38,6 @@ const connectionString = `postgresql://${TEST_CONFIG.user}:${TEST_CONFIG.passwor
37
38
 
38
39
  vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
39
40
 
40
- const createSampleMessageV2 = ({
41
- threadId,
42
- resourceId,
43
- role = 'user',
44
- content,
45
- createdAt,
46
- thread,
47
- }: {
48
- threadId: string;
49
- resourceId?: string;
50
- role?: 'user' | 'assistant';
51
- content?: Partial<MastraMessageContentV2>;
52
- createdAt?: Date;
53
- thread?: StorageThreadType;
54
- }): MastraMessageV2 => {
55
- return {
56
- id: randomUUID(),
57
- threadId,
58
- resourceId: resourceId || thread?.resourceId || 'test-resource',
59
- role,
60
- createdAt: createdAt || new Date(),
61
- content: {
62
- format: 2,
63
- parts: content?.parts || [{ type: 'text', text: content?.content ?? '' }],
64
- content: content?.content || `Sample content ${randomUUID()}`,
65
- ...content,
66
- },
67
- type: 'v2',
68
- };
69
- };
70
-
71
41
  describe('PostgresStore', () => {
72
42
  let store: PostgresStore;
73
43
 
@@ -426,6 +396,75 @@ describe('PostgresStore', () => {
426
396
  expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
427
397
  expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
428
398
  });
399
+
400
+ it('should return messages using both last and include (cross-thread, deduped)', async () => {
401
+ const thread = createSampleThread({ id: 'thread-one' });
402
+ await store.saveThread({ thread });
403
+
404
+ const thread2 = createSampleThread({ id: 'thread-two' });
405
+ await store.saveThread({ thread: thread2 });
406
+
407
+ const now = new Date();
408
+
409
+ // Setup: create messages in two threads
410
+ const messages = [
411
+ createSampleMessageV2({
412
+ threadId: 'thread-one',
413
+ content: { content: 'A' },
414
+ createdAt: new Date(now.getTime()),
415
+ }),
416
+ createSampleMessageV2({
417
+ threadId: 'thread-one',
418
+ content: { content: 'B' },
419
+ createdAt: new Date(now.getTime() + 1000),
420
+ }),
421
+ createSampleMessageV2({
422
+ threadId: 'thread-one',
423
+ content: { content: 'C' },
424
+ createdAt: new Date(now.getTime() + 2000),
425
+ }),
426
+ createSampleMessageV2({
427
+ threadId: 'thread-two',
428
+ content: { content: 'D' },
429
+ createdAt: new Date(now.getTime() + 3000),
430
+ }),
431
+ createSampleMessageV2({
432
+ threadId: 'thread-two',
433
+ content: { content: 'E' },
434
+ createdAt: new Date(now.getTime() + 4000),
435
+ }),
436
+ createSampleMessageV2({
437
+ threadId: 'thread-two',
438
+ content: { content: 'F' },
439
+ createdAt: new Date(now.getTime() + 5000),
440
+ }),
441
+ ];
442
+ await store.saveMessages({ messages, format: 'v2' });
443
+
444
+ // Use last: 2 and include a message from another thread with context
445
+ const result = await store.getMessages({
446
+ threadId: 'thread-one',
447
+ format: 'v2',
448
+ selectBy: {
449
+ last: 2,
450
+ include: [
451
+ {
452
+ id: messages[4].id, // 'E' from thread-bar
453
+ threadId: 'thread-two',
454
+ withPreviousMessages: 1,
455
+ withNextMessages: 1,
456
+ },
457
+ ],
458
+ },
459
+ });
460
+
461
+ // Should include last 2 from thread-one and 3 from thread-two (D, E, F)
462
+ expect(result.map(m => m.content.content).sort()).toEqual(['B', 'C', 'D', 'E', 'F']);
463
+ // Should include 2 from thread-one
464
+ expect(result.filter(m => m.threadId === 'thread-one').map(m => m.content.content)).toEqual(['B', 'C']);
465
+ // Should include 3 from thread-two
466
+ expect(result.filter(m => m.threadId === 'thread-two').map(m => m.content.content)).toEqual(['D', 'E', 'F']);
467
+ });
429
468
  });
430
469
 
431
470
  describe('updateMessages', () => {
@@ -552,6 +591,81 @@ describe('PostgresStore', () => {
552
591
  expect(thread2Messages).toHaveLength(1);
553
592
  expect(thread2Messages[0].id).toBe(message.id);
554
593
  });
594
+ it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
595
+ const thread = await createSampleThread();
596
+ await store.saveThread({ thread });
597
+ const baseMessage = createSampleMessageV2({
598
+ threadId: thread.id,
599
+ createdAt: new Date(),
600
+ content: { content: 'Original' },
601
+ resourceId: thread.resourceId,
602
+ });
603
+
604
+ // Insert the message for the first time
605
+ await store.saveMessages({ messages: [baseMessage], format: 'v2' });
606
+
607
+ // Insert again with the same id and threadId but different content
608
+ const updatedMessage = {
609
+ ...createSampleMessageV2({
610
+ threadId: thread.id,
611
+ createdAt: new Date(),
612
+ content: { content: 'Updated' },
613
+ resourceId: thread.resourceId,
614
+ }),
615
+ id: baseMessage.id,
616
+ };
617
+
618
+ await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
619
+
620
+ // Retrieve messages for the thread
621
+ const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
622
+
623
+ // Only one message should exist for that id+threadId
624
+ expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
625
+
626
+ // The content should be the updated one
627
+ expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
628
+ });
629
+
630
+ it('should upsert messages: duplicate id and different threadid', async () => {
631
+ const thread1 = await createSampleThread();
632
+ const thread2 = await createSampleThread();
633
+ await store.saveThread({ thread: thread1 });
634
+ await store.saveThread({ thread: thread2 });
635
+
636
+ const message = createSampleMessageV2({
637
+ threadId: thread1.id,
638
+ createdAt: new Date(),
639
+ content: { content: 'Thread1 Content' },
640
+ resourceId: thread1.resourceId,
641
+ });
642
+
643
+ // Insert message into thread1
644
+ await store.saveMessages({ messages: [message], format: 'v2' });
645
+
646
+ // Attempt to insert a message with the same id but different threadId
647
+ const conflictingMessage = {
648
+ ...createSampleMessageV2({
649
+ threadId: thread2.id, // different thread
650
+ content: { content: 'Thread2 Content' },
651
+ resourceId: thread2.resourceId,
652
+ }),
653
+ id: message.id,
654
+ };
655
+
656
+ // Save should move the message to the new thread
657
+ await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
658
+
659
+ // Retrieve messages for both threads
660
+ const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
661
+ const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
662
+
663
+ // Thread 1 should NOT have the message with that id
664
+ expect(thread1Messages.find(m => m.id === message.id)).toBeUndefined();
665
+
666
+ // Thread 2 should have the message with that id
667
+ expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
668
+ });
555
669
  });
556
670
 
557
671
  describe('Edge Cases and Error Handling', () => {
@@ -1576,7 +1690,6 @@ describe('PostgresStore', () => {
1576
1690
  selectBy: { pagination: { page: 0, perPage: 5 } },
1577
1691
  format: 'v2',
1578
1692
  });
1579
- console.log(page1);
1580
1693
  expect(page1.messages).toHaveLength(5);
1581
1694
  expect(page1.total).toBe(15);
1582
1695
  expect(page1.page).toBe(0);
@@ -1647,6 +1760,286 @@ describe('PostgresStore', () => {
1647
1760
  );
1648
1761
  }
1649
1762
  });
1763
+
1764
+ it('should save and retrieve messages', async () => {
1765
+ const thread = createSampleThread();
1766
+ await store.saveThread({ thread });
1767
+
1768
+ const messages = [
1769
+ createSampleMessageV1({ threadId: thread.id }),
1770
+ createSampleMessageV1({ threadId: thread.id }),
1771
+ ];
1772
+
1773
+ // Save messages
1774
+ const savedMessages = await store.saveMessages({ messages });
1775
+ expect(savedMessages).toEqual(messages);
1776
+
1777
+ // Retrieve messages
1778
+ const retrievedMessages = await store.getMessagesPaginated({ threadId: thread.id, format: 'v1' });
1779
+ expect(retrievedMessages.messages).toHaveLength(2);
1780
+ const checkMessages = messages.map(m => {
1781
+ const { resourceId, ...rest } = m;
1782
+ return rest;
1783
+ });
1784
+ expect(retrievedMessages.messages).toEqual(expect.arrayContaining(checkMessages));
1785
+ });
1786
+
1787
+ it('should maintain message order', async () => {
1788
+ const thread = createSampleThread();
1789
+ await store.saveThread({ thread });
1790
+
1791
+ const messageContent = ['First', 'Second', 'Third'];
1792
+
1793
+ const messages = messageContent.map(content =>
1794
+ createSampleMessageV2({
1795
+ threadId: thread.id,
1796
+ content: { content, parts: [{ type: 'text', text: content }] },
1797
+ }),
1798
+ );
1799
+
1800
+ await store.saveMessages({ messages, format: 'v2' });
1801
+
1802
+ const retrievedMessages = await store.getMessagesPaginated({ threadId: thread.id, format: 'v2' });
1803
+ expect(retrievedMessages.messages).toHaveLength(3);
1804
+
1805
+ // Verify order is maintained
1806
+ retrievedMessages.messages.forEach((msg, idx) => {
1807
+ expect((msg.content.parts[0] as any).text).toEqual(messageContent[idx]);
1808
+ });
1809
+ });
1810
+
1811
+ it('should rollback on error during message save', async () => {
1812
+ const thread = createSampleThread();
1813
+ await store.saveThread({ thread });
1814
+
1815
+ const messages = [
1816
+ createSampleMessageV1({ threadId: thread.id }),
1817
+ { ...createSampleMessageV1({ threadId: thread.id }), id: null } as any, // This will cause an error
1818
+ ];
1819
+
1820
+ await expect(store.saveMessages({ messages })).rejects.toThrow();
1821
+
1822
+ // Verify no messages were saved
1823
+ const savedMessages = await store.getMessagesPaginated({ threadId: thread.id, format: 'v2' });
1824
+ expect(savedMessages.messages).toHaveLength(0);
1825
+ });
1826
+
1827
+ it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
1828
+ const thread = createSampleThread({ id: 'thread-one' });
1829
+ await store.saveThread({ thread });
1830
+
1831
+ const thread2 = createSampleThread({ id: 'thread-two' });
1832
+ await store.saveThread({ thread: thread2 });
1833
+
1834
+ const thread3 = createSampleThread({ id: 'thread-three' });
1835
+ await store.saveThread({ thread: thread3 });
1836
+
1837
+ const messages: MastraMessageV2[] = [
1838
+ createSampleMessageV2({
1839
+ threadId: 'thread-one',
1840
+ content: { content: 'First' },
1841
+ resourceId: 'cross-thread-resource',
1842
+ }),
1843
+ createSampleMessageV2({
1844
+ threadId: 'thread-one',
1845
+ content: { content: 'Second' },
1846
+ resourceId: 'cross-thread-resource',
1847
+ }),
1848
+ createSampleMessageV2({
1849
+ threadId: 'thread-one',
1850
+ content: { content: 'Third' },
1851
+ resourceId: 'cross-thread-resource',
1852
+ }),
1853
+
1854
+ createSampleMessageV2({
1855
+ threadId: 'thread-two',
1856
+ content: { content: 'Fourth' },
1857
+ resourceId: 'cross-thread-resource',
1858
+ }),
1859
+ createSampleMessageV2({
1860
+ threadId: 'thread-two',
1861
+ content: { content: 'Fifth' },
1862
+ resourceId: 'cross-thread-resource',
1863
+ }),
1864
+ createSampleMessageV2({
1865
+ threadId: 'thread-two',
1866
+ content: { content: 'Sixth' },
1867
+ resourceId: 'cross-thread-resource',
1868
+ }),
1869
+
1870
+ createSampleMessageV2({
1871
+ threadId: 'thread-three',
1872
+ content: { content: 'Seventh' },
1873
+ resourceId: 'other-resource',
1874
+ }),
1875
+ createSampleMessageV2({
1876
+ threadId: 'thread-three',
1877
+ content: { content: 'Eighth' },
1878
+ resourceId: 'other-resource',
1879
+ }),
1880
+ ];
1881
+
1882
+ await store.saveMessages({ messages: messages, format: 'v2' });
1883
+
1884
+ const retrievedMessages = await store.getMessagesPaginated({ threadId: 'thread-one', format: 'v2' });
1885
+ expect(retrievedMessages.messages).toHaveLength(3);
1886
+ expect(retrievedMessages.messages.map((m: any) => m.content.parts[0].text)).toEqual([
1887
+ 'First',
1888
+ 'Second',
1889
+ 'Third',
1890
+ ]);
1891
+
1892
+ const retrievedMessages2 = await store.getMessagesPaginated({ threadId: 'thread-two', format: 'v2' });
1893
+ expect(retrievedMessages2.messages).toHaveLength(3);
1894
+ expect(retrievedMessages2.messages.map((m: any) => m.content.parts[0].text)).toEqual([
1895
+ 'Fourth',
1896
+ 'Fifth',
1897
+ 'Sixth',
1898
+ ]);
1899
+
1900
+ const retrievedMessages3 = await store.getMessagesPaginated({ threadId: 'thread-three', format: 'v2' });
1901
+ expect(retrievedMessages3.messages).toHaveLength(2);
1902
+ expect(retrievedMessages3.messages.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
1903
+
1904
+ const { messages: crossThreadMessages } = await store.getMessagesPaginated({
1905
+ threadId: 'thread-doesnt-exist',
1906
+ format: 'v2',
1907
+ selectBy: {
1908
+ last: 0,
1909
+ include: [
1910
+ {
1911
+ id: messages[1].id,
1912
+ threadId: 'thread-one',
1913
+ withNextMessages: 2,
1914
+ withPreviousMessages: 2,
1915
+ },
1916
+ {
1917
+ id: messages[4].id,
1918
+ threadId: 'thread-two',
1919
+ withPreviousMessages: 2,
1920
+ withNextMessages: 2,
1921
+ },
1922
+ ],
1923
+ },
1924
+ });
1925
+
1926
+ expect(crossThreadMessages).toHaveLength(6);
1927
+ expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
1928
+ expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
1929
+
1930
+ const crossThreadMessages2 = await store.getMessagesPaginated({
1931
+ threadId: 'thread-one',
1932
+ format: 'v2',
1933
+ selectBy: {
1934
+ last: 0,
1935
+ include: [
1936
+ {
1937
+ id: messages[4].id,
1938
+ threadId: 'thread-two',
1939
+ withPreviousMessages: 1,
1940
+ withNextMessages: 1,
1941
+ },
1942
+ ],
1943
+ },
1944
+ });
1945
+
1946
+ expect(crossThreadMessages2.messages).toHaveLength(3);
1947
+ expect(crossThreadMessages2.messages.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
1948
+ expect(crossThreadMessages2.messages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
1949
+
1950
+ const crossThreadMessages3 = await store.getMessagesPaginated({
1951
+ threadId: 'thread-two',
1952
+ format: 'v2',
1953
+ selectBy: {
1954
+ last: 0,
1955
+ include: [
1956
+ {
1957
+ id: messages[1].id,
1958
+ threadId: 'thread-one',
1959
+ withNextMessages: 1,
1960
+ withPreviousMessages: 1,
1961
+ },
1962
+ ],
1963
+ },
1964
+ });
1965
+
1966
+ expect(crossThreadMessages3.messages).toHaveLength(3);
1967
+ expect(crossThreadMessages3.messages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
1968
+ expect(crossThreadMessages3.messages.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
1969
+ });
1970
+
1971
+ it('should return messages using both last and include (cross-thread, deduped)', async () => {
1972
+ const thread = createSampleThread({ id: 'thread-one' });
1973
+ await store.saveThread({ thread });
1974
+
1975
+ const thread2 = createSampleThread({ id: 'thread-two' });
1976
+ await store.saveThread({ thread: thread2 });
1977
+
1978
+ const now = new Date();
1979
+
1980
+ // Setup: create messages in two threads
1981
+ const messages = [
1982
+ createSampleMessageV2({
1983
+ threadId: 'thread-one',
1984
+ content: { content: 'A' },
1985
+ createdAt: new Date(now.getTime()),
1986
+ }),
1987
+ createSampleMessageV2({
1988
+ threadId: 'thread-one',
1989
+ content: { content: 'B' },
1990
+ createdAt: new Date(now.getTime() + 1000),
1991
+ }),
1992
+ createSampleMessageV2({
1993
+ threadId: 'thread-one',
1994
+ content: { content: 'C' },
1995
+ createdAt: new Date(now.getTime() + 2000),
1996
+ }),
1997
+ createSampleMessageV2({
1998
+ threadId: 'thread-two',
1999
+ content: { content: 'D' },
2000
+ createdAt: new Date(now.getTime() + 3000),
2001
+ }),
2002
+ createSampleMessageV2({
2003
+ threadId: 'thread-two',
2004
+ content: { content: 'E' },
2005
+ createdAt: new Date(now.getTime() + 4000),
2006
+ }),
2007
+ createSampleMessageV2({
2008
+ threadId: 'thread-two',
2009
+ content: { content: 'F' },
2010
+ createdAt: new Date(now.getTime() + 5000),
2011
+ }),
2012
+ ];
2013
+ await store.saveMessages({ messages, format: 'v2' });
2014
+
2015
+ // Use last: 2 and include a message from another thread with context
2016
+ const { messages: result } = await store.getMessagesPaginated({
2017
+ threadId: 'thread-one',
2018
+ format: 'v2',
2019
+ selectBy: {
2020
+ last: 2,
2021
+ include: [
2022
+ {
2023
+ id: messages[4].id, // 'E' from thread-bar
2024
+ threadId: 'thread-two',
2025
+ withPreviousMessages: 1,
2026
+ withNextMessages: 1,
2027
+ },
2028
+ ],
2029
+ },
2030
+ });
2031
+
2032
+ // Should include last 2 from thread-one and 3 from thread-two (D, E, F)
2033
+ expect(result.map(m => m.content.content).sort()).toEqual(['B', 'C', 'D', 'E', 'F']);
2034
+ // Should include 2 from thread-one
2035
+ expect(result.filter(m => m.threadId === 'thread-one').map((m: any) => m.content.content)).toEqual(['B', 'C']);
2036
+ // Should include 3 from thread-two
2037
+ expect(result.filter(m => m.threadId === 'thread-two').map((m: any) => m.content.content)).toEqual([
2038
+ 'D',
2039
+ 'E',
2040
+ 'F',
2041
+ ]);
2042
+ });
1650
2043
  });
1651
2044
 
1652
2045
  describe('getThreadsByResourceId with pagination', () => {
@@ -1730,7 +2123,7 @@ describe('PostgresStore', () => {
1730
2123
  record: { id: '1', name: 'Alice' },
1731
2124
  });
1732
2125
 
1733
- const row = await store.load({
2126
+ const row: any = await store.load({
1734
2127
  tableName: camelCaseTable as TABLE_NAMES,
1735
2128
  keys: { id: '1' },
1736
2129
  });
@@ -1750,7 +2143,7 @@ describe('PostgresStore', () => {
1750
2143
  record: { id: '2', name: 'Bob' },
1751
2144
  });
1752
2145
 
1753
- const row = await store.load({
2146
+ const row: any = await store.load({
1754
2147
  tableName: snakeCaseTable as TABLE_NAMES,
1755
2148
  keys: { id: '2' },
1756
2149
  });