Skip to content

Commit 46ce908

Browse files
authored
fix timezones in threads (#11498)
## Description Fix thread timestamps being returned in incorrect timezone from `listThreadsByResourceId`. The `listThreadsByResourceId` method was not selecting or using the timezone-aware columns (`createdAtZ`/`updatedAtZ` which are `TIMESTAMPTZ` type). This caused timestamps to be read from the `TIMESTAMP WITHOUT TIME ZONE` columns and interpreted in local timezone instead of UTC. The fix updates `listThreadsByResourceId` to: 1. Select the timezone-aware columns (`createdAtZ`, `updatedAtZ`) 2. Use them with fallback for legacy data (matching the pattern already used in `getThreadById`) ## Related Issue(s) #11496 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [x] Test update ## Checklist - [x] I have made corresponding changes to the documentation (if applicable) - [x] I have added tests that prove my fix is effective or that my feature works <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Patched @mastra/pg to fix thread timestamp handling by implementing timezone-aware timestamp columns with automatic fallback support for legacy data, ensuring consistent timestamp values across operations * **Tests** * Added comprehensive test to verify timestamp consistency across thread retrieval operations, confirming that created and updated timestamps remain synchronized across different data access methods <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent c042bd0 commit 46ce908

File tree

3 files changed

+60
-5
lines changed

3 files changed

+60
-5
lines changed

.changeset/lemon-turtles-sort.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@mastra/pg': patch
3+
---
4+
5+
Fix thread timestamps being returned in incorrect timezone from listThreadsByResourceId
6+
7+
The method was not using the timezone-aware columns (createdAtZ/updatedAtZ), causing timestamps to be interpreted in local timezone instead of UTC. Now correctly uses TIMESTAMPTZ columns with fallback for legacy data.

stores/_test-utils/src/domains/memory/threads.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,47 @@ export function createThreadsTest({ storage }: { storage: MastraStorage }) {
103103
expect(retrievedThread).toEqual(updatedThread);
104104
});
105105

106+
it('should return consistent timestamps from getThreadById and listThreadsByResourceId (issue #11496)', async () => {
107+
// This test verifies that timestamps are consistent across different retrieval methods.
108+
// The bug was that listThreadsByResourceId returned timestamps from non-timezone-aware columns,
109+
// while getThreadById used timezone-aware columns, causing inconsistent UTC timestamps.
110+
111+
const thread = createSampleThread();
112+
await memoryStorage.saveThread({ thread });
113+
114+
// Update the thread to ensure updatedAt differs from createdAt
115+
await new Promise(resolve => setTimeout(resolve, 50));
116+
const updatedThread = await memoryStorage.updateThread({
117+
id: thread.id,
118+
title: 'Updated for timestamp test',
119+
metadata: { timestampTest: true },
120+
});
121+
122+
// Get thread via getThreadById
123+
const threadById = await memoryStorage.getThreadById({ threadId: thread.id });
124+
125+
// Get thread via listThreadsByResourceId
126+
const { threads } = await memoryStorage.listThreadsByResourceId({
127+
resourceId: thread.resourceId,
128+
page: 0,
129+
perPage: 10,
130+
});
131+
const threadFromList = threads.find(t => t.id === thread.id);
132+
133+
expect(threadById).toBeDefined();
134+
expect(threadFromList).toBeDefined();
135+
136+
// Normalize to timestamps for comparison (handles both Date objects and ISO strings)
137+
const getTimestamp = (date: Date | string) => (date instanceof Date ? date.getTime() : new Date(date).getTime());
138+
139+
// The timestamps should be identical between the two retrieval methods
140+
expect(getTimestamp(threadFromList!.createdAt)).toBe(getTimestamp(threadById!.createdAt));
141+
expect(getTimestamp(threadFromList!.updatedAt)).toBe(getTimestamp(threadById!.updatedAt));
142+
143+
// Also verify updatedAt from updateThread matches
144+
expect(getTimestamp(threadFromList!.updatedAt)).toBe(getTimestamp(updatedThread.updatedAt));
145+
});
146+
106147
it('should delete thread', async () => {
107148
const thread = createSampleThread();
108149
await memoryStorage.saveThread({ thread });

stores/pg/src/storage/domains/memory/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,21 @@ export class MemoryPG extends MemoryStorage {
239239
}
240240

241241
const limitValue = perPageInput === false ? total : perPage;
242-
const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "${field}" ${direction} LIMIT $2 OFFSET $3`;
243-
const rows = await this.#db.client.manyOrNone(dataQuery, [...queryParams, limitValue, offset]);
242+
// Select both standard and timezone-aware columns (*Z) for proper UTC timestamp handling
243+
const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ" ${baseQuery} ORDER BY "${field}" ${direction} LIMIT $2 OFFSET $3`;
244+
const rows = await this.#db.client.manyOrNone<StorageThreadType & { createdAtZ: Date; updatedAtZ: Date }>(
245+
dataQuery,
246+
[...queryParams, limitValue, offset],
247+
);
244248

245249
const threads = (rows || []).map(thread => ({
246-
...thread,
250+
id: thread.id,
251+
resourceId: thread.resourceId,
252+
title: thread.title,
247253
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
248-
createdAt: thread.createdAt,
249-
updatedAt: thread.updatedAt,
254+
// Use timezone-aware columns (*Z) for correct UTC timestamps, with fallback for legacy data
255+
createdAt: thread.createdAtZ || thread.createdAt,
256+
updatedAt: thread.updatedAtZ || thread.updatedAt,
250257
}));
251258

252259
return {

0 commit comments

Comments
 (0)