Skip to content

Commit 7883388

Browse files
committed
[NOID] Fixes #3496: Simple mysql select now(); Throws class java.time.LocalDateTime cannot be cast to class java.sql.Timestamp (#3975)
* Fixes #3496: Simple mysql select now(); Throws class java.time.LocalDateTime cannot be cast to class java.sql.Timestamp * Fix tests and updated implementation package * Removed unused imports * Fixed mysql test
1 parent da4c856 commit 7883388

File tree

8 files changed

+156
-86
lines changed

8 files changed

+156
-86
lines changed

core/src/main/java/apoc/load/util/JdbcUtil.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
*/
1919
package apoc.load.util;
2020

21+
import us.fatehi.utility.datasource.DatabaseConnectionSource;
22+
import us.fatehi.utility.datasource.DatabaseConnectionSources;
23+
import us.fatehi.utility.datasource.MultiUseUserCredentials;
24+
25+
import javax.security.auth.Subject;
26+
import javax.security.auth.callback.Callback;
27+
import javax.security.auth.callback.NameCallback;
28+
import javax.security.auth.callback.PasswordCallback;
29+
import javax.security.auth.login.LoginContext;
2130
import apoc.util.Util;
2231
import java.net.URI;
2332
import java.security.PrivilegedActionException;
@@ -37,12 +46,9 @@ public class JdbcUtil {
3746

3847
private JdbcUtil() {}
3948

40-
public static Connection getConnection(String jdbcUrl, LoadJdbcConfig config) throws Exception {
41-
if (config.hasCredentials()) {
42-
return createConnection(
43-
jdbcUrl,
44-
config.getCredentials().getUser(),
45-
config.getCredentials().getPassword());
49+
public static DatabaseConnectionSource getConnection(String jdbcUrl, LoadJdbcConfig config) throws Exception {
50+
if(config.hasCredentials()) {
51+
return createConnection(jdbcUrl, config.getCredentials().getUser(), config.getCredentials().getPassword());
4652
} else {
4753
URI uri = new URI(jdbcUrl.substring("jdbc:".length()));
4854
String userInfo = uri.getUserInfo();
@@ -68,8 +74,7 @@ private static Connection createConnection(String jdbcUrl, String userName, Stri
6874
lc.login();
6975
Subject subject = lc.getSubject();
7076
try {
71-
return Subject.doAs(subject, (PrivilegedExceptionAction<Connection>)
72-
() -> DriverManager.getConnection(jdbcUrl, userName, password));
77+
return Subject.doAs(subject, (PrivilegedExceptionAction<DatabaseConnectionSource>) () -> DatabaseConnectionSources.newDatabaseConnectionSource(jdbcUrl, new MultiUseUserCredentials(userName, password)));
7378
} catch (PrivilegedActionException pae) {
7479
throw pae.getException();
7580
}

full/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ dependencies {
142142
compileOnly group: 'org.ow2.asm', name: 'asm', version: '5.0.2'
143143

144144
// schemacrawler
145-
implementation group: 'us.fatehi', name: 'schemacrawler', version: '15.04.01'
146-
testImplementation group: 'us.fatehi', name: 'schemacrawler-mysql', version: '15.04.01'
145+
implementation group: 'us.fatehi', name: 'schemacrawler', version: '16.20.8'
146+
testImplementation group: 'us.fatehi', name: 'schemacrawler-mysql', version: '16.20.8'
147147

148148
testImplementation group: 'org.apache.hive', name: 'hive-jdbc', version: '1.2.2', withoutServers
149149

full/src/main/java/apoc/load/Jdbc.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.sql.*;
3030
import java.time.OffsetDateTime;
3131
import java.time.OffsetTime;
32+
import java.time.ZoneId;
3233
import java.util.*;
3334
import java.util.stream.Stream;
3435
import java.util.stream.StreamSupport;
@@ -106,7 +107,7 @@ private Stream<RowResult> executeQuery(
106107
String url = getUrlOrKey(urlOrKey);
107108
String query = getSqlOrKey(tableOrSelect);
108109
try {
109-
Connection connection = getConnection(url, loadJdbcConfig);
110+
Connection connection = getConnection(url, loadJdbcConfig).get();
110111
// see https://jdbc.postgresql.org/documentation/91/query.html#query-with-cursors
111112
connection.setAutoCommit(loadJdbcConfig.isAutoCommit());
112113
try {
@@ -162,7 +163,7 @@ private Stream<RowResult> executeUpdate(
162163
String url = getUrlOrKey(urlOrKey);
163164
LoadJdbcConfig jdbcConfig = new LoadJdbcConfig(config);
164165
try {
165-
Connection connection = getConnection(url, jdbcConfig);
166+
Connection connection = getConnection(url, jdbcConfig).get();
166167
try {
167168
PreparedStatement stmt =
168169
connection.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
@@ -277,21 +278,20 @@ private Object convert(Object value, int sqlType) {
277278
if (Types.TIME_WITH_TIMEZONE == sqlType) {
278279
return OffsetTime.parse(value.toString());
279280
}
281+
ZoneId zoneId = config.getZoneId();
280282
if (Types.TIMESTAMP == sqlType) {
281-
if (config.getZoneId() != null) {
282-
return ((java.sql.Timestamp) value)
283-
.toInstant()
284-
.atZone(config.getZoneId())
283+
if (zoneId != null) {
284+
return ((java.sql.Timestamp)value).toInstant()
285+
.atZone(zoneId)
285286
.toOffsetDateTime();
286287
} else {
287288
return ((java.sql.Timestamp) value).toLocalDateTime();
288289
}
289290
}
290291
if (Types.TIMESTAMP_WITH_TIMEZONE == sqlType) {
291-
if (config.getZoneId() != null) {
292-
return ((java.sql.Timestamp) value)
293-
.toInstant()
294-
.atZone(config.getZoneId())
292+
if (zoneId != null) {
293+
return ((java.sql.Timestamp)value).toInstant()
294+
.atZone(zoneId)
295295
.toOffsetDateTime();
296296
} else {
297297
return OffsetDateTime.parse(value.toString());

full/src/main/java/apoc/model/Model.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ public Stream<DatabaseModel> jdbc(
7979
throws Exception {
8080
String url = getUrlOrKey(urlOrKey);
8181

82-
SchemaCrawlerOptionsBuilder optionsBuilder =
83-
SchemaCrawlerOptionsBuilder.builder().withSchemaInfoLevel(SchemaInfoLevelBuilder.standard());
84-
SchemaCrawlerOptions options = optionsBuilder.toOptions();
82+
SchemaCrawlerOptions options = SchemaCrawlerOptionsBuilder.newSchemaCrawlerOptions();
8583

8684
Catalog catalog = SchemaCrawlerUtility.getCatalog(getConnection(url, new LoadJdbcConfig(config)), options);
8785

full/src/test/java/apoc/load/MySQLJdbcTest.java

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,91 @@
1111
import org.junit.BeforeClass;
1212
import org.junit.ClassRule;
1313
import org.junit.Test;
14+
import org.junit.experimental.runners.Enclosed;
15+
import org.junit.runner.RunWith;
1416
import org.neo4j.test.rule.DbmsRule;
1517
import org.neo4j.test.rule.ImpermanentDbmsRule;
1618

17-
public class MySQLJdbcTest extends AbstractJdbcTest {
19+
import java.time.LocalDate;
20+
import java.time.LocalDateTime;
21+
import java.time.LocalTime;
22+
import java.time.ZonedDateTime;
23+
import java.util.Map;
1824

19-
@ClassRule
20-
public static MySQLContainerExtension mysql = new MySQLContainerExtension();
25+
import static apoc.util.TestUtil.testCall;
26+
import static org.junit.Assert.assertEquals;
27+
import static org.junit.Assert.assertTrue;
2128

22-
@ClassRule
23-
public static DbmsRule db = new ImpermanentDbmsRule();
29+
@RunWith(Enclosed.class)
30+
public class MySQLJdbcTest extends AbstractJdbcTest {
31+
32+
public static class MySQLJdbcLatestVersionTest {
33+
34+
@ClassRule
35+
public static MySQLContainerExtension mysql = new MySQLContainerExtension("mysql:8.0.31");
36+
37+
@ClassRule
38+
public static DbmsRule db = new ImpermanentDbmsRule();
39+
40+
@BeforeClass
41+
public static void setUpContainer() {
42+
mysql.start();
43+
TestUtil.registerProcedure(db, Jdbc.class);
44+
}
45+
@AfterClass
46+
public static void tearDown() {
47+
mysql.stop();
48+
db.shutdown();
49+
}
50+
@Test
51+
public void testLoadJdbc() {
52+
MySQLJdbcTest.testLoadJdbc(db, mysql);
53+
}
2454

25-
@BeforeClass
26-
public static void setUpContainer() {
27-
mysql.start();
28-
TestUtil.registerProcedure(db, Jdbc.class);
55+
@Test
56+
public void testIssue3496() {
57+
MySQLJdbcTest.testIssue3496(db, mysql);
58+
}
2959
}
60+
61+
public static class MySQLJdbcFiveVersionTest {
62+
63+
@ClassRule
64+
public static MySQLContainerExtension mysql = new MySQLContainerExtension("mysql:5.7");
65+
66+
@ClassRule
67+
public static DbmsRule db = new ImpermanentDbmsRule();
3068

31-
@AfterClass
32-
public static void tearDown() {
33-
mysql.stop();
34-
db.shutdown();
69+
@BeforeClass
70+
public static void setUpContainer() {
71+
mysql.start();
72+
TestUtil.registerProcedure(db, Jdbc.class);
73+
}
74+
75+
@AfterClass
76+
public static void tearDown() {
77+
mysql.stop();
78+
db.shutdown();
79+
}
80+
81+
@Test
82+
public void testLoadJdbc() {
83+
MySQLJdbcTest.testLoadJdbc(db, mysql);
84+
}
85+
86+
@Test
87+
public void testIssue3496() {
88+
MySQLJdbcTest.testIssue3496(db, mysql);
89+
}
3590
}
3691

37-
@Test
38-
public void testLoadJdbc() {
39-
testCall(
40-
db,
41-
"CALL apoc.load.jdbc($url, $table, [])",
42-
Util.map("url", mysql.getJdbcUrl(), "table", "country"),
92+
private static void testLoadJdbc(DbmsRule db, MySQLContainerExtension mysql) {
93+
// with the config {timezone: 'UTC'} and `preserveInstants=true&connectionTimeZone=SERVER` to make the result deterministic,
94+
// since `TIMESTAMP` values are automatically converted from the session time zone to UTC for storage, and vice versa.
95+
testCall(db, "CALL apoc.load.jdbc($url, $table, [], {timezone: 'UTC'})",
96+
Util.map(
97+
"url", mysql.getJdbcUrl() + "&preserveInstants=true&connectionTimeZone=SERVER",
98+
"table", "country"),
4399
row -> {
44100
Map<String, Object> expected = Util.map(
45101
"Code", "NLD",
@@ -56,8 +112,36 @@ public void testLoadJdbc() {
56112
"GovernmentForm", "Constitutional Monarchy",
57113
"HeadOfState", "Beatrix",
58114
"Capital", 5,
59-
"Code2", "NL");
60-
assertEquals(expected, row.get("row"));
115+
"Code2", "NL",
116+
"myTime", LocalTime.of(1, 0, 0),
117+
"myTimeStamp", ZonedDateTime.parse("2003-01-01T01:00Z"),
118+
"myDate", LocalDate.parse("2003-01-01"),
119+
"myYear", LocalDate.parse("2003-01-01")
120+
);
121+
Map actual = (Map) row.get("row");
122+
Object myDateTime = actual.remove("myDateTime");
123+
assertTrue(myDateTime instanceof LocalDateTime);
124+
assertEquals(expected, actual);
125+
});
126+
}
127+
128+
private static void testIssue3496(DbmsRule db, MySQLContainerExtension mysql) {
129+
testCall(db, "CALL apoc.load.jdbc($url,'SELECT DATE(NOW()), NOW(), CURDATE(), CURTIME(), UTC_DATE(), UTC_TIME(), UTC_TIMESTAMP(), DATE(UTC_TIMESTAMP());')",
130+
Util.map("url", mysql.getJdbcUrl()),
131+
r -> {
132+
Map row = (Map) r.get("row");
133+
assertEquals(8, row.size());
134+
135+
assertTrue(row.get("UTC_DATE()") instanceof LocalDate);
136+
assertTrue(row.get("CURDATE()") instanceof LocalDate);
137+
138+
assertTrue(row.get("UTC_TIMESTAMP()") instanceof LocalDateTime);
139+
assertTrue(row.get("NOW()") instanceof LocalDateTime);
140+
assertTrue(row.get("DATE(UTC_TIMESTAMP())") instanceof LocalDate);
141+
assertTrue(row.get("DATE(NOW())") instanceof LocalDate);
142+
143+
assertTrue(row.get("CURTIME()") instanceof LocalTime);
144+
assertTrue(row.get("UTC_TIME()") instanceof LocalTime);
61145
});
62146
}
63-
}
147+
}

full/src/test/java/apoc/model/ModelTest.java

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ public void testLoadJdbcSchema() {
9393
assertEquals(0L, count.longValue());
9494
List<Node> nodes = (List<Node>) row.get("nodes");
9595
List<Relationship> rels = (List<Relationship>) row.get("relationships");
96-
assertEquals(28, nodes.size());
97-
assertEquals(27, rels.size());
96+
assertEquals(33, nodes.size());
97+
assertEquals(32, rels.size());
9898

9999
// schema
100100
Node schema = nodes.stream()
@@ -123,25 +123,11 @@ public void testLoadJdbcSchema() {
123123
List<Node> columns = nodes.stream()
124124
.filter(node -> node.hasLabel(Label.label("Column")))
125125
.collect(Collectors.toList());
126-
assertEquals(24, columns.size());
126+
assertEquals(29, columns.size());
127127

128128
List<String> countryNodes = filterColumnsByTableName(columns, "country");
129-
List<String> expectedCountryCols = Arrays.asList(
130-
"Code",
131-
"Name",
132-
"Continent",
133-
"Region",
134-
"SurfaceArea",
135-
"IndepYear",
136-
"Population",
137-
"LifeExpectancy",
138-
"GNP",
139-
"GNPOld",
140-
"LocalName",
141-
"GovernmentForm",
142-
"HeadOfState",
143-
"Capital",
144-
"Code2");
129+
List<String> expectedCountryCols = Arrays.asList("Code", "Name", "Continent", "Region", "SurfaceArea", "IndepYear", "Population", "LifeExpectancy", "GNP", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", "Capital", "Code2",
130+
"myTime", "myDateTime", "myTimeStamp", "myDate", "myYear");
145131
assertEquals(expectedCountryCols, countryNodes);
146132

147133
List<String> cityNodes = filterColumnsByTableName(columns, "city");
@@ -178,8 +164,8 @@ public void testLoadJdbcSchemaWithWriteOperation() {
178164
tx.execute("MATCH (n) RETURN collect(distinct n) AS nodes").columnAs("nodes"));
179165
List<Relationship> rels = Iterators.single(tx.execute("MATCH ()-[r]-() RETURN collect(distinct r) AS rels")
180166
.columnAs("rels"));
181-
assertEquals(28, nodes.size());
182-
assertEquals(27, rels.size());
167+
assertEquals(33, nodes.size());
168+
assertEquals(32, rels.size());
183169

184170
// schema
185171
Node schema = nodes.stream()
@@ -206,25 +192,11 @@ public void testLoadJdbcSchemaWithWriteOperation() {
206192
List<Node> columns = nodes.stream()
207193
.filter(node -> node.hasLabel(Label.label("Column")))
208194
.collect(Collectors.toList());
209-
assertEquals(24, columns.size());
195+
assertEquals(29, columns.size());
210196

211197
List<String> countryNodes = filterColumnsByTableName(columns, "country");
212-
List<String> expectedCountryCols = Arrays.asList(
213-
"Code",
214-
"Name",
215-
"Continent",
216-
"Region",
217-
"SurfaceArea",
218-
"IndepYear",
219-
"Population",
220-
"LifeExpectancy",
221-
"GNP",
222-
"GNPOld",
223-
"LocalName",
224-
"GovernmentForm",
225-
"HeadOfState",
226-
"Capital",
227-
"Code2");
198+
List<String> expectedCountryCols = Arrays.asList("Code", "Name", "Continent", "Region", "SurfaceArea", "IndepYear", "Population", "LifeExpectancy", "GNP", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", "Capital", "Code2",
199+
"myTime", "myDateTime", "myTimeStamp", "myDate", "myYear");
228200
assertEquals(expectedCountryCols, countryNodes);
229201

230202
List<String> cityNodes = filterColumnsByTableName(columns, "city");

full/src/test/resources/init_mysql.sql

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ CREATE TABLE `country` (
2424
`HeadOfState` CHAR(60) DEFAULT NULL,
2525
`Capital` INT(11) DEFAULT NULL,
2626
`Code2` CHAR(2) NOT NULL DEFAULT '',
27+
`myTime` TIME NOT NULL DEFAULT '0',
28+
`myDateTime` DATETIME NOT NULL,
29+
`myTimeStamp` TIMESTAMP NOT NULL,
30+
`myDate` DATE NOT NULL,
31+
`myYear` YEAR NOT NULL,
2732
PRIMARY KEY (`Code`)
2833
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
2934

@@ -32,7 +37,13 @@ CREATE TABLE `country` (
3237
--
3338
-- ORDER BY: `Code`
3439

35-
INSERT INTO `country` VALUES ('NLD','Netherlands','Europe','Western Europe',41526.00,1581,15864000,78.3,371362.00,360478.00,'Nederland','Constitutional Monarchy','Beatrix',5,'NL');
40+
INSERT INTO `country` VALUES ('NLD','Netherlands','Europe','Western Europe',41526.00,1581,15864000,78.3,371362.00,360478.00,'Nederland','Constitutional Monarchy','Beatrix',5,'NL',
41+
TIME('01:00:00'),
42+
DATE('2003-01-01 01:00:00'),
43+
TIMESTAMP('2003-01-01 01:00:00'),
44+
DATE('2003-01-01 01:00:00'),
45+
YEAR('2003-01-01 01:00:00')
46+
);
3647
COMMIT;
3748

3849
--

test-utils/src/main/java/apoc/util/MySQLContainerExtension.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
public class MySQLContainerExtension extends MySQLContainer<MySQLContainerExtension> {
88

9-
public MySQLContainerExtension() {
10-
super("mysql:5.7");
9+
public MySQLContainerExtension(String imageName) {
10+
super(imageName);
1111
this.withInitScript("init_mysql.sql");
1212
this.withUrlParam("user", "test");
1313
this.withUrlParam("password", "test");

0 commit comments

Comments
 (0)