Copilot commented on code in PR #4444:
URL: https://github.com/apache/arrow-adbc/pull/4444#discussion_r3488658020


##########
java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java:
##########
@@ -203,8 +213,143 @@ public void setAutoCommit(boolean enableAutoCommit) 
throws AdbcException {
     }
   }
 
+  @Override
+  @SuppressWarnings("unchecked")
+  public <T> T getOption(TypedKey<T> key) throws AdbcException {
+    final String k = key.getKey();
+
+    if (k.equals(FlightSqlConnectionProperties.SESSION_OPTIONS)) {
+      return 
key.cast(FlightSqlSessionUtil.sessionOptionsToJson(fetchSessionOptions()));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() == Boolean.class) {
+        return key.cast(val.acceptVisitor(FlightSqlSessionUtil.BOOL_VISITOR));
+      }
+      return key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_VISITOR));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX)) 
{
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() != String[].class) {
+        return AdbcConnection.super.getOption(key);
+      }
+      return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_ARRAY_VISITOR));

Review Comment:
   For the string-list prefix, unsupported key types should be rejected/handled 
explicitly. As written, callers using String.class (to get a JSON 
representation) or any other non-String[] type will hit 
AdbcConnection.super.getOption(key), while callers using an unexpected type 
could still end up with a ClassCastException elsewhere. Consider supporting 
String.class here (consistent with cross-language JSON support) and delegating 
all other types to the default implementation.



##########
java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java:
##########
@@ -203,8 +213,143 @@ public void setAutoCommit(boolean enableAutoCommit) 
throws AdbcException {
     }
   }
 
+  @Override
+  @SuppressWarnings("unchecked")
+  public <T> T getOption(TypedKey<T> key) throws AdbcException {
+    final String k = key.getKey();
+
+    if (k.equals(FlightSqlConnectionProperties.SESSION_OPTIONS)) {
+      return 
key.cast(FlightSqlSessionUtil.sessionOptionsToJson(fetchSessionOptions()));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() == Boolean.class) {
+        return key.cast(val.acceptVisitor(FlightSqlSessionUtil.BOOL_VISITOR));
+      }
+      return key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_VISITOR));
+

Review Comment:
   For the session bool prefix, any non-Boolean key type falls through to 
STRING_VISITOR and is then cast via TypedKey.cast(...). If a caller uses an 
unexpected type (e.g., Integer.class), this will throw ClassCastException 
instead of the usual AdbcException.notImplemented/invalidArgument behavior.



