-
Notifications
You must be signed in to change notification settings - Fork 8.9k
bugfix: handle timestamp with time zone in postgresql primary key #7908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 2.x #7908 +/- ##
=========================================
Coverage 71.26% 71.26%
Complexity 835 835
=========================================
Files 1294 1294
Lines 49550 49554 +4
Branches 5883 5884 +1
=========================================
+ Hits 35311 35314 +3
- Misses 11331 11333 +2
+ Partials 2908 2907 -1
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds support for handling PostgreSQL's TIMESTAMP WITH TIMEZONE data type in primary keys and other columns. The fix addresses issue #5624 by adding proper handling for the standard JDBC Types.TIMESTAMP_WITH_TIMEZONE constant (used by PostgreSQL) alongside the existing Oracle-specific timestamp types.
Key Changes:
- Added handling for
Types.TIMESTAMP_WITH_TIMEZONEinTableRecords.buildRecords()to properly read and convert PostgreSQL timestamp values usingOffsetDateTime - Integrated Jackson's
JavaTimeModuleto support serialization/deserialization ofOffsetDateTimeand other Java 8 time types in undo logs - Added comprehensive test coverage for the new timestamp type handling
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| rm-datasource/src/main/java/org/apache/seata/rm/datasource/sql/struct/TableRecords.java | Added new condition to handle Types.TIMESTAMP_WITH_TIMEZONE by retrieving values as OffsetDateTime objects |
| rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParser.java | Registered JavaTimeModule to enable proper JSON serialization/deserialization of OffsetDateTime for undo logs |
| rm-datasource/pom.xml | Added Jackson datatype dependencies (jackson-datatype-jdk8 and jackson-datatype-jsr310) to support Java 8 time types |
| rm-datasource/src/test/java/org/apache/seata/rm/datasource/sql/struct/TableRecordsTest.java | Added test case testBuildRecordsWithOffsetDateTime to verify correct handling of timestamp with timezone fields |
| changes/en-us/2.x.md | Added changelog entry documenting the PostgreSQL timestamp fix |
| changes/zh-cn/2.x.md | Added Chinese changelog entry documenting the PostgreSQL timestamp fix |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| JavaTimeModule javaTimeModule = new JavaTimeModule(); | ||
| mapper.registerModules(module, javaTimeModule); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JavaTimeModule already provides serializers and deserializers for LocalDateTime and other Java 8 date/time types. Adding it alongside custom LocalDateTime serializers (localDateTimeSerializer and localDateTimeDeserializer) may cause conflicts or unexpected behavior. Consider removing the custom LocalDateTime serializer/deserializer registration on lines 184-185, or verify that the custom implementation is intentionally overriding the default JavaTimeModule behavior.
rm-datasource/pom.xml
Outdated
| <artifactId>jackson-datatype-jdk8</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.fasterxml.jackson.datatype</groupId> |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The jackson-datatype-jdk8 dependency appears to be unused in the codebase. This module provides support for JDK 8 features like Optional, OptionalInt, etc., but there's no code that explicitly uses these Jackson JDK 8 serializers. If this dependency is not needed, consider removing it to keep dependencies minimal. Only jackson-datatype-jsr310 is needed for OffsetDateTime and other java.time types.
| <artifactId>jackson-datatype-jdk8</artifactId> | |
| </dependency> | |
| <dependency> | |
| <groupId>com.fasterxml.jackson.datatype</groupId> |
| JavaTimeModule javaTimeModule = new JavaTimeModule(); | ||
| mapper.registerModules(module, javaTimeModule); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a test case in JacksonUndoLogParserTest to verify that OffsetDateTime values can be properly serialized and deserialized. While there's a test for LocalDateTime (lines 114-125), there's no test for OffsetDateTime which is now supported through the JavaTimeModule. This would ensure that undo logs containing PostgreSQL TIMESTAMP WITH TIMEZONE fields can be correctly persisted and restored.
| new Object[] {1, OffsetDateTime.now()}, | ||
| }; |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using OffsetDateTime.now() creates a non-deterministic test value which can make test results harder to reproduce and debug. Consider using a fixed OffsetDateTime value instead, such as OffsetDateTime.of(2024, 1, 15, 10, 30, 45, 0, ZoneOffset.UTC) to ensure consistent and reproducible test behavior.
| MockStatementBase mockStatement = new MockStatement(getPhysicsConnection(dataSource)); | ||
| DataSourceProxy proxy = DataSourceProxyTest.getDataSourceProxy(dataSource); | ||
|
|
||
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | ||
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | ||
|
|
||
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | ||
| .getTableMeta(proxy.getPlainConnection(), "table_records_test", proxy.getResourceId()); | ||
|
|
||
| ResultSet originalResultSet = mockDriver.executeQuery(mockStatement, "select * from table_records_test"); | ||
|
|
||
| ResultSet proxyResultSet = (ResultSet) java.lang.reflect.Proxy.newProxyInstance( | ||
| TableRecordsTest.class.getClassLoader(), new Class[] {ResultSet.class}, (p, method, args) -> { | ||
| if ("getObject".equals(method.getName()) && args.length == 2 && args[1] == OffsetDateTime.class) { | ||
| return originalResultSet.getObject((Integer) args[0]); | ||
| } | ||
| try { | ||
| return method.invoke(originalResultSet, args); | ||
| } catch (java.lang.reflect.InvocationTargetException e) { | ||
| throw e.getTargetException(); | ||
| } | ||
| }); | ||
|
|
||
| TableRecords tableRecords = TableRecords.buildRecords(tableMeta, proxyResultSet); | ||
|
|
||
| Assertions.assertNotNull(tableRecords); | ||
| Assertions.assertEquals(1, tableRecords.size()); | ||
|
|
||
| Row row = tableRecords.getRows().get(0); | ||
| Field timeField = row.getFields().stream() | ||
| .filter(f -> "time_col".equalsIgnoreCase(f.getName())) | ||
| .findFirst() | ||
| .orElseThrow(() -> new RuntimeException("time_col not found")); | ||
|
|
||
| Assertions.assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, timeField.getType()); | ||
| Assertions.assertTrue(timeField.getValue() instanceof OffsetDateTime); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This MockStatement is not always closed on method exit.
| MockStatementBase mockStatement = new MockStatement(getPhysicsConnection(dataSource)); | |
| DataSourceProxy proxy = DataSourceProxyTest.getDataSourceProxy(dataSource); | |
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | |
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | |
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | |
| .getTableMeta(proxy.getPlainConnection(), "table_records_test", proxy.getResourceId()); | |
| ResultSet originalResultSet = mockDriver.executeQuery(mockStatement, "select * from table_records_test"); | |
| ResultSet proxyResultSet = (ResultSet) java.lang.reflect.Proxy.newProxyInstance( | |
| TableRecordsTest.class.getClassLoader(), new Class[] {ResultSet.class}, (p, method, args) -> { | |
| if ("getObject".equals(method.getName()) && args.length == 2 && args[1] == OffsetDateTime.class) { | |
| return originalResultSet.getObject((Integer) args[0]); | |
| } | |
| try { | |
| return method.invoke(originalResultSet, args); | |
| } catch (java.lang.reflect.InvocationTargetException e) { | |
| throw e.getTargetException(); | |
| } | |
| }); | |
| TableRecords tableRecords = TableRecords.buildRecords(tableMeta, proxyResultSet); | |
| Assertions.assertNotNull(tableRecords); | |
| Assertions.assertEquals(1, tableRecords.size()); | |
| Row row = tableRecords.getRows().get(0); | |
| Field timeField = row.getFields().stream() | |
| .filter(f -> "time_col".equalsIgnoreCase(f.getName())) | |
| .findFirst() | |
| .orElseThrow(() -> new RuntimeException("time_col not found")); | |
| Assertions.assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, timeField.getType()); | |
| Assertions.assertTrue(timeField.getValue() instanceof OffsetDateTime); | |
| try (MockStatementBase mockStatement = new MockStatement(getPhysicsConnection(dataSource))) { | |
| DataSourceProxy proxy = DataSourceProxyTest.getDataSourceProxy(dataSource); | |
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | |
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | |
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | |
| .getTableMeta(proxy.getPlainConnection(), "table_records_test", proxy.getResourceId()); | |
| ResultSet originalResultSet = mockDriver.executeQuery(mockStatement, "select * from table_records_test"); | |
| ResultSet proxyResultSet = (ResultSet) java.lang.reflect.Proxy.newProxyInstance( | |
| TableRecordsTest.class.getClassLoader(), new Class[] {ResultSet.class}, (p, method, args) -> { | |
| if ("getObject".equals(method.getName()) && args.length == 2 && args[1] == OffsetDateTime.class) { | |
| return originalResultSet.getObject((Integer) args[0]); | |
| } | |
| try { | |
| return method.invoke(originalResultSet, args); | |
| } catch (java.lang.reflect.InvocationTargetException e) { | |
| throw e.getTargetException(); | |
| } | |
| }); | |
| TableRecords tableRecords = TableRecords.buildRecords(tableMeta, proxyResultSet); | |
| Assertions.assertNotNull(tableRecords); | |
| Assertions.assertEquals(1, tableRecords.size()); | |
| Row row = tableRecords.getRows().get(0); | |
| Field timeField = row.getFields().stream() | |
| .filter(f -> "time_col".equalsIgnoreCase(f.getName())) | |
| .findFirst() | |
| .orElseThrow(() -> new RuntimeException("time_col not found")); | |
| Assertions.assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, timeField.getType()); | |
| Assertions.assertTrue(timeField.getValue() instanceof OffsetDateTime); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | ||
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | ||
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test is using JdbcConstants.MYSQL for a test that is specifically designed to test PostgreSQL's TIMESTAMP_WITH_TIMEZONE handling. This is misleading and could lead to incorrect test behavior. The test should use JdbcConstants.POSTGRESQL instead to properly reflect the database being tested.
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | |
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | |
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | |
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.POSTGRESQL) | |
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | |
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.POSTGRESQL) |
| @Test | ||
| public void testBuildRecordsWithOffsetDateTime() throws SQLException { | ||
| MockDriver mockDriver = new MockDriver( | ||
| returnValueColumnLabelsOffsetDateTime, | ||
| returnValueOffsetDateTime, | ||
| columnMetasOffsetDateTime, | ||
| indexMetas); | ||
|
|
||
| DruidDataSource dataSource = new DruidDataSource(); | ||
| dataSource.setUrl("jdbc:mock:offset"); | ||
| dataSource.setDriver(mockDriver); | ||
|
|
||
| try (MockStatementBase mockStatement = new MockStatement(getPhysicsConnection(dataSource))) { | ||
| DataSourceProxy proxy = DataSourceProxyTest.getDataSourceProxy(dataSource); | ||
| TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | ||
| .refresh(proxy.getPlainConnection(), proxy.getResourceId()); | ||
| TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(JdbcConstants.MYSQL) | ||
| .getTableMeta(proxy.getPlainConnection(), "table_records_test", proxy.getResourceId()); | ||
| ResultSet originalResultSet = mockDriver.executeQuery(mockStatement, "select * from table_records_test"); | ||
| ResultSet proxyResultSet = (ResultSet) java.lang.reflect.Proxy.newProxyInstance( | ||
| TableRecordsTest.class.getClassLoader(), new Class[] {ResultSet.class}, (p, method, args) -> { | ||
| if ("getObject".equals(method.getName()) | ||
| && args.length == 2 | ||
| && args[1] == OffsetDateTime.class) { | ||
| return originalResultSet.getObject((Integer) args[0]); | ||
| } | ||
| try { | ||
| return method.invoke(originalResultSet, args); | ||
| } catch (java.lang.reflect.InvocationTargetException e) { | ||
| throw e.getTargetException(); | ||
| } | ||
| }); | ||
| TableRecords tableRecords = TableRecords.buildRecords(tableMeta, proxyResultSet); | ||
| Assertions.assertNotNull(tableRecords); | ||
| Assertions.assertEquals(1, tableRecords.size()); | ||
| Row row = tableRecords.getRows().get(0); | ||
| Field timeField = row.getFields().stream() | ||
| .filter(f -> "time_col".equalsIgnoreCase(f.getName())) | ||
| .findFirst() | ||
| .orElseThrow(() -> new RuntimeException("time_col not found")); | ||
| Assertions.assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, timeField.getType()); | ||
| Assertions.assertTrue(timeField.getValue() instanceof OffsetDateTime); | ||
| } | ||
| } |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test lacks coverage for the serialization and deserialization of OffsetDateTime through the JacksonUndoLogParser. While the test verifies that OffsetDateTime can be retrieved from the ResultSet and stored in a Field, it doesn't test the critical path of serializing/deserializing this data through the undo log, which is the primary use case affected by the JavaTimeModule registration in JacksonUndoLogParser.
funky-eyes
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Ⅰ. Describe what this PR did
handle timestamp with time zone in postgresql primary key
Ⅱ. Does this pull request fix one issue?
fixes #5624
Ⅲ. Why don't you add test cases (unit test/integration test)?
Ⅳ. Describe how to verify it
Ⅴ. Special notes for reviews