diff --git a/src/arcadedb_embedded/results.py b/src/arcadedb_embedded/results.py
index fb65f82..a8f78f8 100644
--- a/src/arcadedb_embedded/results.py
+++ b/src/arcadedb_embedded/results.py
@@ -529,6 +529,18 @@ def to_json_list(self, batch_size: int = 10_000) -> List[Dict[str, Any]]:
floats. Use ``to_list()`` when you want ``date``, ``datetime`` and
``Decimal`` values: it costs about the same.
+ The encoding is one rule for every row, at the top level and inside a
+ list or a map: a DATE is the epoch milliseconds of its UTC midnight (not
+ a count of days) whatever the JVM time zone, the same number
+ ``to_columns()`` carries as ``datetime64[ms]``. A date too far from 1970
+ for a long of milliseconds (past about year 292 million) arrives as its
+ ISO-8601 text instead of failing the batch.
+
+ This is NOT the form of ``Result.to_json()`` for a record row (``SELECT
+ FROM T``), which is the engine's round-trip JSON of the record: there a
+ DATE column at the top level is epoch DAYS, with the ``@rid``, ``@type``
+ and ``@cat`` metadata. Do not mix the two when decoding a DATE.
+
Args:
batch_size: Rows serialized per Java crossing (default 10000)
@@ -1303,6 +1315,18 @@ def to_json(self) -> str:
"""
Convert result to JSON string.
+ This is the engine's own JSON of the row. For a record row (``SELECT
+ FROM T``) it is the record's round-trip form, which ``fromJSON`` reads
+ back: it carries the ``@rid``, ``@type`` and ``@cat`` metadata and writes
+ a top-level DATE as epoch DAYS (a DATETIME as epoch milliseconds, or as
+ text when its column keeps microseconds or nanoseconds). A DATE inside a
+ list or a map, and a DATE in a projection row (``SELECT a, b FROM T``),
+ is written as the epoch milliseconds of its UTC midnight, the form
+ ``ResultSet.to_json_list()`` uses everywhere; a DATETIME in a projection
+ row is written as text in the schema's date-time format. The engine's
+ JSON is not one encoding across row shapes, which is why
+ ``to_json_list()`` does not delegate to it.
+
Returns:
JSON string representation
diff --git a/src/java/com/arcadedb/python/ColumnBatcher.java b/src/java/com/arcadedb/python/ColumnBatcher.java
index fbedcef..6211ac0 100644
--- a/src/java/com/arcadedb/python/ColumnBatcher.java
+++ b/src/java/com/arcadedb/python/ColumnBatcher.java
@@ -42,7 +42,10 @@ private ColumnBatcher() {
}
/** Wall-clock-consistent epoch millis: naive temporals are read as UTC so
- * numpy's (naive) datetime64 shows the same wall-clock the record carries. */
+ * numpy's (naive) datetime64 shows the same wall-clock the record carries.
+ * A DATE is its UTC midnight in milliseconds, not a count of days: the column is a datetime64[ms], and it is the
+ * number RowBatcher gets from the engine's JSONObject for the same value (#10522, test_temporal_writers_agree.py).
+ * It is not a JSON writer, so there is nothing for it to disagree with in Result.toJSON(). */
private static long toEpochMillis(final Object v) {
if (v instanceof Date d)
return d.getTime();
diff --git a/src/java/com/arcadedb/python/RowBatcher.java b/src/java/com/arcadedb/python/RowBatcher.java
index a08edc7..74249cb 100644
--- a/src/java/com/arcadedb/python/RowBatcher.java
+++ b/src/java/com/arcadedb/python/RowBatcher.java
@@ -12,6 +12,16 @@
* which since 26.7.2 (#4967) serializes primitive arrays like float[] as
* real JSON arrays natively.
*
+ * This class has NO temporal encoding of its own (#10522): every date and
+ * date-time is written by JSONObject.put, the writer the engine uses for the
+ * rows of a projection and for the members of a list or a map of any row, and
+ * ColumnBatcher's epoch milliseconds are the same numbers. A DATE is the epoch
+ * milliseconds of its UTC midnight, never a count of days, whatever the JVM
+ * time zone. The one place the engine writes days is the top level of a
+ * record's own toJSON() (Result.to_json() of a SELECT FROM T row), which is
+ * the form fromJSON() reads back for a DATE column; to_json_list() is not that
+ * round trip, and test_temporal_writers_agree.py pins both.
+ *
* Compiled into arcadedb-python-bridge.jar during the wheel build
* (scripts/Dockerfile.build and scripts/build-native.sh) and consumed by
* ResultSet.to_json_list() in the Python bindings. Values carry JSON-native
@@ -24,14 +34,6 @@
import com.arcadedb.query.sql.executor.ResultSet;
import com.arcadedb.serializer.json.JSONObject;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
public final class RowBatcher {
private RowBatcher() {
@@ -79,49 +81,14 @@ static void closeDrained(final ResultSet rs) {
private static void appendRow(final StringBuilder sb, final Result row) {
final JSONObject obj = new JSONObject();
- for (final String property : row.getPropertyNames())
- obj.put(property, utcDates(row.getProperty(property)));
- sb.append(obj);
- }
-
- /**
- * JSONObject.put writes a LocalDate as the epoch milliseconds of midnight in the JVM's default time zone, while
- * Result.toJSON() and a DATETIME (a UTC wall clock) are written in UTC, so the same row read twice gave two integers
- * and decoded to the previous day east of UTC. A DATE is written as midnight UTC here, at the top level and inside a
- * list or map. A collection without a LocalDate is returned as it is.
- *
- * A java.util.Calendar (arcadedb.dateTimeImplementation / dateImplementation) is written as its epoch milliseconds, as
- * a java.util.Date is. The engine's JSONObject wrote it as its toString() before #9630, and this bridge runs against
- * whatever engine the wheel bundles, so it does not rely on that fix.
- */
- private static Object utcDates(final Object value) {
- if (value instanceof LocalDate date)
- return date.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
- if (value instanceof Calendar calendar)
- return calendar.getTimeInMillis();
- if (value instanceof List> list) {
- List