diff --git a/CHANGES.md b/CHANGES.md
index 3eefa7f43092..6ace76ebc888 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -67,6 +67,7 @@
* Support for reading from Delta Lake added (Java) ([#38551](https://github.com/apache/beam/issues/38551)).
* Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
+* ClickHouseIO: support writing `DateTime64(precision[, 'timezone'])` columns with sub-second precision (Java) ([#38466](https://github.com/apache/beam/issues/38466)).
## New Features / Improvements
diff --git a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java
index a8875407b43c..6798e2f2bd75 100644
--- a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java
+++ b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java
@@ -38,6 +38,8 @@
import org.apache.beam.sdk.schemas.FieldAccessDescriptor;
import org.apache.beam.sdk.schemas.Schema;
import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes;
+import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant;
+import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes;
import org.apache.beam.sdk.schemas.transforms.Select;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.PTransform;
@@ -137,6 +139,7 @@
*
| {@link TableSchema.TypeName#UINT64} | {@link Schema.TypeName#INT64} |
* | {@link TableSchema.TypeName#DATE} | {@link Schema.TypeName#DATETIME} |
* | {@link TableSchema.TypeName#DATETIME} | {@link Schema.TypeName#DATETIME} |
+ * | {@link TableSchema.TypeName#DATETIME64} | {@link Schema.TypeName#DATETIME} (precision ≤ 3), {@link SqlTypes#TIMESTAMP} (4–6), or {@link NanosInstant} (≥ 7) |
* | {@link TableSchema.TypeName#ARRAY} | {@link Schema.TypeName#ARRAY} |
* | {@link TableSchema.TypeName#ENUM8} | {@link Schema.TypeName#STRING} |
* | {@link TableSchema.TypeName#ENUM16} | {@link Schema.TypeName#STRING} |
diff --git a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java
index 73735f568646..4d9f072e598f 100644
--- a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java
+++ b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java
@@ -39,6 +39,39 @@
public class ClickHouseWriter {
private static final Instant EPOCH_INSTANT = new Instant(0L);
+ // 10^0 through 10^9 inclusive — precision is validated in [0, 9] by ColumnType.dateTime64.
+ private static final long[] POW10 = {
+ 1L, 10L, 100L, 1_000L, 10_000L, 100_000L, 1_000_000L, 10_000_000L, 100_000_000L, 1_000_000_000L
+ };
+
+ /**
+ * Encodes a timestamp into ClickHouse's {@code DateTime64(precision)} representation: a signed
+ * 64-bit integer counting ticks of size 10-precision seconds since the Unix epoch.
+ *
+ * Accepts either a Joda {@link ReadableInstant} (millisecond precision) or a {@link
+ * java.time.Instant} (nanosecond precision). Sub-tick fractions are truncated toward negative
+ * infinity, matching ClickHouse's own encoding for negative timestamps.
+ */
+ static long encodeDateTime64(Object value, int precision) {
+ long epochSecond;
+ int nanoOfSecond;
+ if (value instanceof java.time.Instant) {
+ java.time.Instant inst = (java.time.Instant) value;
+ epochSecond = inst.getEpochSecond();
+ nanoOfSecond = inst.getNano();
+ } else if (value instanceof ReadableInstant) {
+ long millis = ((ReadableInstant) value).getMillis();
+ epochSecond = Math.floorDiv(millis, 1000L);
+ nanoOfSecond = (int) Math.floorMod(millis, 1000L) * 1_000_000;
+ } else {
+ throw new IllegalArgumentException(
+ "DateTime64 requires a Joda ReadableInstant or java.time.Instant, got "
+ + (value == null ? "null" : value.getClass().getName()));
+ }
+ long subSecondTicks = nanoOfSecond / POW10[9 - precision];
+ return Math.addExact(Math.multiplyExact(epochSecond, POW10[precision]), subSecondTicks);
+ }
+
@SuppressWarnings("unchecked")
static void writeNullableValue(ClickHouseOutputStream stream, ColumnType columnType, Object value)
throws IOException {
@@ -138,6 +171,13 @@ static void writeValue(ClickHouseOutputStream stream, ColumnType columnType, Obj
BinaryStreamUtils.writeUnsignedInt32(stream, epochSeconds);
break;
+ case DATETIME64:
+ int precision =
+ Preconditions.checkNotNull(
+ columnType.precision(), "DateTime64 column is missing precision");
+ BinaryStreamUtils.writeInt64(stream, encodeDateTime64(value, precision));
+ break;
+
case ARRAY:
List