Skip to content

Commit 1e00a90

Browse files
jeffjensenclaude
andcommitted
fix(dataset): Stop TimestampDataType double-shifting TIMESTAMP WITH TIME ZONE writes
Issue #711's fix made setSqlValue() pass a Calendar built from the dataset's own timezone offset to statement.setTimestamp(column, ts, cal). That is correct for TIMESTAMP WITHOUT TIME ZONE columns, but some JDBC drivers (confirmed with H2) use the Calendar only to compute the wall-clock digits sent to a TIMESTAMP WITH TIME ZONE column, then tag the stored value with the JVM's default timezone instead of the Calendar's - silently discarding the dataset's own timezone and storing the wrong instant whenever the JVM and dataset timezones differ. * Detect a TIMESTAMP WITH TIME ZONE destination column via PreparedStatement.getParameterMetaData() and write it with setObject(OffsetDateTime) instead, which drivers supporting the SQL type map unambiguously. Fall back to the existing Calendar-based behavior if parameter metadata is unavailable or unsupported. * Columns without a timezone keep the existing #711 behavior unchanged. * Add TimestampDataTypeTest coverage against a real H2 TIMESTAMP WITH TIME ZONE column reproducing the bug (ported from yoshida16729438's dbunit-320-timestamp reproducer), plus a paired TIMESTAMP-without-zone test guarding against regressing #711. * Add TimestampDataTypeIT, a Failsafe companion that runs both cases through DatabaseEnvironment against whichever database profile is active. The plain-TIMESTAMP case runs unconditionally, substituting DATETIME2 on SQL Server, whose TIMESTAMP type is an unrelated, unwritable row-versioning type rather than a date/time type. * Gate the zoned-column case behind a new TestFeature.TIMESTAMP_WITH_TIMEZONE flag, declared unsupported via dbunit.profile.unsupportedFeatures on Derby, MySQL, MSSQL, and DB2 (whose dialects reject the column syntax outright) and on Oracle (whose JDBC driver throws SQLFeatureNotSupportedException from getParameterMetaData() itself, a known driver limitation rather than a dialect gap). Declaring the gap in the profile up front, rather than discovering it by catching whatever exception a given driver happens to throw, means any other setup failure surfaces as a real test error instead of being silently treated as unsupported. H2, HSQLDB, and PostgreSQL need no such declaration: H2 and HSQLDB run the assertion for real, and PostgreSQL already skips itself cleanly via the pre-existing parameter-metadata type check (pgjdbc reports a different constant without throwing). * Document TIMESTAMP_WITH_TIMEZONE in the "Unsupported Features Values" table in src/site/asciidoc/integrationtests.adoc, alongside the existing feature entries. Refs: 831 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fr791v2SnTRnyb1eU7LNB
1 parent 892b2a1 commit 1e00a90

13 files changed

Lines changed: 298 additions & 9 deletions

src/changes/changes.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</properties>
1414