##########
java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlSessionUtil.java:
##########
@@ -0,0 +1,211 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.apache.arrow.flight.SessionOptionValueVisitor;
+
+/** Helpers for Flight SQL session option serialization and type conversion. */
+final class FlightSqlSessionUtil {
+
+  private FlightSqlSessionUtil() {}
+
+  // -- JSON helpers --
+
+  static String sessionOptionsToJson(Map<String, SessionOptionValue> opts) {
+    final StringBuilder sb = new StringBuilder("{");
+    boolean first = true;
+    for (Map.Entry<String, SessionOptionValue> e : opts.entrySet()) {
+      if (!first) sb.append(',');
+      
sb.append('"').append(escapeJson(e.getKey())).append("\":").append(sessionValueToJson(e.getValue()));
+      first = false;
+    }
+    return sb.append('}').toString();
+  }
+
+  static String sessionValueToJson(SessionOptionValue val) {
+    return val.acceptVisitor(JSON_VALUE_VISITOR);
+  }
+
+  static String toJsonArray(String[] values) {
+    final StringBuilder sb = new StringBuilder("[");
+    for (int i = 0; i < values.length; i++) {
+      if (i > 0) sb.append(',');
+      sb.append('"').append(escapeJson(values[i])).append('"');
+    }
+    return sb.append(']').toString();
+  }
+
+  static String[] parseJsonArray(String json) throws AdbcException {
+    final String trimmed = json.trim();
+    if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
+      throw AdbcException.invalidArgument(
+          "[Flight SQL] Expected JSON array for string list option, got: " + 
json);
+    }
+    final String inner = trimmed.substring(1, trimmed.length() - 1).trim();
+    if (inner.isEmpty()) {
+      return new String[0];
+    }
+    final List<String> result = new ArrayList<>();
+    int i = 0;
+    while (i < inner.length()) {
+      while (i < inner.length() && Character.isWhitespace(inner.charAt(i))) 
i++;
+      if (i >= inner.length()) break;
+      if (inner.charAt(i) != '"') {
+        throw AdbcException.invalidArgument(
+            "[Flight SQL] Invalid JSON string array element at position " + i 
+ " in: " + json);
+      }
+      i++; // skip opening quote
+      final StringBuilder elem = new StringBuilder();
+      while (i < inner.length() && inner.charAt(i) != '"') {
+        if (inner.charAt(i) == '\\' && i + 1 < inner.length()) {
+          i++;
+          switch (inner.charAt(i)) {
+            case '"': elem.append('"'); break;
+            case '\\': elem.append('\\'); break;
+            case '/': elem.append('/'); break;
+            case 'n': elem.append('\n'); break;
+            case 'r': elem.append('\r'); break;
+            case 't': elem.append('\t'); break;
+            case 'b': elem.append('\b'); break;
+            case 'f': elem.append('\f'); break;
+            case 'u':
+              if (i + 4 < inner.length()) {
+                final String hex = inner.substring(i + 1, i + 5);
+                try {
+                  elem.append((char) Integer.parseInt(hex, 16));
+                  i += 4;
+                } catch (NumberFormatException e) {
+                  elem.append('u');
+                }
+              } else {
+                elem.append('u');
+              }
+              break;

Review Comment:
   parseJsonArray silently accepts invalid \\uXXXX escapes: on malformed/short 
hex it appends a literal 'u' instead of failing. That can corrupt option values 
while still appearing to parse successfully. Since this helper is meant for 
cross-language JSON compatibility, it should reject invalid escapes with 
INVALID_ARGUMENT.



##########
java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlSessionUtil.java:
##########
@@ -0,0 +1,211 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.apache.arrow.flight.SessionOptionValueVisitor;
+
+/** Helpers for Flight SQL session option serialization and type conversion. */
+final class FlightSqlSessionUtil {
+
+  private FlightSqlSessionUtil() {}
+
+  // -- JSON helpers --
+
+  static String sessionOptionsToJson(Map<String, SessionOptionValue> opts) {
+    final StringBuilder sb = new StringBuilder("{");
+    boolean first = true;
+    for (Map.Entry<String, SessionOptionValue> e : opts.entrySet()) {
+      if (!first) sb.append(',');
+      
sb.append('"').append(escapeJson(e.getKey())).append("\":").append(sessionValueToJson(e.getValue()));
+      first = false;
+    }
+    return sb.append('}').toString();
+  }
+
+  static String sessionValueToJson(SessionOptionValue val) {
+    return val.acceptVisitor(JSON_VALUE_VISITOR);
+  }
+
+  static String toJsonArray(String[] values) {
+    final StringBuilder sb = new StringBuilder("[");
+    for (int i = 0; i < values.length; i++) {
+      if (i > 0) sb.append(',');
+      sb.append('"').append(escapeJson(values[i])).append('"');
+    }
+    return sb.append(']').toString();
+  }
+
+  static String[] parseJsonArray(String json) throws AdbcException {
+    final String trimmed = json.trim();
+    if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
+      throw AdbcException.invalidArgument(
+          "[Flight SQL] Expected JSON array for string list option, got: " + 
json);
+    }
+    final String inner = trimmed.substring(1, trimmed.length() - 1).trim();
+    if (inner.isEmpty()) {
+      return new String[0];
+    }
+    final List<String> result = new ArrayList<>();
+    int i = 0;
+    while (i < inner.length()) {
+      while (i < inner.length() && Character.isWhitespace(inner.charAt(i))) 
i++;
+      if (i >= inner.length()) break;
+      if (inner.charAt(i) != '"') {
+        throw AdbcException.invalidArgument(
+            "[Flight SQL] Invalid JSON string array element at position " + i 
+ " in: " + json);
+      }
+      i++; // skip opening quote
+      final StringBuilder elem = new StringBuilder();
+      while (i < inner.length() && inner.charAt(i) != '"') {
+        if (inner.charAt(i) == '\\' && i + 1 < inner.length()) {
+          i++;
+          switch (inner.charAt(i)) {
+            case '"': elem.append('"'); break;
+            case '\\': elem.append('\\'); break;
+            case '/': elem.append('/'); break;
+            case 'n': elem.append('\n'); break;
+            case 'r': elem.append('\r'); break;
+            case 't': elem.append('\t'); break;
+            case 'b': elem.append('\b'); break;
+            case 'f': elem.append('\f'); break;
+            case 'u':
+              if (i + 4 < inner.length()) {
+                final String hex = inner.substring(i + 1, i + 5);
+                try {
+                  elem.append((char) Integer.parseInt(hex, 16));
+                  i += 4;
+                } catch (NumberFormatException e) {
+                  elem.append('u');
+                }
+              } else {
+                elem.append('u');
+              }
+              break;
+            default: elem.append(inner.charAt(i));
+          }
+        } else {
+          elem.append(inner.charAt(i));
+        }
+        i++;
+      }
+      if (i >= inner.length()) {
+        throw AdbcException.invalidArgument(
+            "[Flight SQL] Unterminated string in JSON array: " + json);
+      }
+      i++; // skip closing quote
+      result.add(elem.toString());
+      while (i < inner.length() && Character.isWhitespace(inner.charAt(i))) 
i++;
+      if (i < inner.length() && inner.charAt(i) == ',') i++;
+    }

Review Comment:
   parseJsonArray currently allows invalid JSON like missing commas between 
elements and trailing commas (because the comma is optional after each 
element). This permissiveness can mask typos and lead to inconsistent behavior 
across languages/parsers.



##########
java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java:
##########
@@ -203,8 +213,143 @@ public void setAutoCommit(boolean enableAutoCommit) 
throws AdbcException {
     }
   }
 
+  @Override
+  @SuppressWarnings("unchecked")
+  public <T> T getOption(TypedKey<T> key) throws AdbcException {
+    final String k = key.getKey();
+
+    if (k.equals(FlightSqlConnectionProperties.SESSION_OPTIONS)) {
+      return 
key.cast(FlightSqlSessionUtil.sessionOptionsToJson(fetchSessionOptions()));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() == Boolean.class) {
+        return key.cast(val.acceptVisitor(FlightSqlSessionUtil.BOOL_VISITOR));
+      }
+      return key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_VISITOR));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX)) 
{
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() != String[].class) {
+        return AdbcConnection.super.getOption(key);
+      }
+      return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_ARRAY_VISITOR));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      try {
+        if (key.getType() == Long.class) {
+          return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.LONG_VISITOR));
+        } else if (key.getType() == Double.class) {
+          return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.DOUBLE_VISITOR));
+        }
+      } catch (NumberFormatException e) {
+        throw AdbcException.invalidArgument(
+            "[Flight SQL] Session option '" + name + "' value cannot be parsed 
as "
+                + key.getType().getSimpleName() + ": " + e.getMessage());
+      }
+      return key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_VISITOR));
+    }

