Skip to content

Commit 18768d6

Browse files
committed
Oracle client exception when retrieving temporal generated values (#1322)
Fixes #1276 When retrieving temporal generated (default) values, the expected class type must be provided. Signed-off-by: Thomas Segismont <tsegismont@gmail.com>
1 parent d1cfb6c commit 18768d6

File tree

4 files changed

+247
-15
lines changed

4 files changed

+247
-15
lines changed

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleQueryCommand.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import io.vertx.oracleclient.impl.OracleRow;
2222
import io.vertx.oracleclient.impl.OracleRowDesc;
2323
import io.vertx.sqlclient.Row;
24+
import io.vertx.sqlclient.desc.ColumnDescriptor;
2425
import io.vertx.sqlclient.impl.RowDesc;
2526
import oracle.jdbc.OracleConnection;
2627
import oracle.jdbc.OraclePreparedStatement;
28+
import oracle.sql.TIMESTAMPTZ;
2729

2830
import java.sql.*;
2931
import java.time.Instant;
@@ -254,13 +256,22 @@ private void decodeReturnedKeys(Statement statement, OracleResponse<R> response)
254256
int cols = metaData.getColumnCount();
255257
if (cols > 0) {
256258
RowDesc keysDesc = OracleRowDesc.create(metaData);
257-
258259
OracleRow keys = new OracleRow(keysDesc);
259260
for (int i = 1; i <= cols; i++) {
260-
Object res = Helper.convertSqlValue(keysRS.getObject(i));
261+
ColumnDescriptor columnDesc = keysDesc.columnDescriptor().get(i - 1);
262+
Object res;
263+
switch (columnDesc.jdbcType()) {
264+
case TIMESTAMP:
265+
res = Helper.convertSqlValue(keysRS.getObject(i, Timestamp.class));
266+
break;
267+
case TIMESTAMP_WITH_TIMEZONE:
268+
res = Helper.convertSqlValue(keysRS.getObject(i, TIMESTAMPTZ.class));
269+
break;
270+
default:
271+
res = Helper.convertSqlValue(keysRS.getObject(i));
272+
}
261273
keys.addValue(res);
262274
}
263-
264275
response.returnedKeys(keys);
265276
}
266277
}

vertx-oracle-client/src/test/java/io/vertx/oracleclient/test/OracleGeneratedKeysTestBase.java

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
import org.junit.Test;
2929
import org.junit.runner.RunWith;
3030

31+
import java.math.BigDecimal;
32+
import java.time.LocalDateTime;
33+
import java.time.OffsetDateTime;
34+
import java.time.ZoneOffset;
35+
import java.util.List;
3136
import java.util.function.Consumer;
3237
import java.util.function.Function;
3338
import java.util.function.Supplier;
@@ -44,13 +49,39 @@ public abstract class OracleGeneratedKeysTestBase extends OracleTestBase {
4449
private static final String DROP = "DROP TABLE EntityWithIdentity";
4550
private static final String CREATE = "CREATE TABLE EntityWithIdentity\n" +
4651
"(\n" +
47-
" id NUMBER(19, 0) GENERATED AS IDENTITY,\n" +
48-
" name VARCHAR2(255 CHAR),\n" +
49-
" position NUMBER(10, 0),\n" +
52+
" id NUMBER(19, 0) GENERATED AS IDENTITY,\n" +
53+
" name VARCHAR2(255 CHAR),\n" +
54+
" position NUMBER(10, 0),\n" +
55+
" string VARCHAR2(255) DEFAULT 'default' NOT NULL,\n" +
56+
" localDate DATE DEFAULT date '2019-11-04' NOT NULL,\n" +
57+
" localDateTime TIMESTAMP DEFAULT timestamp '2018-11-04 00:00:00' NOT NULL,\n" +
58+
" inte NUMBER(10) DEFAULT 42 NOT NULL,\n" +
59+
" longe NUMBER(19) DEFAULT 84 NOT NULL,\n" +
60+
" floate BINARY_FLOAT DEFAULT '42.42' NOT NULL,\n" +
61+
" doublee BINARY_DOUBLE DEFAULT '84.84' NOT NULL,\n" +
62+
" bigDecimal NUMBER(3, 1) DEFAULT '4.2' NOT NULL,\n" +
63+
" offsetDateTime TIMESTAMP WITH TIME ZONE DEFAULT timestamp '2019-11-04 00:00:00 +01:02' NOT NULL,\n" +
5064
" PRIMARY KEY (id)\n" +
5165
")";
5266
private static final String INSERT = "INSERT INTO EntityWithIdentity (name, position) VALUES (?, ?)";
5367

68+
private static final List<Tuple> GENERATED_COLUMNS;
69+
70+
static {
71+
GENERATED_COLUMNS = List.of(
72+
Tuple.of("ID", 1),
73+
Tuple.of("STRING", 4, "default"),
74+
Tuple.of("LOCALDATE", 5, LocalDateTime.of(2019, 11, 4, 0, 0)),
75+
Tuple.of("LOCALDATETIME", 6, LocalDateTime.of(2018, 11, 4, 0, 0)),
76+
Tuple.of("INTE", 7, new BigDecimal("42")),
77+
Tuple.of("LONGE", 8, new BigDecimal("84")),
78+
Tuple.of("FLOATE", 9, 42.42F),
79+
Tuple.of("DOUBLEE", 10, 84.84D),
80+
Tuple.of("BIGDECIMAL", 11, new BigDecimal("4.2")),
81+
Tuple.of("OFFSETDATETIME", 12, OffsetDateTime.of(LocalDateTime.of(2019, 11, 4, 0, 0), ZoneOffset.ofHoursMinutes(1, 2)))
82+
);
83+
}
84+
5485
@ClassRule
5586
public static OracleRule oracle = OracleRule.SHARED_INSTANCE;
5687

@@ -74,23 +105,37 @@ public void setUp(TestContext ctx) throws Exception {
74105
public void shouldRetrieveRowId(TestContext ctx) {
75106
doTest(ctx, () -> {
76107
return new OraclePrepareOptions().setAutoGeneratedKeys(true);
77-
}, generated -> verifyGenerated(generated, "ROWID", byte[].class));
108+
}, generated -> {
109+
assertNotNull(generated);
110+
assertEquals(1, generated.size());
111+
verifyGeneratedId(generated, "ROWID", byte[].class);
112+
});
78113
}
79114