1515
<body>
16-
<release version="3.4.0-SNAPSHOT" date="TBD" description="Un-skip and strengthen remaining disabled and no-op tests: implement DefaultPrepAndExpectedTestCaseTest stubs, convert ForwardOnlyDataSetTest no-ops to assertThrows, implement ExcludeTableFilterTest reverse-order tests, relocate ToStringViewTest's package-blocked cases, restore commented-out unit tests in CsvParserTest/FlatXmlTableTest/BiDirectionalEdgesDepthFirstSearchTest, implement ReplacementTableTest.testGetMissingValue, add assertions to applyColumnFilters tests, fix DatabaseTestCase tear-down exceptions obscuring test failures, rewrite obsolete DbUnitTaskIT queryset test, delete two dead commented-out test fragments, stop swallowing failures in PostgreSQL xtest* methods, add CachingConnectionProvider for opt-in IDatabaseConnection reuse across test methods, reduce DefaultPrepAndExpectedTestCase's per-test connection churn, fix DefaultPrepAndExpectedTestCase.runTest() losing a test failure to a cleanup exception, fix DefaultPrepAndExpectedTestCase column-name matching under non-English default locales, harmonize DefaultPrepAndExpectedTestCase's null-databaseTester handling on IllegalStateException, apply code-quality cleanups to DefaultPrepAndExpectedTestCase, pin remaining identifier case folding to Locale.ENGLISH for Turkish-family default locales, fix NullPointerException exporting NO_VALUE cells to flat XML and YAML dumping the raw sentinel, fix SQLHelper's schema-correction notice being silently dropped by a guard/call level mismatch, close BytesDataType file/URL streams and read them in chunks, fail fast on invalid BytesDataType [text]/[base64] values instead of silently storing garbage or NULL, fix QualifiedTableName escaping a single-character segment before a dot as part of the next segment, fix the Ant export step rejecting an uppercase CSV format and leaving an empty output file for unsupported formats, preserve the verify failure as suppressed when DefaultPrepAndExpectedTestCase.postTest's cleanup also fails, fix InsertOperation binding NULL into NOT NULL DEFAULT columns depending on row order, round-trip supplementary characters and replace unrepresentable control characters with U+FFFD in XML export, fix TimestampDataType mis-encoding pre-epoch fractional-second timestamps with a timezone suffix, roll back TransactionOperation on Error instead of implicitly committing, fix XlsTable negative row count and missing-row NullPointerException, pin YAML/DTD/OracleXMLType writer charsets to UTF-8 matching their readers, skip a redundant per-table metadata query in DatabaseDataSet.getTableMetaData, guard or remove per-cell debug logging in the datatype package, fix a data race that could mask the real producer failure in StreamingIterator, fix CsvParser duplicating the continuation line on a short multi-line parse, use DatabaseConfig.getFeature for the qualified-table-names check in DatabaseDataSet, fix FileHelper.copyFile leaking the source stream when the destination cannot be opened, keep DefaultPrepAndExpectedTestCase.configureTest's connection open for the test lifecycle instead of closing and immediately reacquiring it, remove a dead field and harden the close path in RefreshOperation, route Base64's stderr diagnostics through slf4j, remove XmlWriter's dead setEncoding(String) overload, fix CsvProducer truncating table names containing .csv before the final suffix, remove unused duplicate slf4jVersion/snakeYamlVersion pom properties, fix SQLHelper.logDebugIfValueChanged brace indentation, centralize duplicated Turkish-locale test setup into a shared @TurkishDefaultLocale JUnit 5 extension">
16+
<release version="3.4.0-SNAPSHOT" date="TBD" description="Un-skip and strengthen remaining disabled and no-op tests: implement DefaultPrepAndExpectedTestCaseTest stubs, convert ForwardOnlyDataSetTest no-ops to assertThrows, implement ExcludeTableFilterTest reverse-order tests, relocate ToStringViewTest's package-blocked cases, restore commented-out unit tests in CsvParserTest/FlatXmlTableTest/BiDirectionalEdgesDepthFirstSearchTest, implement ReplacementTableTest.testGetMissingValue, add assertions to applyColumnFilters tests, fix DatabaseTestCase tear-down exceptions obscuring test failures, rewrite obsolete DbUnitTaskIT queryset test, delete two dead commented-out test fragments, stop swallowing failures in PostgreSQL xtest* methods, add CachingConnectionProvider for opt-in IDatabaseConnection reuse across test methods, reduce DefaultPrepAndExpectedTestCase's per-test connection churn, fix DefaultPrepAndExpectedTestCase.runTest() losing a test failure to a cleanup exception, fix DefaultPrepAndExpectedTestCase column-name matching under non-English default locales, harmonize DefaultPrepAndExpectedTestCase's null-databaseTester handling on IllegalStateException, apply code-quality cleanups to DefaultPrepAndExpectedTestCase, pin remaining identifier case folding to Locale.ENGLISH for Turkish-family default locales, fix NullPointerException exporting NO_VALUE cells to flat XML and YAML dumping the raw sentinel, fix SQLHelper's schema-correction notice being silently dropped by a guard/call level mismatch, close BytesDataType file/URL streams and read them in chunks, fail fast on invalid BytesDataType [text]/[base64] values instead of silently storing garbage or NULL, fix QualifiedTableName escaping a single-character segment before a dot as part of the next segment, fix the Ant export step rejecting an uppercase CSV format and leaving an empty output file for unsupported formats, preserve the verify failure as suppressed when DefaultPrepAndExpectedTestCase.postTest's cleanup also fails, fix InsertOperation binding NULL into NOT NULL DEFAULT columns depending on row order, round-trip supplementary characters and replace unrepresentable control characters with U+FFFD in XML export, fix TimestampDataType mis-encoding pre-epoch fractional-second timestamps with a timezone suffix, roll back TransactionOperation on Error instead of implicitly committing, fix XlsTable negative row count and missing-row NullPointerException, pin YAML/DTD/OracleXMLType writer charsets to UTF-8 matching their readers, skip a redundant per-table metadata query in DatabaseDataSet.getTableMetaData, guard or remove per-cell debug logging in the datatype package, fix a data race that could mask the real producer failure in StreamingIterator, fix CsvParser duplicating the continuation line on a short multi-line parse, use DatabaseConfig.getFeature for the qualified-table-names check in DatabaseDataSet, fix FileHelper.copyFile leaking the source stream when the destination cannot be opened, keep DefaultPrepAndExpectedTestCase.configureTest's connection open for the test lifecycle instead of closing and immediately reacquiring it, remove a dead field and harden the close path in RefreshOperation, route Base64's stderr diagnostics through slf4j, remove XmlWriter's dead setEncoding(String) overload, fix CsvProducer truncating table names containing .csv before the final suffix, remove unused duplicate slf4jVersion/snakeYamlVersion pom properties, fix SQLHelper.logDebugIfValueChanged brace indentation, centralize duplicated Turkish-locale test setup into a shared @TurkishDefaultLocale JUnit 5 extension, fix TimestampDataType writing the wrong instant to TIMESTAMP WITH TIME ZONE columns when some JDBC drivers tag the value with the JVM's default time zone instead of the dataset's own time zone">
1717
<action dev="jeffjensen" type="test" issue="797" system="github" due-to="jeffjensen">
1818
Implement the 8 empty, @Disabled DefaultPrepAndExpectedTestCaseTest stub methods covering preTest, postTest, setupData, verifyData, cleanupData, and makeCompositeDataSet, using the class's existing Mockito-based connection harness. Move the test fixture's databaseTester/tc construction from field initializers to a @BeforeEach method so @Mock injection (MockitoExtension) completes before they are built.
1919
</action>
@@ -152,6 +152,9 @@
152152
<action dev="jeffjensen" type="test" issue="833" system="github" due-to="jeffjensen">
153153
Centralize the duplicated Turkish-locale save/mutate/restore boilerplate across 13 test classes into a shared @TurkishDefaultLocale JUnit 5 extension.
154154
</action>
155+
<action dev="jeffjensen" type="fix" issue="831" system="github" due-to="yoshida16729438">
156+
Fix TimestampDataType writing the wrong instant to TIMESTAMP WITH TIME ZONE columns: the issue #711 fix's Calendar-based setTimestamp(Timestamp, Calendar) call let some JDBC drivers tag the stored value with the JVM's default time zone instead of the dataset's own time zone, silently discarding it. Detect such columns via PreparedStatement.getParameterMetaData() and write them with setObject(OffsetDateTime) instead, which drivers supporting the SQL type map unambiguously; columns without a time zone keep the existing Calendar-based behavior from #711 unchanged.
157+
</action>
155158
</release>
156159
<release version="3.3.0" date="Jul 22, 2026" description="Performance improvements and fixes: speed up column index lookups, suppress RowOutOfBoundsException stack traces, buffer XML, CSV, DTD, and YAML output, cache DecimalFormat in XlsTable, restore DepthFirstSearch tests, fix DepthFirstSearch depth limit, cache DatabaseSequenceFilter dependency edges, topological sort in DatabaseSequenceFilter, single column-metadata fetch in ResultSetTableMetaData, precompute sort keys in SortedTable, reduce assertion loop overhead, reduce ReplacementTable scanning, flatten CompositeDataSet table merging, detect columns without exception flow in FlatXmlProducer, fix PrimaryKeyFilter result set leak, fix XlsDataSetWriter memory leak, skip timezone regex for plain timestamps, guard hot-path debug logging, fix SQLHelper result set leak, reuse query metadata in QueryTableIterator, fix XlsDataSet file stream leak, release consumed CSV rows early, lookup map for OrderedTableNameMap original names, primitive row index storage in RowFilterTable, set-based membership in Columns.mergeColumnsByName, extend ResultSetTableMetaData column-metadata cache to MySqlMetadataHandler and Db2MetadataHandler, fix XlsDataSetWriter StringIndexOutOfBoundsException for BigDecimal scale greater than 52, restore execution of 45 test methods silenced by missing @Test annotations, fix corrupted assertion in HandlersTest">
157160
<!-- dependabot:dep=actions/checkout:new=7:pr=756 -->

