2626import java .sql .Statement ;
2727import java .sql .Time ;
2828import java .sql .Timestamp ;
29+ import java .time .DateTimeException ;
30+ import java .time .Instant ;
2931import java .time .LocalDate ;
3032import java .time .LocalDateTime ;
3133import java .time .LocalTime ;
32- import java .time .format .DateTimeParseException ;
34+ import java .time .ZoneId ;
35+ import java .time .ZoneOffset ;
3336import java .util .Calendar ;
3437import java .util .HashMap ;
3538import java .util .List ;
@@ -632,43 +635,20 @@ public byte[] getBytes(int columnIndex) throws SQLException {
632635
633636 @ Override
634637 public Date getDate (int columnIndex ) throws SQLException {
635- String s = getString (columnIndex );
636- if (s == null ) return null ;
637- try {
638- return Date .valueOf (s );
639- } catch (IllegalArgumentException e ) {
640- try {
641- return new Date (Long .parseLong (s ));
642- } catch (NumberFormatException nfe ) {
643- return null ;
644- }
645- }
638+ LocalDateTime value = dateTimeValue (columnIndex , "Date" );
639+ return value == null ? null : Date .valueOf (value .toLocalDate ());
646640 }
647641
648642 @ Override
649643 public Time getTime (int columnIndex ) throws SQLException {
650- String s = getString (columnIndex );
651- if (s == null ) return null ;
652- try {
653- return Time .valueOf (s );
654- } catch (IllegalArgumentException e ) {
655- return null ;
656- }
644+ LocalDateTime value = dateTimeValue (columnIndex , "Time" );
645+ return value == null ? null : Time .valueOf (value .toLocalTime ());
657646 }
658647
659648 @ Override
660649 public Timestamp getTimestamp (int columnIndex ) throws SQLException {
661- String s = getString (columnIndex );
662- if (s == null ) return null ;
663- try {
664- return Timestamp .valueOf (s );
665- } catch (IllegalArgumentException e ) {
666- try {
667- return new Timestamp (Long .parseLong (s ));
668- } catch (NumberFormatException nfe ) {
669- return null ;
670- }
671- }
650+ LocalDateTime value = dateTimeValue (columnIndex , "Timestamp" );
651+ return value == null ? null : Timestamp .valueOf (value );
672652 }
673653
674654 @ Override
@@ -1001,9 +981,11 @@ public void setFetchSize(int rows) throws SQLException {
1001981 /**
1002982 * Returns a column value converted to the requested Java type.
1003983 *
1004- * <p>The conversion matrix matches sqlite-jdbc 3.53.4.0. Numeric wrapper targets are gated on
1005- * the current value's SQLite storage class; any other storage class raises {@code Bad value
1006- * for type Integer} (or the corresponding target type name):
984+ * <p>The numeric and text conversions match sqlite-jdbc 3.53.4.0. Date and time conversions
985+ * follow the SQLite storage-class conventions instead of parsing the value's text form.
986+ * Numeric wrapper targets are gated on the current value's SQLite storage class; any other
987+ * storage class raises {@code Bad value for type Integer} (or the corresponding target type
988+ * name):
1007989 *
1008990 * <table>
1009991 * <caption>Target type by source storage class</caption>
@@ -1037,7 +1019,7 @@ public void setFetchSize(int rows) throws SQLException {
10371019 * <tr>
10381020 * <td>{@code Date}, {@code Time}, {@code Timestamp}, {@code LocalDate}, {@code LocalTime},
10391021 * {@code LocalDateTime}</td>
1040- * <td>text parse </td><td>text parse </td><td>text parse </td><td>text parse </td>
1022+ * <td>epoch millis </td><td>Julian day </td><td>date/time text </td><td>bad value </td>
10411023 * <td>{@code null}</td>
10421024 * </tr>
10431025 * <tr>
@@ -1046,11 +1028,19 @@ public void setFetchSize(int rows) throws SQLException {
10461028 * </tr>
10471029 * </table>
10481030 *
1031+ * <p>INTEGER date and time values are milliseconds since the Unix epoch, FLOAT values are
1032+ * Julian days, and TEXT values use the SQLite date and time formats, including a bare date and
1033+ * a bare time. A bare time uses SQLite's 2000-01-01 default date. The SQLite TEXT format is
1034+ * {@code yyyy-MM-dd HH:mm:ss.SSS}, with optional fractional digits and an optional time zone
1035+ * suffix. All six date and time targets are built from the same storage-class value, so
1036+ * {@code getObject(column, LocalDate.class)} and {@code getDate(column)} agree.
1037+ *
10491038 * <p>{@code Short} and {@code Byte} are rejected rather than narrowed from the numeric
10501039 * targets, preserving the sqlite-jdbc 3.53.4.0 contract.
10511040 *
10521041 * @throws SQLException if {@code type} is {@code null}, a numeric target reads a value from
1053- * any storage class other than INTEGER or FLOAT, or the value cannot be converted
1042+ * any storage class other than INTEGER or FLOAT, a date or time target reads a BLOB or
1043+ * unparseable TEXT, or the value cannot be converted
10541044 * @throws SQLFeatureNotSupportedException if {@code type} is not a supported target type
10551045 */
10561046 @ Override
@@ -1120,46 +1110,124 @@ public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
11201110 }
11211111
11221112 private LocalDate getLocalDate (int columnIndex ) throws SQLException {
1123- String s = getString (columnIndex );
1124- if (s == null ) {
1125- return null ;
1126- }
1113+ LocalDateTime value = dateTimeValue (columnIndex , "LocalDate" );
1114+ return value == null ? null : value .toLocalDate ();
1115+ }
1116+
1117+ private LocalTime getLocalTime (int columnIndex ) throws SQLException {
1118+ LocalDateTime value = dateTimeValue (columnIndex , "LocalTime" );
1119+ return value == null ? null : value .toLocalTime ();
1120+ }
1121+
1122+ private LocalDateTime getLocalDateTime (int columnIndex ) throws SQLException {
1123+ return dateTimeValue (columnIndex , "LocalDateTime" );
1124+ }
1125+
1126+ /**
1127+ * Reads a column as a local date and time using SQLite storage-class conventions: INTEGER is
1128+ * epoch milliseconds, FLOAT is a Julian day, and TEXT is a SQLite date/time string.
1129+ */
1130+ private LocalDateTime dateTimeValue (int columnIndex , String targetType ) throws SQLException {
1131+ Value value = getCurrentValue (columnIndex );
1132+ return switch (value ) {
1133+ case Value .Null n -> null ;
1134+ case Value .Integer i -> epochMillisToLocalDateTime (i .value (), targetType );
1135+ case Value .Float f ->
1136+ epochMillisToLocalDateTime (julianDayToEpochMillis (f .value ()), targetType );
1137+ case Value .Text t -> parseSqliteDateTime (t .value (), targetType );
1138+ case Value .Blob b -> throw new SQLException ("Bad value for type " + targetType );
1139+ };
1140+ }
1141+
1142+ private static LocalDateTime epochMillisToLocalDateTime (long epochMillis , String targetType )
1143+ throws SQLException {
11271144 try {
1128- return LocalDate .parse (s .length () >= 10 ? s .substring (0 , 10 ) : s );
1129- } catch (DateTimeParseException e ) {
1130- throw new SQLException ("Bad value for type LocalDate: " + s , e );
1145+ return LocalDateTime .ofInstant (
1146+ Instant .ofEpochMilli (epochMillis ), ZoneId .systemDefault ());
1147+ } catch (DateTimeException e ) {
1148+ throw new SQLException ("Bad value for type " + targetType , e );
11311149 }
11321150 }
11331151
1134- private LocalTime getLocalTime (int columnIndex ) throws SQLException {
1135- String s = getString (columnIndex );
1136- if (s == null ) {
1137- return null ;
1152+ private static long julianDayToEpochMillis (double julianDay ) {
1153+ return Math .round ((julianDay - 2440587.5 ) * 86_400_000.0 );
1154+ }
1155+
1156+ private static LocalDateTime parseSqliteDateTime (String text , String targetType )
1157+ throws SQLException {
1158+ String value = text .trim ();
1159+ ZoneOffset offset = null ;
1160+ if (value .endsWith ("Z" ) || value .endsWith ("z" )) {
1161+ offset = ZoneOffset .UTC ;
1162+ value = value .substring (0 , value .length () - 1 );
1163+ } else if (value .length () > 10 ) {
1164+ int sign = Math .max (value .lastIndexOf ('+' ), value .lastIndexOf ('-' ));
1165+ if (sign > 9 ) {
1166+ try {
1167+ offset = ZoneOffset .of (value .substring (sign ));
1168+ } catch (DateTimeException e ) {
1169+ throw new SQLException ("Bad value for type " + targetType + ": " + text , e );
1170+ }
1171+ value = value .substring (0 , sign );
1172+ }
11381173 }
1139- int separator = Math .max (s .indexOf (' ' ), s .indexOf ('T' ));
1140- String value = separator >= 0 ? s .substring (separator + 1 ) : s ;
1174+
11411175 try {
1142- return LocalTime .parse (value );
1143- } catch (DateTimeParseException e ) {
1144- throw new SQLException ("Bad value for type LocalTime: " + s , e );
1176+ int separator = -1 ;
1177+ for (int i = 0 ; i < value .length (); i ++) {
1178+ char c = value .charAt (i );
1179+ if (c == ' ' || c == 'T' ) {
1180+ separator = i ;
1181+ break ;
1182+ }
1183+ }
1184+
1185+ LocalDate date ;
1186+ LocalTime time ;
1187+ if (separator >= 0 ) {
1188+ date = LocalDate .parse (value .substring (0 , separator ));
1189+ time = parseSqliteTime (value .substring (separator + 1 ));
1190+ } else if (value .length () >= 10 && value .charAt (4 ) == '-' ) {
1191+ date = LocalDate .parse (value );
1192+ time = LocalTime .MIDNIGHT ;
1193+ } else {
1194+ // SQLite uses 2000-01-01 when a time string carries no date.
1195+ date = LocalDate .of (2000 , 1 , 1 );
1196+ time = parseSqliteTime (value );
1197+ }
1198+
1199+ LocalDateTime local = LocalDateTime .of (date , time );
1200+ return offset == null
1201+ ? local
1202+ : local .atOffset (offset )
1203+ .atZoneSameInstant (ZoneId .systemDefault ())
1204+ .toLocalDateTime ();
1205+ } catch (DateTimeException | NumberFormatException e ) {
1206+ throw new SQLException ("Bad value for type " + targetType + ": " + text , e );
11451207 }
11461208 }
11471209
1148- private LocalDateTime getLocalDateTime (int columnIndex ) throws SQLException {
1149- String s = getString (columnIndex );
1150- if (s == null ) {
1151- return null ;
1210+ private static LocalTime parseSqliteTime (String text ) {
1211+ int dot = text .indexOf ('.' );
1212+ String clock = dot >= 0 ? text .substring (0 , dot ) : text ;
1213+ String [] parts = clock .split (":" , -1 );
1214+ if (parts .length < 2 || parts .length > 3 ) {
1215+ throw new DateTimeException ("Bad time: " + text );
11521216 }
1153- String value = s .indexOf (' ' ) >= 0 ? s .replace (' ' , 'T' ) : s ;
1154- try {
1155- return LocalDateTime .parse (value );
1156- } catch (DateTimeParseException e ) {
1157- try {
1158- return LocalDate .parse (value ).atStartOfDay ();
1159- } catch (DateTimeParseException e2 ) {
1160- throw new SQLException ("Bad value for type LocalDateTime: " + s , e2 );
1217+ int hour = Integer .parseInt (parts [0 ]);
1218+ int minute = Integer .parseInt (parts [1 ]);
1219+ int second = parts .length == 3 ? Integer .parseInt (parts [2 ]) : 0 ;
1220+
1221+ int nanos = 0 ;
1222+ if (dot >= 0 ) {
1223+ String fraction = text .substring (dot + 1 );
1224+ String digits = fraction .length () > 9 ? fraction .substring (0 , 9 ) : fraction ;
1225+ nanos = Integer .parseInt (digits );
1226+ for (int i = digits .length (); i < 9 ; i ++) {
1227+ nanos *= 10 ;
11611228 }
11621229 }
1230+ return LocalTime .of (hour , minute , second , nanos );
11631231 }
11641232
11651233 @ Override
0 commit comments