From 478e616560e870648f940ee9ad5a79b937701bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=AF=85?= Date: Fri, 31 Jul 2026 16:14:36 +0800 Subject: [PATCH 1/2] fix: MySQL compatibility for InstantTypeHandler and admin memory query - InstantTypeHandler: write java.time.Instant as JDBC Timestamp instead of Instant.toString() (ISO-8601 with 'Z' + nanosecond precision). MySQL DATETIME rejects the ISO string with 'Incorrect datetime value'; SQLite TEXT and PostgreSQL TIMESTAMPTZ tolerated it. setTimestamp is portable across drivers. - MemoryAdminService.listMemories: the COUNT query wraps a derived table without an alias; MySQL requires every derived table to have an alias ('Every derived table must have its own alias'). Add 'memory_count' alias. --- .../mybatis/handler/InstantTypeHandler.java | 16 ++++++++++------ .../service/memory/MemoryAdminService.java | 4 +++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/main/java/com/openmemind/ai/memory/plugin/store/mybatis/handler/InstantTypeHandler.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/main/java/com/openmemind/ai/memory/plugin/store/mybatis/handler/InstantTypeHandler.java index d5cd3f68..dc4c2e38 100644 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/main/java/com/openmemind/ai/memory/plugin/store/mybatis/handler/InstantTypeHandler.java +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/main/java/com/openmemind/ai/memory/plugin/store/mybatis/handler/InstantTypeHandler.java @@ -17,19 +17,23 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.time.Instant; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedTypes; /** - * MyBatis TypeHandler: Converts Java {@link Instant} to and from SQLite TEXT column. + * MyBatis TypeHandler: Converts Java {@link Instant} to and from SQL temporal columns. * - *

Writing: {@code Instant} → ISO-8601 string (e.g. {@code 2024-01-01T12:00:00Z})
- * Reading: ISO-8601 string → {@code Instant} - * - *

Solves the problem that SQLite JDBC cannot directly parse epoch milliseconds to Timestamp. + *

Writing: {@link Instant} → {@link Timestamp} (standard JDBC).
+ * Reading: accepts ISO-8601 (with {@code Z}), {@code yyyy-MM-dd HH:mm:ss[.SSS]} (space-separated, + * treated as UTC), or epoch millis. * + *

Previously wrote {@code Instant.toString()} (ISO-8601 with {@code Z} + nanosecond precision) + * as a String — SQLite TEXT and PostgreSQL TIMESTAMPTZ tolerated it, but MySQL {@code DATETIME} + * rejects it ({@code Incorrect datetime value}). Using {@code setTimestamp} is portable across all + * three drivers. */ @MappedTypes(Instant.class) public class InstantTypeHandler extends BaseTypeHandler { @@ -37,7 +41,7 @@ public class InstantTypeHandler extends BaseTypeHandler { @Override public void setNonNullParameter( PreparedStatement ps, int i, Instant parameter, JdbcType jdbcType) throws SQLException { - ps.setString(i, parameter.toString()); + ps.setTimestamp(i, Timestamp.from(parameter)); } @Override diff --git a/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/MemoryAdminService.java b/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/MemoryAdminService.java index caf55d94..8d082bdc 100644 --- a/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/MemoryAdminService.java +++ b/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/MemoryAdminService.java @@ -65,7 +65,9 @@ public PageResponse listMemories(MemoryPageQuery query Long total = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM (SELECT memory_id " + where + " GROUP BY memory_id)", + "SELECT COUNT(*) FROM (SELECT memory_id " + + where + + " GROUP BY memory_id) memory_count", Long.class, args.toArray()); long totalItems = total == null ? 0 : total; From 062e946394530b30930d8e22c6275c1e700f8f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=AF=85?= Date: Fri, 31 Jul 2026 16:56:25 +0800 Subject: [PATCH 2/2] fix: MySQL compatibility for JDBC plugin temporal writes The JDBC plugin (memind-plugin-jdbc-core) writes java.time.Instant as ISO-8601 strings (Instant.toString()) via Jdbi and graph operations. MySQL DATETIME(3) rejects those strings with 'Incorrect datetime value'; only SQLite TEXT and PostgreSQL TIMESTAMPTZ tolerated them. - JdbiTemporalSupport: bind Instant as JDBC Timestamp; parseInstant now also accepts the 'yyyy-MM-dd HH:mm:ss[.SSS]' space-separated format. - JdbcGraphOperations: dialect-aware setTemporal() (setString for SQLite, setTimestamp for MySQL/PostgreSQL) used by entity/mention/link/alias/ cooccurrence writes; the varargs binder passes Instant directly instead of a pre-formatted string. SQLite behavior is unchanged (still stores ISO-8601 text). --- .../internal/graph/JdbcGraphOperations.java | 45 ++++++++++++------- .../internal/jdbi/JdbiTemporalSupport.java | 6 ++- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/graph/JdbcGraphOperations.java b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/graph/JdbcGraphOperations.java index 6b8d565b..85fbf889 100644 --- a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/graph/JdbcGraphOperations.java +++ b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/graph/JdbcGraphOperations.java @@ -32,6 +32,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -129,8 +130,8 @@ public void upsertEntities( statement.setString(5, entity.displayName()); statement.setString(6, entity.entityType().name()); statement.setString(7, jsonCodec.toJson(entity.metadata())); - statement.setString(8, formatInstant(entity.createdAt())); - statement.setString(9, formatInstant(entity.updatedAt())); + setTemporal(statement, 8, entity.createdAt()); + setTemporal(statement, 9, entity.updatedAt()); }); } @@ -164,7 +165,7 @@ public void upsertItemEntityMentions( statement.setString(5, mention.entityKey()); statement.setObject(6, mention.confidence()); statement.setString(7, jsonCodec.toJson(mention.metadata())); - statement.setString(8, formatInstant(mention.createdAt())); + setTemporal(statement, 8, mention.createdAt()); }); } @@ -250,7 +251,7 @@ public void upsertItemLinks(Connection connection, MemoryId memoryId, List listItemEntityMentions( @@ -621,7 +622,7 @@ private List queryList( handle -> queryList(handle.getConnection(), sql, mapper::map, params)); } - private static List queryList( + private List queryList( Connection connection, String sql, ResultSetMapper mapper, Object... params) { try (PreparedStatement statement = connection.prepareStatement(sql)) { setParams(statement, params); @@ -637,7 +638,7 @@ private static List queryList( } } - private static int executeUpdate(Connection connection, String sql, Object... params) { + private int executeUpdate(Connection connection, String sql, Object... params) { try (PreparedStatement statement = connection.prepareStatement(sql)) { setParams(statement, params); return statement.executeUpdate(); @@ -659,10 +660,28 @@ private static void executeBatch( } } - private static void setParams(PreparedStatement statement, Object... params) + private void setParams(PreparedStatement statement, Object... params) throws SQLException { for (int i = 0; i < params.length; i++) { - statement.setObject(i + 1, params[i]); + if (params[i] instanceof Instant instant) { + setTemporal(statement, i + 1, instant); + } else { + statement.setObject(i + 1, params[i]); + } + } + } + + /** + * Binds an {@link Instant} timestamp. SQLite stores it as TEXT (ISO-8601); MySQL + * DATETIME / PostgreSQL TIMESTAMPTZ reject the ISO string with nanosecond precision + * ('Incorrect datetime value'), so use the standard JDBC {@link Timestamp} there. + */ + private void setTemporal(PreparedStatement statement, int index, Instant instant) + throws SQLException { + if (dialect == JdbcGraphDialect.SQLITE) { + statement.setString(index, instant == null ? null : instant.toString()); + } else { + statement.setTimestamp(index, instant == null ? null : Timestamp.from(instant)); } } @@ -908,10 +927,6 @@ private static String placeholders(int count) { return String.join(", ", java.util.Collections.nCopies(count, "?")); } - private static String formatInstant(Instant instant) { - return instant == null ? null : instant.toString(); - } - private static Instant parseInstant(String value) { if (value == null || value.isBlank()) { return null; diff --git a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/jdbi/JdbiTemporalSupport.java b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/jdbi/JdbiTemporalSupport.java index 5166ae29..db2e655e 100644 --- a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/jdbi/JdbiTemporalSupport.java +++ b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-core/src/main/java/com/openmemind/ai/memory/plugin/jdbc/internal/jdbi/JdbiTemporalSupport.java @@ -13,6 +13,7 @@ */ package com.openmemind.ai.memory.plugin.jdbc.internal.jdbi; +import java.sql.Timestamp; import java.time.Instant; import java.util.Optional; import org.jdbi.v3.core.Jdbi; @@ -34,7 +35,7 @@ static void register(Jdbi jdbi) { if (value instanceof Instant instant) { return Optional.of( (position, statement, context) -> - statement.setString(position, instant.toString())); + statement.setTimestamp(position, Timestamp.from(instant))); } return Optional.empty(); }; @@ -45,6 +46,7 @@ private static Instant parseInstant(String value) { if (value.chars().allMatch(Character::isDigit)) { return Instant.ofEpochMilli(Long.parseLong(value)); } - return Instant.parse(value); + String normalized = value.contains("T") ? value : value.replace(' ', 'T') + "Z"; + return Instant.parse(normalized.endsWith("Z") ? normalized : normalized + "Z"); } }