diff --git a/java/driver/flight-sql/pom.xml b/java/driver/flight-sql/pom.xml
index da97becb76..3d1cc854eb 100644
--- a/java/driver/flight-sql/pom.xml
+++ b/java/driver/flight-sql/pom.xml
@@ -37,6 +37,11 @@
caffeine
3.2.4
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${dep.jackson.version}
+
com.google.protobuf
protobuf-java
diff --git a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlClientWithCallOptions.java b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlClientWithCallOptions.java
index 881013fc69..d53eee9762 100644
--- a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlClientWithCallOptions.java
+++ b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlClientWithCallOptions.java
@@ -26,12 +26,18 @@
import org.apache.arrow.flight.CallOption;
import org.apache.arrow.flight.CancelFlightInfoRequest;
import org.apache.arrow.flight.CancelFlightInfoResult;
+import org.apache.arrow.flight.CloseSessionRequest;
+import org.apache.arrow.flight.CloseSessionResult;
import org.apache.arrow.flight.FlightDescriptor;
import org.apache.arrow.flight.FlightEndpoint;
import org.apache.arrow.flight.FlightInfo;
import org.apache.arrow.flight.FlightStream;
+import org.apache.arrow.flight.GetSessionOptionsRequest;
+import org.apache.arrow.flight.GetSessionOptionsResult;
import org.apache.arrow.flight.RenewFlightEndpointRequest;
import org.apache.arrow.flight.SchemaResult;
+import org.apache.arrow.flight.SetSessionOptionsRequest;
+import org.apache.arrow.flight.SetSessionOptionsResult;
import org.apache.arrow.flight.Ticket;
import org.apache.arrow.flight.sql.CancelResult;
import org.apache.arrow.flight.sql.FlightSqlClient;
@@ -287,6 +293,20 @@ public FlightEndpoint renewFlightEndpoint(
return client.renewFlightEndpoint(request, combine(options));
}
+ public SetSessionOptionsResult setSessionOptions(
+ SetSessionOptionsRequest request, CallOption... options) {
+ return client.setSessionOptions(request, combine(options));
+ }
+
+ public GetSessionOptionsResult getSessionOptions(
+ GetSessionOptionsRequest request, CallOption... options) {
+ return client.getSessionOptions(request, combine(options));
+ }
+
+ public CloseSessionResult closeSession(CloseSessionRequest request, CallOption... options) {
+ return client.closeSession(request, combine(options));
+ }
+
@Override
public void close() throws Exception {
AutoCloseables.close(client);
diff --git a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java
index 99c9fb7350..fd43185eba 100644
--- a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java
+++ b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java
@@ -38,13 +38,22 @@
import org.apache.arrow.adbc.core.AdbcStatement;
import org.apache.arrow.adbc.core.AdbcStatusCode;
import org.apache.arrow.adbc.core.BulkIngestMode;
+import org.apache.arrow.adbc.core.TypedKey;
import org.apache.arrow.adbc.sql.SqlQuirks;
import org.apache.arrow.flight.CallOption;
+import org.apache.arrow.flight.CloseSessionRequest;
import org.apache.arrow.flight.FlightCallHeaders;
import org.apache.arrow.flight.FlightClient;
import org.apache.arrow.flight.FlightEndpoint;
+import org.apache.arrow.flight.FlightRuntimeException;
+import org.apache.arrow.flight.FlightStatusCode;
+import org.apache.arrow.flight.GetSessionOptionsRequest;
import org.apache.arrow.flight.HeaderCallOption;
import org.apache.arrow.flight.Location;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.apache.arrow.flight.SessionOptionValueFactory;
+import org.apache.arrow.flight.SetSessionOptionsRequest;
+import org.apache.arrow.flight.SetSessionOptionsResult;
import org.apache.arrow.flight.Ticket;
import org.apache.arrow.flight.auth2.BasicAuthCredentialWriter;
import org.apache.arrow.flight.client.ClientCookieMiddleware;
@@ -208,11 +217,125 @@ public void setAutoCommit(boolean enableAutoCommit) throws AdbcException {
}
}
+ @Override
+ public T getOption(TypedKey key) throws AdbcException {
+ final String k = key.getKey();
+
+ if (k.equals(FlightSqlConnectionProperties.SESSION_OPTIONS)) {
+ if (key.getType() != String.class) {
+ return AdbcConnection.super.getOption(key);
+ }
+ return key.cast(FlightSqlSessionUtil.toJson(fetchSessionOptionsOrEmpty()));
+ }
+
+ final String prefix;
+ if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX)) {
+ prefix = FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX;
+ } else if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX)) {
+ prefix = FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX;
+ } else if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX)) {
+ prefix = FlightSqlConnectionProperties.SESSION_OPTION_PREFIX;
+ } else {
+ return AdbcConnection.super.getOption(key);
+ }
+
+ final String name = k.substring(prefix.length());
+ if (name.isEmpty()) {
+ throw AdbcException.invalidArgument("[Flight SQL] Session option name must not be empty");
+ }
+ final Object raw =
+ FlightSqlSessionUtil.require(fetchSessionOptionsOrEmpty(), name)
+ .acceptVisitor(FlightSqlSessionUtil.TO_JAVA);
+ if (raw == null) {
+ throw new AdbcException(
+ "[Flight SQL] Session option not found: " + name,
+ null,
+ AdbcStatusCode.NOT_FOUND,
+ null,
+ 0);
+ }
+ final T result = FlightSqlSessionUtil.cast(key, raw, name);
+ return result != null ? result : AdbcConnection.super.getOption(key);
+ }
+
+ @Override
+ public void setOption(TypedKey key, T value) throws AdbcException {
+ final String k = key.getKey();
+
+ if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_ERASE_PREFIX)) {
+ final String name =
+ k.substring(FlightSqlConnectionProperties.SESSION_OPTION_ERASE_PREFIX.length());
+ doSetSessionOption(name, SessionOptionValueFactory.makeEmptySessionOptionValue());
+
+ } else if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX)) {
+ if (value == null) {
+ throw invalidNullValue(k);
+ }
+ final String name =
+ k.substring(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX.length());
+ final boolean b;
+ if (value instanceof Boolean) {
+ b = (Boolean) value;
+ } else {
+ b = FlightSqlSessionUtil.parseStrictBoolean(value.toString(), name);
+ }
+ doSetSessionOption(name, SessionOptionValueFactory.makeSessionOptionValue(b));
+
+ } else if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX)) {
+ if (value == null) {
+ throw invalidNullValue(k);
+ }
+ final String name =
+ k.substring(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX.length());
+ final String[] arr;
+ if (value instanceof String[]) {
+ arr = (String[]) value;
+ } else {
+ arr = FlightSqlSessionUtil.parseJsonArray(value.toString());
+ }
+ doSetSessionOption(name, SessionOptionValueFactory.makeSessionOptionValue(arr));
+
+ } else if (k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX)) {
+ if (value == null) {
+ throw invalidNullValue(k);
+ }
+ final String name = k.substring(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX.length());
+ final SessionOptionValue sv;
+ if (value instanceof Long) {
+ sv = SessionOptionValueFactory.makeSessionOptionValue((Long) value);
+ } else if (value instanceof Double) {
+ sv = SessionOptionValueFactory.makeSessionOptionValue((Double) value);
+ } else {
+ sv = SessionOptionValueFactory.makeSessionOptionValue(value.toString());
+ }
+ doSetSessionOption(name, sv);
+
+ } else if (k.equals(FlightSqlConnectionProperties.SESSION_OPTIONS)) {
+ throw AdbcException.notImplemented(
+ "[Flight SQL] adbc.flight.sql.session.options is read-only");
+
+ } else {
+ AdbcConnection.super.setOption(key, value);
+ }
+ }
+
+ private static AdbcException invalidNullValue(String key) {
+ return AdbcException.invalidArgument(
+ "[Flight SQL] null value not allowed for key: "
+ + key
+ + " - use adbc.flight.sql.session.optionerase. to erase an option");
+ }
+
@Override
public void close() throws AdbcException {
- clientCache.invalidateAll();
try {
- AutoCloseables.close(client, allocator);
+ // Best-effort: the Go driver also ignores all errors closing the session.
+ client.closeSession(new CloseSessionRequest(), callOptions);
+ } catch (FlightRuntimeException e) {
+ // ignore
+ }
+ try {
+ AutoCloseables.close(clientCache::invalidateAll, client, allocator);
} catch (Exception e) {
throw AdbcException.internal("[Flight SQL] Failed to close connection").withCause(e);
}
@@ -223,6 +346,39 @@ public String toString() {
return "FlightSqlConnection{" + "client=" + client + '}';
}
+ private Map fetchSessionOptionsOrEmpty() throws AdbcException {
+ try {
+ return client.getSessionOptions(new GetSessionOptionsRequest()).getSessionOptions();
+ } catch (FlightRuntimeException e) {
+ // Go also treats INVALID_ARGUMENT as "server doesn't support sessions" here.
+ if (e.status().code() == FlightStatusCode.UNIMPLEMENTED
+ || e.status().code() == FlightStatusCode.INVALID_ARGUMENT) {
+ return Collections.emptyMap();
+ }
+ throw FlightSqlDriverUtil.fromFlightException(e);
+ }
+ }
+
+ private void doSetSessionOption(String name, SessionOptionValue value) throws AdbcException {
+ if (name.isEmpty()) {
+ throw AdbcException.invalidArgument("[Flight SQL] Session option name must not be empty");
+ }
+ final SetSessionOptionsResult result;
+ try {
+ result =
+ client.setSessionOptions(
+ new SetSessionOptionsRequest(Collections.singletonMap(name, value)));
+ } catch (FlightRuntimeException e) {
+ throw FlightSqlDriverUtil.fromFlightException(e);
+ }
+ if (result.hasErrors()) {
+ final SetSessionOptionsResult.Error err = result.getErrors().get(name);
+ final String errType = (err != null) ? err.value.name() : "UNKNOWN";
+ throw AdbcException.invalidArgument(
+ "[Flight SQL] Failed to set session option '" + name + "': " + errType);
+ }
+ }
+
/**
* Initialize cached data to share between connections and create, test, and authenticate the
* first connection.
diff --git a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnectionProperties.java b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnectionProperties.java
index 4ab1955a1b..39da07a03f 100644
--- a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnectionProperties.java
+++ b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnectionProperties.java
@@ -35,4 +35,10 @@ public interface FlightSqlConnectionProperties {
TypedKey WITH_COOKIE_MIDDLEWARE =
new TypedKey<>("adbc.flight.sql.rpc.with_cookie_middleware", Boolean.class);
String RPC_CALL_HEADER_PREFIX = "adbc.flight.sql.rpc.call_header.";
+
+ String SESSION_OPTIONS = "adbc.flight.sql.session.options";
+ String SESSION_OPTION_PREFIX = "adbc.flight.sql.session.option.";
+ String SESSION_OPTION_BOOL_PREFIX = "adbc.flight.sql.session.optionbool.";
+ String SESSION_OPTION_STRING_LIST_PREFIX = "adbc.flight.sql.session.optionstringlist.";
+ String SESSION_OPTION_ERASE_PREFIX = "adbc.flight.sql.session.optionerase.";
}
diff --git a/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlSessionUtil.java b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlSessionUtil.java
new file mode 100644
index 0000000000..d500c5de33
--- /dev/null
+++ b/java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlSessionUtil.java
@@ -0,0 +1,174 @@
+/*
+ * 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.flightsql;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.core.AdbcStatusCode;
+import org.apache.arrow.adbc.core.TypedKey;
+import org.apache.arrow.flight.NoOpSessionOptionValueVisitor;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Package-private helpers for Flight SQL session option serialization and type conversion. */
+final class FlightSqlSessionUtil {
+
+ static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /**
+ * Extracts a native Java value from a {@link SessionOptionValue}. JSON has no representation for
+ * NaN/Infinity, so non-finite doubles are converted to their {@link String} form (e.g. {@code
+ * "NaN"}) here rather than special-cased later; String[] is defensively cloned; Void returns
+ * {@code null} (callers must handle null before calling {@link #cast}).
+ */
+ static final NoOpSessionOptionValueVisitor