This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git


The following commit(s) were added to refs/heads/master by this push:
     new 148cad5d Patched up version of PR #436: Reject out-of-range sql 
date/time fields in DateTimeConverter.toDate
148cad5d is described below

commit 148cad5d3105ccad82830edad3454a17148e0e59
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Aug 23 08:03:08 2026 -0400

    Patched up version of PR #436: Reject out-of-range sql date/time fields
    in DateTimeConverter.toDate
    
    The problematic bug I fixed is three instances of double parsing.
    
    Instead of going round and round, I applied the diff locally, made the
    following changes, and pushed to master.
    
    Double parsing for the `java.sql.Date` case.
    Validation parses with the strict formatter and valueOf parses again.
    Functionally correct, but wasteful. For example:
    LocalDate.parse(value, SQL_DATE_FORMAT);
    return type.cast(java.sql.Date.valueOf(value));
    The parsed LocalDate is discarded. A more efficient form would be
    Date.valueOf(LocalDate.parse(...)). Same for Time / Timestamp.
    
    Double parsing of for the `java.sql.Time` case. Same as above.
    
    Double parsing of for the `java.sql.Timestamp` case. Same as above.
    
    I added details to Javadocs to clarify intent.
    
    I added the follow unit tests:
    
    In SqlDateConverterTest
    
    testDefaultStringToTypeConvertValidLeapYear
    testDefaultStringToTypeConvertInvalidNonLeapYearFeb29
    testDefaultStringToTypeConvertValidBoundaryDates
    testDefaultStringToTypeConvertStrictValidationMessage
    
    In SqlTimeConverterTest
    
    testDefaultStringToTypeConvertValidBoundaryTimes
    testDefaultStringToTypeConvertInvalidTimeHourMinuteSecond
    testDefaultStringToTypeConvertStrictValidationMessage
    
    In SqlTimestampConverterTest
    
    testDefaultStringToTypeConvertValidTimestampWithFraction
    testDefaultStringToTypeConvertInvalidTimestampOutOfRangeDate
    testDefaultStringToTypeConvertInvalidTimestampOutOfRangeTime
    testDefaultStringToTypeConvertStrictValidationMessage
    
    These tests cover valid leap-year and boundary values, invalid
    out-of-range values for all three SQL types, timestamp fraction handling
    and verification that the new ConversionException messages mention
    strict validation.
---
 .../beanutils2/converters/DateTimeConverter.java   | 122 ++++++++++++++++++---
 .../beanutils2/converters/DateConverterTest.java   |  32 +++---
 .../sql/converters/SqlDateConverterTest.java       |  41 +++++++
 .../sql/converters/SqlTimeConverterTest.java       |  34 +++++-
 .../sql/converters/SqlTimestampConverterTest.java  |  50 ++++++++-
 5 files changed, 243 insertions(+), 36 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java 
b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java
index 3e7952df..444d42b1 100644
--- 
a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java
+++ 
b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java
@@ -22,10 +22,16 @@ import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.time.format.SignStyle;
+import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
 import java.util.Calendar;
 import java.util.Date;
@@ -49,7 +55,6 @@ import org.apache.commons.beanutils2.ConversionException;
  * <li>{@link java.sql.Time}</li>
  * <li>{@link java.sql.Timestamp}</li>
  * </ul>
- *
  * <h2>String Conversions (to and from)</h2> This class provides a number of 
ways in which date/time conversions to/from Strings can be achieved:
  * <ul>
  * <li>Using the SHORT date format for the default Locale, configure using:
@@ -76,15 +81,81 @@ import org.apache.commons.beanutils2.ConversionException;
  * <li>If none of the above are configured the {@code toDate(String)} method 
is used to convert from String to Date and the Dates's {@code toString()} method
  * used to convert from Date to String.</li>
  * </ul>
