Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Sources/SparkConnect/CaseInsensitiveDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// 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.
//
import Foundation

/// A dictionary in which keys are case insensitive. The input dictionary can be
/// accessed for cases where case-sensitive information is required.
public struct CaseInsensitiveDictionary: Sendable {
public var originalDictionary: [String: Sendable]
private var keyLowerCasedDictionary: [String: Sendable] = [:]

init(_ originalDictionary: [String: Sendable] = [:]) {
self.originalDictionary = originalDictionary
for (key, value) in originalDictionary {
keyLowerCasedDictionary[key.lowercased()] = value
}
}

subscript(key: String) -> Sendable? {
get {
return keyLowerCasedDictionary[key.lowercased()]
}
set {
var newMap = originalDictionary.filter { $0.key.caseInsensitiveCompare(key) != .orderedSame }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about

    set {
      let lowerKey = key.lowercased()
      if let newValue = newValue {
        keyLowerCasedDictionary[lowerKey] = newValue
      } else {
        keyLowerCasedDictionary.removeValue(forKey: lowerKey)
      }
      originalDictionary = originalDictionary.filter { $0.key.lowercased() != lowerKey }
      if let newValue = newValue {
        originalDictionary[key] = newValue
      }
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me update with yours.

newMap[key] = newValue
self.originalDictionary = newMap
self.keyLowerCasedDictionary[key.lowercased()] = newValue
}
}

public func toDictionary() -> [String: Sendable] {
return originalDictionary
}

public func toStringDictionary() -> [String: String] {
var dict = [String: String]()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return originalDictionary.mapValues { String(describing: $0) } appears to be more concise, but it might be slower than the current implementation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I updated it with your suggestion.

for (key, value) in originalDictionary {
dict[key] = String(describing: value)
}
return dict
}

public var count: Int {
return keyLowerCasedDictionary.count
}
}
5 changes: 2 additions & 3 deletions Sources/SparkConnect/DataFrameReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ public actor DataFrameReader: Sendable {

var paths: [String] = []

// TODO: Case-insensitive Map
var extraOptions: [String: String] = [:]
var extraOptions: CaseInsensitiveDictionary = CaseInsensitiveDictionary([:])

let sparkSession: SparkSession

Expand Down Expand Up @@ -84,7 +83,7 @@ public actor DataFrameReader: Sendable {
var dataSource = DataSource()
dataSource.format = self.source
dataSource.paths = self.paths
dataSource.options = self.extraOptions
dataSource.options = self.extraOptions.toStringDictionary()

var read = Read()
read.dataSource = dataSource
Expand Down
5 changes: 2 additions & 3 deletions Sources/SparkConnect/DataFrameWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ public actor DataFrameWriter: Sendable {

var saveMode: String = "default"

// TODO: Case-insensitive Map
var extraOptions: [String: String] = [:]
var extraOptions: CaseInsensitiveDictionary = CaseInsensitiveDictionary()

var partitioningColumns: [String]? = nil

Expand Down Expand Up @@ -146,7 +145,7 @@ public actor DataFrameWriter: Sendable {
write.bucketBy = bucketBy
}

for option in self.extraOptions {
for option in self.extraOptions.toStringDictionary() {
write.options[option.key] = option.value
}

Expand Down
87 changes: 87 additions & 0 deletions Tests/SparkConnectTests/CaseInsensitiveDictionaryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// 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.
//

import Foundation
import Testing

@testable import SparkConnect

/// A test suite for `CaseInsensitiveDictionary`
struct CaseInsensitiveDictionaryTests {
@Test
func empty() async throws {
let dict = CaseInsensitiveDictionary([:])
#expect(dict.count == 0)
}

@Test
func originalDictionary() async throws {
let dict = CaseInsensitiveDictionary([
"key1": "value1",
"KEY1": "VALUE1",
])
#expect(dict.count == 1)
#expect(dict.originalDictionary.count == 2)
}

@Test
func toDictionary() async throws {
let dict = CaseInsensitiveDictionary([
"key1": "value1",
"KEY1": "VALUE1",
])
#expect(dict.toDictionary().count == 2)
}

@Test
func `subscript`() async throws {
var dict = CaseInsensitiveDictionary([:])
#expect(dict.count == 0)

dict["KEY1"] = "value1"
#expect(dict.count == 1)
#expect(dict["key1"] as! String == "value1")
#expect(dict["KEY1"] as! String == "value1")
#expect(dict["KeY1"] as! String == "value1")

dict["key2"] = false
#expect(dict.count == 2)
#expect(dict["kEy2"] as! Bool == false)

dict["key3"] = 2025
#expect(dict.count == 3)
#expect(dict["key3"] as! Int == 2025)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the scenario where dict["key3"] = nil is supported as expected? I tested it, and the result seems to be correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the removal of keys are working as you expected.

Initially, I thought you are saying that supporting nil as a valid value. That is not supported because we are using [String: Sendable] type. To support nil as a value, we need to use [String: Sendable?].

Anyway, you are right in terms of the removal of keys. I accepted all the other code suggestions too.

}

@Test
func updatedOriginalDictionary() async throws {
var dict = CaseInsensitiveDictionary([
"key1": "value1",
"KEY1": "VALUE1",
])
#expect(dict.count == 1)
#expect(dict.originalDictionary.count == 2)

dict["KEY1"] = "Swift"
#expect(dict["KEY1"] as! String == "Swift")
#expect(dict.count == 1)
#expect(dict.originalDictionary.count == 1)
#expect(dict.toDictionary().count == 1)
}
}
Loading