Review Comment:
   For the generic session option prefix, unsupported TypedKey types will 
currently be cast from STRING_VISITOR output, which can throw 
ClassCastException. It’s safer to only return values for the explicitly 
supported types (String/Long/Double) and delegate everything else to 
AdbcConnection.super.getOption(key).



##########
java/driver/flight-sql/src/main/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlConnection.java:
##########
@@ -203,8 +213,143 @@ public void setAutoCommit(boolean enableAutoCommit) 
throws AdbcException {
     }
   }
 
+  @Override
+  @SuppressWarnings("unchecked")
+  public <T> T getOption(TypedKey<T> key) throws AdbcException {
+    final String k = key.getKey();
+
+    if (k.equals(FlightSqlConnectionProperties.SESSION_OPTIONS)) {
+      return 
key.cast(FlightSqlSessionUtil.sessionOptionsToJson(fetchSessionOptions()));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() == Boolean.class) {
+        return key.cast(val.acceptVisitor(FlightSqlSessionUtil.BOOL_VISITOR));
+      }
+      return key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_VISITOR));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX)) 
{
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      if (key.getType() != String[].class) {
+        return AdbcConnection.super.getOption(key);
+      }
+      return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_ARRAY_VISITOR));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_PREFIX.length());
+      final SessionOptionValue val = fetchSessionOptions().get(name);
+      if (val == null) {
+        throw new AdbcException(
+            "[Flight SQL] Session option not found: " + name,
+            null,
+            AdbcStatusCode.NOT_FOUND,
+            null,
+            0);
+      }
+      try {
+        if (key.getType() == Long.class) {
+          return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.LONG_VISITOR));
+        } else if (key.getType() == Double.class) {
+          return 
key.cast(val.acceptVisitor(FlightSqlSessionUtil.DOUBLE_VISITOR));
+        }
+      } catch (NumberFormatException e) {
+        throw AdbcException.invalidArgument(
+            "[Flight SQL] Session option '" + name + "' value cannot be parsed 
as "
+                + key.getType().getSimpleName() + ": " + e.getMessage());
+      }
+      return key.cast(val.acceptVisitor(FlightSqlSessionUtil.STRING_VISITOR));
+    }
+
+    return AdbcConnection.super.getOption(key);
+  }
+
+  @Override
+  public <T> void setOption(TypedKey<T> key, T value) throws AdbcException {
+    final String k = key.getKey();
+
+    if (value == null) {
+      throw AdbcException.invalidArgument(
+          "[Flight SQL] null value not allowed for key: " + k
+              + " — use adbc.flight.sql.session.optionerase.<name> to erase an 
option");
+    }
+
+    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)) {
+      final String name =
+          
k.substring(FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX.length());
+      final boolean b;
+      if (value instanceof Boolean) {
+        b = (Boolean) value;
+      } else {
+        b = Boolean.parseBoolean(value.toString());
+      }
+      doSetSessionOption(name, 
SessionOptionValueFactory.makeSessionOptionValue(b));
+
+    } else if 
(k.startsWith(FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX)) 
{
+      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)) {
+      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);
+    }
+  }
+
   @Override
   public void close() throws Exception {
+    try {
+      client.closeSession(new CloseSessionRequest());
+    } catch (FlightRuntimeException ignored) {
+      // best-effort: UNIMPLEMENTED is normal; other errors are silently 
swallowed at close time
+    }

Review Comment:
   close() currently swallows *all* FlightRuntimeException from CloseSession. 
The linked issue/PR description calls out best-effort handling specifically for 
UNIMPLEMENTED; swallowing other statuses (e.g., UNAUTHENTICATED/UNAVAILABLE) 
can hide real shutdown failures and make debugging harder.



##########
java/driver/flight-sql/src/test/java/org/apache/arrow/adbc/driver/flightsql/FlightSqlSessionTest.java:
##########
@@ -0,0 +1,466 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.arrow.adbc.core.AdbcConnection;
+import org.apache.arrow.adbc.core.AdbcDatabase;
+import org.apache.arrow.adbc.core.AdbcDriver;
+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.CallStatus;
+import org.apache.arrow.flight.CloseSessionRequest;
+import org.apache.arrow.flight.Criteria;
+import org.apache.arrow.flight.CloseSessionResult;
+import org.apache.arrow.flight.FlightDescriptor;
+import org.apache.arrow.flight.FlightInfo;
+import org.apache.arrow.flight.FlightServer;
+import org.apache.arrow.flight.FlightStream;
+import org.apache.arrow.flight.GetSessionOptionsRequest;
+import org.apache.arrow.flight.GetSessionOptionsResult;
+import org.apache.arrow.flight.Location;
+import org.apache.arrow.flight.PutResult;
+import org.apache.arrow.flight.Result;
+import org.apache.arrow.flight.SchemaResult;
+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.sql.FlightSqlProducer;
+import org.apache.arrow.flight.sql.impl.FlightSql;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.util.AutoCloseables;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for Flight SQL session management (get/set/erase options, 
CloseSession). */
+class FlightSqlSessionTest {
+  static BufferAllocator allocator;
+  static SessionProducer producer;
+  static FlightServer server;
+  static AdbcDriver driver;
+  static AdbcDatabase database;
+  AdbcConnection connection;
+
+  @BeforeAll
+  static void beforeAll() throws Exception {
+    allocator = new RootAllocator();
+    producer = new SessionProducer();
+    server =
+        FlightServer.builder()
+            .allocator(allocator)
+            .producer(producer)
+            .location(Location.forGrpcInsecure("localhost", 0))
+            .build();
+    server.start();
+    driver = new FlightSqlDriver(allocator);
+    Map<String, Object> parameters = new HashMap<>();
+    AdbcDriver.PARAM_URI.set(
+        parameters, Location.forGrpcInsecure("localhost", 
server.getPort()).getUri().toString());
+    database = driver.open(parameters);
+  }
+
+  @BeforeEach
+  void beforeEach() throws Exception {
+    producer.reset();
+    connection = database.connect();
+  }
+
+  @AfterEach
+  void afterEach() throws Exception {
+    AutoCloseables.close(connection);
+  }
+
+  @AfterAll
+  static void afterAll() throws Exception {
+    AutoCloseables.close(database, server, allocator);
+  }
+
+  @Test
+  void testSetGetStringOption() throws Exception {
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_PREFIX + "catalog", 
String.class),
+        "my_catalog");
+
+    String value =
+        connection.getOption(
+            new TypedKey<>(
+                FlightSqlConnectionProperties.SESSION_OPTION_PREFIX + 
"catalog", String.class));
+    assertThat(value).isEqualTo("my_catalog");
+  }
+
+  @Test
+  void testSetGetBoolOption() throws Exception {
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX + "flag", 
Boolean.class),
+        true);
+
+    Boolean value =
+        connection.getOption(
+            new TypedKey<>(
+                FlightSqlConnectionProperties.SESSION_OPTION_BOOL_PREFIX + 
"flag", Boolean.class));
+    assertThat(value).isTrue();
+  }
+
+  @Test
+  void testSetGetStringListOptionAsArray() throws Exception {
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX + 
"tags",
+            String[].class),
+        new String[] {"a", "b", "c"});
+
+    String[] value =
+        connection.getOption(
+            new TypedKey<>(
+                
FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX + "tags",
+                String[].class));
+    assertThat(value).containsExactly("a", "b", "c");
+  }
+
+  @Test
+  void testSetGetStringListOptionAsJson() throws Exception {
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX + 
"tags", String.class),
+        "[\"x\",\"y\"]");
+
+    String[] value =
+        connection.getOption(
+            new TypedKey<>(
+                
FlightSqlConnectionProperties.SESSION_OPTION_STRING_LIST_PREFIX + "tags",
+                String[].class));
+    assertThat(value).containsExactly("x", "y");
+  }
+
+  @Test
+  void testEraseOption() throws Exception {
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_PREFIX + "toErase", 
String.class),
+        "value");
+
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_ERASE_PREFIX + 
"toErase", String.class),
+        "");
+
+    AdbcException ex =
+        assertThrows(
+            AdbcException.class,
+            () ->
+                connection.getOption(
+                    new TypedKey<>(
+                        FlightSqlConnectionProperties.SESSION_OPTION_PREFIX + 
"toErase",
+                        String.class)));
+    assertThat(ex.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+  }
+
+  @Test
+  void testGetSessionOptionsBlob() throws Exception {
+    connection.setOption(
+        new TypedKey<>(
+            FlightSqlConnectionProperties.SESSION_OPTION_PREFIX + "k1", 
String.class),
+        "v1");
+
+    String blob =
+        connection.getOption(
+            new TypedKey<>(FlightSqlConnectionProperties.SESSION_OPTIONS, 
String.class));
+    assertThat(blob).contains("\"k1\"");
+    assertThat(blob).contains("\"v1\"");
+  }
+
+  @Test
+  void testCloseSessionCalledOnClose() throws Exception {
+    producer.closeSessionCalled.set(false);
+    connection.close();
+    connection = null; // prevent double-close in afterEach
+    assertThat(producer.closeSessionCalled.get()).isTrue();

Review Comment:
   PR description says the new unit tests cover graceful close when the server 
returns UNIMPLEMENTED for CloseSession, but this test class only exercises the 
successful CloseSession path. Either the description should be updated, or a 
test should be added that makes closeSession return UNIMPLEMENTED and asserts 
connection.close() remains best-effort.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to