80115
@Test
81116
public void shouldRetrieveGeneratedKeyByName(TestContext ctx) {
82117
doTest(ctx, () -> {
83-
return new OraclePrepareOptions()
84-
.setAutoGeneratedKeysIndexes(new JsonArray().add("id"));
85-
}, generated -> verifyGenerated(generated, "ID", Number.class));
118+
JsonArray indexes = GENERATED_COLUMNS.stream().map(tuple -> tuple.getString(0)).collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
119+
return new OraclePrepareOptions().setAutoGeneratedKeysIndexes(indexes);
120+
}, generated -> {
121+
assertNotNull(generated);
122+
assertEquals(10, generated.size());
123+
verifyGeneratedId(generated, "ID", Number.class);
124+
verifyGeneratedColumns(generated);
125+
});
86126
}
87127

88128
@Test
89129
public void shouldRetrieveGeneratedKeyByIndex(TestContext ctx) {
90130
doTest(ctx, () -> {
91-
return new OraclePrepareOptions()
92-
.setAutoGeneratedKeysIndexes(new JsonArray().add(1));
93-
}, generated -> verifyGenerated(generated, "ID", Number.class));
131+
JsonArray indexes = GENERATED_COLUMNS.stream().map(tuple -> tuple.getInteger(1)).collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
132+
return new OraclePrepareOptions().setAutoGeneratedKeysIndexes(indexes);
133+
}, generated -> {
134+
assertNotNull(generated);
135+
assertEquals(10, generated.size());
136+
verifyGeneratedId(generated, "ID", Number.class);
137+
verifyGeneratedColumns(generated);
138+
});
94139
}
95140