src/main/java/org/dbunit/dataset/datatype/TimestampDataType.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
package org.dbunit.dataset.datatype;
2323

2424
import java.math.BigInteger;
25+
import java.sql.ParameterMetaData;
2526
import java.sql.PreparedStatement;
2627
import java.sql.ResultSet;
2728
import java.sql.SQLException;
2829
import java.sql.Timestamp;
2930
import java.sql.Types;
3031
import java.time.LocalDateTime;
32+
import java.time.OffsetDateTime;
33+
import java.time.ZoneId;
3134
import java.time.format.DateTimeParseException;
3235
import java.util.Calendar;
3336
import java.util.TimeZone;
@@ -270,13 +273,63 @@ private void setSqlValueFromString(final String value, final int column,
270273
if (timezoneMatcher.matches() && timezoneMatcher.group(2) != null)
271274
{
272275
final Calendar cal = makeCalendar(timezoneMatcher);
273-
statement.setTimestamp(column, ts, cal);
276+
if (isTimestampWithTimeZoneColumn(column, statement))
277+
{
278+
final ZoneId zoneId = cal.getTimeZone().toZoneId();
279+
final OffsetDateTime offsetDateTime =
280+
OffsetDateTime.ofInstant(ts.toInstant(), zoneId);
281+
statement.setObject(column, offsetDateTime);
282+
} else
283+
{
284+
statement.setTimestamp(column, ts, cal);
285+
}
274286
} else
275287
{
276288
statement.setTimestamp(column, ts);
277289
}
278290
}
279291