- *
+ * <p>
+ * For {@link java.sql.Date}, {@link java.sql.Time} and {@link 
java.sql.Timestamp} the default String conversion, that is when {@code 
useLocaleFormat} is
+ * {@code false} and no patterns are configured, requires the JDBC escape 
format and validation is strict: out-of-range fields are rejected with a
+ * {@link ConversionException} instead of being rolled over.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This strict JDBC validation applies only to the 
default String conversion path. When the converter is configured with patterns 
via
+ * {@link #setPattern(String)}/{@link #setPatterns(String[])} or with {@link 
#setUseLocaleFormat(boolean)} = {@code true}, conversion is performed through
+ * {@link java.text.DateFormat}/{@link java.util.Calendar} and the strict JDBC 
validation is bypassed.
+ * </p>
  * <p>
  * The <strong>Time Zone</strong> to use with the date format can be specified 
using the {@link #setTimeZone(TimeZone)} method.
+ * </p>
  *
  * @param <D> The default value type.
  * @since 1.8.0
  */
 public abstract class DateTimeConverter<D> extends AbstractConverter<D> {
 
+    /**
+     * Strict DateTimeFormatter for the JDBC {@code java.sql.Date} escape 
format, rejecting out-of-range fields that {@code valueOf} would roll over.
+     * <p>
+     * Values follow this STRICT format:
+     * <pre>
+     * YEAR 4-10 + '-' + MONTH + '-' + DAY
+     * </pre>
+     */
+    // @formatter:off
+    private static final DateTimeFormatter SQL_DATE_FORMAT = new 
DateTimeFormatterBuilder()
+            .appendValue(ChronoField.YEAR, 4, 10, 
SignStyle.EXCEEDS_PAD).appendLiteral('-')
+            .appendValue(ChronoField.MONTH_OF_YEAR).appendLiteral('-')
+            .appendValue(ChronoField.DAY_OF_MONTH)
+            .toFormatter()
+            .withResolverStyle(ResolverStyle.STRICT);
+    // @formatter:on
+
+    /**
+     * Strict DateTimeFormatter for the JDBC {@code java.sql.Time} escape 
format, rejecting out-of-range fields that {@code valueOf} would roll over.
+     * <p>
+     * Values follow this STRICT format:
+     * <pre>
+     * HOUR_OF_DAY ':' MINUTE ':' SECOND
+     * </pre>
+     */
+    // @formatter:off
+    private static final DateTimeFormatter SQL_TIME_FORMAT = new 
DateTimeFormatterBuilder()
+            .appendValue(ChronoField.HOUR_OF_DAY).appendLiteral(':')
+            .appendValue(ChronoField.MINUTE_OF_HOUR).appendLiteral(':')
+            .appendValue(ChronoField.SECOND_OF_MINUTE)
+            .toFormatter()
+            .withResolverStyle(ResolverStyle.STRICT);
+    // @formatter:on
+
+    /**
+     * Strict DateTimeFormatter for the JDBC {@code java.sql.Timestamp} escape 
format, rejecting out-of-range fields that {@code valueOf} would roll over.
+     * <p>
+     * Values follow this STRICT format:
+     * <pre>
+     * YEAR '-' MONTH '-' DAY ' ' HOUR ':' MINUTE ':' SECOND [fraction]
+     * </pre>
+     */
+    // @formatter:off
+    private static final DateTimeFormatter SQL_TIMESTAMP_FORMAT = new 
DateTimeFormatterBuilder()
+            .appendValue(ChronoField.YEAR, 4, 10, 
SignStyle.EXCEEDS_PAD).appendLiteral('-')
+            .appendValue(ChronoField.MONTH_OF_YEAR).appendLiteral('-')
+            .appendValue(ChronoField.DAY_OF_MONTH).appendLiteral(' ')
+            .appendValue(ChronoField.HOUR_OF_DAY).appendLiteral(':')
+            .appendValue(ChronoField.MINUTE_OF_HOUR).appendLiteral(':')
+            .appendValue(ChronoField.SECOND_OF_MINUTE)
+            .optionalStart()
+                .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
+            .optionalEnd().toFormatter()
+            .withResolverStyle(ResolverStyle.STRICT);
+    // @formatter:on
+
     private String[] patterns;
     private String displayPatterns;
     private Locale locale;
@@ -569,9 +640,10 @@ public abstract class DateTimeConverter<D> extends 
AbstractConverter<D> {
     }
 
     /**
-     * Default String to Date conversion.
+     * Default String to date/time conversion.
      * <p>
-     * This method handles conversion from a String to the following types:
+     * This method handles conversion from a {@code String} to the following 
types:
+     * </p>
      * <ul>
      * <li>{@link java.sql.Date}</li>
      * <li>{@link java.sql.Time}</li>
@@ -579,36 +651,50 @@ public abstract class DateTimeConverter<D> extends 
AbstractConverter<D> {
      * <li>{@link java.time.Instant}</li>
      * </ul>
      * <p>
-     * <strong>N.B.</strong> No default String conversion mechanism is 
provided for {@link java.util.Date} and {@link java.util.Calendar} type.
+     * For the {@code java.sql} types the String must be in the JDBC escape 
format and validation is strict: out-of-range fields, for example {@code 
2006-02-31}
+     * or {@code 25:70:90} are rejected with a {@link ConversionException} 
instead of being rolled over.
+     * </p>
+     * <p>
+     * <strong>Note:</strong> This strict JDBC validation applies only to the 
default String conversion path when no patterns are configured and
+     * {@code useLocaleFormat} is {@code false}. When the converter is 
configured with patterns or {@link #setUseLocaleFormat(boolean)} = {@code true},
+     * conversion is performed through {@link java.text.DateFormat}/{@link 
java.util.Calendar} and the strict JDBC validation is bypassed.
+     * </p>
+     * <p>
+     * No default String conversion mechanism is provided for {@link 
java.util.Date} and {@link java.util.Calendar}.
+     * </p>
      *
-     * @param <T>   The target type
-     * @param type  The date type to convert to
-     * @param value The String value to convert.
-     * @return The converted Number value.
+     * @param <T>   the target type
+     * @param type  the date/time type to convert to
+     * @param value the String value to convert
+     * @return the converted date/time value
+     * @throws ConversionException if the String cannot be converted to the 
target type
      */
     private <T> T toDate(final Class<T> type, final String value) {
         // java.sql.Date
         if (type.equals(java.sql.Date.class)) {
             try {
-                return type.cast(java.sql.Date.valueOf(value));
-            } catch (final IllegalArgumentException e) {
-                throw new ConversionException("String must be in JDBC format 
[yyyy-MM-dd] to create a java.sql.Date");
+                return type.cast(java.sql.Date.valueOf(LocalDate.parse(value, 
SQL_DATE_FORMAT)));
+            } catch (final IllegalArgumentException | DateTimeParseException 
e) {
+                throw new ConversionException(
+                        "String must be in JDBC format [yyyy-MM-dd] to create 
a java.sql.Date; validation is strict, out-of-range fields are rejected");
             }
         }
         // java.sql.Time
         if (type.equals(java.sql.Time.class)) {
             try {
-                return type.cast(java.sql.Time.valueOf(value));
-            } catch (final IllegalArgumentException e) {
-                throw new ConversionException("String must be in JDBC format 
[HH:mm:ss] to create a java.sql.Time");
+                return type.cast(java.sql.Time.valueOf(LocalTime.parse(value, 
SQL_TIME_FORMAT)));
+            } catch (final IllegalArgumentException | DateTimeParseException 
e) {
+                throw new ConversionException(
+                        "String must be in JDBC format [HH:mm:ss] to create a 
java.sql.Time; validation is strict, out-of-range fields are rejected");
             }
         }
         // java.sql.Timestamp
         if (type.equals(java.sql.Timestamp.class)) {
             try {
-                return type.cast(java.sql.Timestamp.valueOf(value));
-            } catch (final IllegalArgumentException e) {
-                throw new ConversionException("String must be in JDBC format 
[yyyy-MM-dd HH:mm:ss.fffffffff] to create a java.sql.Timestamp");
+                return 
type.cast(java.sql.Timestamp.valueOf(LocalDateTime.parse(value, 
SQL_TIMESTAMP_FORMAT)));
+            } catch (final IllegalArgumentException | DateTimeParseException 
e) {
+                throw new ConversionException("String must be in JDBC format 
[yyyy-MM-dd HH:mm:ss.fffffffff] to create a java.sql.Timestamp; "
+                        + "validation is strict, out-of-range fields are 
rejected");
             }
         }
         // java.time.Instant
diff --git 
a/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java 
b/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java
index 7bb5380a..ed154453 100644
--- 
a/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java
+++ 
b/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java
@@ -62,14 +62,16 @@ class DateConverterTest extends 
AbstractDateConverterTest<Date> {
     }
 
     /**
-     * Convert from a Calendar to the appropriate Date type
-     *
-     * @param value The Calendar value to convert
-     * @return The converted value
+     * For {@code getTime()} in {@code [Long.MIN_VALUE, Long.MIN_VALUE + 807]} 
the whole-second term
+     * {@code Math.floorDiv(getTime(), 1000) * 1000} wraps around {@link 
Long#MIN_VALUE}, but adding the non-negative
+     * {@code getNanos() / 1_000_000} wraps it back: the two terms reconstruct 
{@code getTime()} exactly in
+     * two's-complement arithmetic, so no overflow guard is needed.
      */
-    @Override
-    protected Date toType(final Calendar value) {
-        return value.getTime();
+    @Test
+    void testConvertExtremePreEpochSqlTimestamp() {
+        assertEquals(Long.MIN_VALUE, 
makeConverter().convert(getExpectedType(), new 
Timestamp(Long.MIN_VALUE)).getTime());
+        // last value whose whole-second term still wraps
+        assertEquals(Long.MIN_VALUE + 807, 
makeConverter().convert(getExpectedType(), new Timestamp(Long.MIN_VALUE + 
807)).getTime());
     }
 
     /**
@@ -85,15 +87,13 @@ class DateConverterTest extends 
AbstractDateConverterTest<Date> {
     }
 
     /**
-     * For {@code getTime()} in {@code [Long.MIN_VALUE, Long.MIN_VALUE + 807]} 
the whole-second term
-     * {@code Math.floorDiv(getTime(), 1000) * 1000} wraps around {@link 
Long#MIN_VALUE}, but adding the non-negative
-     * {@code getNanos() / 1_000_000} wraps it back: the two terms reconstruct 
{@code getTime()} exactly in
-     * two's-complement arithmetic, so no overflow guard is needed.
+     * Convert from a Calendar to the appropriate Date type
+     *
+     * @param value The Calendar value to convert
+     * @return The converted value
      */
-    @Test
-    void testConvertExtremePreEpochSqlTimestamp() {
-        assertEquals(Long.MIN_VALUE, 
makeConverter().convert(getExpectedType(), new 
Timestamp(Long.MIN_VALUE)).getTime());
-        // last value whose whole-second term still wraps
-        assertEquals(Long.MIN_VALUE + 807, 
makeConverter().convert(getExpectedType(), new Timestamp(Long.MIN_VALUE + 
807)).getTime());
+    @Override
+    protected Date toType(final Calendar value) {
+        return value.getTime();
     }
 }
diff --git 
a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java
 
b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java
index 18cbe3eb..821f03ef 100644
--- 
a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java
+++ 
b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java
@@ -18,10 +18,13 @@
 package org.apache.commons.beanutils2.sql.converters;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.sql.Date;
 import java.util.Calendar;
 
+import org.apache.commons.beanutils2.ConversionException;
 import org.apache.commons.beanutils2.converters.AbstractDateConverterTest;
 import org.apache.commons.beanutils2.converters.DateTimeConverter;
 import org.junit.jupiter.api.Test;
@@ -62,6 +65,7 @@ class SqlDateConverterTest extends 
AbstractDateConverterTest<Date> {
         return new SqlDateConverter(defaultValue);
     }
 
+
     /**
      * Test default String to java.sql.Date conversion
      */
@@ -80,6 +84,43 @@ class SqlDateConverterTest extends 
AbstractDateConverterTest<Date> {
 
         // Invalid String --> java.sql.Date Conversion
         invalidConversion(converter, "01/01/2006");
+
+        // Out-of-range fields must be rejected, not silently rolled over 
(2006-02-31 -> 2006-03-03)
+        invalidConversion(converter, "2006-02-31");
+        invalidConversion(converter, "2006-13-01");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertInvalidNonLeapYearFeb29() {
+        final SqlDateConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        invalidConversion(converter, "2005-02-29");
+        invalidConversion(converter, "1900-02-29");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertStrictValidationMessage() {
+        final SqlDateConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        final ConversionException ex = assertThrows(ConversionException.class, 
() -> converter.convert(getExpectedType(), "2006-02-31"));
+        assertTrue(ex.getMessage().contains("validation is strict"), "Message 
must mention strict validation");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertValidBoundaryDates() {
+        final SqlDateConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        validConversion(converter, toType("0001-01-01", "yyyy-MM-dd", null), 
"0001-01-01");
+        validConversion(converter, toType("9999-12-31", "yyyy-MM-dd", null), 
"9999-12-31");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertValidLeapYear() {
+        final SqlDateConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        final String testString = "2004-02-29";
+        final Object expected = toType(testString, "yyyy-MM-dd", null);
+        validConversion(converter, expected, testString);
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java
 
b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java
index 918290c7..4fe4a8c7 100644
--- 
a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java
+++ 
b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java
@@ -17,10 +17,14 @@
 
 package org.apache.commons.beanutils2.sql.converters;
 
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.sql.Time;
 import java.util.Calendar;
 import java.util.Locale;
 
+import org.apache.commons.beanutils2.ConversionException;
 import org.apache.commons.beanutils2.converters.AbstractDateConverterTest;
 import org.junit.jupiter.api.Test;
 
@@ -77,6 +81,35 @@ class SqlTimeConverterTest extends 
AbstractDateConverterTest<Time> {
 
         // Invalid String --> java.sql.Time Conversion
         invalidConversion(converter, "15:36");
+
+        // Out-of-range fields must be rejected, not silently rolled over 
(25:70:90 -> 02:11:30)
+        invalidConversion(converter, "25:70:90");
+        invalidConversion(converter, "-1:-1:-1");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertInvalidTimeHourMinuteSecond() {
+        final SqlTimeConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        invalidConversion(converter, "24:00:00");
+        invalidConversion(converter, "12:60:00");
+        invalidConversion(converter, "12:00:60");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertStrictValidationMessage() {
+        final SqlTimeConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        final ConversionException ex = assertThrows(ConversionException.class, 
() -> converter.convert(getExpectedType(), "25:70:90"));
+        assertTrue(ex.getMessage().contains("validation is strict"));
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertValidBoundaryTimes() {
+        final SqlTimeConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        validConversion(converter, toType("00:00:00", "HH:mm:ss", null), 
"00:00:00");
+        validConversion(converter, toType("23:59:59", "HH:mm:ss", null), 
"23:59:59");
     }
 
     /**
@@ -127,5 +160,4 @@ class SqlTimeConverterTest extends 
AbstractDateConverterTest<Time> {
     protected Time toType(final Calendar value) {
         return new Time(getTimeInMillis(value));
     }
-
 }
diff --git 
a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java
 
b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java
index 902735e4..72415c6d 100644
--- 
a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java
+++ 
b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java
@@ -88,6 +88,55 @@ class SqlTimestampConverterTest extends 
AbstractDateConverterTest<Timestamp> {
         invalidConversion(converter, "2006/09/21 15:36:01.0");
         invalidConversion(converter, "2006-10-22");
         invalidConversion(converter, "15:36:01");
+
+        // Out-of-range fields must be rejected, not silently rolled over 
(2006-02-31 25:70:90 -> 2006-03-04 02:11:30)
+        invalidConversion(converter, "2006-02-31 15:36:01.0");
+        invalidConversion(converter, "2006-10-23 25:70:90.0");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertInvalidTimestampOutOfRangeDate() {
+        final SqlTimestampConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        invalidConversion(converter, "2006-02-31 15:36:01");
+        invalidConversion(converter, "2006-13-01 00:00:00");
+    }
+
+    @Test
+    void testDefaultStringToTypeConvertInvalidTimestampOutOfRangeTime() {
+        final SqlTimestampConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        invalidConversion(converter, "2006-10-23 25:00:00");
+        invalidConversion(converter, "2006-10-23 12:60:00");
+    }
+
+    /**
+     * Test default String to {@code java.sql.Timestamp} conversion with 
fractional seconds.
+     * <p>
+     * The strict {@link DateTimeConverter#SQL_TIMESTAMP_FORMAT} accepts an 
optional fractional second part. The helper
+     * {@link AbstractDateConverterTest#toType(String,String,Locale)} builds 
the expected value via {@link SimpleDateFormat}, which only supports millisecond
+     * precision {@code S} = 1-3 digits. Therefore, the test uses a 3-digit 
fraction {@code .123} for the {@code toType} comparison and additionally 
verifies
+     * that {@code Timestamp.valueOf} correctly handles the full 9-digit 
nanosecond fraction that the strict formatter accepts. This avoids the
+     * {@code ParseException} that occurs with {@code SSSSSSSSS} in {@code 
SimpleDateFormat}.
+     * </p>
+     */
+    @Test
+    void testDefaultStringToTypeConvertValidTimestampWithFraction() {
+        final SqlTimestampConverter converter = makeConverter();
+        converter.setUseLocaleFormat(false);
+        // 3-digit fraction – SimpleDateFormat can create the expected value
+        final String testString1 = "2006-10-23 15:36:01.123";
+        final Object expected1 = toType(testString1, "yyyy-MM-dd 
HH:mm:ss.SSS", null);
+        validConversion(converter, expected1, testString1);
+        // No fraction – also valid
+        final String testString2 = "2006-10-23 15:36:01";
+        final Object expected2 = toType(testString2, "yyyy-MM-dd HH:mm:ss", 
null);
+        validConversion(converter, expected2, testString2);
+        // Full nanosecond fraction – expected built directly with valueOf
+        // valueOf handles up to 9 digits, toType cannot, so build expected 
manually
+        final String testString3 = "2006-10-23 15:36:01.123456789";
+        final java.sql.Timestamp expected3 = 
java.sql.Timestamp.valueOf("2006-10-23 15:36:01.123456789");
+        validConversion(converter, expected3, testString3);
     }
 
     /**
@@ -142,5 +191,4 @@ class SqlTimestampConverterTest extends 
AbstractDateConverterTest<Timestamp> {
     protected Timestamp toType(final Calendar value) {
         return new Timestamp(getTimeInMillis(value));
     }
-
 }

Reply via email to