Skip to content

Commit d2492a6

Browse files
sobychackoilayaperumalg
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. Auto-cherry-pick to 1.0.x Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
1 parent 99ccf9b commit d2492a6

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
@@ -185,6 +185,30 @@ void testMessageOrder() {
185185
"4-Fourth message");
186186
}
187187

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

0 commit comments

Comments
 (0)