96141
private void doTest(TestContext ctx, Supplier<OraclePrepareOptions> supplier, Consumer<Row> checks) {
@@ -111,12 +156,19 @@ private void doTest(TestContext ctx, Supplier<OraclePrepareOptions> supplier, Co
111156

112157
protected abstract <T> void withSqlClient(Function<SqlClient, Future<T>> function, Handler<AsyncResult<T>> handler);
113158

114-
private void verifyGenerated(Row generated, String expectedColumnName, Class<?> expectedClass) {
115-
assertNotNull(generated);
159+
private void verifyGeneratedId(Row generated, String expectedColumnName, Class<?> expectedClass) {
116160
assertEquals(expectedColumnName, generated.getColumnName(0));
117161
assertThat(generated.getValue(expectedColumnName), is(instanceOf(expectedClass)));
118162
}
119163

164+
private void verifyGeneratedColumns(Row generated) {
165+
for (int i = 1; i < GENERATED_COLUMNS.size(); i++) {
166+
Tuple tuple = GENERATED_COLUMNS.get(i);
167+
assertEquals(tuple.getString(0), generated.getColumnName(i));
168+
assertEquals(tuple.getValue(2), generated.getValue(i));
169+
}
170+
}
171+
120172
@After
121173
public void tearDown(TestContext ctx) throws Exception {
122174
pool.close().onComplete(ctx.asyncAssertSuccess());
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.oracleclient.test;
13+
14+
import io.vertx.ext.unit.TestContext;
15+
import io.vertx.ext.unit.junit.VertxUnitRunner;
16+
import io.vertx.oracleclient.OraclePool;
17+
import io.vertx.oracleclient.test.junit.OracleRule;
18+
import io.vertx.sqlclient.PoolOptions;
19+
import io.vertx.sqlclient.Row;
20+
import io.vertx.sqlclient.Tuple;
21+
import io.vertx.sqlclient.desc.ColumnDescriptor;
22+
import org.junit.After;
23+
import org.junit.Before;
24+
import org.junit.ClassRule;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
28+
import java.sql.JDBCType;
29+
import java.time.LocalDateTime;
30+
import java.time.OffsetDateTime;
31+
import java.time.ZoneOffset;
32+
33+
@RunWith(VertxUnitRunner.class)
34+
public class OracleTemporalDataTypesTest extends OracleTestBase {
35+
36+
@ClassRule
37+
public static OracleRule oracle = OracleRule.SHARED_INSTANCE;
38+
39+
OraclePool pool;
40+
41+
@Before
42+
public void setUp() throws Exception {
43+
pool = OraclePool.pool(vertx, oracle.options(), new PoolOptions());
44+
}
45+
46+
@After
47+
public void tearDown(TestContext ctx) throws Exception {
48+
pool.close().onComplete(ctx.asyncAssertSuccess());
49+
}
50+
51+
@Test
52+
public void testEncodeDate(TestContext ctx) {
53+
testEncode(ctx, "test_date", LocalDateTime.class, LocalDateTime.of(2019, 11, 4, 0, 0));
54+
}
55+
56+
@Test
57+
public void testEncodeTimestamp(TestContext ctx) {
58+
testEncode(ctx, "test_timestamp", LocalDateTime.class, LocalDateTime.of(2018, 11, 4, 15, 13, 28));
59+
}
60+
61+
@Test
62+
public void testEncodeTimestampWithTimezone(TestContext ctx) {
63+
OffsetDateTime expected = OffsetDateTime.of(LocalDateTime.of(2019, 11, 4, 15, 13, 28), ZoneOffset.ofHoursMinutes(1, 2));
64+
testEncode(ctx, "test_timestamp_with_timezone", OffsetDateTime.class, expected);
65+
}
66+
67+
private <T> void testEncode(TestContext ctx, String columnName, Class<T> clazz, T expected) {
68+
pool
69+
.preparedQuery("UPDATE temporal_data_types SET " + columnName + " = ? WHERE id = 2")
70+
.execute(Tuple.of(expected))
71+
.onComplete(ctx.asyncAssertSuccess(updateResult -> {
72+
pool
73+
.preparedQuery("SELECT " + columnName + " FROM temporal_data_types WHERE id = 2")
74+
.execute()
75+
.onComplete(ctx.asyncAssertSuccess(result -> {
76+
ctx.assertEquals(1, result.size());
77+
Row row = result.iterator().next();
78+
ctx.assertEquals(1, row.size());
79+
ctx.assertEquals(expected, row.get(clazz, 0));
80+
ctx.assertEquals(expected, row.get(clazz, columnName));
81+
ctx.assertEquals(expected, row.getValue(0));
82+
ctx.assertEquals(expected, row.getValue(columnName));
83+
}));
84+
}));
85+
}
86+
87+
@Test
88+
public void testDecodeDate(TestContext ctx) {
89+
testDecode(ctx, "test_date", JDBCType.TIMESTAMP, LocalDateTime.class, LocalDateTime.of(2019, 11, 4, 0, 0));
90+
}
91+
92+
@Test
93+
public void testDecodeTimestamp(TestContext ctx) {
94+
testDecode(ctx, "test_timestamp", JDBCType.TIMESTAMP, LocalDateTime.class, LocalDateTime.of(2018, 11, 4, 15, 13, 28));
95+
}
96+
97+
@Test
98+
public void testDecodeTimestampWithTimezone(TestContext ctx) {
99+
OffsetDateTime expected = OffsetDateTime.of(LocalDateTime.of(2019, 11, 4, 15, 13, 28), ZoneOffset.ofHoursMinutes(1, 2));
100+
testDecode(ctx, "test_timestamp_with_timezone", JDBCType.TIMESTAMP_WITH_TIMEZONE, OffsetDateTime.class, expected);
101+
}
102+
103+
private <T> void testDecode(TestContext ctx, String columnName, JDBCType jdbcType, Class<?> clazz, T expected) {
104+
pool
105+
.preparedQuery("SELECT " + columnName + " FROM temporal_data_types WHERE id = 1")
106+
.execute()
107+
.onComplete(ctx.asyncAssertSuccess(result -> {
108+
ctx.assertEquals(1, result.size());
109+
Row row = result.iterator().next();
110+
ctx.assertEquals(expected, row.get(clazz, 0));
111+
ctx.assertEquals(expected, row.get(clazz, columnName));
112+
ctx.assertEquals(expected, row.getValue(0));
113+
ctx.assertEquals(expected, row.getValue(columnName));
114+
ColumnDescriptor columnDescriptor = result.columnDescriptors().get(0);
115+
ctx.assertEquals(jdbcType, columnDescriptor.jdbcType());
116+
ctx.assertNotNull(columnDescriptor);
117+
}));
118+
}
119+
120+
@Test
121+
public void testEncodeNull(TestContext ctx) {
122+
pool
123+
.preparedQuery("UPDATE temporal_data_types SET test_date = ?, test_timestamp = ?, test_timestamp_with_timezone = ? WHERE id = 2")
124+
.execute(Tuple.tuple().addValue(null).addValue(null).addValue(null))
125+
.onComplete(ctx.asyncAssertSuccess(updateResult -> {
126+
pool
127+
.preparedQuery("SELECT * FROM temporal_data_types WHERE id = 2")
128+
.execute()
129+
.onComplete(ctx.asyncAssertSuccess(result -> {
130+
ctx.assertEquals(1, result.size());
131+
Row row = result.iterator().next();
132+
ctx.assertEquals(4, row.size());
133+
ctx.assertEquals(2, row.getInteger(0));
134+
for (int i = 1; i < row.size(); i++) {
135+
ctx.assertNull(row.getValue(i));
136+
}
137+
}));
138+
}));
139+
}
140+
141+
@Test
142+
public void testDecodeNull(TestContext ctx) {
143+
pool
144+
.preparedQuery("SELECT test_date, test_timestamp, test_timestamp_with_timezone FROM temporal_data_types WHERE id = 3")
145+
.execute()
146+
.onComplete(ctx.asyncAssertSuccess(result -> {
147+
ctx.assertEquals(1, result.size());
148+
Row row = result.iterator().next();
149+
ctx.assertEquals(3, row.size());
150+
for (int i = 0; i < row.size(); i++) {
151+
ctx.assertNull(row.getValue(i));
152+
}
153+
}));
154+
}
155+
}

vertx-oracle-client/src/test/resources/tck/import.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,19 @@ VALUES (2, UTL_RAW.CAST_TO_RAW('See you space cowboy...'), UTL_RAW.CAST_TO_RAW('
140140
INSERT INTO binary_data_types(id, test_raw, test_blob)
141141
VALUES (3, NULL, NULL);
142142

143+
CREATE TABLE temporal_data_types
144+
(
145+
id INT,
146+
test_date DATE,
147+
test_timestamp TIMESTAMP,
148+
test_timestamp_with_timezone TIMESTAMP WITH TIME ZONE
149+
);
150+
INSERT INTO temporal_data_types(id, test_date, test_timestamp, test_timestamp_with_timezone)
151+
VALUES (1, date '2019-11-04', timestamp '2018-11-04 15:13:28', timestamp '2019-11-04 15:13:28 +01:02');
152+
INSERT INTO temporal_data_types(id, test_date, test_timestamp, test_timestamp_with_timezone)
153+
VALUES (2, date '2019-11-04', timestamp '2018-11-04 15:13:28', timestamp '2019-11-04 15:13:28 +01:02');
154+
INSERT INTO temporal_data_types(id, test_date, test_timestamp, test_timestamp_with_timezone)
155+
VALUES (3, NULL, NULL, NULL);
156+
143157
-- Don't forget to commit...
144158
COMMIT;

0 commit comments

Comments
 (0)