This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-swift.git


The following commit(s) were added to refs/heads/main by this push:
     new 4372aad  [SPARK-60063] Fix 
Catalog.listDatabases/getDatabase/getFunction to not crash on null locationUri 
and className
4372aad is described below

commit 4372aad10988b65e3cd925eab2223b41b947b919
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Wed Oct 7 16:15:28 2026 -0700

    [SPARK-60063] Fix Catalog.listDatabases/getDatabase/getFunction to not 
crash on null locationUri and className
    
    ### What changes were proposed in this pull request?
    
    This PR aims to fix `Catalog.listDatabases`, `Catalog.getDatabase`, and 
`Catalog.getFunction` to handle nullable `locationUri` and `className` columns 
gracefully by falling back to an empty string, like `Catalog.listFunctions` 
already does.
    
    ### Why are the changes needed?
    
    Previously, these APIs force-cast the `locationUri` and `className` columns 
with `as! String`, which crashes the client process when the server returns 
`null`.
    
    - Apache Spark returns `locationUri = null` for V2 catalog namespaces 
without a location property (e.g., `JDBCTableCatalog`).
    - Apache Spark returns `className = null` for virtual operators such as 
`case`, `<>`, `!=`, and `||`.
    
    ```
    Could not cast value of type 'Swift.Optional<Any>' to 'Swift.String'.
    ```
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. These APIs now return an empty string instead of crashing. There is no 
API change because `Database.locationUri` and `Function.className` remain 
`String`.
    
    ### How was this patch tested?
    
    Pass the CIs with the newly added test cases. I also verified them manually 
against an Apache Spark 4.2.0 Connect server.
    
    ```
    $ swift test --no-parallel --filter CatalogTests
    ...
    Test run with 41 tests in 1 suite passed
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5.5
    
    Closes #589 from dongjoon-hyun/SPARK-60063.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 Sources/SparkConnect/Catalog.swift         |  8 +++----
 Tests/SparkConnectTests/CatalogTests.swift | 34 ++++++++++++++++++++++++++++++
 2 files changed, 38 insertions(+), 4 deletions(-)

diff --git a/Sources/SparkConnect/Catalog.swift 
b/Sources/SparkConnect/Catalog.swift
index a36789c..cbd44e5 100644
--- a/Sources/SparkConnect/Catalog.swift
+++ b/Sources/SparkConnect/Catalog.swift
@@ -229,7 +229,7 @@ public actor Catalog: Sendable {
     return try await df.collect().map {
       try Database(
         name: $0[0] as! String, catalog: $0[1] as? String, description: $0[2] 
as? String,
-        locationUri: $0[3] as! String)
+        locationUri: ($0[3] as? String) ?? "")
     }
   }
 
@@ -247,7 +247,7 @@ public actor Catalog: Sendable {
     return try await df.collect().map {
       try Database(
         name: $0[0] as! String, catalog: $0[1] as? String, description: $0[2] 
as? String,
-        locationUri: $0[3] as! String)
+        locationUri: ($0[3] as? String) ?? "")
     }.firstOrThrow()
   }
 
@@ -538,7 +538,7 @@ public actor Catalog: Sendable {
         catalog: $0[1] as? String,
         namespace: $0[2] as? [String],
         description: $0[3] as? String,
-        className: $0[4] as! String,
+        className: ($0[4] as? String) ?? "",
         isTemporary: $0[5] as! Bool)
     }.firstOrThrow()
   }
@@ -563,7 +563,7 @@ public actor Catalog: Sendable {
         catalog: $0[1] as? String,
         namespace: $0[2] as? [String],
         description: $0[3] as? String,
-        className: $0[4] as! String,
+        className: ($0[4] as? String) ?? "",
         isTemporary: $0[5] as! Bool)
     }.firstOrThrow()
   }
diff --git a/Tests/SparkConnectTests/CatalogTests.swift 
b/Tests/SparkConnectTests/CatalogTests.swift
index e05a9f4..8927022 100644
--- a/Tests/SparkConnectTests/CatalogTests.swift
+++ b/Tests/SparkConnectTests/CatalogTests.swift
@@ -109,6 +109,30 @@ struct CatalogTests {
     await spark.stop()
   }
 
+  @Test
+  func listAndGetDatabaseWithoutLocation() async throws {
+    let spark = try await SparkSession.builder.getOrCreate()
+    let catalog = "derby"
+    try await spark.conf.set(
+      "spark.sql.catalog.\(catalog)",
+      "org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog")
+    try await spark.conf.set(
+      "spark.sql.catalog.\(catalog).url", 
"jdbc:derby:memory:\(UUID().uuidString);create=true")
+    try await spark.conf.set(
+      "spark.sql.catalog.\(catalog).driver", 
"org.apache.derby.jdbc.EmbeddedDriver")
+    try await spark.catalog.setCurrentCatalog(catalog)
+
+    let dbs = try await spark.catalog.listDatabases(pattern: "APP")
+    #expect(dbs.count == 1)
+    #expect(dbs[0].name == "APP")
+    #expect(dbs[0].catalog == catalog)
+    #expect(dbs[0].locationUri == "")
+
+    let db = try await spark.catalog.getDatabase("APP")
+    #expect(db == dbs[0])
+    await spark.stop()
+  }
+
   @Test
   func createDatabase() async throws {
     let spark = try await SparkSession.builder.getOrCreate()
@@ -380,6 +404,16 @@ struct CatalogTests {
     await spark.stop()
   }
 
+  @Test
+  func getFunctionWithoutClassName() async throws {
+    let spark = try await SparkSession.builder.getOrCreate()
+    let function = try await spark.catalog.getFunction("case")
+    #expect(function.name == "case")
+    #expect(function.isTemporary)
+    #expect(function.className == "")
+    await spark.stop()
+  }
+
   @Test
   func listFunctions() async throws {
     let spark = try await SparkSession.builder.getOrCreate()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to