[NO ISSUE] Add tests for the JDBC driver
Details:
- Add test framework and testcases for the JDBC driver
- Fix issues found by these tests
Change-Id: Ibf5d8f964867d5a5789d4b971f786f62e404fdd5
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb-clients/+/16130
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Dmitry Lychagin <dmitry.lychagin@couchbase.com>
Reviewed-by: Ian Maxon <imaxon@uci.edu>
diff --git a/asterix-opt-bom/pom.xml b/asterix-opt-bom/pom.xml
new file mode 100644
index 0000000..ae43491
--- /dev/null
+++ b/asterix-opt-bom/pom.xml
@@ -0,0 +1,33 @@
+<!--
+ ! Licensed to the Apache Software Foundation (ASF) under one
+ ! or more contributor license agreements. See the NOTICE file
+ ! distributed with this work for additional information
+ ! regarding copyright ownership. The ASF licenses this file
+ ! to you under the Apache License, Version 2.0 (the
+ ! "License"); you may not use this file except in compliance
+ ! with the License. You may obtain a copy of the License at
+ !
+ ! http://www.apache.org/licenses/LICENSE-2.0
+ !
+ ! Unless required by applicable law or agreed to in writing,
+ ! software distributed under the License is distributed on an
+ ! "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ! KIND, either express or implied. See the License for the
+ ! specific language governing permissions and limitations
+ ! under the License.
+ !-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>asterix-opt-bom</artifactId>
+ <version>0.9.8-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <parent>
+ <groupId>org.apache.asterix.clients</groupId>
+ <artifactId>asterix-opt</artifactId>
+ <version>0.9.8-SNAPSHOT</version>
+ </parent>
+
+</project>
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBConnection.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBConnection.java
index 2a068f1..ca61992 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBConnection.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBConnection.java
@@ -174,7 +174,7 @@
private void checkClosed() throws SQLException {
if (isClosed()) {
- throw getErrorReporter().errorObjectClosed(Connection.class);
+ throw getErrorReporter().errorObjectClosed(Connection.class, ADBErrorReporter.SQLState.CONNECTION_CLOSED);
}
}
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBErrorReporter.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBErrorReporter.java
index 07abd87..7009090 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBErrorReporter.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBErrorReporter.java
@@ -40,6 +40,10 @@
return new SQLException(String.format("%s is closed", jdbcInterface.getSimpleName()));
}
+ public SQLException errorObjectClosed(Class<?> jdbcInterface, SQLState sqlState) {
+ return new SQLException(String.format("%s is closed", jdbcInterface.getSimpleName()), sqlState.code);
+ }
+
public SQLFeatureNotSupportedException errorMethodNotSupported(Class<?> jdbcInterface, String methodName) {
return new SQLFeatureNotSupportedException(
String.format("Method %s.%s() is not supported", jdbcInterface.getName(), methodName));
@@ -213,6 +217,7 @@
public enum SQLState {
CONNECTION_FAILURE("08001"), // TODO:08006??
+ CONNECTION_CLOSED("08003"),
INVALID_AUTH_SPEC("28000"),
INVALID_DATE_TYPE("HY004"),
INVALID_CURSOR_POSITION("HY108");
@@ -222,5 +227,10 @@
SQLState(String code) {
this.code = Objects.requireNonNull(code);
}
+
+ @Override
+ public String toString() {
+ return code;
+ }
}
}
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBParameterMetaData.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBParameterMetaData.java
index a42def1..1663db0 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBParameterMetaData.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBParameterMetaData.java
@@ -20,7 +20,6 @@
package org.apache.asterix.jdbc.core;
import java.sql.ParameterMetaData;
-import java.sql.Types;
import java.util.Objects;
public class ADBParameterMetaData extends ADBWrapperSupport implements ParameterMetaData {
@@ -46,12 +45,12 @@
@Override
public int getParameterType(int parameterIndex) {
- return Types.OTHER; // any
+ return ADBDatatype.ANY.getJdbcType().getVendorTypeNumber();
}
@Override
public String getParameterTypeName(int parameterIndex) {
- return "";
+ return ADBDatatype.ANY.getTypeName();
}
@Override
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBPreparedStatement.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBPreparedStatement.java
index f87eede..ff28790 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBPreparedStatement.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBPreparedStatement.java
@@ -336,22 +336,30 @@
@Override
public void setObject(int parameterIndex, Object v, int targetSqlType) throws SQLException {
- setObject(parameterIndex, v); // TODO:revisit
+ setObject(parameterIndex, v);
}
@Override
public void setObject(int parameterIndex, Object v, SQLType targetSqlType) throws SQLException {
- setObject(parameterIndex, v, targetSqlType.getVendorTypeNumber()); // TODO:revisit
+ if (targetSqlType == null) {
+ setObject(parameterIndex, v);
+ } else {
+ setObject(parameterIndex, v, targetSqlType.getVendorTypeNumber());
+ }
}
@Override
public void setObject(int parameterIndex, Object v, int targetSqlType, int scaleOrLength) throws SQLException {
- setObject(parameterIndex, v, targetSqlType); // TODO:revisit
+ setObject(parameterIndex, v, targetSqlType);
}
@Override
public void setObject(int parameterIndex, Object v, SQLType targetSqlType, int scaleOrLength) throws SQLException {
- setObject(parameterIndex, v, targetSqlType.getVendorTypeNumber()); // TODO:revisit
+ if (targetSqlType == null) {
+ setObject(parameterIndex, v);
+ } else {
+ setObject(parameterIndex, v, targetSqlType.getVendorTypeNumber());
+ }
}
// Unsupported
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBResultSet.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBResultSet.java
index 1d46b4e..2af4585 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBResultSet.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBResultSet.java
@@ -332,17 +332,20 @@
}
@Override
- public boolean isBeforeFirst() {
+ public boolean isBeforeFirst() throws SQLException {
+ checkClosed();
return state == ST_BEFORE_FIRST;
}
@Override
- public boolean isAfterLast() {
+ public boolean isAfterLast() throws SQLException {
+ checkClosed();
return state == ST_AFTER_LAST;
}
@Override
- public boolean isFirst() {
+ public boolean isFirst() throws SQLException {
+ checkClosed();
return state == ST_NEXT && rowNumber == 1;
}
@@ -354,7 +357,7 @@
@Override
public int getRow() throws SQLException {
checkClosed();
- return (int) rowNumber;
+ return state == ST_NEXT ? (int) rowNumber : 0;
}
private void checkCursorPosition() throws SQLException {
@@ -862,7 +865,7 @@
}
private InputStream getAsciiStreamImpl(int columnIndex) throws SQLException {
- String value = getString(columnIndex);
+ String value = getStringImpl(columnIndex);
return value != null ? new ByteArrayInputStream(value.getBytes(StandardCharsets.US_ASCII)) : null;
}
@@ -881,7 +884,7 @@
}
private InputStream getUnicodeStreamImpl(int columnIndex) throws SQLException {
- String value = getString(columnIndex);
+ String value = getStringImpl(columnIndex);
return value != null ? new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_16)) : null;
}
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBRowStore.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBRowStore.java
index ed6995d..764d3b0 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBRowStore.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBRowStore.java
@@ -979,7 +979,8 @@
}
private LocalDate toLocalDateFromDatetimeChronon(long datetimeChrononInMillis) {
- return LocalDate.ofEpochDay(TimeUnit.MILLISECONDS.toDays(datetimeChrononInMillis));
+ // TODO: use LocalDate.ofInstant() in JDK 9+
+ return toLocalDateTimeFromDatetimeChronon(datetimeChrononInMillis).toLocalDate();
}
private Time toTimeFromTimeChronon(long timeChrononInMillis, TimeZone tz) {
@@ -996,7 +997,8 @@
}
private LocalTime toLocalTimeFromDatetimeChronon(long datetimeChrononInMillis) {
- return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(datetimeChrononInMillis));
+ // TODO: use LocalTime.ofInstant() in JDK 9+
+ return toLocalDateTimeFromDatetimeChronon(datetimeChrononInMillis).toLocalTime();
}
private Timestamp toTimestampFromDatetimeChronon(long datetimeChrononInMillis, TimeZone tz) {
diff --git a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBStatement.java b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBStatement.java
index cdbece6..c024f21 100644
--- a/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBStatement.java
+++ b/asterixdb-jdbc/asterix-jdbc-core/src/main/java/org/apache/asterix/jdbc/core/ADBStatement.java
@@ -22,6 +22,7 @@
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
+import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
@@ -692,6 +693,7 @@
// Long is serialized as JSON number by Jackson
registerSerializer(serializerMap, createFloatSerializer());
registerSerializer(serializerMap, createDoubleSerializer());
+ registerSerializer(serializerMap, createBigDecimalSerializer());
registerSerializer(serializerMap, createStringSerializer());
registerSerializer(serializerMap, createSqlDateSerializer());
registerSerializer(serializerMap, createSqlDateWithCalendarSerializer());
@@ -755,6 +757,16 @@
};
}
+ protected static ATaggedValueSerializer createBigDecimalSerializer() {
+ return new ATaggedValueSerializer(BigDecimal.class, ADBDatatype.DOUBLE) {
+ @Override
+ protected void serializeNonTaggedValue(Object value, StringBuilder out) {
+ long bits = Double.doubleToLongBits(((BigDecimal) value).doubleValue());
+ out.append(bits);
+ }
+ };
+ }
+
protected static ATaggedValueSerializer createSqlDateSerializer() {
return new ATaggedValueSerializer(java.sql.Date.class, ADBDatatype.DATE) {
@Override
diff --git a/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/ADBProtocol.java b/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/ADBProtocol.java
index bb477b0..53ab47c 100644
--- a/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/ADBProtocol.java
+++ b/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/ADBProtocol.java
@@ -34,6 +34,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -87,6 +88,8 @@
private static final String QUERY_RESULT_ENDPOINT_PATH = "/query/service/result";
private static final String ACTIVE_REQUESTS_ENDPOINT_PATH = "/admin/requests/running";
+ private static final int CONNECTION_REQUEST_TIMEOUT = 50; // ms
+
static final List<Class<? extends IOException>> TIMEOUT_CONNECTION_ERRORS =
Collections.singletonList(ConnectTimeoutException.class);
@@ -100,8 +103,8 @@
final HttpClientContext httpClientContext;
final CloseableHttpClient httpClient;
- public ADBProtocol(String host, int port, Map<ADBDriverProperty, Object> params, ADBDriverContext driverContext)
- throws SQLException {
+ public ADBProtocol(String host, int port, Map<ADBDriverProperty, Object> params, ADBDriverContext driverContext,
+ int loginTimeoutSeconds) throws SQLException {
super(driverContext, params);
boolean sslEnabled = (Boolean) ADBDriverProperty.Common.SSL.fetchPropertyValue(params);
URI queryEndpoint = createEndpointUri(sslEnabled, host, port, QUERY_SERVICE_ENDPOINT_PATH,
@@ -126,13 +129,14 @@
}
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
Number connectTimeoutMillis = (Number) ADBDriverProperty.Common.CONNECT_TIMEOUT.fetchPropertyValue(params);
- if (connectTimeoutMillis != null) {
- requestConfigBuilder.setConnectionRequestTimeout(connectTimeoutMillis.intValue());
- requestConfigBuilder.setConnectTimeout(connectTimeoutMillis.intValue());
- }
+ int connectTimeout = Math.max(0, connectTimeoutMillis != null ? connectTimeoutMillis.intValue()
+ : (int) TimeUnit.SECONDS.toMillis(loginTimeoutSeconds));
+ requestConfigBuilder.setConnectTimeout(connectTimeout);
if (socketTimeoutMillis != null) {
requestConfigBuilder.setSocketTimeout(socketTimeoutMillis.intValue());
}
+ requestConfigBuilder.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);
+
RequestConfig requestConfig = requestConfigBuilder.build();
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(httpConnectionManager);
diff --git a/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/Driver.java b/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/Driver.java
index ac1701f..65e4af0 100644
--- a/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/Driver.java
+++ b/asterixdb-jdbc/asterix-jdbc-driver/src/main/java/org/apache/asterix/jdbc/Driver.java
@@ -22,6 +22,7 @@
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
+import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
@@ -53,7 +54,8 @@
@Override
protected ADBProtocol createProtocol(String host, int port, Map<ADBDriverProperty, Object> properties,
ADBDriverContext driverContext) throws SQLException {
- return new ADBProtocol(host, port, properties, driverContext);
+ int loginTimeoutSeconds = DriverManager.getLoginTimeout();
+ return new ADBProtocol(host, port, properties, driverContext, loginTimeoutSeconds);
}
@Override
diff --git a/asterixdb-jdbc/asterix-jdbc-test/pom.xml b/asterixdb-jdbc/asterix-jdbc-test/pom.xml
new file mode 100644
index 0000000..c294c48
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/pom.xml
@@ -0,0 +1,101 @@
+<!--
+ ! Licensed to the Apache Software Foundation (ASF) under one
+ ! or more contributor license agreements. See the NOTICE file
+ ! distributed with this work for additional information
+ ! regarding copyright ownership. The ASF licenses this file
+ ! to you under the Apache License, Version 2.0 (the
+ ! "License"); you may not use this file except in compliance
+ ! with the License. You may obtain a copy of the License at
+ !
+ ! http://www.apache.org/licenses/LICENSE-2.0
+ !
+ ! Unless required by applicable law or agreed to in writing,
+ ! software distributed under the License is distributed on an
+ ! "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ! KIND, either express or implied. See the License for the
+ ! specific language governing permissions and limitations
+ ! under the License.
+ !-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>asterix-jdbc-test</artifactId>
+ <version>0.9.8-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <parent>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>apache-asterixdb</artifactId>
+ <version>0.9.8-SNAPSHOT</version>
+ <relativePath>../../../../asterixdb/pom.xml</relativePath> <!-- asterixdb/pom.xml -->
+ </parent>
+
+ <properties>
+ <root.dir>${basedir}/..</root.dir>
+ <asterix-app.dir>${root.dir}/../../asterix-app</asterix-app.dir>
+ <testLog4jConfigFile>${asterix-app.dir}/src/test/resources/log4j2-asterixdb-test.xml</testLog4jConfigFile>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables combine.children="append">
+ <asterix-app.dir>${asterix-app.dir}</asterix-app.dir>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables combine.children="append">
+ <asterix-app.dir>${asterix-app.dir}</asterix-app.dir>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>asterix-jdbc-driver</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.hyracks</groupId>
+ <artifactId>hyracks-storage-am-lsm-btree-test</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>asterix-test-framework</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>asterix-app</artifactId>
+ <version>${project.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>asterix-app</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
+
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcConnectionTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcConnectionTester.java
new file mode 100644
index 0000000..00cfaea
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcConnectionTester.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.asterix.jdbc.Driver;
+import org.apache.asterix.jdbc.core.ADBConnection;
+import org.junit.Assert;
+
+class JdbcConnectionTester extends JdbcTester {
+
+ public void testGetConnectionViaDriverManager() throws SQLException {
+ DriverManager.getConnection(testContext.getJdbcUrl()).close();
+ DriverManager.getConnection(testContext.getJdbcUrl(), null).close();
+ DriverManager.getConnection(testContext.getJdbcUrl(), new Properties()).close();
+ DriverManager.getConnection(testContext.getJdbcUrl(), null, null).close();
+ }
+
+ public void testGetConnectionDirect() throws SQLException {
+ Driver driver = new Driver();
+ driver.connect(testContext.getJdbcUrl(), null).close();
+ driver.connect(testContext.getJdbcUrl(), new Properties()).close();
+ }
+
+ public void testLifecycle() throws SQLException {
+ Connection c = createConnection();
+ Assert.assertNull(c.getWarnings());
+ Assert.assertTrue(c.isValid( /*timeout in seconds*/ 30));
+ Assert.assertFalse(c.isClosed());
+
+ c.close();
+ Assert.assertTrue(c.isClosed());
+
+ // ok to call close() on a closed connection
+ c.close();
+ Assert.assertTrue(c.isClosed());
+
+ // ok to call isValid() on a closed connection
+ Assert.assertFalse(c.isValid(0));
+
+ // errors on a closed connection
+ assertErrorOnClosed(c, Connection::clearWarnings, "clearWarnings");
+ assertErrorOnClosed(c, Connection::createStatement, "createStatement");
+ assertErrorOnClosed(c, Connection::getAutoCommit, "getAutoCommit");
+ assertErrorOnClosed(c, Connection::getCatalog, "getCatalog");
+ assertErrorOnClosed(c, Connection::getClientInfo, "getClientInfo");
+ assertErrorOnClosed(c, Connection::getHoldability, "getHoldability");
+ assertErrorOnClosed(c, Connection::getMetaData, "getMetadata");
+ assertErrorOnClosed(c, Connection::getSchema, "getSchema");
+ assertErrorOnClosed(c, Connection::getTransactionIsolation, "getTransactionIsolation");
+ assertErrorOnClosed(c, Connection::getWarnings, "getWarnings");
+ assertErrorOnClosed(c, Connection::getTypeMap, "getTypeMap");
+ assertErrorOnClosed(c, Connection::isReadOnly, "isReadOnly");
+ assertErrorOnClosed(c, ci -> ci.prepareStatement("select 1"), "prepareStatement");
+ }
+
+ public void testCatalogSchema() throws SQLException {
+ try (Connection c = createConnection()) {
+ Assert.assertEquals(DEFAULT_DATAVERSE_NAME, c.getCatalog());
+ Assert.assertNull(c.getSchema());
+ }
+
+ try (Connection c = createConnection(METADATA_DATAVERSE_NAME)) {
+ Assert.assertEquals(METADATA_DATAVERSE_NAME, c.getCatalog());
+ Assert.assertNull(c.getSchema());
+ }
+
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testCatalogSchema");
+ String dvCanon = getCanonicalDataverseName(dataverse);
+ String dataset = "ds1";
+ s.execute(printCreateDataverse(dataverse));
+ s.execute(printCreateDataset(dataverse, dataset));
+ s.execute(printInsert(dataverse, dataset, dataGen("x", 1, 2, 3)));
+ try (Connection c2 = createConnection(dvCanon); Statement s2 = c2.createStatement()) {
+ Assert.assertEquals(dvCanon, c2.getCatalog());
+ Assert.assertNull(c.getSchema());
+ try (ResultSet rs2 =
+ s2.executeQuery(String.format("select count(*) from %s", printIdentifier(dataset)))) {
+ Assert.assertTrue(rs2.next());
+ Assert.assertEquals(3, rs2.getInt(1));
+ }
+ } finally {
+ s.execute(printDropDataverse(dataverse));
+ }
+ }
+ }
+
+ // Connection.setReadOnly() hint is currently ignored
+ // Connection.isReadOnly() always returns 'false'
+ public void testReadOnlyMode() throws SQLException {
+ try (Connection c = createConnection()) {
+ Assert.assertFalse(c.isReadOnly());
+ c.setReadOnly(true);
+ Assert.assertFalse(c.isReadOnly());
+ }
+ }
+
+ public void testWrapper() throws SQLException {
+ try (Connection c = createConnection()) {
+ Assert.assertTrue(c.isWrapperFor(ADBConnection.class));
+ Assert.assertNotNull(c.unwrap(ADBConnection.class));
+ }
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcDriverTest.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcDriverTest.java
new file mode 100644
index 0000000..b763de2
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcDriverTest.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.asterix.common.api.INcApplicationContext;
+import org.apache.asterix.test.runtime.ExecutionTestUtil;
+import org.apache.hyracks.control.nc.NodeControllerService;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class JdbcDriverTest {
+
+ static final String ASTERIX_APP_PATH_PROPERTY = "asterix-app.dir";
+
+ static final String ASTERIX_APP_PATH = System.getProperty(ASTERIX_APP_PATH_PROPERTY);
+
+ static final List<Class<? extends JdbcTester>> TESTER_CLASSES =
+ Arrays.asList(JdbcMetadataTester.class, JdbcConnectionTester.class, JdbcStatementTester.class,
+ JdbcPreparedStatementTester.class, JdbcResultSetTester.JdbcStatementResultSetTester.class,
+ JdbcResultSetTester.JdbcPreparedStatementResultSetTester.class, JdbcStatementParameterTester.class);
+
+ public static final String TEST_METHOD_PREFIX = "test";
+
+ private static JdbcTester.JdbcTestContext testContext;
+
+ private final Class<? extends JdbcTester> testerClass;
+
+ private final Method testMethod;
+
+ public JdbcDriverTest(String simpleClassName, String methodName) throws Exception {
+ Optional<Class<? extends JdbcTester>> testerClassRef =
+ TESTER_CLASSES.stream().filter(c -> c.getSimpleName().equals(simpleClassName)).findFirst();
+ if (testerClassRef.isEmpty()) {
+ throw new Exception("Cannot find class: " + simpleClassName);
+ }
+ testerClass = testerClassRef.get();
+ Optional<Method> testMethodRef = Arrays.stream(testerClassRef.get().getMethods())
+ .filter(m -> m.getName().equals(methodName)).findFirst();
+ if (testMethodRef.isEmpty()) {
+ throw new Exception("Cannot find method: " + methodName + " in class " + testerClass.getName());
+ }
+ testMethod = testMethodRef.get();
+ }
+
+ @Parameterized.Parameters(name = "JdbcDriverTest {index}: {0}.{1}")
+ public static Collection<Object[]> tests() {
+ List<Object[]> testsuite = new ArrayList<>();
+ for (Class<? extends JdbcTester> testerClass : TESTER_CLASSES) {
+ Arrays.stream(testerClass.getMethods()).map(Method::getName).filter(n -> n.startsWith(TEST_METHOD_PREFIX))
+ .sorted().forEach(n -> testsuite.add(new Object[] { testerClass.getSimpleName(), n }));
+ }
+ return testsuite;
+ }
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ if (ASTERIX_APP_PATH == null) {
+ throw new Exception(String.format("Property %s is not set", ASTERIX_APP_PATH_PROPERTY));
+ }
+
+ Path ccConfigFile = Path.of(ASTERIX_APP_PATH, "src", TEST_METHOD_PREFIX, "resources", "cc.conf");
+
+ ExecutionTestUtil.setUp(true, ccConfigFile.toString(), ExecutionTestUtil.integrationUtil, false,
+ Collections.emptyList());
+
+ NodeControllerService nc = ExecutionTestUtil.integrationUtil.ncs[0];
+ String host = InetAddress.getLoopbackAddress().getHostAddress();
+ INcApplicationContext appCtx = (INcApplicationContext) nc.getApplicationContext();
+ int apiPort = appCtx.getExternalProperties().getNcApiPort();
+
+ testContext = JdbcTester.createTestContext(host, apiPort);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ ExecutionTestUtil.tearDown(true, false);
+ }
+
+ @Test
+ public void test() throws Exception {
+ JdbcTester tester = testerClass.getDeclaredConstructor().newInstance();
+ tester.setTestContext(testContext);
+ testMethod.invoke(tester);
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcMetadataTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcMetadataTester.java
new file mode 100644
index 0000000..80199fe
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcMetadataTester.java
@@ -0,0 +1,1092 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.JDBCType;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLType;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.asterix.jdbc.core.ADBDatabaseMetaData;
+import org.apache.hyracks.algebricks.common.utils.Pair;
+import org.junit.Assert;
+
+class JdbcMetadataTester extends JdbcTester {
+
+ static final String TABLE_CAT = "TABLE_CAT";
+ static final String TABLE_CATALOG = "TABLE_CATALOG";
+ static final String TABLE_SCHEM = "TABLE_SCHEM";
+ static final String TABLE_NAME = "TABLE_NAME";
+ static final String TABLE_TYPE = "TABLE_TYPE";
+ static final String TABLE = "TABLE";
+ static final String VIEW = "VIEW";
+ static final String COLUMN_NAME = "COLUMN_NAME";
+ static final String DATA_TYPE = "DATA_TYPE";
+ static final String TYPE_NAME = "TYPE_NAME";
+ static final String ORDINAL_POSITION = "ORDINAL_POSITION";
+ static final String NULLABLE = "NULLABLE";
+ static final String KEY_SEQ = "KEY_SEQ";
+ static final String PKTABLE_CAT = "PKTABLE_CAT";
+ static final String PKTABLE_SCHEM = "PKTABLE_SCHEM";
+ static final String PKTABLE_NAME = "PKTABLE_NAME";
+ static final String PKCOLUMN_NAME = "PKCOLUMN_NAME";
+ static final String FKTABLE_CAT = "FKTABLE_CAT";
+ static final String FKTABLE_SCHEM = "FKTABLE_SCHEM";
+ static final String FKTABLE_NAME = "FKTABLE_NAME";
+ static final String FKCOLUMN_NAME = "FKCOLUMN_NAME";
+
+ static final String STRING = "string";
+ static final String BIGINT = "int64";
+ static final String DOUBLE = "double";
+
+ static final List<String> DATASET_COLUMN_NAMES = Arrays.asList("tc", "ta", "tb");
+ static final List<String> VIEW_COLUMN_NAMES = Arrays.asList("vb", "vc", "va");
+ static final List<String> DATASET_COLUMN_TYPES = Arrays.asList(STRING, BIGINT, DOUBLE);
+ static final List<SQLType> DATASET_COLUMN_JDBC_TYPES =
+ Arrays.asList(JDBCType.VARCHAR, JDBCType.BIGINT, JDBCType.DOUBLE);
+ static final int DATASET_PK_LEN = 2;
+
+ public void testLifecycle() throws SQLException {
+ Connection c = createConnection();
+ Assert.assertSame(c, c.getMetaData().getConnection());
+ c.close();
+ try {
+ c.getMetaData();
+ Assert.fail("Got metadata on a closed connection");
+ } catch (SQLException e) {
+ Assert.assertEquals(SQL_STATE_CONNECTION_CLOSED, e.getSQLState());
+ }
+ }
+
+ public void testProperties() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ Assert.assertEquals(testContext.getJdbcUrl(), md.getURL());
+ Assert.assertNotNull(md.getDriverName());
+ Assert.assertNotNull(md.getDriverVersion());
+ Assert.assertNotNull(md.getDatabaseProductName());
+ Assert.assertNotNull(md.getDatabaseProductVersion());
+ Assert.assertEquals(4, md.getJDBCMajorVersion());
+ Assert.assertEquals(2, md.getJDBCMinorVersion());
+ Assert.assertTrue(md.isCatalogAtStart());
+ Assert.assertEquals(".", md.getCatalogSeparator());
+ Assert.assertEquals("`", md.getIdentifierQuoteString());
+ Assert.assertTrue(md.allTablesAreSelectable());
+ Assert.assertTrue(md.nullsAreSortedLow());
+ Assert.assertFalse(md.nullsAreSortedHigh());
+ Assert.assertFalse(md.nullsAreSortedAtStart());
+ Assert.assertFalse(md.nullsAreSortedAtEnd());
+ Assert.assertFalse(md.supportsCatalogsInTableDefinitions());
+ Assert.assertFalse(md.supportsCatalogsInIndexDefinitions());
+ Assert.assertFalse(md.supportsCatalogsInDataManipulation());
+ Assert.assertFalse(md.supportsSchemasInTableDefinitions());
+ Assert.assertFalse(md.supportsSchemasInIndexDefinitions());
+ Assert.assertFalse(md.supportsSchemasInDataManipulation());
+ Assert.assertTrue(md.supportsSubqueriesInComparisons());
+ Assert.assertTrue(md.supportsSubqueriesInExists());
+ Assert.assertTrue(md.supportsSubqueriesInIns());
+ Assert.assertTrue(md.supportsCorrelatedSubqueries());
+ Assert.assertTrue(md.supportsOrderByUnrelated());
+ Assert.assertTrue(md.supportsExpressionsInOrderBy());
+ Assert.assertTrue(md.supportsGroupBy());
+ Assert.assertTrue(md.supportsGroupByUnrelated());
+ Assert.assertTrue(md.supportsGroupByBeyondSelect());
+ Assert.assertTrue(md.supportsOuterJoins());
+ Assert.assertTrue(md.supportsMinimumSQLGrammar());
+ Assert.assertTrue(md.supportsTableCorrelationNames());
+ Assert.assertTrue(md.supportsUnionAll());
+ }
+ }
+
+ public void testGetCatalogs() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getCatalogs()) {
+ assertColumnValues(rs, TABLE_CAT, BUILT_IN_DATAVERSE_NAMES);
+ }
+ List<List<String>> newDataverseList = new ArrayList<>();
+ try {
+ createDataverses(s, newDataverseList);
+
+ List<String> allCatalogs = new ArrayList<>(BUILT_IN_DATAVERSE_NAMES);
+ for (List<String> n : newDataverseList) {
+ allCatalogs.add(getCanonicalDataverseName(n));
+ }
+ try (ResultSet rs = md.getCatalogs()) {
+ assertColumnValues(rs, TABLE_CAT, allCatalogs);
+ }
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ public void testGetCatalogsResultSetLifecycle() throws SQLException {
+ // check that Connection.close() closes metadata ResultSet
+ Connection c = createConnection();
+ DatabaseMetaData md = c.getMetaData();
+ ResultSet rs = md.getCatalogs();
+ Assert.assertFalse(rs.isClosed());
+ c.close();
+ Assert.assertTrue(rs.isClosed());
+ }
+
+ public void testGetSchemas() throws SQLException {
+ // get schemas in the default dataverse
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getSchemas()) {
+ assertColumnValues(rs, Arrays.asList(TABLE_SCHEM, TABLE_CATALOG), Arrays
+ .asList(Collections.singletonList(null), Collections.singletonList(DEFAULT_DATAVERSE_NAME)));
+ }
+ }
+
+ // get schemas in the connection's dataverse
+ try (Connection c = createConnection(METADATA_DATAVERSE_NAME)) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getSchemas()) {
+ assertColumnValues(rs, Arrays.asList(TABLE_SCHEM, TABLE_CATALOG), Arrays
+ .asList(Collections.singletonList(null), Collections.singletonList(METADATA_DATAVERSE_NAME)));
+ }
+ }
+
+ // get schemas in the connection's dataverse #2
+ try (Connection c = createConnection()) {
+ c.setCatalog(METADATA_DATAVERSE_NAME);
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getSchemas()) {
+ assertColumnValues(rs, Arrays.asList(TABLE_SCHEM, TABLE_CATALOG), Arrays
+ .asList(Collections.singletonList(null), Collections.singletonList(METADATA_DATAVERSE_NAME)));
+ }
+ }
+
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ // we don't have any schemas without catalogs
+ try (ResultSet rs = md.getSchemas("", null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ }
+
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ try {
+ createDataverses(s, newDataverseList);
+
+ List<String> allCatalogs = new ArrayList<>(BUILT_IN_DATAVERSE_NAMES);
+ for (List<String> n : newDataverseList) {
+ allCatalogs.add(getCanonicalDataverseName(n));
+ }
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getSchemas("", null)) {
+ Assert.assertFalse(rs.next());
+ }
+ try (ResultSet rs = md.getSchemas(null, null)) {
+ assertColumnValues(rs, Arrays.asList(TABLE_SCHEM, TABLE_CATALOG),
+ Arrays.asList(Collections.nCopies(allCatalogs.size(), null), allCatalogs));
+ }
+ try (ResultSet rs = md.getSchemas("x", null)) {
+ assertColumnValues(rs, Arrays.asList(TABLE_SCHEM, TABLE_CATALOG),
+ Arrays.asList(Collections.singletonList(null), Collections.singletonList("x")));
+ }
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ public void testGetTableTypes() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getTableTypes()) {
+ assertColumnValues(rs, TABLE_TYPE, Arrays.asList(TABLE, VIEW));
+ }
+ }
+ }
+
+ public void testGetTables() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getTables(METADATA_DATAVERSE_NAME, null, null, null)) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 10);
+ }
+ try (ResultSet rs = md.getTables(METADATA_DATAVERSE_NAME, null, "Data%", null)) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 2);
+ }
+ // we don't have any tables without catalogs
+ try (ResultSet rs = md.getTables("", null, null, null)) {
+ int n = countRows(rs);
+ Assert.assertEquals(0, n);
+ }
+ }
+
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ List<Pair<List<String>, String>> newDatasetList = new ArrayList<>();
+ List<Pair<List<String>, String>> newViewList = new ArrayList<>();
+
+ try {
+ createDataversesDatasetsViews(s, newDataverseList, newDatasetList, newViewList);
+
+ DatabaseMetaData md = c.getMetaData();
+ List<String> expectedColumns = Arrays.asList(TABLE_CAT, TABLE_SCHEM, TABLE_NAME, TABLE_TYPE);
+ List<String> expectedTableCat = new ArrayList<>();
+ List<String> expectedTableSchem = new ArrayList<>();
+ List<String> expectedTableName = new ArrayList<>();
+ List<String> expectedTableType = new ArrayList<>();
+
+ // Test getTables() in all catalogs
+ for (Pair<List<String>, String> p : newDatasetList) {
+ expectedTableCat.add(getCanonicalDataverseName(p.first));
+ expectedTableSchem.add(null);
+ expectedTableName.add(p.second);
+ expectedTableType.add(TABLE);
+ }
+ // using table name pattern
+ try (ResultSet rs = md.getTables(null, null, "t%", null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedTableType));
+ }
+ // using table type
+ try (ResultSet rs = md.getTables(null, null, null, new String[] { TABLE })) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedTableType),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+ // all tables
+ for (Pair<List<String>, String> p : newViewList) {
+ expectedTableCat.add(getCanonicalDataverseName(p.first));
+ expectedTableSchem.add(null);
+ expectedTableName.add(p.second);
+ expectedTableType.add(VIEW);
+ }
+ try (ResultSet rs = md.getTables(null, null, null, null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedTableType),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+ try (ResultSet rs = md.getTables(null, "", null, null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedTableType),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+ try (ResultSet rs = md.getTables(null, null, null, new String[] { TABLE, VIEW })) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedTableType),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+
+ // Test getTables() in a particular catalog
+ for (List<String> dvi : newDataverseList) {
+ expectedTableCat.clear();
+ expectedTableSchem.clear();
+ expectedTableName.clear();
+ expectedTableType.clear();
+ String dvic = getCanonicalDataverseName(dvi);
+ for (Pair<List<String>, String> p : newDatasetList) {
+ String dv = getCanonicalDataverseName(p.first);
+ if (dv.equals(dvic)) {
+ expectedTableCat.add(dv);
+ expectedTableSchem.add(null);
+ expectedTableName.add(p.second);
+ expectedTableType.add(TABLE);
+ }
+ }
+ // using table name pattern
+ try (ResultSet rs = md.getTables(dvic, null, "t%", null)) {
+ assertColumnValues(rs, expectedColumns, Arrays.asList(expectedTableCat, expectedTableSchem,
+ expectedTableName, expectedTableType));
+ }
+ // using table type
+ try (ResultSet rs = md.getTables(dvic, null, null, new String[] { TABLE })) {
+ assertColumnValues(rs, expectedColumns, Arrays.asList(expectedTableCat, expectedTableSchem,
+ expectedTableName, expectedTableType));
+ }
+ for (Pair<List<String>, String> p : newViewList) {
+ String dv = getCanonicalDataverseName(p.first);
+ if (dv.equals(dvic)) {
+ expectedTableCat.add(dv);
+ expectedTableSchem.add(null);
+ expectedTableName.add(p.second);
+ expectedTableType.add(VIEW);
+ }
+ }
+ try (ResultSet rs = md.getTables(dvic, null, null, null)) {
+ assertColumnValues(rs, expectedColumns, Arrays.asList(expectedTableCat, expectedTableSchem,
+ expectedTableName, expectedTableType));
+ }
+ try (ResultSet rs = md.getTables(dvic, "", null, null)) {
+ assertColumnValues(rs, expectedColumns, Arrays.asList(expectedTableCat, expectedTableSchem,
+ expectedTableName, expectedTableType));
+ }
+ try (ResultSet rs = md.getTables(dvic, null, null, new String[] { TABLE, VIEW })) {
+ assertColumnValues(rs, expectedColumns, Arrays.asList(expectedTableCat, expectedTableSchem,
+ expectedTableName, expectedTableType));
+ }
+ }
+
+ // non-existent catalog
+ try (ResultSet rs = md.getTables("UNKNOWN", null, null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent schema
+ try (ResultSet rs = md.getTables(null, "UNKNOWN", null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table name
+ try (ResultSet rs = md.getTables(null, null, "UNKNOWN", null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table type
+ try (ResultSet rs = md.getTables(null, null, null, new String[] { "UNKNOWN" })) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ public void testGetColumns() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getColumns(METADATA_DATAVERSE_NAME, null, null, null)) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 50);
+ }
+ try (ResultSet rs = md.getColumns(METADATA_DATAVERSE_NAME, null, "Data%", null)) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 20);
+ }
+ // we don't have any columns without catalogs
+ try (ResultSet rs = md.getColumns("", null, null, null)) {
+ int n = countRows(rs);
+ Assert.assertEquals(0, n);
+ }
+ }
+
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ List<Pair<List<String>, String>> newDatasetList = new ArrayList<>();
+ List<Pair<List<String>, String>> newViewList = new ArrayList<>();
+
+ try {
+ createDataversesDatasetsViews(s, newDataverseList, newDatasetList, newViewList);
+
+ DatabaseMetaData md = c.getMetaData();
+
+ List<String> expectedColumns = Arrays.asList(TABLE_CAT, TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE,
+ TYPE_NAME, ORDINAL_POSITION, NULLABLE);
+ List<String> expectedTableCat = new ArrayList<>();
+ List<String> expectedTableSchem = new ArrayList<>();
+ List<String> expectedTableName = new ArrayList<>();
+ List<String> expectedColumnName = new ArrayList<>();
+ List<Integer> expectedDataType = new ArrayList<>();
+ List<String> expectedTypeName = new ArrayList<>();
+ List<Integer> expectedOrdinalPosition = new ArrayList<>();
+ List<Integer> expectedNullable = new ArrayList<>();
+
+ // Test getColumns() in all catalogs
+
+ // datasets only
+ for (Pair<List<String>, String> p : newDatasetList) {
+ for (int i = 0, n = DATASET_COLUMN_NAMES.size(); i < n; i++) {
+ String columnName = DATASET_COLUMN_NAMES.get(i);
+ String columnType = DATASET_COLUMN_TYPES.get(i);
+ SQLType columnJdbcType = DATASET_COLUMN_JDBC_TYPES.get(i);
+ expectedTableCat.add(getCanonicalDataverseName(p.first));
+ expectedTableSchem.add(null);
+ expectedTableName.add(p.second);
+ expectedColumnName.add(columnName);
+ expectedDataType.add(columnJdbcType.getVendorTypeNumber());
+ expectedTypeName.add(columnType);
+ expectedOrdinalPosition.add(i + 1);
+ expectedNullable.add(
+ i < DATASET_PK_LEN ? DatabaseMetaData.columnNoNulls : DatabaseMetaData.columnNullable);
+ }
+ }
+ // using column name pattern
+ try (ResultSet rs = md.getColumns(null, null, null, "t%")) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedDataType, expectedTypeName, expectedOrdinalPosition, expectedNullable));
+ }
+ // using table name pattern
+ try (ResultSet rs = md.getColumns(null, null, "t%", null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedDataType, expectedTypeName, expectedOrdinalPosition, expectedNullable));
+ }
+ // all columns
+ expectedTableCat.clear();
+ expectedTableSchem.clear();
+ expectedTableName.clear();
+ expectedColumnName.clear();
+ expectedDataType.clear();
+ expectedTypeName.clear();
+ expectedOrdinalPosition.clear();
+ expectedNullable.clear();
+
+ int dsIdx = 0, vIdx = 0;
+ for (List<String> dvName : newDataverseList) {
+ String dvNameCanonical = getCanonicalDataverseName(dvName);
+ for (; dsIdx < newDatasetList.size() && newDatasetList.get(dsIdx).first.equals(dvName); dsIdx++) {
+ String dsName = newDatasetList.get(dsIdx).second;
+ addExpectedColumnNamesForGetColumns(dvNameCanonical, dsName, DATASET_COLUMN_NAMES,
+ expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedDataType, expectedTypeName, expectedOrdinalPosition, expectedNullable);
+ }
+ for (; vIdx < newViewList.size() && newViewList.get(vIdx).first.equals(dvName); vIdx++) {
+ String vName = newViewList.get(vIdx).second;
+ addExpectedColumnNamesForGetColumns(dvNameCanonical, vName, VIEW_COLUMN_NAMES, expectedTableCat,
+ expectedTableSchem, expectedTableName, expectedColumnName, expectedDataType,
+ expectedTypeName, expectedOrdinalPosition, expectedNullable);
+ }
+ }
+
+ try (ResultSet rs = md.getColumns(null, null, null, null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedDataType, expectedTypeName, expectedOrdinalPosition, expectedNullable),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+ try (ResultSet rs = md.getColumns(null, "", null, null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedDataType, expectedTypeName, expectedOrdinalPosition, expectedNullable),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+
+ // Test getColumns() in a particular catalog
+ for (List<String> dvName : newDataverseList) {
+ expectedTableCat.clear();
+ expectedTableSchem.clear();
+ expectedTableName.clear();
+ expectedColumnName.clear();
+ expectedDataType.clear();
+ expectedTypeName.clear();
+ expectedOrdinalPosition.clear();
+ expectedNullable.clear();
+
+ String dvNameCanonical = getCanonicalDataverseName(dvName);
+ for (Pair<List<String>, String> p : newDatasetList) {
+ if (dvName.equals(p.first)) {
+ addExpectedColumnNamesForGetColumns(dvNameCanonical, p.second, DATASET_COLUMN_NAMES,
+ expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedDataType, expectedTypeName, expectedOrdinalPosition, expectedNullable);
+ }
+ }
+ try (ResultSet rs = md.getColumns(dvNameCanonical, null, "t%", null)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName,
+ expectedColumnName, expectedDataType, expectedTypeName, expectedOrdinalPosition,
+ expectedNullable),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+ try (ResultSet rs = md.getColumns(dvNameCanonical, null, null, "t%")) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedTableCat, expectedTableSchem, expectedTableName,
+ expectedColumnName, expectedDataType, expectedTypeName, expectedOrdinalPosition,
+ expectedNullable),
+ JdbcMetadataTester::isMetadataCatalog);
+ }
+ }
+
+ // non-existent catalog
+ try (ResultSet rs = md.getColumns("UNKNOWN", null, null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent schema
+ try (ResultSet rs = md.getColumns(null, "UNKNOWN", null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table name
+ try (ResultSet rs = md.getColumns(null, null, "UNKNOWN", null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent column names
+ try (ResultSet rs = md.getColumns(null, null, null, "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ private void addExpectedColumnNamesForGetColumns(String dvNameCanonical, String dsName, List<String> columnNames,
+ List<String> outTableCat, List<String> outTableSchem, List<String> outTableName, List<String> outColumnName,
+ List<Integer> outDataType, List<String> outTypeName, List<Integer> outOrdinalPosition,
+ List<Integer> outNullable) {
+ for (int i = 0; i < columnNames.size(); i++) {
+ String columnName = columnNames.get(i);
+ String columnType = DATASET_COLUMN_TYPES.get(i);
+ SQLType columnJdbcType = DATASET_COLUMN_JDBC_TYPES.get(i);
+ outTableCat.add(dvNameCanonical);
+ outTableSchem.add(null);
+ outTableName.add(dsName);
+ outColumnName.add(columnName);
+ outDataType.add(columnJdbcType.getVendorTypeNumber());
+ outTypeName.add(columnType);
+ outOrdinalPosition.add(i + 1);
+ outNullable.add(i < JdbcMetadataTester.DATASET_PK_LEN ? DatabaseMetaData.columnNoNulls
+ : DatabaseMetaData.columnNullable);
+ }
+ }
+
+ public void testGetPrimaryKeys() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getPrimaryKeys(METADATA_DATAVERSE_NAME, null, null)) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 20);
+ }
+ try (ResultSet rs = md.getPrimaryKeys(METADATA_DATAVERSE_NAME, null, "Data%")) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 4);
+ }
+ // we don't have any tables without catalogs
+ try (ResultSet rs = md.getPrimaryKeys("", null, null)) {
+ int n = countRows(rs);
+ Assert.assertEquals(0, n);
+ }
+ }
+
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ List<Pair<List<String>, String>> newDatasetList = new ArrayList<>();
+ List<Pair<List<String>, String>> newViewList = new ArrayList<>();
+
+ try {
+ createDataversesDatasetsViews(s, newDataverseList, newDatasetList, newViewList);
+
+ DatabaseMetaData md = c.getMetaData();
+
+ List<String> expectedColumns = Arrays.asList(TABLE_CAT, TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, KEY_SEQ);
+ List<String> expectedTableCat = new ArrayList<>();
+ List<String> expectedTableSchem = new ArrayList<>();
+ List<String> expectedTableName = new ArrayList<>();
+ List<String> expectedColumnName = new ArrayList<>();
+ List<Integer> expectedKeySeq = new ArrayList<>();
+
+ // Test getPrimaryKeys() for a particular dataset/view
+ for (int i = 0, n = newDatasetList.size(); i < n; i++) {
+ for (int j = 0; j < 2; j++) {
+ Pair<List<String>, String> p = j == 0 ? newDatasetList.get(i) : newViewList.get(i);
+ List<String> columnNames = j == 0 ? DATASET_COLUMN_NAMES : VIEW_COLUMN_NAMES;
+ String dvNameCanonical = getCanonicalDataverseName(p.first);
+ String dsName = p.second;
+
+ expectedTableCat.clear();
+ expectedTableSchem.clear();
+ expectedTableName.clear();
+ expectedColumnName.clear();
+ expectedKeySeq.clear();
+
+ List<String> pkColumnNames = columnNames.subList(0, DATASET_PK_LEN);
+ addExpectedColumnNamesForGetPrimaryKeys(dvNameCanonical, dsName, pkColumnNames,
+ expectedTableCat, expectedTableSchem, expectedTableName, expectedColumnName,
+ expectedKeySeq);
+
+ try (ResultSet rs = md.getPrimaryKeys(dvNameCanonical, null, dsName)) {
+ assertColumnValues(rs, expectedColumns, Arrays.asList(expectedTableCat, expectedTableSchem,
+ expectedTableName, expectedColumnName, expectedKeySeq));
+ }
+ }
+ }
+
+ // non-existent catalog
+ try (ResultSet rs = md.getPrimaryKeys("UNKNOWN", null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent schema
+ try (ResultSet rs = md.getPrimaryKeys(null, "UNKNOWN", null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table name
+ try (ResultSet rs = md.getPrimaryKeys(null, null, "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ private void addExpectedColumnNamesForGetPrimaryKeys(String dvNameCanonical, String dsName,
+ List<String> pkColumnNames, List<String> outTableCat, List<String> outTableSchem, List<String> outTableName,
+ List<String> outColumnName, List<Integer> outKeySeq) {
+ List<String> pkColumnNamesSorted = new ArrayList<>(pkColumnNames);
+ Collections.sort(pkColumnNamesSorted);
+ for (int i = 0; i < pkColumnNames.size(); i++) {
+ String pkColumnName = pkColumnNamesSorted.get(i);
+ outTableCat.add(dvNameCanonical);
+ outTableSchem.add(null);
+ outTableName.add(dsName);
+ outColumnName.add(pkColumnName);
+ outKeySeq.add(pkColumnNames.indexOf(pkColumnName) + 1);
+ }
+ }
+
+ public void testGetImportedKeys() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ List<Pair<List<String>, String>> newDatasetList = new ArrayList<>();
+ List<Pair<List<String>, String>> newViewList = new ArrayList<>();
+
+ try {
+ createDataversesDatasetsViews(s, newDataverseList, newDatasetList, newViewList);
+
+ DatabaseMetaData md = c.getMetaData();
+
+ List<String> expectedColumns = Arrays.asList(PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, PKCOLUMN_NAME,
+ FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, FKCOLUMN_NAME, KEY_SEQ);
+ List<String> expectedPKTableCat = new ArrayList<>();
+ List<String> expectedPKTableSchem = new ArrayList<>();
+ List<String> expectedPKTableName = new ArrayList<>();
+ List<String> expectedPKColumnName = new ArrayList<>();
+ List<String> expectedFKTableCat = new ArrayList<>();
+ List<String> expectedFKTableSchem = new ArrayList<>();
+ List<String> expectedFKTableName = new ArrayList<>();
+ List<String> expectedFKColumnName = new ArrayList<>();
+ List<Integer> expectedKeySeq = new ArrayList<>();
+
+ // Test getImportedKeys() for a particular view
+ for (int i = 0, n = newViewList.size(); i < n; i++) {
+ Pair<List<String>, String> p = newViewList.get(i);
+ List<String> dvName = p.first;
+ String dvNameCanonical = getCanonicalDataverseName(dvName);
+ String viewName = p.second;
+
+ expectedPKTableCat.clear();
+ expectedPKTableSchem.clear();
+ expectedPKTableName.clear();
+ expectedPKColumnName.clear();
+ expectedFKTableCat.clear();
+ expectedFKTableSchem.clear();
+ expectedFKTableName.clear();
+ expectedFKColumnName.clear();
+
+ expectedKeySeq.clear();
+
+ List<String> pkFkColumnNames = VIEW_COLUMN_NAMES.subList(0, DATASET_PK_LEN);
+ List<String> fkRefs = IntStream.range(0, i).mapToObj(newViewList::get)
+ .filter(p2 -> p2.first.equals(dvName)).map(p2 -> p2.second).collect(Collectors.toList());
+
+ addExpectedColumnNamesForGetImportedKeys(dvNameCanonical, viewName, pkFkColumnNames, fkRefs,
+ expectedPKTableCat, expectedPKTableSchem, expectedPKTableName, expectedPKColumnName,
+ expectedFKTableCat, expectedFKTableSchem, expectedFKTableName, expectedFKColumnName,
+ expectedKeySeq);
+
+ try (ResultSet rs = md.getImportedKeys(dvNameCanonical, null, viewName)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedPKTableCat, expectedPKTableSchem, expectedPKTableName,
+ expectedPKColumnName, expectedFKTableCat, expectedFKTableSchem,
+ expectedFKTableName, expectedFKColumnName, expectedKeySeq));
+ }
+ }
+
+ // non-existent catalog
+ try (ResultSet rs = md.getImportedKeys("UNKNOWN", null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent schema
+ try (ResultSet rs = md.getImportedKeys(null, "UNKNOWN", null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table name
+ try (ResultSet rs = md.getImportedKeys(null, null, "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ private void addExpectedColumnNamesForGetImportedKeys(String dvNameCanonical, String dsName,
+ List<String> pkFkColumnNames, List<String> fkRefs, List<String> outPKTableCat, List<String> outPKTableSchem,
+ List<String> outPKTableName, List<String> outPKColumnName, List<String> outFKTableCat,
+ List<String> outFKTableSchem, List<String> outFKTableName, List<String> outFKColumnName,
+ List<Integer> outKeySeq) {
+ for (String fkRef : fkRefs) {
+ for (int i = 0; i < pkFkColumnNames.size(); i++) {
+ String pkFkColumn = pkFkColumnNames.get(i);
+ outPKTableCat.add(dvNameCanonical);
+ outPKTableSchem.add(null);
+ outPKTableName.add(fkRef);
+ outPKColumnName.add(pkFkColumn);
+ outFKTableCat.add(dvNameCanonical);
+ outFKTableSchem.add(null);
+ outFKTableName.add(dsName);
+ outFKColumnName.add(pkFkColumn);
+ outKeySeq.add(i + 1);
+ }
+ }
+ }
+
+ public void testGetExportedKeys() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ List<Pair<List<String>, String>> newDatasetList = new ArrayList<>();
+ List<Pair<List<String>, String>> newViewList = new ArrayList<>();
+
+ try {
+ createDataversesDatasetsViews(s, newDataverseList, newDatasetList, newViewList);
+
+ DatabaseMetaData md = c.getMetaData();
+
+ List<String> expectedColumns = Arrays.asList(PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, PKCOLUMN_NAME,
+ FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, FKCOLUMN_NAME, KEY_SEQ);
+ List<String> expectedPKTableCat = new ArrayList<>();
+ List<String> expectedPKTableSchem = new ArrayList<>();
+ List<String> expectedPKTableName = new ArrayList<>();
+ List<String> expectedPKColumnName = new ArrayList<>();
+ List<String> expectedFKTableCat = new ArrayList<>();
+ List<String> expectedFKTableSchem = new ArrayList<>();
+ List<String> expectedFKTableName = new ArrayList<>();
+ List<String> expectedFKColumnName = new ArrayList<>();
+ List<Integer> expectedKeySeq = new ArrayList<>();
+
+ // Test getExportedKeys() for a particular view
+ for (int i = 0, n = newViewList.size(); i < n; i++) {
+ Pair<List<String>, String> p = newViewList.get(i);
+ List<String> dvName = p.first;
+ String dvNameCanonical = getCanonicalDataverseName(dvName);
+ String viewName = p.second;
+
+ expectedPKTableCat.clear();
+ expectedPKTableSchem.clear();
+ expectedPKTableName.clear();
+ expectedPKColumnName.clear();
+ expectedFKTableCat.clear();
+ expectedFKTableSchem.clear();
+ expectedFKTableName.clear();
+ expectedFKColumnName.clear();
+ expectedKeySeq.clear();
+
+ List<String> pkFkColumnNames = VIEW_COLUMN_NAMES.subList(0, DATASET_PK_LEN);
+ List<String> fkRefs = IntStream.range(i + 1, newViewList.size()).mapToObj(newViewList::get)
+ .filter(p2 -> p2.first.equals(dvName)).map(p2 -> p2.second).collect(Collectors.toList());
+
+ addExpectedColumnNamesForGetExportedKeys(dvNameCanonical, viewName, pkFkColumnNames, fkRefs,
+ expectedPKTableCat, expectedPKTableSchem, expectedPKTableName, expectedPKColumnName,
+ expectedFKTableCat, expectedFKTableSchem, expectedFKTableName, expectedFKColumnName,
+ expectedKeySeq);
+
+ try (ResultSet rs = md.getExportedKeys(dvNameCanonical, null, viewName)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedPKTableCat, expectedPKTableSchem, expectedPKTableName,
+ expectedPKColumnName, expectedFKTableCat, expectedFKTableSchem,
+ expectedFKTableName, expectedFKColumnName, expectedKeySeq));
+ }
+ }
+
+ // non-existent catalog
+ try (ResultSet rs = md.getExportedKeys("UNKNOWN", null, null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent schema
+ try (ResultSet rs = md.getExportedKeys(null, "UNKNOWN", null)) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table name
+ try (ResultSet rs = md.getExportedKeys(null, null, "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ private void addExpectedColumnNamesForGetExportedKeys(String dvNameCanonical, String dsName,
+ List<String> pkFkColumnNames, List<String> fkRefs, List<String> outPKTableCat, List<String> outPKTableSchem,
+ List<String> outPKTableName, List<String> outPKColumnName, List<String> outFKTableCat,
+ List<String> outFKTableSchem, List<String> outFKTableName, List<String> outFKColumnName,
+ List<Integer> outKeySeq) {
+ for (String fkRef : fkRefs) {
+ for (int i = 0; i < pkFkColumnNames.size(); i++) {
+ String pkFkColumn = pkFkColumnNames.get(i);
+ outPKTableCat.add(dvNameCanonical);
+ outPKTableSchem.add(null);
+ outPKTableName.add(dsName);
+ outPKColumnName.add(pkFkColumn);
+ outFKTableCat.add(dvNameCanonical);
+ outFKTableSchem.add(null);
+ outFKTableName.add(fkRef);
+ outFKColumnName.add(pkFkColumn);
+ outKeySeq.add(i + 1);
+ }
+ }
+ }
+
+ public void testGetCrossReference() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<List<String>> newDataverseList = new ArrayList<>();
+ List<Pair<List<String>, String>> newDatasetList = new ArrayList<>();
+ List<Pair<List<String>, String>> newViewList = new ArrayList<>();
+
+ try {
+ createDataversesDatasetsViews(s, newDataverseList, newDatasetList, newViewList);
+
+ DatabaseMetaData md = c.getMetaData();
+
+ List<String> expectedColumns = Arrays.asList(PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, PKCOLUMN_NAME,
+ FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, FKCOLUMN_NAME, KEY_SEQ);
+ List<String> expectedPKTableCat = new ArrayList<>();
+ List<String> expectedPKTableSchem = new ArrayList<>();
+ List<String> expectedPKTableName = new ArrayList<>();
+ List<String> expectedPKColumnName = new ArrayList<>();
+ List<String> expectedFKTableCat = new ArrayList<>();
+ List<String> expectedFKTableSchem = new ArrayList<>();
+ List<String> expectedFKTableName = new ArrayList<>();
+ List<String> expectedFKColumnName = new ArrayList<>();
+ List<Integer> expectedKeySeq = new ArrayList<>();
+
+ boolean testUnknown = true;
+ // Test getCrossReference() for a particular view
+ for (int i = 0, n = newViewList.size(); i < n; i++) {
+ Pair<List<String>, String> p = newViewList.get(i);
+ List<String> dvName = p.first;
+ String dvNameCanonical = getCanonicalDataverseName(dvName);
+ String viewName = p.second;
+
+ List<String> pkFkColumnNames = VIEW_COLUMN_NAMES.subList(0, DATASET_PK_LEN);
+ Iterator<String> fkRefIter = IntStream.range(i + 1, newViewList.size()).mapToObj(newViewList::get)
+ .filter(p2 -> p2.first.equals(dvName)).map(p2 -> p2.second).iterator();
+ boolean hasFkRefs = fkRefIter.hasNext();
+ while (fkRefIter.hasNext()) {
+ String fkRef = fkRefIter.next();
+
+ expectedPKTableCat.clear();
+ expectedPKTableSchem.clear();
+ expectedPKTableName.clear();
+ expectedPKColumnName.clear();
+ expectedFKTableCat.clear();
+ expectedFKTableSchem.clear();
+ expectedFKTableName.clear();
+ expectedFKColumnName.clear();
+ expectedKeySeq.clear();
+
+ addExpectedColumnNamesForGetCrossReference(dvNameCanonical, viewName, pkFkColumnNames, fkRef,
+ expectedPKTableCat, expectedPKTableSchem, expectedPKTableName, expectedPKColumnName,
+ expectedFKTableCat, expectedFKTableSchem, expectedFKTableName, expectedFKColumnName,
+ expectedKeySeq);
+
+ try (ResultSet rs =
+ md.getCrossReference(dvNameCanonical, null, viewName, dvNameCanonical, null, fkRef)) {
+ assertColumnValues(rs, expectedColumns,
+ Arrays.asList(expectedPKTableCat, expectedPKTableSchem, expectedPKTableName,
+ expectedPKColumnName, expectedFKTableCat, expectedFKTableSchem,
+ expectedFKTableName, expectedFKColumnName, expectedKeySeq));
+ }
+ }
+
+ if (testUnknown && hasFkRefs) {
+ testUnknown = false;
+ // non-existent catalog
+ try (ResultSet rs =
+ md.getCrossReference(dvNameCanonical, null, viewName, "UNKNOWN", null, "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent schema
+ try (ResultSet rs = md.getCrossReference(dvNameCanonical, null, viewName, dvNameCanonical,
+ "UNKNOWN", "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ // non-existent table name
+ try (ResultSet rs = md.getCrossReference(dvNameCanonical, null, viewName, dvNameCanonical, null,
+ "UNKNOWN")) {
+ Assert.assertEquals(0, countRows(rs));
+ }
+ }
+ }
+
+ } finally {
+ dropDataverses(s, newDataverseList);
+ }
+ }
+ }
+
+ private void addExpectedColumnNamesForGetCrossReference(String dvNameCanonical, String dsName,
+ List<String> pkFkColumnNames, String fkRef, List<String> outPKTableCat, List<String> outPKTableSchem,
+ List<String> outPKTableName, List<String> outPKColumnName, List<String> outFKTableCat,
+ List<String> outFKTableSchem, List<String> outFKTableName, List<String> outFKColumnName,
+ List<Integer> outKeySeq) {
+ for (int i = 0; i < pkFkColumnNames.size(); i++) {
+ String pkFkColumn = pkFkColumnNames.get(i);
+ outPKTableCat.add(dvNameCanonical);
+ outPKTableSchem.add(null);
+ outPKTableName.add(dsName);
+ outPKColumnName.add(pkFkColumn);
+ outFKTableCat.add(dvNameCanonical);
+ outFKTableSchem.add(null);
+ outFKTableName.add(fkRef);
+ outFKColumnName.add(pkFkColumn);
+ outKeySeq.add(i + 1);
+ }
+ }
+
+ public void testGetTypeInfo() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ try (ResultSet rs = md.getTypeInfo()) {
+ int n = countRows(rs);
+ Assert.assertTrue(String.valueOf(n), n > 10);
+ }
+ }
+ }
+
+ private static boolean isMetadataCatalog(ResultSet rs) throws SQLException {
+ return METADATA_DATAVERSE_NAME.equals(rs.getString(TABLE_CAT));
+ }
+
+ private void createDataverses(Statement stmt, List<List<String>> outDataverseList) throws SQLException {
+ for (String p1 : new String[] { "x", "y" }) {
+ List<String> dv1 = Collections.singletonList(p1);
+ stmt.execute(printCreateDataverse(dv1));
+ outDataverseList.add(dv1);
+ for (int p2i = 0; p2i <= 9; p2i++) {
+ String p2 = "z" + p2i;
+ List<String> dv2 = Arrays.asList(p1, p2);
+ stmt.execute(printCreateDataverse(dv2));
+ outDataverseList.add(dv2);
+ }
+ }
+ }
+
+ private void createDataversesDatasetsViews(Statement stmt, List<List<String>> outDataverseList,
+ List<Pair<List<String>, String>> outDatasetList, List<Pair<List<String>, String>> outViewList)
+ throws SQLException {
+ for (String p1 : new String[] { "x", "y" }) {
+ for (int p2i = 0; p2i < 2; p2i++) {
+ String p2 = "z" + p2i;
+ List<String> dv = Arrays.asList(p1, p2);
+ stmt.execute(printCreateDataverse(dv));
+ outDataverseList.add(dv);
+ for (int i = 0; i < 3; i++) {
+ // create dataset
+ String datasetName = createDatasetName(i);
+ stmt.execute(printCreateDataset(dv, datasetName, DATASET_COLUMN_NAMES, DATASET_COLUMN_TYPES,
+ DATASET_PK_LEN));
+ outDatasetList.add(new Pair<>(dv, datasetName));
+ // create tabular view
+ String viewName = createViewName(i);
+ String viewQuery = "select r va, r vb, r vc from range(1,2) r";
+ List<String> fkRefs = IntStream.range(0, i).mapToObj(JdbcMetadataTester::createViewName)
+ .collect(Collectors.toList());
+ stmt.execute(printCreateView(dv, viewName, VIEW_COLUMN_NAMES, DATASET_COLUMN_TYPES, DATASET_PK_LEN,
+ fkRefs, viewQuery));
+ outViewList.add(new Pair<>(dv, viewName));
+ }
+ }
+ }
+ }
+
+ private static String createDatasetName(int id) {
+ return "t" + id;
+ }
+
+ private static String createViewName(int id) {
+ return "v" + id;
+ }
+
+ private void dropDataverses(Statement stmt, List<List<String>> dataverseList) throws SQLException {
+ for (List<String> dv : dataverseList) {
+ stmt.execute(printDropDataverse(dv));
+ }
+ }
+
+ private void assertColumnValues(ResultSet rs, String column, List<?> values) throws SQLException {
+ assertColumnValues(rs, Collections.singletonList(column), Collections.singletonList(values));
+ }
+
+ private void assertColumnValues(ResultSet rs, List<String> columns, List<List<?>> values) throws SQLException {
+ assertColumnValues(rs, columns, values, null);
+ }
+
+ private void assertColumnValues(ResultSet rs, List<String> columns, List<List<?>> values,
+ JdbcPredicate<ResultSet> skipRowTest) throws SQLException {
+ int columnCount = columns.size();
+ Assert.assertEquals(columnCount, values.size());
+ List<Iterator<?>> valueIters = values.stream().map(List::iterator).collect(Collectors.toList());
+ while (rs.next()) {
+ if (skipRowTest != null && skipRowTest.test(rs)) {
+ continue;
+ }
+ for (int i = 0; i < columnCount; i++) {
+ String column = columns.get(i);
+ Object expectedValue = valueIters.get(i).next();
+ Object actualValue;
+ if (expectedValue instanceof String) {
+ actualValue = rs.getString(column);
+ } else if (expectedValue instanceof Integer) {
+ actualValue = rs.getInt(column);
+ } else if (expectedValue instanceof Long) {
+ actualValue = rs.getLong(column);
+ } else {
+ actualValue = rs.getObject(column);
+ }
+ if (rs.wasNull()) {
+ Assert.assertNull(expectedValue);
+ } else {
+ Assert.assertEquals(expectedValue, actualValue);
+ }
+ }
+ }
+ for (Iterator<?> i : valueIters) {
+ if (i.hasNext()) {
+ Assert.fail(String.valueOf(i.next()));
+ }
+ }
+ }
+
+ private int countRows(ResultSet rs) throws SQLException {
+ int n = 0;
+ while (rs.next()) {
+ n++;
+ }
+ return n;
+ }
+
+ public void testWrapper() throws SQLException {
+ try (Connection c = createConnection()) {
+ DatabaseMetaData md = c.getMetaData();
+ Assert.assertTrue(md.isWrapperFor(ADBDatabaseMetaData.class));
+ Assert.assertNotNull(md.unwrap(ADBDatabaseMetaData.class));
+ }
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcPreparedStatementTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcPreparedStatementTester.java
new file mode 100644
index 0000000..4ff78d2
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcPreparedStatementTester.java
@@ -0,0 +1,291 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.jdbc.core.ADBPreparedStatement;
+import org.junit.Assert;
+
+class JdbcPreparedStatementTester extends JdbcTester {
+
+ public void testLifecycle() throws SQLException {
+ Connection c = createConnection();
+ PreparedStatement s = c.prepareStatement(Q1);
+ Assert.assertFalse(s.isClosed());
+ Assert.assertSame(c, s.getConnection());
+
+ s.close();
+ Assert.assertTrue(s.isClosed());
+
+ // ok to call close() on a closed statement
+ s.close();
+ Assert.assertTrue(s.isClosed());
+ }
+
+ public void testAutoCloseOnConnectionClose() throws SQLException {
+ Connection c = createConnection();
+ // check that a statement is automatically closed when the connection is closed
+ PreparedStatement s = c.prepareStatement(Q1);
+ Assert.assertFalse(s.isClosed());
+ c.close();
+ Assert.assertTrue(s.isClosed());
+ }
+
+ public void testCloseOnCompletion() throws SQLException {
+ try (Connection c = createConnection()) {
+ PreparedStatement s = c.prepareStatement(Q1);
+ Assert.assertFalse(s.isCloseOnCompletion());
+ s.closeOnCompletion();
+ Assert.assertTrue(s.isCloseOnCompletion());
+ Assert.assertFalse(s.isClosed());
+ ResultSet rs = s.executeQuery();
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(rs.next());
+ rs.close();
+ Assert.assertTrue(s.isClosed());
+ }
+ }
+
+ public void testExecuteQuery() throws SQLException {
+ try (Connection c = createConnection()) {
+ // Query -> ok
+ try (PreparedStatement s1 = c.prepareStatement(Q1); ResultSet rs1 = s1.executeQuery()) {
+ Assert.assertTrue(rs1.next());
+ Assert.assertEquals(1, rs1.getMetaData().getColumnCount());
+ Assert.assertEquals(V1, rs1.getInt(1));
+ Assert.assertFalse(rs1.next());
+ Assert.assertFalse(rs1.isClosed());
+ }
+
+ // DDL -> error
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testExecuteQuery");
+ try {
+ PreparedStatement s2 = c.prepareStatement(printCreateDataverse(dataverse));
+ s2.executeQuery();
+ Assert.fail("DDL did not fail in executeQuery()");
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains(ErrorCode.PROHIBITED_STATEMENT_CATEGORY.errorCode()));
+ }
+
+ // DML -> error
+ String dataset = "ds1";
+ PreparedStatement s3 = c.prepareStatement(printCreateDataverse(dataverse));
+ s3.execute();
+ PreparedStatement s4 = c.prepareStatement(printCreateDataset(dataverse, dataset));
+ s4.execute();
+ try {
+ PreparedStatement s5 = c.prepareStatement(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ s5.executeQuery();
+ Assert.fail("DML did not fail in executeQuery()");
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains(ErrorCode.PROHIBITED_STATEMENT_CATEGORY.errorCode()));
+ }
+
+ // Cleanup
+ PreparedStatement s6 = c.prepareStatement(printDropDataverse(dataverse));
+ s6.execute();
+ }
+ }
+
+ public void testExecuteUpdate() throws SQLException {
+ try (Connection c = createConnection()) {
+ // DDL -> ok
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testExecuteUpdate");
+ PreparedStatement s1 = c.prepareStatement(printCreateDataverse(dataverse));
+ int res = s1.executeUpdate();
+ Assert.assertEquals(0, res);
+ String dataset = "ds1";
+ PreparedStatement s2 = c.prepareStatement(printCreateDataset(dataverse, dataset));
+ res = s2.executeUpdate();
+ Assert.assertEquals(0, res);
+
+ // DML -> ok
+ PreparedStatement s3 = c.prepareStatement(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ res = s3.executeUpdate();
+ // currently, DML statements always return update count = 1
+ Assert.assertEquals(1, res);
+
+ // Query -> error
+ try {
+ PreparedStatement s4 = c.prepareStatement(Q1);
+ s4.executeUpdate();
+ Assert.fail("Query did not fail in executeUpdate()");
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains("Invalid statement category"));
+ }
+
+ // Cleanup
+ PreparedStatement s5 = c.prepareStatement(printDropDataverse(dataverse));
+ s5.executeUpdate();
+ }
+ }
+
+ public void testExecute() throws SQLException {
+ try (Connection c = createConnection()) {
+ // Query -> ok
+ PreparedStatement s1 = c.prepareStatement(Q1);
+ boolean res = s1.execute();
+ Assert.assertTrue(res);
+ Assert.assertEquals(-1, s1.getUpdateCount());
+ try (ResultSet rs = s1.getResultSet()) {
+ Assert.assertTrue(rs.next());
+ Assert.assertEquals(1, rs.getMetaData().getColumnCount());
+ Assert.assertEquals(V1, rs.getInt(1));
+ Assert.assertFalse(rs.next());
+ Assert.assertFalse(rs.isClosed());
+ }
+
+ // DDL -> ok
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testExecute");
+ PreparedStatement s2 = c.prepareStatement(printCreateDataverse(dataverse));
+ res = s2.execute();
+ Assert.assertFalse(res);
+ Assert.assertEquals(0, s2.getUpdateCount());
+ String dataset = "ds1";
+ PreparedStatement s3 = c.prepareStatement(printCreateDataset(dataverse, dataset));
+ res = s3.execute();
+ Assert.assertFalse(res);
+
+ // DML -> ok
+ PreparedStatement s4 = c.prepareStatement(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ res = s4.execute();
+ Assert.assertFalse(res);
+ // currently, DML statements always return update count = 1
+ Assert.assertEquals(1, s4.getUpdateCount());
+
+ // Cleanup
+ PreparedStatement s5 = c.prepareStatement(printDropDataverse(dataverse));
+ s5.execute();
+ }
+ }
+
+ public void testGetResultSet() throws SQLException {
+ try (Connection c = createConnection()) {
+ // Query
+ PreparedStatement s1 = c.prepareStatement(Q1);
+ boolean res = s1.execute();
+ Assert.assertTrue(res);
+ ResultSet rs = s1.getResultSet();
+ Assert.assertFalse(rs.isClosed());
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(s1.getMoreResults()); // closes current ResultSet
+ Assert.assertTrue(rs.isClosed());
+
+ PreparedStatement s2 = c.prepareStatement(Q1);
+ res = s2.execute();
+ Assert.assertTrue(res);
+ rs = s2.getResultSet();
+ Assert.assertFalse(rs.isClosed());
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(s2.getMoreResults(Statement.KEEP_CURRENT_RESULT));
+ Assert.assertFalse(rs.isClosed());
+ rs.close();
+
+ // DDL
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testGetResultSet");
+ PreparedStatement s3 = c.prepareStatement(printCreateDataverse(dataverse));
+ res = s3.execute();
+ Assert.assertFalse(res);
+ Assert.assertNull(s3.getResultSet());
+ Assert.assertFalse(s3.getMoreResults());
+
+ String dataset = "ds1";
+ PreparedStatement s4 = c.prepareStatement(printCreateDataset(dataverse, dataset));
+ res = s4.execute();
+ Assert.assertFalse(res);
+
+ // DML
+ PreparedStatement s5 = c.prepareStatement(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ res = s5.execute();
+ Assert.assertFalse(res);
+ Assert.assertNull(s5.getResultSet());
+ Assert.assertFalse(s5.getMoreResults());
+ }
+ }
+
+ public void testMaxRows() throws SQLException {
+ try (Connection c = createConnection()) {
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testMaxRows");
+ String dataset = "ds1";
+ String field = "x";
+ PreparedStatement s1 = c.prepareStatement(printCreateDataverse(dataverse));
+ s1.execute();
+ PreparedStatement s2 = c.prepareStatement(printCreateDataset(dataverse, dataset));
+ s2.execute();
+ PreparedStatement s3 = c.prepareStatement(printInsert(dataverse, dataset, dataGen(field, 1, 2, 3)));
+ s3.execute();
+
+ PreparedStatement s4 = c.prepareStatement(String.format("select %s from %s.%s", field,
+ printDataverseName(dataverse), printIdentifier(dataset)));
+ s4.setMaxRows(2);
+ Assert.assertEquals(2, s4.getMaxRows());
+ try (ResultSet rs = s4.executeQuery()) {
+ Assert.assertTrue(rs.next());
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(rs.next());
+ }
+ }
+ }
+
+ public void testWarnings() throws SQLException {
+ try (Connection c = createConnection();
+ PreparedStatement s = c.prepareStatement("select double('x'), bigint('y')"); // --> NULL with warning
+ ResultSet rs = s.executeQuery()) {
+ Assert.assertTrue(rs.next());
+ rs.getDouble(1);
+ Assert.assertTrue(rs.wasNull());
+ rs.getLong(2);
+ Assert.assertTrue(rs.wasNull());
+
+ SQLWarning w = s.getWarnings();
+ Assert.assertNotNull(w);
+ String msg = w.getMessage();
+ Assert.assertTrue(msg, msg.contains(ErrorCode.INVALID_FORMAT.errorCode()));
+
+ SQLWarning w2 = w.getNextWarning();
+ Assert.assertNotNull(w2);
+ String msg2 = w.getMessage();
+ Assert.assertTrue(msg2, msg2.contains(ErrorCode.INVALID_FORMAT.errorCode()));
+
+ Assert.assertNull(w2.getNextWarning());
+ s.clearWarnings();
+ Assert.assertNull(s.getWarnings());
+ }
+ }
+
+ public void testWrapper() throws SQLException {
+ try (Connection c = createConnection(); PreparedStatement s = c.prepareStatement(Q1)) {
+ Assert.assertTrue(s.isWrapperFor(ADBPreparedStatement.class));
+ Assert.assertNotNull(s.unwrap(ADBPreparedStatement.class));
+ }
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcResultSetTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcResultSetTester.java
new file mode 100644
index 0000000..9b1acd1
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcResultSetTester.java
@@ -0,0 +1,672 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.Date;
+import java.sql.JDBCType;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Period;
+import java.time.ZoneOffset;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.asterix.jdbc.core.ADBResultSet;
+import org.apache.commons.io.IOUtils;
+import org.apache.hyracks.algebricks.common.utils.Pair;
+import org.junit.Assert;
+
+abstract class JdbcResultSetTester extends JdbcTester {
+
+ protected abstract CloseablePair<Statement, ResultSet> executeQuery(Connection c, String query) throws SQLException;
+
+ public void testLifecycle() throws SQLException {
+ try (Connection c = createConnection()) {
+ Pair<Statement, ResultSet> p = executeQuery(c, Q2);
+ Statement s = p.getFirst();
+ ResultSet rs = p.getSecond();
+ Assert.assertFalse(rs.isClosed());
+ Assert.assertSame(s, rs.getStatement());
+ rs.close();
+ Assert.assertTrue(rs.isClosed());
+
+ // ok to call close() on a closed result set
+ rs.close();
+ Assert.assertTrue(rs.isClosed());
+ }
+ }
+
+ // test that Statement.close() closes its ResultSet
+ public void testAutoCloseOnStatementClose() throws SQLException {
+ try (Connection c = createConnection()) {
+ Pair<Statement, ResultSet> p = executeQuery(c, Q2);
+ Statement s = p.getFirst();
+ ResultSet rs = p.getSecond();
+ Assert.assertFalse(rs.isClosed());
+ s.close();
+ Assert.assertTrue(rs.isClosed());
+ }
+ }
+
+ // test that Connection.close() closes all Statements and their ResultSets
+ public void testAutoCloseOnConnectionClose() throws SQLException {
+ Connection c = createConnection();
+ Pair<Statement, ResultSet> p1 = executeQuery(c, Q2);
+ Statement s1 = p1.getFirst();
+ ResultSet rs1 = p1.getSecond();
+ Assert.assertFalse(rs1.isClosed());
+ Pair<Statement, ResultSet> p2 = executeQuery(c, Q2);
+ Statement s2 = p2.getFirst();
+ ResultSet rs2 = p2.getSecond();
+ Assert.assertFalse(rs2.isClosed());
+ c.close();
+ Assert.assertTrue(rs1.isClosed());
+ Assert.assertTrue(s1.isClosed());
+ Assert.assertTrue(rs2.isClosed());
+ Assert.assertTrue(s2.isClosed());
+ }
+
+ public void testNavigation() throws SQLException {
+ try (Connection c = createConnection()) {
+ Pair<Statement, ResultSet> p = executeQuery(c, Q2);
+ ResultSet rs = p.getSecond();
+ Assert.assertEquals(ResultSet.TYPE_FORWARD_ONLY, rs.getType());
+ Assert.assertEquals(ResultSet.FETCH_FORWARD, rs.getFetchDirection());
+ Assert.assertTrue(rs.isBeforeFirst());
+ Assert.assertFalse(rs.isFirst());
+ // Assert.assertFalse(rs.isLast()); -- Not supported
+ Assert.assertFalse(rs.isAfterLast());
+ Assert.assertEquals(0, rs.getRow());
+
+ for (int r = 1; r <= 9; r++) {
+ boolean next = rs.next();
+ Assert.assertTrue(next);
+ Assert.assertFalse(rs.isBeforeFirst());
+ Assert.assertEquals(r == 1, rs.isFirst());
+ Assert.assertFalse(rs.isAfterLast());
+ Assert.assertEquals(r, rs.getRow());
+ }
+
+ boolean next = rs.next();
+ Assert.assertFalse(next);
+ Assert.assertFalse(rs.isBeforeFirst());
+ Assert.assertFalse(rs.isFirst());
+ Assert.assertTrue(rs.isAfterLast());
+ Assert.assertEquals(0, rs.getRow());
+
+ next = rs.next();
+ Assert.assertFalse(next);
+
+ rs.close();
+ assertErrorOnClosed(rs, ResultSet::isBeforeFirst, "isBeforeFirst");
+ assertErrorOnClosed(rs, ResultSet::isFirst, "isFirst");
+ assertErrorOnClosed(rs, ResultSet::isAfterLast, "isAfterLast");
+ assertErrorOnClosed(rs, ResultSet::getRow, "getRow");
+ assertErrorOnClosed(rs, ResultSet::next, "next");
+ }
+ }
+
+ public void testColumReadBasic() throws SQLException {
+ String qProject = IntStream.range(1, 10).mapToObj(i -> String.format("r*10+%d as c%d", i, i))
+ .collect(Collectors.joining(","));
+ String q = String.format("select %s from range(1, 2) r order by r", qProject);
+ try (Connection c = createConnection(); CloseablePair<Statement, ResultSet> p = executeQuery(c, q)) {
+ ResultSet rs = p.getSecond();
+ for (int r = 1; rs.next(); r++) {
+ for (int col = 1; col < 10; col++) {
+ int expected = r * 10 + col;
+ Assert.assertEquals(expected, rs.getInt(col));
+ Assert.assertEquals(expected, rs.getInt("c" + col));
+ Assert.assertEquals(expected, rs.getInt(rs.findColumn("c" + col)));
+ }
+ }
+ }
+ }
+
+ public void testColumnRead() throws SQLException, IOException {
+ try (Connection c = createConnection(); CloseablePair<Statement, ResultSet> p = executeQuery(c, Q3)) {
+ ResultSet rs = p.getSecond();
+ for (int r = -1; rs.next(); r++) {
+ int v = r * 2;
+ verifyReadColumnOfNumericType(rs, 1, Q3_COLUMNS[0], v == 0 ? null : (byte) v);
+ verifyReadColumnOfNumericType(rs, 2, Q3_COLUMNS[1], v == 0 ? null : (short) v);
+ verifyReadColumnOfNumericType(rs, 3, Q3_COLUMNS[2], v == 0 ? null : v);
+ verifyReadColumnOfNumericType(rs, 4, Q3_COLUMNS[3], v == 0 ? null : (long) v);
+ verifyReadColumnOfNumericType(rs, 5, Q3_COLUMNS[4], v == 0 ? null : (float) v);
+ verifyReadColumnOfNumericType(rs, 6, Q3_COLUMNS[5], v == 0 ? null : (double) v);
+ verifyReadColumnOfStringType(rs, 7, Q3_COLUMNS[6], v == 0 ? null : "a" + v);
+ verifyReadColumnOfBooleanType(rs, 8, Q3_COLUMNS[7], v == 0 ? null : v > 0);
+ verifyReadColumnOfDateType(rs, 9, Q3_COLUMNS[8], v == 0 ? null : LocalDate.ofEpochDay(v));
+ verifyReadColumnOfTimeType(rs, 10, Q3_COLUMNS[9], v == 0 ? null : LocalTime.ofSecondOfDay(v + 3));
+ verifyReadColumnOfDatetimeType(rs, 11, Q3_COLUMNS[10],
+ v == 0 ? null : LocalDateTime.ofEpochSecond(v, 0, ZoneOffset.UTC));
+ verifyReadColumnOfYearMonthDurationType(rs, 12, Q3_COLUMNS[11], v == 0 ? null : Period.ofMonths(v));
+ verifyReadColumnOfDayTimeDurationType(rs, 13, Q3_COLUMNS[12], v == 0 ? null : Duration.ofMillis(v));
+ verifyReadColumnOfDurationType(rs, 14, Q3_COLUMNS[13], v == 0 ? null : Period.ofMonths(v + 3),
+ v == 0 ? null : Duration.ofMillis(TimeUnit.SECONDS.toMillis(v + 3)));
+ verifyReadColumnOfUuidType(rs, 15, Q3_COLUMNS[14],
+ v == 0 ? null : UUID.fromString("5c848e5c-6b6a-498f-8452-8847a295742" + (v + 3)));
+ }
+ }
+ }
+
+ public void testColumnMetadata() throws SQLException {
+ try (Connection c = createConnection(); CloseablePair<Statement, ResultSet> p = executeQuery(c, Q3)) {
+ ResultSet rs = p.getSecond();
+ int expectedColumnCount = Q3_COLUMNS.length;
+ ResultSetMetaData rsmd = rs.getMetaData();
+ Assert.assertEquals(expectedColumnCount, rsmd.getColumnCount());
+ for (int i = 1; i <= expectedColumnCount; i++) {
+ String expectedColumnName = Q3_COLUMNS[i - 1];
+ JDBCType expectedColumnTypeJdbc = Q3_COLUMN_TYPES_JDBC[i - 1];
+ String expectedColumnTypeAdb = Q3_COLUMN_TYPES_ADB[i - 1];
+ Class<?> expectedColumnTypeJava = Q3_COLUMN_TYPES_JAVA[i - 1];
+ Assert.assertEquals(i, rs.findColumn(expectedColumnName));
+ Assert.assertEquals(expectedColumnName, rsmd.getColumnName(i));
+ Assert.assertEquals(expectedColumnTypeJdbc.getVendorTypeNumber().intValue(), rsmd.getColumnType(i));
+ Assert.assertEquals(expectedColumnTypeAdb, rsmd.getColumnTypeName(i));
+ Assert.assertEquals(expectedColumnTypeJava.getName(), rsmd.getColumnClassName(i));
+ }
+ }
+ }
+
+ private void verifyGetColumnAsByte(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ byte expectedByte = expectedValue == null ? 0 : expectedValue.byteValue();
+ byte v1 = rs.getByte(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedByte, v1);
+ byte v2 = rs.getByte(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedByte, v2);
+ }
+
+ private void verifyGetColumnAsShort(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ short expectedShort = expectedValue == null ? 0 : expectedValue.shortValue();
+ short v1 = rs.getShort(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedShort, v1);
+ short v2 = rs.getShort(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedShort, v2);
+ }
+
+ private void verifyGetColumnAsInt(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ int expectedInt = expectedValue == null ? 0 : expectedValue.intValue();
+ int v1 = rs.getInt(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedInt, v1);
+ int v2 = rs.getInt(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedInt, v2);
+ }
+
+ private void verifyGetColumnAsLong(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ long expectedLong = expectedValue == null ? 0 : expectedValue.longValue();
+ long v1 = rs.getLong(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedLong, v1);
+ long v2 = rs.getLong(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedLong, v2);
+ }
+
+ private void verifyGetColumnAsFloat(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ float expectedFloat = expectedValue == null ? 0f : expectedValue.floatValue();
+ float v1 = rs.getFloat(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedFloat, v1, 0);
+ float v2 = rs.getFloat(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedFloat, v2, 0);
+ }
+
+ private void verifyGetColumnAsDouble(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ double expectedDouble = expectedValue == null ? 0d : expectedValue.doubleValue();
+ double v1 = rs.getDouble(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedDouble, v1, 0);
+ double v2 = rs.getDouble(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedDouble, v2, 0);
+ }
+
+ private void verifyGetColumnAsDecimal(ResultSet rs, int columnIndex, String columnName, Number expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ BigDecimal expectedDecimal = expectedValue == null ? null : new BigDecimal(expectedValue.toString());
+ int expectedDecimalScale = expectedValue == null ? 0 : expectedDecimal.scale();
+ BigDecimal v1 = rs.getBigDecimal(columnIndex, expectedDecimalScale);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedDecimal, v1);
+ BigDecimal v2 = rs.getBigDecimal(columnName, expectedDecimalScale);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedDecimal, v2);
+ }
+
+ private void verifyGetColumnAsBoolean(ResultSet rs, int columnIndex, String columnName, Boolean expectedValue)
+ throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ boolean expectedBoolean = expectedNull ? false : expectedValue;
+ boolean v1 = rs.getBoolean(columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedBoolean, v1);
+ boolean v2 = rs.getBoolean(columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ Assert.assertEquals(expectedBoolean, v2);
+ }
+
+ private void verifyGetColumnAsString(ResultSet rs, int columnIndex, String columnName, String expectedValue)
+ throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, ResultSet::getString, ResultSet::getString);
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, ResultSet::getNString,
+ ResultSet::getNString);
+ }
+
+ private void verifyGetColumnAsObject(ResultSet rs, int columnIndex, String columnName, Object expectedValue)
+ throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, ResultSet::getObject, ResultSet::getObject);
+ }
+
+ private <V> void verifyGetColumnAsObject(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ Function<Object, V> valueConverter) throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, valueConverter, ResultSet::getObject,
+ ResultSet::getObject);
+ }
+
+ private <V> void verifyGetColumnAsObject(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ Class<V> type) throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, ResultSet::getObject, ResultSet::getObject,
+ type);
+ }
+
+ private <V, T> void verifyGetColumnAsObject(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ Class<T> type, Function<T, V> valueConverter) throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, valueConverter, ResultSet::getObject,
+ ResultSet::getObject, type);
+ }
+
+ private void verifyGetColumnAsSqlDate(ResultSet rs, int columnIndex, String columnName, LocalDate expectedValue)
+ throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, java.sql.Date::toLocalDate,
+ ResultSet::getDate, ResultSet::getDate);
+ }
+
+ private void verifyGetColumnAsSqlTime(ResultSet rs, int columnIndex, String columnName, LocalTime expectedValue)
+ throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, java.sql.Time::toLocalTime,
+ ResultSet::getTime, ResultSet::getTime);
+ }
+
+ private void verifyGetColumnAsSqlTimestamp(ResultSet rs, int columnIndex, String columnName,
+ LocalDateTime expectedValue) throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, java.sql.Timestamp::toLocalDateTime,
+ ResultSet::getTimestamp, ResultSet::getTimestamp);
+ }
+
+ private <V> void verifyGetColumnGeneric(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ GetColumnByIndex<V> columnByIndexAccessor, GetColumnByName<V> columnByNameAccessor) throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, Function.identity(), columnByIndexAccessor,
+ columnByNameAccessor);
+ }
+
+ private <V, T> void verifyGetColumnGeneric(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ Function<T, V> valueConverter, GetColumnByIndex<T> columnByIndexAccessor,
+ GetColumnByName<T> columnByNameAccessor) throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ T v1 = columnByIndexAccessor.get(rs, columnIndex);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(v1);
+ } else {
+ Assert.assertEquals(expectedValue, valueConverter.apply(v1));
+ }
+ T v2 = columnByNameAccessor.get(rs, columnName);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(v2);
+ } else {
+ Assert.assertEquals(expectedValue, valueConverter.apply(v2));
+ }
+ }
+
+ private <V, P> void verifyGetColumnGeneric(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ GetColumnByIndexWithParam<V, P> columnByIndexAccessor, GetColumnByNameWithParam<V, P> columnByNameAccessor,
+ P accessorParamValue) throws SQLException {
+ verifyGetColumnGeneric(rs, columnIndex, columnName, expectedValue, Function.identity(), columnByIndexAccessor,
+ columnByNameAccessor, accessorParamValue);
+ }
+
+ private <V, T, P> void verifyGetColumnGeneric(ResultSet rs, int columnIndex, String columnName, V expectedValue,
+ Function<T, V> valueConverter, GetColumnByIndexWithParam<T, P> columnByIndexAccessor,
+ GetColumnByNameWithParam<T, P> columnByNameAccessor, P accessorParamValue) throws SQLException {
+ boolean expectedNull = expectedValue == null;
+ T v1 = columnByIndexAccessor.get(rs, columnIndex, accessorParamValue);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(v1);
+ } else {
+ Assert.assertEquals(expectedValue, valueConverter.apply(v1));
+ }
+ T v2 = columnByNameAccessor.get(rs, columnName, accessorParamValue);
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(v2);
+ } else {
+ Assert.assertEquals(expectedValue, valueConverter.apply(v2));
+ }
+ }
+
+ private void verifyGetColumnAsCharacterStream(ResultSet rs, int columnIndex, String columnName,
+ char[] expectedValue, GetColumnByIndex<Reader> columnByIndexAccessor,
+ GetColumnByName<Reader> columnByNameAccessor) throws SQLException, IOException {
+ boolean expectedNull = expectedValue == null;
+ try (Reader s1 = columnByIndexAccessor.get(rs, columnIndex)) {
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(s1);
+ } else {
+ Assert.assertArrayEquals(expectedValue, IOUtils.toCharArray(s1));
+ }
+ }
+ try (Reader s2 = columnByNameAccessor.get(rs, columnName)) {
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(s2);
+ } else {
+ Assert.assertArrayEquals(expectedValue, IOUtils.toCharArray(s2));
+ }
+ }
+ }
+
+ private void verifyGetColumnAsBinaryStream(ResultSet rs, int columnIndex, String columnName, byte[] expectedValue,
+ GetColumnByIndex<InputStream> columnByIndexAccessor, GetColumnByName<InputStream> columnByNameAccessor)
+ throws SQLException, IOException {
+ boolean expectedNull = expectedValue == null;
+ try (InputStream s1 = columnByIndexAccessor.get(rs, columnIndex)) {
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(s1);
+ } else {
+ Assert.assertArrayEquals(expectedValue, IOUtils.toByteArray(s1));
+ }
+ }
+ try (InputStream s2 = columnByNameAccessor.get(rs, columnName)) {
+ Assert.assertEquals(expectedNull, rs.wasNull());
+ if (expectedNull) {
+ Assert.assertNull(s2);
+ } else {
+ Assert.assertArrayEquals(expectedValue, IOUtils.toByteArray(s2));
+ }
+ }
+ }
+
+ private void verifyReadColumnOfNumericType(ResultSet rs, int columnIndex, String columnName,
+ Number expectedNumericValue) throws SQLException {
+ String expectedStringValue = expectedNumericValue == null ? null : expectedNumericValue.toString();
+ Byte expectedByteValue = expectedNumericValue == null ? null : expectedNumericValue.byteValue();
+ Short expectedShortValue = expectedNumericValue == null ? null : expectedNumericValue.shortValue();
+ Integer expectedIntValue = expectedNumericValue == null ? null : expectedNumericValue.intValue();
+ Long expectedLongValue = expectedNumericValue == null ? null : expectedNumericValue.longValue();
+ Float expectedFloatValue = expectedNumericValue == null ? null : expectedNumericValue.floatValue();
+ Double expectedDoubleValue = expectedNumericValue == null ? null : expectedNumericValue.doubleValue();
+ BigDecimal expectedDecimalValue =
+ expectedNumericValue == null ? null : new BigDecimal(expectedStringValue.replace(".0", ""));
+ Boolean expectedBooleanValue = toBoolean(expectedNumericValue);
+ verifyGetColumnAsByte(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsShort(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsInt(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsLong(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsFloat(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsDouble(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsDecimal(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsBoolean(rs, columnIndex, columnName, expectedBooleanValue);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedNumericValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedByteValue, Byte.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedShortValue, Short.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedIntValue, Integer.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedLongValue, Long.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedFloatValue, Float.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDoubleValue, Double.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDecimalValue, BigDecimal.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedBooleanValue, Boolean.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfStringType(ResultSet rs, int columnIndex, String columnName,
+ String expectedStringValue) throws SQLException, IOException {
+ char[] expectedCharArray = expectedStringValue == null ? null : expectedStringValue.toCharArray();
+ byte[] expectedUtf8Array =
+ expectedStringValue == null ? null : expectedStringValue.getBytes(StandardCharsets.UTF_8);
+ byte[] expectedUtf16Array =
+ expectedStringValue == null ? null : expectedStringValue.getBytes(StandardCharsets.UTF_16);
+ byte[] expectedAsciiArray =
+ expectedStringValue == null ? null : expectedStringValue.getBytes(StandardCharsets.US_ASCII);
+ verifyGetColumnAsCharacterStream(rs, columnIndex, columnName, expectedCharArray, ResultSet::getCharacterStream,
+ ResultSet::getCharacterStream);
+ verifyGetColumnAsCharacterStream(rs, columnIndex, columnName, expectedCharArray, ResultSet::getNCharacterStream,
+ ResultSet::getNCharacterStream);
+ verifyGetColumnAsBinaryStream(rs, columnIndex, columnName, expectedUtf8Array, ResultSet::getBinaryStream,
+ ResultSet::getBinaryStream);
+ verifyGetColumnAsBinaryStream(rs, columnIndex, columnName, expectedUtf16Array, ResultSet::getUnicodeStream,
+ ResultSet::getUnicodeStream);
+ verifyGetColumnAsBinaryStream(rs, columnIndex, columnName, expectedAsciiArray, ResultSet::getAsciiStream,
+ ResultSet::getAsciiStream);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfBooleanType(ResultSet rs, int columnIndex, String columnName,
+ Boolean expectedBooleanValue) throws SQLException {
+ Number expectedNumberValue = expectedBooleanValue == null ? null : expectedBooleanValue ? 1 : 0;
+ String expectedStringValue = expectedBooleanValue == null ? null : Boolean.toString(expectedBooleanValue);
+ verifyGetColumnAsBoolean(rs, columnIndex, columnName, expectedBooleanValue);
+ verifyGetColumnAsByte(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsShort(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsInt(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsLong(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsFloat(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsDouble(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsDecimal(rs, columnIndex, columnName, expectedNumberValue);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedBooleanValue);
+ }
+
+ private void verifyReadColumnOfDateType(ResultSet rs, int columnIndex, String columnName,
+ LocalDate expectedDateValue) throws SQLException {
+ LocalDateTime expectedDateTimeValue = expectedDateValue == null ? null : expectedDateValue.atStartOfDay();
+ String expectedStringValue = expectedDateValue == null ? null : expectedDateValue.toString();
+ verifyGetColumnAsSqlDate(rs, columnIndex, columnName, expectedDateValue);
+ verifyGetColumnAsSqlTimestamp(rs, columnIndex, columnName, expectedDateTimeValue);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateValue, v -> ((java.sql.Date) v).toLocalDate());
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateValue, java.sql.Date.class, Date::toLocalDate);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateTimeValue, java.sql.Timestamp.class,
+ Timestamp::toLocalDateTime);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateValue, LocalDate.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateTimeValue, LocalDateTime.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfTimeType(ResultSet rs, int columnIndex, String columnName,
+ LocalTime expectedTimeValue) throws SQLException {
+ String expectedStringValue = expectedTimeValue == null ? null : expectedTimeValue.toString();
+ verifyGetColumnAsSqlTime(rs, columnIndex, columnName, expectedTimeValue);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedTimeValue, v -> ((java.sql.Time) v).toLocalTime());
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedTimeValue, java.sql.Time.class,
+ java.sql.Time::toLocalTime);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedTimeValue, LocalTime.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfDatetimeType(ResultSet rs, int columnIndex, String columnName,
+ LocalDateTime expectedDateTimeValue) throws SQLException {
+ LocalDate expectedDateValue = expectedDateTimeValue == null ? null : expectedDateTimeValue.toLocalDate();
+ LocalTime expectedTimeValue = expectedDateTimeValue == null ? null : expectedDateTimeValue.toLocalTime();
+ String expectedStringValue = expectedDateTimeValue == null ? null : expectedDateTimeValue.toString();
+ verifyGetColumnAsSqlTimestamp(rs, columnIndex, columnName, expectedDateTimeValue);
+ verifyGetColumnAsSqlDate(rs, columnIndex, columnName, expectedDateValue);
+ verifyGetColumnAsSqlTime(rs, columnIndex, columnName, expectedTimeValue);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateTimeValue,
+ v -> ((java.sql.Timestamp) v).toLocalDateTime());
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateTimeValue, java.sql.Timestamp.class,
+ java.sql.Timestamp::toLocalDateTime);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateValue, java.sql.Date.class,
+ java.sql.Date::toLocalDate);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedTimeValue, java.sql.Time.class,
+ java.sql.Time::toLocalTime);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateTimeValue, LocalDateTime.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDateValue, LocalDate.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedTimeValue, LocalTime.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfYearMonthDurationType(ResultSet rs, int columnIndex, String columnName,
+ Period expectedPeriodValue) throws SQLException {
+ String expectedStringValue = expectedPeriodValue == null ? null : expectedPeriodValue.toString();
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedPeriodValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedPeriodValue, Period.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfDayTimeDurationType(ResultSet rs, int columnIndex, String columnName,
+ Duration expectedDurationValue) throws SQLException {
+ String expectedStringValue = expectedDurationValue == null ? null : expectedDurationValue.toString();
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDurationValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDurationValue, Duration.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue, String.class);
+ }
+
+ private void verifyReadColumnOfDurationType(ResultSet rs, int columnIndex, String columnName,
+ Period expectedPeriodValue, Duration expectedDurationValue) throws SQLException {
+ String expectedStringValue = expectedPeriodValue == null && expectedDurationValue == null ? null
+ : expectedPeriodValue + String.valueOf(expectedDurationValue).substring(1);
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedPeriodValue, Period.class);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedDurationValue, Duration.class);
+ }
+
+ private void verifyReadColumnOfUuidType(ResultSet rs, int columnIndex, String columnName, UUID expectedUuidValue)
+ throws SQLException {
+ String expectedStringValue = expectedUuidValue == null ? null : expectedUuidValue.toString();
+ verifyGetColumnAsString(rs, columnIndex, columnName, expectedStringValue);
+ verifyGetColumnAsObject(rs, columnIndex, columnName, expectedUuidValue);
+ }
+
+ public void testWrapper() throws SQLException {
+ try (Connection c = createConnection(); CloseablePair<Statement, ResultSet> p = executeQuery(c, Q2)) {
+ ResultSet rs = p.getSecond();
+ Assert.assertTrue(rs.isWrapperFor(ADBResultSet.class));
+ Assert.assertNotNull(rs.unwrap(ADBResultSet.class));
+ }
+ }
+
+ interface GetColumnByIndex<R> {
+ R get(ResultSet rs, int columnIndex) throws SQLException;
+ }
+
+ interface GetColumnByIndexWithParam<R, T> {
+ R get(ResultSet rs, int columnIndex, T param) throws SQLException;
+ }
+
+ interface GetColumnByName<R> {
+ R get(ResultSet rs, String columnName) throws SQLException;
+ }
+
+ interface GetColumnByNameWithParam<R, T> {
+ R get(ResultSet rs, String columnName, T param) throws SQLException;
+ }
+
+ static Boolean toBoolean(Number v) {
+ if (v == null) {
+ return null;
+ }
+ switch (v.toString()) {
+ case "0":
+ case "0.0":
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ static class JdbcPreparedStatementResultSetTester extends JdbcResultSetTester {
+ @Override
+ protected CloseablePair<Statement, ResultSet> executeQuery(Connection c, String query) throws SQLException {
+ PreparedStatement s = c.prepareStatement(query);
+ try {
+ ResultSet rs = s.executeQuery();
+ return new CloseablePair<>(s, rs);
+ } catch (SQLException e) {
+ s.close();
+ throw e;
+ }
+ }
+ }
+
+ static class JdbcStatementResultSetTester extends JdbcResultSetTester {
+ @Override
+ protected CloseablePair<Statement, ResultSet> executeQuery(Connection c, String query) throws SQLException {
+ Statement s = c.createStatement();
+ try {
+ ResultSet rs = s.executeQuery(query);
+ return new CloseablePair<>(s, rs);
+ } catch (SQLException e) {
+ s.close();
+ throw e;
+ }
+ }
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcStatementParameterTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcStatementParameterTester.java
new file mode 100644
index 0000000..b87527e
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcStatementParameterTester.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.JDBCType;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Period;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.BiPredicate;
+
+import org.junit.Assert;
+
+class JdbcStatementParameterTester extends JdbcTester {
+
+ public void testParameterBinding() throws SQLException {
+ String[] sqlppValues = new String[] { "int8('10')", "int16('20')", "int32('30')", "int64('40')", "float('1.5')",
+ "double('2.25')", "true", "'abc'", "date('2000-10-20')", "time('02:03:04')",
+ "datetime('2000-10-20T02:03:04')", "get_year_month_duration(duration_from_months(2))",
+ "get_day_time_duration(duration_from_ms(1234))", "uuid('5c848e5c-6b6a-498f-8452-8847a2957421')" };
+ try (Connection c = createConnection()) {
+ Byte i1 = (byte) 10;
+ verifyParameterBinding(c, sqlppValues, i1, PreparedStatement::setByte, ResultSet::getByte);
+ verifyParameterBinding(c, sqlppValues, i1, PreparedStatement::setObject, ResultSet::getByte);
+
+ Short i2 = (short) 20;
+ verifyParameterBinding(c, sqlppValues, i2, PreparedStatement::setShort, ResultSet::getShort);
+ verifyParameterBinding(c, sqlppValues, i2, PreparedStatement::setObject, ResultSet::getShort);
+
+ Integer i4 = 30;
+ verifyParameterBinding(c, sqlppValues, i4, PreparedStatement::setInt, ResultSet::getInt);
+ verifyParameterBinding(c, sqlppValues, i4, PreparedStatement::setObject, ResultSet::getInt);
+
+ Long i8 = 40L;
+ verifyParameterBinding(c, sqlppValues, i8, PreparedStatement::setLong, ResultSet::getLong);
+ verifyParameterBinding(c, sqlppValues, i8, PreparedStatement::setObject, ResultSet::getLong);
+
+ Float r4 = 1.5f;
+ verifyParameterBinding(c, sqlppValues, r4, PreparedStatement::setFloat, ResultSet::getFloat);
+ verifyParameterBinding(c, sqlppValues, r4, PreparedStatement::setObject, ResultSet::getFloat);
+
+ Double r8 = 2.25;
+ verifyParameterBinding(c, sqlppValues, r8, PreparedStatement::setDouble, ResultSet::getDouble);
+ verifyParameterBinding(c, sqlppValues, r8, PreparedStatement::setObject, ResultSet::getDouble);
+
+ BigDecimal dec = new BigDecimal("2.25");
+ verifyParameterBinding(c, sqlppValues, dec, PreparedStatement::setBigDecimal, ResultSet::getBigDecimal);
+ verifyParameterBinding(c, sqlppValues, dec, PreparedStatement::setObject, ResultSet::getBigDecimal);
+
+ Boolean b = true;
+ verifyParameterBinding(c, sqlppValues, b, PreparedStatement::setBoolean, ResultSet::getBoolean);
+ verifyParameterBinding(c, sqlppValues, b, PreparedStatement::setObject, ResultSet::getBoolean);
+
+ String s = "abc";
+ verifyParameterBinding(c, sqlppValues, s, PreparedStatement::setString, ResultSet::getString);
+ verifyParameterBinding(c, sqlppValues, s, PreparedStatement::setObject, ResultSet::getString);
+ verifyParameterBinding(c, sqlppValues, s, PreparedStatement::setNString, ResultSet::getString);
+
+ LocalDate date = LocalDate.of(2000, 10, 20);
+ verifyParameterBinding(c, sqlppValues, java.sql.Date.valueOf(date), PreparedStatement::setDate,
+ ResultSet::getDate);
+ verifyParameterBinding(c, sqlppValues, java.sql.Date.valueOf(date), PreparedStatement::setObject,
+ ResultSet::getDate);
+ verifyParameterBinding(c, sqlppValues, date, PreparedStatement::setObject,
+ (rs, i) -> rs.getObject(i, LocalDate.class));
+
+ LocalTime time = LocalTime.of(2, 3, 4);
+ verifyParameterBinding(c, sqlppValues, java.sql.Time.valueOf(time), PreparedStatement::setTime,
+ ResultSet::getTime, JdbcStatementParameterTester::sqlTimeEquals);
+ verifyParameterBinding(c, sqlppValues, java.sql.Time.valueOf(time), PreparedStatement::setObject,
+ ResultSet::getTime, JdbcStatementParameterTester::sqlTimeEquals);
+ verifyParameterBinding(c, sqlppValues, time, PreparedStatement::setObject,
+ (rs, i) -> rs.getObject(i, LocalTime.class));
+
+ LocalDateTime datetime = LocalDateTime.of(date, time);
+ verifyParameterBinding(c, sqlppValues, java.sql.Timestamp.valueOf(datetime),
+ PreparedStatement::setTimestamp, ResultSet::getTimestamp);
+ verifyParameterBinding(c, sqlppValues, java.sql.Timestamp.valueOf(datetime), PreparedStatement::setObject,
+ ResultSet::getTimestamp);
+ verifyParameterBinding(c, sqlppValues, datetime, PreparedStatement::setObject,
+ (rs, i) -> rs.getObject(i, LocalDateTime.class));
+
+ Period ymDuration = Period.ofMonths(2);
+ verifyParameterBinding(c, sqlppValues, ymDuration, PreparedStatement::setObject,
+ (rs, i) -> rs.getObject(i, Period.class));
+
+ Duration dtDuration = Duration.ofMillis(1234);
+ verifyParameterBinding(c, sqlppValues, dtDuration, PreparedStatement::setObject,
+ (rs, i) -> rs.getObject(i, Duration.class));
+
+ UUID uuid = UUID.fromString("5c848e5c-6b6a-498f-8452-8847a2957421");
+ verifyParameterBinding(c, sqlppValues, uuid, PreparedStatement::setObject, ResultSet::getObject);
+ }
+ }
+
+ private <T> void verifyParameterBinding(Connection c, String[] sqlppValues, T value, SetParameterByIndex<T> setter,
+ JdbcResultSetTester.GetColumnByIndex<T> getter) throws SQLException {
+ verifyParameterBinding(c, sqlppValues, value, setter, getter, Objects::equals);
+ }
+
+ private <T> void verifyParameterBinding(Connection c, String[] sqlppValues, T value, SetParameterByIndex<T> setter,
+ JdbcResultSetTester.GetColumnByIndex<T> getter, BiPredicate<T, T> cmp) throws SQLException {
+ try (PreparedStatement s =
+ c.prepareStatement(String.format("select ? from [%s] v where v = ?", String.join(",", sqlppValues)))) {
+ for (int i = 1; i <= 2; i++) {
+ setter.set(s, i, value);
+ }
+ try (ResultSet rs = s.executeQuery()) {
+ if (rs.next()) {
+ T outValue = getter.get(rs, 1);
+ if (!cmp.test(value, outValue)) {
+ Assert.fail(String.format("%s != %s", value, outValue));
+ }
+ } else {
+ Assert.fail(String.format("Empty result (expected value '%s' was not returned)", value));
+ }
+ }
+ }
+ }
+
+ public void testParameterMetadata() throws SQLException {
+ String q = "select r from range(1, 10) r where r = ? or r = ? or r = ?";
+ int paramCount = 3;
+ try (Connection c = createConnection(); PreparedStatement s = c.prepareStatement(q)) {
+ ParameterMetaData pmd = s.getParameterMetaData();
+ Assert.assertEquals(paramCount, pmd.getParameterCount());
+ for (int i = 1; i <= paramCount; i++) {
+ Assert.assertEquals(JDBCType.OTHER.getVendorTypeNumber().intValue(), pmd.getParameterType(i));
+ Assert.assertEquals("any", pmd.getParameterTypeName(i));
+ Assert.assertEquals(ParameterMetaData.parameterModeIn, pmd.getParameterMode(i));
+ }
+ }
+ }
+
+ interface SetParameterByIndex<T> {
+ void set(PreparedStatement s, int paramIndex, T paramValue) throws SQLException;
+ }
+
+ private static boolean sqlTimeEquals(java.sql.Time v1, java.sql.Time v2) {
+ // java.sql.Time.equals() compares millis since epoch,
+ // but we only want to compare time components
+ return v1.toLocalTime().equals(v2.toLocalTime());
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcStatementTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcStatementTester.java
new file mode 100644
index 0000000..e2729ef
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcStatementTester.java
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.jdbc.core.ADBStatement;
+import org.junit.Assert;
+
+class JdbcStatementTester extends JdbcTester {
+
+ public void testLifecycle() throws SQLException {
+ Connection c = createConnection();
+ Statement s = c.createStatement();
+ Assert.assertFalse(s.isClosed());
+ Assert.assertSame(c, s.getConnection());
+
+ s.close();
+ Assert.assertTrue(s.isClosed());
+
+ // ok to call close() on a closed statement
+ s.close();
+ Assert.assertTrue(s.isClosed());
+ }
+
+ public void testAutoCloseOnConnectionClose() throws SQLException {
+ Connection c = createConnection();
+ // check that a statement is automatically closed when the connection is closed
+ Statement s = c.createStatement();
+ Assert.assertFalse(s.isClosed());
+ c.close();
+ Assert.assertTrue(s.isClosed());
+ }
+
+ public void testCloseOnCompletion() throws SQLException {
+ try (Connection c = createConnection()) {
+ Statement s = c.createStatement();
+ Assert.assertFalse(s.isCloseOnCompletion());
+ s.closeOnCompletion();
+ Assert.assertTrue(s.isCloseOnCompletion());
+ Assert.assertFalse(s.isClosed());
+ ResultSet rs = s.executeQuery(Q1);
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(rs.next());
+ rs.close();
+ Assert.assertTrue(s.isClosed());
+ }
+ }
+
+ public void testExecuteQuery() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ // Query -> ok
+ try (ResultSet rs = s.executeQuery(Q1)) {
+ Assert.assertTrue(rs.next());
+ Assert.assertEquals(1, rs.getMetaData().getColumnCount());
+ Assert.assertEquals(V1, rs.getInt(1));
+ Assert.assertFalse(rs.next());
+ Assert.assertFalse(rs.isClosed());
+ }
+
+ // DDL -> error
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testExecuteQuery");
+ try {
+ s.executeQuery(printCreateDataverse(dataverse));
+ Assert.fail("DDL did not fail in executeQuery()");
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains(ErrorCode.PROHIBITED_STATEMENT_CATEGORY.errorCode()));
+ }
+
+ // DML -> error
+ String dataset = "ds1";
+ s.execute(printCreateDataverse(dataverse));
+ s.execute(printCreateDataset(dataverse, dataset));
+ try {
+ s.executeQuery(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ Assert.fail("DML did not fail in executeQuery()");
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains(ErrorCode.PROHIBITED_STATEMENT_CATEGORY.errorCode()));
+ }
+
+ // Cleanup
+ s.execute(printDropDataverse(dataverse));
+ }
+ }
+
+ public void testExecuteUpdate() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ // DDL -> ok
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testExecuteUpdate");
+ int res = s.executeUpdate(printCreateDataverse(dataverse));
+ Assert.assertEquals(0, res);
+ String dataset = "ds1";
+ res = s.executeUpdate(printCreateDataset(dataverse, dataset));
+ Assert.assertEquals(0, res);
+
+ // DML -> ok
+ res = s.executeUpdate(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ // currently, DML statements always return update count = 1
+ Assert.assertEquals(1, res);
+
+ // Query -> error
+ try {
+ s.executeUpdate(Q1);
+ Assert.fail("Query did not fail in executeUpdate()");
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains("Invalid statement category"));
+ }
+
+ // Cleanup
+ s.executeUpdate(printDropDataverse(dataverse));
+ }
+ }
+
+ public void testExecute() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ // Query -> ok
+ boolean res = s.execute(Q1);
+ Assert.assertTrue(res);
+ Assert.assertEquals(-1, s.getUpdateCount());
+ try (ResultSet rs = s.getResultSet()) {
+ Assert.assertTrue(rs.next());
+ Assert.assertEquals(1, rs.getMetaData().getColumnCount());
+ Assert.assertEquals(V1, rs.getInt(1));
+ Assert.assertFalse(rs.next());
+ Assert.assertFalse(rs.isClosed());
+ }
+
+ // DDL -> ok
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testExecute");
+ res = s.execute(printCreateDataverse(dataverse));
+ Assert.assertFalse(res);
+ Assert.assertEquals(0, s.getUpdateCount());
+ String dataset = "ds1";
+ res = s.execute(printCreateDataset(dataverse, dataset));
+ Assert.assertFalse(res);
+
+ // DML -> ok
+ res = s.execute(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ Assert.assertFalse(res);
+ // currently, DML statements always return update count = 1
+ Assert.assertEquals(1, s.getUpdateCount());
+
+ // Cleanup
+ s.execute(printDropDataverse(dataverse));
+ }
+ }
+
+ public void testGetResultSet() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ // Query
+ boolean res = s.execute(Q1);
+ Assert.assertTrue(res);
+ ResultSet rs = s.getResultSet();
+ Assert.assertFalse(rs.isClosed());
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(s.getMoreResults()); // closes current ResultSet
+ Assert.assertTrue(rs.isClosed());
+
+ res = s.execute(Q1);
+ Assert.assertTrue(res);
+ rs = s.getResultSet();
+ Assert.assertFalse(rs.isClosed());
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(s.getMoreResults(Statement.KEEP_CURRENT_RESULT));
+ Assert.assertFalse(rs.isClosed());
+ rs.close();
+
+ // DDL
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testGetResultSet");
+ res = s.execute(printCreateDataverse(dataverse));
+ Assert.assertFalse(res);
+ Assert.assertNull(s.getResultSet());
+ Assert.assertFalse(s.getMoreResults());
+
+ String dataset = "ds1";
+ res = s.execute(printCreateDataset(dataverse, dataset));
+ Assert.assertFalse(res);
+
+ // DML
+ res = s.execute(printInsert(dataverse, dataset, dataGen("x", 1, 2)));
+ Assert.assertFalse(res);
+ Assert.assertNull(s.getResultSet());
+ Assert.assertFalse(s.getMoreResults());
+ }
+ }
+
+ public void testMaxRows() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ List<String> dataverse = Arrays.asList(getClass().getSimpleName(), "testMaxRows");
+ String dataset = "ds1";
+ String field = "x";
+ s.execute(printCreateDataverse(dataverse));
+ s.execute(printCreateDataset(dataverse, dataset));
+ s.execute(printInsert(dataverse, dataset, dataGen(field, 1, 2, 3)));
+
+ s.setMaxRows(2);
+ Assert.assertEquals(2, s.getMaxRows());
+ try (ResultSet rs = s.executeQuery(String.format("select %s from %s.%s", field,
+ printDataverseName(dataverse), printIdentifier(dataset)))) {
+ Assert.assertTrue(rs.next());
+ Assert.assertTrue(rs.next());
+ Assert.assertFalse(rs.next());
+ }
+ }
+ }
+
+ public void testWarnings() throws SQLException {
+ try (Connection c = createConnection();
+ Statement s = c.createStatement();
+ ResultSet rs = s.executeQuery("select double('x'), bigint('y')")) { // --> NULL with warning
+ Assert.assertTrue(rs.next());
+ rs.getDouble(1);
+ Assert.assertTrue(rs.wasNull());
+ rs.getLong(2);
+ Assert.assertTrue(rs.wasNull());
+
+ SQLWarning w = s.getWarnings();
+ Assert.assertNotNull(w);
+ String msg = w.getMessage();
+ Assert.assertTrue(msg, msg.contains(ErrorCode.INVALID_FORMAT.errorCode()));
+
+ SQLWarning w2 = w.getNextWarning();
+ Assert.assertNotNull(w2);
+ String msg2 = w.getMessage();
+ Assert.assertTrue(msg2, msg2.contains(ErrorCode.INVALID_FORMAT.errorCode()));
+
+ Assert.assertNull(w2.getNextWarning());
+ s.clearWarnings();
+ Assert.assertNull(s.getWarnings());
+ }
+ }
+
+ public void testWrapper() throws SQLException {
+ try (Connection c = createConnection(); Statement s = c.createStatement()) {
+ Assert.assertTrue(s.isWrapperFor(ADBStatement.class));
+ Assert.assertNotNull(s.unwrap(ADBStatement.class));
+ }
+ }
+}
diff --git a/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcTester.java b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcTester.java
new file mode 100644
index 0000000..d631e0f
--- /dev/null
+++ b/asterixdb-jdbc/asterix-jdbc-test/src/test/java/org/apache/asterix/test/jdbc/JdbcTester.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.test.jdbc;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.JDBCType;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.time.Period;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.apache.hyracks.algebricks.common.utils.Pair;
+import org.junit.Assert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+abstract class JdbcTester {
+
+ static final String DEFAULT_DATAVERSE_NAME = "Default";
+
+ static final String METADATA_DATAVERSE_NAME = "Metadata";
+
+ static final List<String> BUILT_IN_DATAVERSE_NAMES = Arrays.asList(DEFAULT_DATAVERSE_NAME, METADATA_DATAVERSE_NAME);
+
+ static final String SQL_STATE_CONNECTION_CLOSED = "08003";
+
+ static final char IDENTIFIER_QUOTE = '`';
+
+ static final int V1 = 42;
+
+ static final String Q1 = printSelect(V1);
+
+ static final String Q2 = "select r x, r * 11 y from range(1, 9) r order by r";
+
+ static final String Q3_PROJECT = "int8(v) c1_i1, int16(v) c2_i2, int32(v) c3_i4, int64(v) c4_i8, float(v) c5_r4, "
+ + "double(v) c6_r8, 'a' || string(v) c7_s, boolean(v+2) c8_b, date_from_unix_time_in_days(v) c9_d, "
+ + "time_from_unix_time_in_ms((v+3)*1000) c10_t, datetime_from_unix_time_in_secs(v) c11_dt,"
+ + "get_year_month_duration(duration_from_months(v)) c12_um, "
+ + "get_day_time_duration(duration_from_ms(v)) c13_ut, "
+ + "duration('P'||string(v+3)||'MT'||string(v+3)||'S') c14_uu, "
+ + "uuid('5c848e5c-6b6a-498f-8452-8847a295742' || string(v+3)) c15_id";
+
+ static final String Q3 = String.format("select %s from range(-1, 1) r let v=nullif(r,0)*2 order by r", Q3_PROJECT);
+
+ static String[] Q3_COLUMNS = new String[] { "c1_i1", "c2_i2", "c3_i4", "c4_i8", "c5_r4", "c6_r8", "c7_s", "c8_b",
+ "c9_d", "c10_t", "c11_dt", "c12_um", "c13_ut", "c14_uu", "c15_id" };
+
+ static JDBCType[] Q3_COLUMN_TYPES_JDBC = new JDBCType[] { JDBCType.TINYINT, JDBCType.SMALLINT, JDBCType.INTEGER,
+ JDBCType.BIGINT, JDBCType.REAL, JDBCType.DOUBLE, JDBCType.VARCHAR, JDBCType.BOOLEAN, JDBCType.DATE,
+ JDBCType.TIME, JDBCType.TIMESTAMP, JDBCType.OTHER, JDBCType.OTHER, JDBCType.OTHER, JDBCType.OTHER };
+
+ static String[] Q3_COLUMN_TYPES_ADB = new String[] { "int8", "int16", "int32", "int64", "float", "double", "string",
+ "boolean", "date", "time", "datetime", "year-month-duration", "day-time-duration", "duration", "uuid" };
+
+ static Class<?>[] Q3_COLUMN_TYPES_JAVA = new Class<?>[] { Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, String.class, Boolean.class, java.sql.Date.class, java.sql.Time.class,
+ java.sql.Timestamp.class, Period.class, Duration.class, String.class, UUID.class };
+
+ protected JdbcTestContext testContext;
+
+ protected JdbcTester() {
+ }
+
+ void setTestContext(JdbcTestContext testContext) {
+ this.testContext = Objects.requireNonNull(testContext);
+ }
+
+ static JdbcTestContext createTestContext(String host, int port) {
+ return new JdbcTestContext(host, port);
+ }
+
+ protected Connection createConnection() throws SQLException {
+ return DriverManager.getConnection(testContext.getJdbcUrl());
+ }
+
+ protected Connection createConnection(String dataverseName) throws SQLException {
+ return createConnection(Collections.singletonList(dataverseName));
+ }
+
+ protected Connection createConnection(List<String> dataverseName) throws SQLException {
+ return DriverManager.getConnection(testContext.getJdbcUrl(getCanonicalDataverseName(dataverseName)));
+ }
+
+ protected static String getCanonicalDataverseName(List<String> dataverseName) {
+ return String.join("/", dataverseName);
+ }
+
+ protected static String printDataverseName(List<String> dataverseName) {
+ return dataverseName.stream().map(JdbcTester::printIdentifier).collect(Collectors.joining("."));
+ }
+
+ protected static String printIdentifier(String ident) {
+ return IDENTIFIER_QUOTE + ident + IDENTIFIER_QUOTE;
+ }
+
+ protected static String printCreateDataverse(List<String> dataverseName) {
+ return String.format("create dataverse %s", printDataverseName(dataverseName));
+ }
+
+ protected static String printDropDataverse(List<String> dataverseName) {
+ return String.format("drop dataverse %s", printDataverseName(dataverseName));
+ }
+
+ protected static String printCreateDataset(List<String> dataverseName, String datasetName) {
+ return String.format("create dataset %s.%s(_id uuid) open type primary key _id autogenerated",
+ printDataverseName(dataverseName), printIdentifier(datasetName));
+ }
+
+ protected static String printCreateDataset(List<String> dataverseName, String datasetName, List<String> fieldNames,
+ List<String> fieldTypes, int pkLen) {
+ return String.format("create dataset %s.%s(%s) open type primary key %s", printDataverseName(dataverseName),
+ printIdentifier(datasetName), printSchema(fieldNames, fieldTypes),
+ printIdentifierList(fieldNames.subList(0, pkLen)));
+ }
+
+ protected static String printCreateView(List<String> dataverseName, String viewName, List<String> fieldNames,
+ List<String> fieldTypes, int pkLen, List<String> fkRefs, String viewQuery) {
+ List<String> pkFieldNames = fieldNames.subList(0, pkLen);
+ String pkDecl = String.format(" primary key (%s) not enforced", printIdentifierList(pkFieldNames));
+ String fkDecl =
+ fkRefs.stream()
+ .map(fkRef -> String.format("foreign key (%s) references %s not enforced",
+ printIdentifierList(pkFieldNames), printIdentifier(fkRef)))
+ .collect(Collectors.joining(" "));
+ return String.format("create view %s.%s(%s) default null %s %s as %s", printDataverseName(dataverseName),
+ printIdentifier(viewName), printSchema(fieldNames, fieldTypes), pkDecl, fkDecl, viewQuery);
+ }
+
+ protected static String printSchema(List<String> fieldNames, List<String> fieldTypes) {
+ StringBuilder schema = new StringBuilder(128);
+ for (int i = 0, n = fieldNames.size(); i < n; i++) {
+ if (i > 0) {
+ schema.append(',');
+ }
+ schema.append(printIdentifier(fieldNames.get(i))).append(' ').append(fieldTypes.get(i));
+ }
+ return schema.toString();
+ }
+
+ protected static String printIdentifierList(List<String> fieldNames) {
+ return fieldNames.stream().map(JdbcTester::printIdentifier).collect(Collectors.joining(","));
+ }
+
+ protected static String printInsert(List<String> dataverseName, String datasetName, ArrayNode values) {
+ return String.format("insert into %s.%s (%s)", printDataverseName(dataverseName), printIdentifier(datasetName),
+ values);
+ }
+
+ protected static String printSelect(Object... values) {
+ return String.format("select %s", Arrays.stream(values).map(String::valueOf).collect(Collectors.joining(",")));
+ }
+
+ protected static ArrayNode dataGen(String fieldName1, Object... data1) {
+ ObjectMapper om = new ObjectMapper();
+ ArrayNode values = om.createArrayNode();
+ for (Object v : data1) {
+ ObjectNode obj = om.createObjectNode();
+ obj.putPOJO(fieldName1, v);
+ values.add(obj);
+ }
+ return values;
+ }
+
+ protected static <T> void assertErrorOnClosed(T param, JdbcConnectionTester.JdbcRunnable<T> cmd,
+ String description) {
+ try {
+ cmd.run(param);
+ Assert.fail(String.format("Unexpected: %s succeeded on a closed %s", description,
+ param.getClass().getSimpleName()));
+ } catch (SQLException e) {
+ String msg = e.getMessage();
+ Assert.assertTrue(msg, msg.contains("closed"));
+ }
+ }
+
+ static class JdbcTestContext {
+
+ private static final String JDBC_URL_TEMPLATE = "jdbc:asterixdb://%s:%d";
+
+ private final String jdbcUrl;
+
+ private JdbcTestContext(String host, int port) {
+ jdbcUrl = String.format(JDBC_URL_TEMPLATE, host, port);
+ }
+
+ public String getJdbcUrl() {
+ return jdbcUrl;
+ }
+
+ public String getJdbcUrl(String dataverseName) {
+ return jdbcUrl + '/' + dataverseName;
+ }
+ }
+
+ interface JdbcRunnable<T> {
+ void run(T param) throws SQLException;
+ }
+
+ interface JdbcPredicate<T> {
+ boolean test(T param) throws SQLException;
+ }
+
+ static class CloseablePair<K extends AutoCloseable, V extends AutoCloseable> extends Pair<K, V>
+ implements AutoCloseable {
+ CloseablePair(K first, V second) {
+ super(first, second);
+ }
+
+ @Override
+ public void close() throws SQLException {
+ try {
+ if (second != null) {
+ try {
+ second.close();
+ } catch (SQLException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SQLException(e);
+ }
+ }
+ } finally {
+ if (first != null) {
+ try {
+ first.close();
+ } catch (SQLException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SQLException(e);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/asterixdb-jdbc/pom.xml b/asterixdb-jdbc/pom.xml
index 25d007d..a2c7d06 100644
--- a/asterixdb-jdbc/pom.xml
+++ b/asterixdb-jdbc/pom.xml
@@ -485,4 +485,19 @@
<module>asterix-jdbc-driver</module>
<module>asterix-jdbc-taco</module>
</modules>
+
+ <profiles>
+ <profile>
+ <id>asterix-jdbc-test-enabled</id>
+ <activation>
+ <file>
+ <exists>${basedir}/../../asterix-app/pom.xml</exists>
+ </file>
+ </activation>
+ <modules>
+ <module>asterix-jdbc-test</module>
+ </modules>
+ </profile>
+ </profiles>
+
</project>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b8c6bd0
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,35 @@
+<!--
+ ! Licensed to the Apache Software Foundation (ASF) under one
+ ! or more contributor license agreements. See the NOTICE file
+ ! distributed with this work for additional information
+ ! regarding copyright ownership. The ASF licenses this file
+ ! to you under the Apache License, Version 2.0 (the
+ ! "License"); you may not use this file except in compliance
+ ! with the License. You may obtain a copy of the License at
+ !
+ ! http://www.apache.org/licenses/LICENSE-2.0
+ !
+ ! Unless required by applicable law or agreed to in writing,
+ ! software distributed under the License is distributed on an
+ ! "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ! KIND, either express or implied. See the License for the
+ ! specific language governing permissions and limitations
+ ! under the License.
+ !-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.asterix.clients</groupId>
+ <artifactId>asterix-opt</artifactId>
+ <version>0.9.8-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <parent>
+ <groupId>org.apache.asterix</groupId>
+ <artifactId>apache-asterixdb</artifactId>
+ <version>0.9.8-SNAPSHOT</version>
+ </parent>
+ <modules>
+ <module>asterixdb-jdbc</module>
+ <module>asterix-opt-bom</module>
+ </modules>
+</project>