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 @@ -477,6 +477,16 @@
}
]
},
{
"type": "org.duckdb.DuckDBTimestampNS",
"jniAccessible": true,
"methods": [
{
"name": "getNanosEpoch",
"parameterTypes": []
}
]
},
{
"type": "org.duckdb.DuckDBTimestampTZ",
"jniAccessible": true,
Expand Down
4 changes: 4 additions & 0 deletions src/jni/refs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jclass J_String;
jclass J_Timestamp;
jmethodID J_Timestamp_valueOf;
jclass J_TimestampTZ;
jclass J_TimestampNS;
jclass J_BigDecimal;
jclass J_HugeInt;
jclass J_ByteArray;
Expand All @@ -44,6 +45,7 @@ jmethodID J_Float_floatValue;
jmethodID J_Double_doubleValue;
jmethodID J_Timestamp_getMicrosEpoch;
jmethodID J_TimestampTZ_getMicrosEpoch;
jmethodID J_TimestampNS_getNanosEpoch;
jmethodID J_BigDecimal_precision;
jmethodID J_BigDecimal_scale;
jmethodID J_BigDecimal_scaleByPowTen;
Expand Down Expand Up @@ -218,6 +220,7 @@ void create_refs(JNIEnv *env) {
J_Timestamp = make_class_ref(env, "org/duckdb/DuckDBTimestamp");
J_Timestamp_valueOf = get_static_method_id(env, J_Timestamp, "valueOf", "(Ljava/lang/Object;)Ljava/lang/Object;");
J_TimestampTZ = make_class_ref(env, "org/duckdb/DuckDBTimestampTZ");
J_TimestampNS = make_class_ref(env, "org/duckdb/DuckDBTimestampNS");

J_DuckDBDate = make_class_ref(env, "org/duckdb/DuckDBDate");
J_DuckDBDate_getDaysSinceEpoch = get_method_id(env, J_DuckDBDate, "getDaysSinceEpoch", "()J");
Expand Down Expand Up @@ -271,6 +274,7 @@ void create_refs(JNIEnv *env) {
J_Double_doubleValue = get_method_id(env, J_Double, "doubleValue", "()D");
J_Timestamp_getMicrosEpoch = get_method_id(env, J_Timestamp, "getMicrosEpoch", "()J");
J_TimestampTZ_getMicrosEpoch = get_method_id(env, J_TimestampTZ, "getMicrosEpoch", "()J");
J_TimestampNS_getNanosEpoch = get_method_id(env, J_TimestampNS, "getNanosEpoch", "()J");
J_BigDecimal_precision = get_method_id(env, J_BigDecimal, "precision", "()I");
J_BigDecimal_scale = get_method_id(env, J_BigDecimal, "scale", "()I");
J_BigDecimal_scaleByPowTen = get_method_id(env, J_BigDecimal, "scaleByPowerOfTen", "(I)Ljava/math/BigDecimal;");
Expand Down
2 changes: 2 additions & 0 deletions src/jni/refs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ extern jclass J_String;
extern jclass J_Timestamp;
extern jmethodID J_Timestamp_valueOf;
extern jclass J_TimestampTZ;
extern jclass J_TimestampNS;
extern jclass J_BigDecimal;
extern jclass J_HugeInt;
extern jclass J_ByteArray;
Expand All @@ -41,6 +42,7 @@ extern jmethodID J_Float_floatValue;
extern jmethodID J_Double_doubleValue;
extern jmethodID J_Timestamp_getMicrosEpoch;
extern jmethodID J_TimestampTZ_getMicrosEpoch;
extern jmethodID J_TimestampNS_getNanosEpoch;
extern jmethodID J_BigDecimal_precision;
extern jmethodID J_BigDecimal_scale;
extern jmethodID J_BigDecimal_scaleByPowTen;
Expand Down
4 changes: 4 additions & 0 deletions src/jni/types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ duckdb::Value to_duckdb_value(JNIEnv *env, jobject param, duckdb::ClientContext
jlong val = env->CallLongMethod(param, J_Timestamp_getMicrosEpoch);
check_java_exception_and_rethrow(env);
return duckdb::Value::TIME(static_cast<duckdb::dtime_t>(val));
} else if (env->IsInstanceOf(param, J_TimestampNS)) { // Check for subclass before superclass!
jlong val = env->CallLongMethod(param, J_TimestampNS_getNanosEpoch);
check_java_exception_and_rethrow(env);
return duckdb::Value::TIMESTAMPNS(static_cast<duckdb::timestamp_ns_t>(val));
} else if (env->IsInstanceOf(param, J_Timestamp)) {
jlong val = env->CallLongMethod(param, J_Timestamp_getMicrosEpoch);
check_java_exception_and_rethrow(env);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/duckdb/DuckDBPreparedStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ public void setObject(int parameterIndex, Object x) throws SQLException {
if (x instanceof BigInteger) {
x = new DuckDBHugeInt((BigInteger) x);
}
// TIMESTAMP_NS parameters keep the full nanosecond value; every other type keeps the regular conversion.
if ((x instanceof Timestamp || x instanceof LocalDateTime) &&
"TIMESTAMP_NS".equals(getParameterMetaData().getParameterTypeName(parameterIndex))) {
x = x instanceof Timestamp ? new DuckDBTimestampNS(((Timestamp) x).toLocalDateTime())
: new DuckDBTimestampNS((LocalDateTime) x);
}
params[parameterIndex - 1] = x;
}

Expand Down
72 changes: 72 additions & 0 deletions src/main/java/org/duckdb/DuckDBTimestampNS.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.duckdb;

import static org.duckdb.JdbcUtils.createSQLException;

import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
* Internal carrier for a TIMESTAMP_NS parameter value.
*
* <p>{@link DuckDBTimestamp} only keeps microseconds since the epoch, so binding a raw nanosecond value through it
* silently loses the sub-microsecond part. This subclass keeps the exact epoch nanoseconds and is converted by the JNI
* layer into a native {@code TIMESTAMP_NS} value instead of a {@code TIMESTAMP} one.
*
* <p>Package private on purpose: instances are only created by {@link DuckDBPreparedStatement} for parameters whose
* metadata type is exactly TIMESTAMP_NS, and only read back through {@link #getNanosEpoch()} from the JNI layer.
*/
final class DuckDBTimestampNS extends DuckDBTimestamp {

private static final long NANOS_PER_SECOND = 1_000_000_000L;
private static final long NANOS_PER_MICRO = 1_000L;

private final long epochNanos;

DuckDBTimestampNS(LocalDateTime localDateTime) throws SQLException {
this(toEpochNanos(localDateTime));
}

private DuckDBTimestampNS(long epochNanos) throws SQLException {
// The infinity sentinels are rejected before the microsecond floor of the super constructor runs, so an
// unsupported value is always reported as a TIMESTAMP_NS range error.
super(Math.floorDiv(checkedNanos(epochNanos), NANOS_PER_MICRO));
this.epochNanos = epochNanos;
}

long getNanosEpoch() {
return epochNanos;
}

private static long toEpochNanos(LocalDateTime localDateTime) throws SQLException {
long epochSecond = localDateTime.toEpochSecond(ZoneOffset.UTC);
int nanoOfSecond = localDateTime.getNano();
try {
if (epochSecond < 0 && nanoOfSecond > 0) {
// epochSecond * 1e9 alone overflows for representable values near the negative boundary, so move one
// second into the nanosecond part and let the exact integer arithmetic do the carry.
return Math.addExact(Math.multiplyExact(epochSecond + 1, NANOS_PER_SECOND),
nanoOfSecond - NANOS_PER_SECOND);
}
return Math.addExact(Math.multiplyExact(epochSecond, NANOS_PER_SECOND), (long) nanoOfSecond);
} catch (ArithmeticException e) {
throw outOfRange(localDateTime, e);
}
}

private static long checkedNanos(long epochNanos) throws SQLException {
// Only +/-Long.MAX_VALUE are reserved: native timestamp_base_t uses them as the infinity sentinels, every
// other int64 value (including Long.MIN_VALUE) is a finite TIMESTAMP_NS.
if (epochNanos == Long.MAX_VALUE || epochNanos == -Long.MAX_VALUE) {
throw outOfRange(null, null);
}
return epochNanos;
}

private static SQLException outOfRange(LocalDateTime localDateTime, Throwable cause) {
String value = localDateTime == null ? "" : " (value: " + localDateTime + ")";
return createSQLException("Value is out of range for TIMESTAMP_NS: nanoseconds since the epoch must be a "
+ "finite timestamp" + value,
ErrorCode.PREPARED_CONVERSION, cause);
}
}
38 changes: 38 additions & 0 deletions src/test/external/graal/Hello.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import java.sql.*;
import java.time.LocalDateTime;

public class Hello {
public static void main(String[] a) {
Expand All @@ -10,6 +11,7 @@ public static void main(String[] a) {
System.out.println(r.getInt(1));
}
}
checkTimestampNs();
} catch (Throwable t) {
// The default uncaught exception handler can fail to print in a native image,
// masking the real cause, so report it explicitly here.
Expand All @@ -27,4 +29,40 @@ public static void main(String[] a) {
System.exit(1);
}
}

private static void checkTimestampNs() throws Exception {
LocalDateTime expected = LocalDateTime.of(2026, 2, 20, 12, 0, 0, 123456789);
long expectedNanos = 1771588800123456789L;
try (Connection c = DriverManager.getConnection("jdbc:duckdb:"); Statement s = c.createStatement()) {
s.execute("CREATE TABLE ns_bind (id INT, ts TIMESTAMP_NS)");
try (PreparedStatement ps = c.prepareStatement("INSERT INTO ns_bind VALUES (?, ?)")) {
ps.setInt(1, 1);
ps.setTimestamp(2, Timestamp.valueOf(expected));
ps.execute();
ps.setInt(1, 2);
ps.setObject(2, expected);
ps.execute();
}
try (ResultSet r = s.executeQuery("SELECT epoch_ns(ts), ts FROM ns_bind ORDER BY id")) {
for (int i = 0; i < 2; i++) {
if (!r.next()) {
throw new IllegalStateException("TIMESTAMP_NS round trip returned no row " + i);
}
long actual = r.getLong(1);
if (actual != expectedNanos) {
throw new IllegalStateException("TIMESTAMP_NS epoch_ns mismatch on row " + i + ": expected " +
expectedNanos + ", actual " + actual);
}
LocalDateTime readBack = r.getObject(2, LocalDateTime.class);
if (!expected.equals(readBack)) {
throw new IllegalStateException("TIMESTAMP_NS read back mismatch on row " + i + ": expected " +
expected + ", actual " + readBack);
}
}
if (r.next()) {
throw new IllegalStateException("TIMESTAMP_NS round trip returned extra rows");
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/test/java/org/duckdb/TestAppender.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ public static void test_appender_timestamp_local() throws Exception {
assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T12:34:46");
assertEquals(rs.getObject(3, LocalDateTime.class).toString(), "2025-06-25T12:34:45.679");
assertEquals(rs.getObject(4, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901");
assertEquals(rs.getObject(5, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901");
assertEquals(rs.getObject(5, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901234");
assertTrue(rs.next());
assertEquals(rs.getInt(1), 42);
assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T12:34:45");
Expand Down
Loading
Loading