292+
/**
293+
* Determines whether a prepared-statement parameter targets a column of
294+
* SQL type {@code TIMESTAMP WITH TIME ZONE}.
295+
* <p>
296+
* {@link PreparedStatement#setTimestamp(int, Timestamp, Calendar)} only
297+
* changes the wall-clock digits sent to the database. Some drivers write
298+
* those digits into a time-zone-aware column tagged with the JVM's
299+
* default time zone rather than the given calendar's zone, which
300+
* silently discards the dataset's own time zone. Columns of this type
301+
* must instead be written with
302+
* {@link PreparedStatement#setObject(int, Object)} using an
303+
* {@link OffsetDateTime}, which drivers supporting this SQL type map
304+
* unambiguously.
305+
* @param column The one-based parameter index.
306+
* @param statement The statement the parameter belongs to.
307+
* @return {@code true} if the parameter's declared SQL type is
308+
* {@link Types#TIMESTAMP_WITH_TIMEZONE}, {@code false} if it is
309+
* not, or if that could not be determined.
310+
*/
311+
private boolean isTimestampWithTimeZoneColumn(final int column,
312+
final PreparedStatement statement)
313+
{
314+
try
315+
{
316+
final ParameterMetaData parameterMetaData =
317+
statement.getParameterMetaData();
318+
final int parameterType =
319+
parameterMetaData.getParameterType(column);
320+
return parameterType == Types.TIMESTAMP_WITH_TIMEZONE;
321+
} catch (final Exception e)
322+
{
323+
// Driver support for ParameterMetaData is inconsistent; treat any
324+
// failure as "unknown" and fall back to the safe, existing
325+
// behavior instead of failing the whole operation.
326+
logger.debug(
327+
"Could not determine parameter type for column {}, assuming no timezone.",
328+
column, e);
329+
return false;
330+
}
331+
}
332+
280333
private Calendar makeCalendar(final Matcher timezoneMatcher)
281334
{
282335
final String zoneValue = timezoneMatcher.group(2);

src/site/asciidoc/integrationtests.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ The valid feature names, their descriptions, and the databases that do not suppo
178178
|`XML_TYPE`
179179
|Oracle `XMLTYPE` column type; tests inserting and updating XML_TYPE_TABLE rows.
180180
|DB2, Derby, H2, HSQLDB, MySQL, MSSQL, PostgreSQL
181+
182+
|`TIMESTAMP_WITH_TIMEZONE`
183+
|`TIMESTAMP WITH TIME ZONE` column type; tests `TimestampDataType` preserving the dataset's own offset instead of the JVM's default time zone.
184+
|DB2, Derby, MySQL, MSSQL, Oracle
181185
|===
182186

183187
== Running Integration Tests with IDEs

src/test/java/org/dbunit/TestFeature.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class TestFeature
4141
public static final TestFeature SDO_GEOMETRY =
4242
new TestFeature("SDO_GEOMETRY");
4343
public static final TestFeature XML_TYPE = new TestFeature("XML_TYPE");
44+
public static final TestFeature TIMESTAMP_WITH_TIMEZONE =
45+
new TestFeature("TIMESTAMP_WITH_TIMEZONE");
4446

4547
private final String _name;
4648

0 commit comments

Comments
 (0)