parameters, String... choices)
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcFieldInfoExtra.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcFieldInfoExtra.java
new file mode 100644
index 0000000000..b6741a7cc6
--- /dev/null
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcFieldInfoExtra.java
@@ -0,0 +1,163 @@
+/*
+ * 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.arrow.adbc.driver.jdbc.adapter;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.apache.arrow.adapter.jdbc.JdbcFieldInfo;
+
+/**
+ * Information about a column from JDBC for inferring column type.
+ *
+ * Extension of the upstream {@link JdbcFieldInfo} which lacks necessary fields for certain
+ * databases.
+ */
+public final class JdbcFieldInfoExtra {
+ final JdbcFieldInfo info;
+ final String typeName;
+ final int numPrecRadix;
+ final String remarks;
+ final String columnDef;
+ final int sqlDataType;
+ final int sqlDatetimeSub;
+ final int charOctetLength;
+ final int ordinalPosition;
+
+ /**
+ * Create a JdbcFieldInfoExtra from the result of a {@link
+ * java.sql.DatabaseMetaData#getColumns(String, String, String, String)}.
+ *
+ * @param rs The result set.
+ */
+ public JdbcFieldInfoExtra(ResultSet rs) throws SQLException {
+ final int dataType = rs.getInt("DATA_TYPE");
+ this.typeName = rs.getString("TYPE_NAME");
+ final int columnSize = rs.getInt("COLUMN_SIZE");
+ final int decimalDigits = rs.getInt("DECIMAL_DIGITS");
+ this.numPrecRadix = rs.getInt("NUM_PREC_RADIX");
+ final int nullable = rs.getInt("NULLABLE");
+ this.remarks = rs.getString("REMARKS");
+ this.columnDef = rs.getString("COLUMN_DEF");
+ this.sqlDataType = rs.getInt("SQL_DATA_TYPE");
+ this.sqlDatetimeSub = rs.getInt("SQL_DATETIME_SUB");
+ this.charOctetLength = rs.getInt("CHAR_OCTET_LENGTH");
+ this.ordinalPosition = rs.getInt("ORDINAL_POSITION");
+
+ this.info = new JdbcFieldInfo(dataType, nullable, columnSize, decimalDigits);
+ }
+
+ /** Get the base Arrow version. */
+ public JdbcFieldInfo getFieldInfo() {
+ return info;
+ }
+
+ /** The {@link java.sql.Types} type. */
+ public int getJdbcType() {
+ return info.getJdbcType();
+ }
+
+ public String getTypeName() {
+ return typeName;
+ }
+
+ public int getNumPrecRadix() {
+ return numPrecRadix;
+ }
+
+ public String getRemarks() {
+ return remarks;
+ }
+
+ public String getColumnDef() {
+ return columnDef;
+ }
+
+ public int getSqlDataType() {
+ return sqlDataType;
+ }
+
+ public int getSqlDatetimeSub() {
+ return sqlDatetimeSub;
+ }
+
+ public int getCharOctetLength() {
+ return charOctetLength;
+ }
+
+ public int getOrdinalPosition() {
+ return ordinalPosition;
+ }
+
+ /** The nullability. */
+ public int isNullable() {
+ return info.isNullable();
+ }
+
+ /**
+ * The numeric precision, for {@link java.sql.Types#NUMERIC} and {@link java.sql.Types#DECIMAL}
+ * types.
+ */
+ public int getPrecision() {
+ return info.getPrecision();
+ }
+
+ /**
+ * The numeric scale, for {@link java.sql.Types#NUMERIC} and {@link java.sql.Types#DECIMAL} types.
+ */
+ public int getScale() {
+ return info.getScale();
+ }
+
+ /** The column index for query column. */
+ public int getColumn() {
+ return info.getColumn();
+ }
+
+ @Override
+ public String toString() {
+ return "JdbcFieldInfoExtra{"
+ + "dataType="
+ + info.getJdbcType()
+ + ", typeName='"
+ + typeName
+ + '\''
+ + ", columnSize="
+ + info.getPrecision()
+ + ", decimalDigits="
+ + info.getScale()
+ + ", numPrecRadix="
+ + numPrecRadix
+ + ", nullable="
+ + info.isNullable()
+ + ", remarks='"
+ + remarks
+ + '\''
+ + ", columnDef='"
+ + columnDef
+ + '\''
+ + ", sqlDataType="
+ + sqlDataType
+ + ", sqlDatetimeSub="
+ + sqlDatetimeSub
+ + ", charOctetLength="
+ + charOctetLength
+ + ", ordinalPosition="
+ + ordinalPosition
+ + '}';
+ }
+}
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcToArrowTypeConverter.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcToArrowTypeConverter.java
new file mode 100644
index 0000000000..95545f691d
--- /dev/null
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcToArrowTypeConverter.java
@@ -0,0 +1,24 @@
+/*
+ * 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.arrow.adbc.driver.jdbc.adapter;
+
+import java.util.function.Function;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+@FunctionalInterface
+public interface JdbcToArrowTypeConverter extends Function {}
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcToArrowTypeConverters.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcToArrowTypeConverters.java
new file mode 100644
index 0000000000..928bfdf8ee
--- /dev/null
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/adapter/JdbcToArrowTypeConverters.java
@@ -0,0 +1,75 @@
+/*
+ * 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.arrow.adbc.driver.jdbc.adapter;
+
+import java.sql.Types;
+import org.apache.arrow.adapter.jdbc.JdbcToArrowUtils;
+import org.apache.arrow.vector.types.TimeUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+public final class JdbcToArrowTypeConverters {
+ public static final JdbcToArrowTypeConverter MICROSOFT_SQL_SERVER =
+ JdbcToArrowTypeConverters::mssql;
+ public static final JdbcToArrowTypeConverter POSTGRESQL = JdbcToArrowTypeConverters::postgresql;
+
+ private static ArrowType mssql(JdbcFieldInfoExtra field) {
+ switch (field.getJdbcType()) {
+ // DATETIME2
+ // Precision is "100 nanoseconds" -> TimeUnit is NANOSECOND
+ case Types.TIMESTAMP:
+ return new ArrowType.Timestamp(TimeUnit.NANOSECOND, /*timezone*/ null);
+ // DATETIMEOFFSET
+ // Precision is "100 nanoseconds" -> TimeUnit is NANOSECOND
+ case -155:
+ return new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC");
+ default:
+ return JdbcToArrowUtils.getArrowTypeFromJdbcType(field.getFieldInfo(), /*calendar*/ null);
+ }
+ }
+
+ private static ArrowType postgresql(JdbcFieldInfoExtra field) {
+ switch (field.getJdbcType()) {
+ case Types.TIMESTAMP:
+ {
+ int decimalDigits = field.getScale();
+ final TimeUnit unit;
+ if (decimalDigits == 0) {
+ unit = TimeUnit.SECOND;
+ } else if (decimalDigits > 0 && decimalDigits <= 3) {
+ unit = TimeUnit.MILLISECOND;
+ } else if (decimalDigits > 0 && decimalDigits <= 6) {
+ unit = TimeUnit.MICROSECOND;
+ } else if (decimalDigits > 6) {
+ unit = TimeUnit.NANOSECOND;
+ } else {
+ // Negative precision?
+ return null;
+ }
+ if ("timestamptz".equals(field.getTypeName())) {
+ return new ArrowType.Timestamp(unit, "UTC");
+ } else if ("timestamp".equals(field.getTypeName())) {
+ return new ArrowType.Timestamp(unit, /*timezone*/ null);
+ }
+ // Unknown type
+ return null;
+ }
+ default:
+ return JdbcToArrowUtils.getArrowTypeFromJdbcType(field.getFieldInfo(), /*calendar*/ null);
+ }
+ }
+}
diff --git a/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractSqlTypeTest.java b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractSqlTypeTest.java
new file mode 100644
index 0000000000..e231548b0d
--- /dev/null
+++ b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractSqlTypeTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.arrow.adbc.driver.testsuite;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+import org.apache.arrow.adbc.core.AdbcConnection;
+import org.apache.arrow.adbc.core.AdbcDatabase;
+import org.apache.arrow.adbc.core.AdbcStatement;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.util.AutoCloseables;
+import org.apache.arrow.util.Preconditions;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.DateDayVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.TimeStampMicroTZVector;
+import org.apache.arrow.vector.TimeStampMilliTZVector;
+import org.apache.arrow.vector.TimeStampNanoTZVector;
+import org.apache.arrow.vector.TimeStampSecTZVector;
+import org.apache.arrow.vector.TimeStampVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.DateUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.arrow.vector.util.Text;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Test the Arrow type/value for each SQL type/value, for SQL-based databases. */
+public class AbstractSqlTypeTest {
+ /** Must be initialized by the subclass. */
+ protected static SqlValidationQuirks quirks;
+
+ protected BufferAllocator allocator;
+ protected AdbcDatabase database;
+ protected AdbcConnection connection;
+ protected AdbcStatement statement;
+ protected SqlTestUtil util;
+
+ @BeforeEach
+ public void beforeEach() throws Exception {
+ Preconditions.checkNotNull(quirks, "Must initialize quirks in subclass with @BeforeAll");
+ allocator = new RootAllocator();
+ database = quirks.initDatabase(allocator);
+ connection = database.connect();
+ util = new SqlTestUtil(quirks);
+
+ final String setupSql = getResource(this.getClass().getSimpleName() + ".sql");
+ try (final AdbcStatement stmt = connection.createStatement()) {
+ stmt.setSqlQuery(setupSql);
+ stmt.executeUpdate();
+ }
+ }
+
+ @AfterEach
+ public void afterEach() throws Exception {
+ AutoCloseables.close(connection, database, allocator);
+ }
+
+ protected static String getResource(final String name) throws Exception {
+ try (InputStream is =
+ Thread.currentThread().getContextClassLoader().getResourceAsStream(name)) {
+ if (is == null) {
+ throw new RuntimeException("Could not find resource " + name);
+ }
+ try (InputStreamReader isr = new InputStreamReader(is);
+ BufferedReader reader = new BufferedReader(isr)) {
+ return reader.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ }
+ }
+
+ protected ArrowType assertValue(
+ final String column, final Class> vectorType, final Object expectedValue) throws Exception {
+ try (final AdbcStatement stmt = connection.createStatement()) {
+ stmt.setSqlQuery("SELECT " + column + " FROM adbc_alltypes");
+ try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+ ArrowReader reader = result.getReader();
+ assertThat(reader.loadNextBatch()).isTrue();
+ assertThat(reader.getVectorSchemaRoot().getRowCount()).isEqualTo(2);
+ FieldVector vector = reader.getVectorSchemaRoot().getVector(0);
+ assertThat(vector).isInstanceOf(vectorType);
+ assertThat(vector.getObject(0)).isEqualTo(expectedValue);
+ assertThat(vector.getObject(1)).isNull();
+ return vector.getField().getType();
+ }
+ }
+ }
+
+ @Test
+ protected void bigintType() throws Exception {
+ final Schema schema = connection.getTableSchema(null, null, "adbc_alltypes");
+ assertThat(schema.findField("bigint_t").getType())
+ .asInstanceOf(InstanceOfAssertFactories.type(ArrowType.Int.class))
+ .extracting(ArrowType.Int::getBitWidth, ArrowType.Int::getIsSigned)
+ .containsExactly(64, true);
+ }
+
+ @Test
+ protected void bigintValue() throws Exception {
+ assertValue("bigint_t", BigIntVector.class, 42L);
+ }
+
+ @Test
+ protected void dateType() throws Exception {
+ final Schema schema = connection.getTableSchema(null, null, "adbc_alltypes");
+ assertThat(schema.findField("date_t").getType()).isEqualTo(new ArrowType.Date(DateUnit.DAY));
+ }
+
+ @Test
+ protected void dateValue() throws Exception {
+ assertValue("date_t", DateDayVector.class, 10957);
+ }
+
+ @Test
+ protected void intType() throws Exception {
+ final Schema schema = connection.getTableSchema(null, null, "adbc_alltypes");
+ assertThat(schema.findField("int_t").getType())
+ .asInstanceOf(InstanceOfAssertFactories.type(ArrowType.Int.class))
+ .extracting(ArrowType.Int::getBitWidth, ArrowType.Int::getIsSigned)
+ .containsExactly(32, true);
+ }
+
+ @Test
+ protected void intValue() throws Exception {
+ assertValue("int_t", IntVector.class, 42);
+ }
+
+ @Test
+ protected void textType() throws Exception {
+ final Schema schema = connection.getTableSchema(null, null, "adbc_alltypes");
+ assertThat(schema.findField("text_t").getType()).isInstanceOf(ArrowType.Utf8.class);
+ }
+
+ @Test
+ protected void textValue() throws Exception {
+ assertValue("text_t", VarCharVector.class, new Text("foo"));
+ }
+
+ @Test
+ protected void timestampWithoutTimeZoneType() throws Exception {
+ final Schema schema = connection.getTableSchema(null, null, "adbc_alltypes");
+ assertThat(schema.findField("timestamp_without_time_zone_t").getType())
+ .isEqualTo(new ArrowType.Timestamp(quirks.defaultTimestampUnit(), null));
+ }
+
+ @Test
+ protected void timestampWithoutTimeZoneValue() throws Exception {
+ final ArrowType type;
+ switch (quirks.defaultTimestampUnit()) {
+ case SECOND:
+ type = assertValue("timestamp_without_time_zone_t", TimeStampVector.class, 946_782_245L);
+ break;
+ case MILLISECOND:
+ type =
+ assertValue("timestamp_without_time_zone_t", TimeStampVector.class, 946_782_245_123L);
+ break;
+ case MICROSECOND:
+ type =
+ assertValue(
+ "timestamp_without_time_zone_t", TimeStampVector.class, 946_782_245_123_000L);
+ break;
+ case NANOSECOND:
+ type =
+ assertValue(
+ "timestamp_without_time_zone_t", TimeStampVector.class, 946_782_245_123_000_000L);
+ break;
+ default:
+ throw new UnsupportedOperationException();
+ }
+ assertThat(type).isEqualTo(new ArrowType.Timestamp(quirks.defaultTimestampUnit(), null));
+ }
+
+ @Test
+ protected void timestampWithTimeZoneType() throws Exception {
+ final Schema schema = connection.getTableSchema(null, null, "adbc_alltypes");
+ assertThat(schema.findField("timestamp_with_time_zone_t").getType())
+ .isEqualTo(new ArrowType.Timestamp(quirks.defaultTimestampUnit(), "UTC"));
+ }
+
+ @Test
+ protected void timestampWithTimeZoneValue() throws Exception {
+ switch (quirks.defaultTimestampUnit()) {
+ case SECOND:
+ assertValue("timestamp_with_time_zone_t", TimeStampSecTZVector.class, 946_760_645L);
+ break;
+ case MILLISECOND:
+ assertValue("timestamp_with_time_zone_t", TimeStampMilliTZVector.class, 946_760_645_123L);
+ break;
+ case MICROSECOND:
+ assertValue(
+ "timestamp_with_time_zone_t", TimeStampMicroTZVector.class, 946_760_645_123_000L);
+ break;
+ case NANOSECOND:
+ assertValue(
+ "timestamp_with_time_zone_t", TimeStampNanoTZVector.class, 946_760_645_123_000_000L);
+ break;
+ }
+ }
+}
diff --git a/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java
index 895c3a0f3a..a8d259cfb7 100644
--- a/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java
+++ b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java
@@ -22,6 +22,7 @@
import org.apache.arrow.adbc.core.AdbcDatabase;
import org.apache.arrow.adbc.core.AdbcException;
import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.types.TimeUnit;
/** Account for driver/vendor-specific quirks in implementing validation tests. */
public abstract class SqlValidationQuirks {
@@ -85,4 +86,8 @@ public String generateAddForeignKeyQuery(
+ caseFoldColumnName(referenceColumn)
+ ") ";
}
+
+ public TimeUnit defaultTimestampUnit() {
+ return TimeUnit.MILLISECOND;
+ }
}
diff --git a/java/pom.xml b/java/pom.xml
index 216ace5c0a..4b1d09d58a 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -85,6 +85,7 @@
driver/flight-sql-validation
driver/jdbc
driver/jdbc-validation-derby
+ driver/jdbc-validation-mssqlserver
driver/jdbc-validation-postgresql
driver/validation
driver-manager