Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
});
}

Expand Down Expand Up @@ -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());
});
}

Expand Down Expand Up @@ -250,7 +251,7 @@ public void upsertItemLinks(Connection connection, MemoryId memoryId, List<ItemL
statement.setString(6, link.linkType().name());
statement.setObject(7, link.strength());
statement.setString(8, jsonCodec.toJson(link.metadata()));
statement.setString(9, formatInstant(link.createdAt()));
setTemporal(statement, 9, link.createdAt());
statement.setString(10, link.relationCode());
statement.setString(11, link.evidenceSource());
});
Expand Down Expand Up @@ -287,8 +288,8 @@ public void upsertEntityAliases(
statement.setString(6, alias.normalizedAlias());
statement.setInt(7, alias.evidenceCount());
statement.setString(8, jsonCodec.toJson(alias.metadata()));
statement.setString(9, formatInstant(alias.createdAt()));
statement.setString(10, formatInstant(alias.updatedAt()));
setTemporal(statement, 9, alias.createdAt());
setTemporal(statement, 10, alias.updatedAt());
});
}

Expand Down Expand Up @@ -499,7 +500,7 @@ private void upsertCooccurrence(
cooccurrence.rightEntityKey(),
cooccurrence.cooccurrenceCount(),
jsonCodec.toJson(cooccurrence.metadata()),
formatInstant(cooccurrence.updatedAt()));
cooccurrence.updatedAt());
}

private List<ItemEntityMention> listItemEntityMentions(
Expand Down Expand Up @@ -621,7 +622,7 @@ private List<EntityCooccurrence> queryList(
handle -> queryList(handle.getConnection(), sql, mapper::map, params));
}

private static <T> List<T> queryList(
private <T> List<T> queryList(
Connection connection, String sql, ResultSetMapper<T> mapper, Object... params) {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
setParams(statement, params);
Expand All @@ -637,7 +638,7 @@ private static <T> List<T> 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();
Expand All @@ -659,10 +660,28 @@ private static <T> 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));
}
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
};
Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,31 @@
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.
*
* <p>Writing: {@code Instant} → ISO-8601 string (e.g. {@code 2024-01-01T12:00:00Z})<br>
* Reading: ISO-8601 string → {@code Instant}
*
* <p>Solves the problem that SQLite JDBC cannot directly parse epoch milliseconds to Timestamp.
* <p>Writing: {@link Instant} → {@link Timestamp} (standard JDBC).<br>
* Reading: accepts ISO-8601 (with {@code Z}), {@code yyyy-MM-dd HH:mm:ss[.SSS]} (space-separated,
* treated as UTC), or epoch millis.
*
* <p>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<Instant> {

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ public PageResponse<AdminMemoryWorkspaceView> 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;
Expand Down
Loading