Skip to content

Commit 393d9fa

Browse files
sobychackospring-builds
authored andcommitted
GH-3442: Fix MySQL/MariaDB message ordering in JdbcChatMemoryRepository
Fixes #3442 Change timestamp generation to use second-level granularity instead of milliseconds to ensure compatibility with MySQL/MariaDB default TIMESTAMP precision (0 decimal places). The old code used millisecond timestamps that were truncated to seconds on storage, causing messages saved within the same second to have identical timestamps and random ordering. The timestamp field functions as a sequence ID for message ordering rather than a precise temporal record. Using second-level granularity with proper incrementing ensures correct ordering across all database timestamp precisions without requiring schema changes. Also adds testMessageOrderWithLargeBatch() that saves 50 messages to validate ordering is preserved. The original test with only 4 messages was passing by chance despite the underlying bug. Signed-off-by: Soby Chacko <[email protected]> (cherry picked from commit d2492a6)
1 parent 9d3d7e1 commit 393d9fa

File tree

2 files changed

+32
-3
lines changed

2 files changed

+32
-3
lines changed

memory/repository/spring-ai-model-chat-memory-repository-jdbc/src/main/java/org/springframework/ai/chat/memory/repository/jdbc/JdbcChatMemoryRepository.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,13 @@ public static Builder builder() {
111111
}
112112

113113
private record AddBatchPreparedStatement(String conversationId, List<Message> messages,
114-
AtomicLong instantSeq) implements BatchPreparedStatementSetter {
114+
AtomicLong sequenceId) implements BatchPreparedStatementSetter {
115115

116116
private AddBatchPreparedStatement(String conversationId, List<Message> messages) {
117-
this(conversationId, messages, new AtomicLong(Instant.now().toEpochMilli()));
117+
// Use second-level granularity to ensure compatibility with all database
118+
// timestamp precisions. The timestamp serves as a sequence number for
119+
// message ordering, not as a precise temporal record.
120+
this(conversationId, messages, new AtomicLong(Instant.now().getEpochSecond()));
118121
}
119122

120123
@Override
@@ -124,7 +127,9 @@ public void setValues(PreparedStatement ps, int i) throws SQLException {
124127
ps.setString(1, this.conversationId);
125128
ps.setString(2, message.getText());
126129
ps.setString(3, message.getMessageType().name());
127-
ps.setTimestamp(4, new Timestamp(this.instantSeq.getAndIncrement()));
130+
// Convert seconds to milliseconds for Timestamp constructor.
131+
// Each message gets a unique second value, ensuring proper ordering.
132+
ps.setTimestamp(4, new Timestamp(this.sequenceId.getAndIncrement() * 1000L));
128133
}
129134

130135
@Override

memory/repository/spring-ai-model-chat-memory-repository-jdbc/src/test/java/org/springframework/ai/chat/memory/repository/jdbc/AbstractJdbcChatMemoryRepositoryIT.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,30 @@ void testMessageOrder() {
186186
"4-Fourth message");
187187
}
188188

189+
@Test
190+
void testMessageOrderWithLargeBatch() {
191+
var conversationId = UUID.randomUUID().toString();
192+
193+
// Create a large batch of 50 messages to ensure timestamp ordering issues
194+
// are detected. With the old millisecond-precision code, MySQL/MariaDB's
195+
// second-precision TIMESTAMP columns would truncate all timestamps to the
196+
// same value, causing random ordering. This test validates the fix.
197+
List<Message> messages = new java.util.ArrayList<>();
198+
for (int i = 0; i < 50; i++) {
199+
messages.add(new UserMessage("Message " + i));
200+
}
201+
202+
this.chatMemoryRepository.saveAll(conversationId, messages);
203+
204+
List<Message> retrievedMessages = this.chatMemoryRepository.findByConversationId(conversationId);
205+
206+
// Verify we got all messages back in the exact order they were saved
207+
assertThat(retrievedMessages).hasSize(50);
208+
for (int i = 0; i < 50; i++) {
209+
assertThat(retrievedMessages.get(i).getText()).isEqualTo("Message " + i);
210+
}
211+
}
212+
189213
/**
190214
* Base configuration for all integration tests.
191215
*/

0 commit comments

Comments
 (0)