Skip to content

Commit

Permalink
PersistenceContainer is Sendable
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Aug 31, 2024
1 parent c08b1fa commit 244cafb
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 71 deletions.
3 changes: 0 additions & 3 deletions GRDB/QueryInterface/SQL/SQLRelation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1027,9 +1027,6 @@ extension Row: ColumnAddressable {
/// PersistenceContainer has columns
extension PersistenceContainer: ColumnAddressable {
func index(forColumn column: String) -> String? { column }
func databaseValue(at column: String) -> DatabaseValue {
self[caseInsensitive: column]?.databaseValue ?? .null
}
}

// MARK: - Merging
Expand Down
97 changes: 40 additions & 57 deletions GRDB/Record/EncodableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ extension EncodableRecord {
/// database representation.
public var databaseDictionary: [String: DatabaseValue] {
get throws {
try Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null }
try Dictionary(uniqueKeysWithValues: PersistenceContainer(self))
}
}
}
Expand Down Expand Up @@ -336,19 +336,30 @@ extension EncodableRecord {
///
/// `PersistenceContainer` is the argument of the
/// ``EncodableRecord/encode(to:)-k9pf`` method.
public struct PersistenceContainer {
// fileprivate for Row(_:PersistenceContainer)
public struct PersistenceContainer: Sendable {
// The ordering of the OrderedDictionary helps generating always the same
// SQL queries, and hit the statement cache.
fileprivate var storage: OrderedDictionary<String, (any DatabaseValueConvertible)?>
private var storage: OrderedDictionary<CaseInsensitiveIdentifier, DatabaseValue>

/// The value associated with the given column.
///
/// The setter accepts any ``DatabaseValueConvertible`` type, but the
/// getter always returns a ``DatabaseValue``.
public subscript(_ column: String) -> (any DatabaseValueConvertible)? {
get { self[caseInsensitive: column] }
set { storage.updateValue(newValue, forKey: column) }
get {
storage[CaseInsensitiveIdentifier(rawValue: column)]
}
set {
storage.updateValue(
newValue?.databaseValue ?? .null,
forKey: CaseInsensitiveIdentifier(rawValue: column))
}
}

/// The value associated with the given column.
///
/// The setter accepts any ``DatabaseValueConvertible`` type, but the
/// getter always returns a ``DatabaseValue``.
public subscript(_ column: some ColumnExpression) -> (any DatabaseValueConvertible)? {
get { self[column.name] }
set { self[column.name] = newValue }
Expand Down Expand Up @@ -378,80 +389,52 @@ public struct PersistenceContainer {
}

/// Columns stored in the container, ordered like values.
var columns: [String] { Array(storage.keys) }
var columns: [String] { storage.keys.map(\.rawValue) }

/// Values stored in the container, ordered like columns.
var values: [(any DatabaseValueConvertible)?] { Array(storage.values) }

/// Accesses the value associated with the given column, in a
/// case-insensitive fashion.
subscript(caseInsensitive column: String) -> (any DatabaseValueConvertible)? {
get {
if let value = storage[column] {
return value
}
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value
}
return nil
}
set {
if storage[column] != nil {
storage[column] = newValue
return
}
let lowercaseColumn = column.lowercased()
for key in storage.keys where key.lowercased() == lowercaseColumn {
storage[key] = newValue
return
}

storage[column] = newValue
}
}
var values: [DatabaseValue] { storage.values }

// Returns nil if column is not defined
func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? {
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value?.databaseValue ?? .null
}
return nil
}

var isEmpty: Bool { storage.isEmpty }

/// An iterator over the (column, value) pairs
func makeIterator() -> IndexingIterator<OrderedDictionary<String, (any DatabaseValueConvertible)?>> {
storage.makeIterator()
/// Returns ``DatabaseValue/null`` if column is not defined
func databaseValue(at column: String) -> DatabaseValue {
storage[CaseInsensitiveIdentifier(rawValue: column)] ?? .null
}

@usableFromInline
func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> {
var newValueIterator = makeIterator()
var newValueIterator = storage.makeIterator()
return AnyIterator {
// Loop until we find a change, or exhaust columns:
while let (column, newValue) = newValueIterator.next() {
let oldValue = container[caseInsensitive: column]
let oldDbValue = oldValue?.databaseValue ?? .null
let newDbValue = newValue?.databaseValue ?? .null
while let (column, newDbValue) = newValueIterator.next() {
let oldDbValue = container.storage[column] ?? .null
if newDbValue != oldDbValue {
return (column, oldDbValue)
return (column.rawValue, oldDbValue)
}
}
return nil
}
}
}

extension PersistenceContainer: RandomAccessCollection {
public typealias Index = Int

public var startIndex: Int { storage.startIndex }
public var endIndex: Int { storage.endIndex }

/// Returns the (column, value) pair at given index.
public subscript(position: Int) -> (String, DatabaseValue) {
let element = storage[position]
return (element.key.rawValue, element.value)
}
}

extension Row {
convenience init<Record: EncodableRecord>(_ record: Record) throws {
try self.init(PersistenceContainer(record))
}

convenience init(_ container: PersistenceContainer) {
self.init(Dictionary(container.storage))
self.init(impl: ArrayRowImpl(columns: container.lazy.map { ($0, $1) }))
}
}

Expand Down
10 changes: 5 additions & 5 deletions GRDB/Record/MutablePersistableRecord+DAO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ final class DAO<Record: MutablePersistableRecord> {
// Fail early if primary key does not resolve to a database row.
let primaryKeyColumns = primaryKey.columns
let primaryKeyValues = primaryKeyColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
persistenceContainer.databaseValue(at: $0)
}
if primaryKeyValues.allSatisfy({ $0.isNull }) {
return nil
Expand Down Expand Up @@ -173,7 +173,7 @@ final class DAO<Record: MutablePersistableRecord> {
}

let updatedValues = updatedColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
persistenceContainer.databaseValue(at: $0)
}

let query = UpdateQuery(
Expand All @@ -193,7 +193,7 @@ final class DAO<Record: MutablePersistableRecord> {
// Fail early if primary key does not resolve to a database row.
let primaryKeyColumns = primaryKey.columns
let primaryKeyValues = primaryKeyColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
persistenceContainer.databaseValue(at: $0)
}
if primaryKeyValues.allSatisfy({ $0.isNull }) {
return nil
Expand All @@ -212,7 +212,7 @@ final class DAO<Record: MutablePersistableRecord> {
// Fail early if primary key does not resolve to a database row.
let primaryKeyColumns = primaryKey.columns
let primaryKeyValues = primaryKeyColumns.map {
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
persistenceContainer.databaseValue(at: $0)
}
if primaryKeyValues.allSatisfy({ $0.isNull }) {
return nil
Expand All @@ -229,7 +229,7 @@ final class DAO<Record: MutablePersistableRecord> {
/// Throws a RecordError.recordNotFound error
func recordNotFound() throws -> Never {
let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map {
($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null)
($0, persistenceContainer.databaseValue(at: $0))
})
throw RecordError.recordNotFound(
databaseTableName: databaseTableName,
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Record/MutablePersistableRecord+Insert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ extension MutablePersistableRecord {
// to false in its `aroundInsert` callback.
var persistenceContainer = dao.persistenceContainer
if let rowIDColumn {
persistenceContainer[caseInsensitive: rowIDColumn] = rowid
persistenceContainer[rowIDColumn] = rowid
}

let inserted = InsertionSuccess(
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Record/MutablePersistableRecord+Save.swift
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ extension MutablePersistableRecord {
let primaryKeyInfo = try db.primaryKey(databaseTableName)
let container = try PersistenceContainer(db, self)
let primaryKey = Dictionary(uniqueKeysWithValues: primaryKeyInfo.columns.map {
($0, container[caseInsensitive: $0]?.databaseValue ?? .null)
($0, container.databaseValue(at: $0))
})
if primaryKey.allSatisfy({ $0.value.isNull }) {
return nil
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Record/MutablePersistableRecord+Upsert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ extension MutablePersistableRecord {
var persistenceContainer = dao.persistenceContainer
let rowIDColumn = dao.primaryKey.rowIDColumn
if let rowIDColumn {
persistenceContainer[caseInsensitive: rowIDColumn] = rowid
persistenceContainer[rowIDColumn] = rowid
}

let inserted = InsertionSuccess(
Expand Down
3 changes: 1 addition & 2 deletions GRDB/Record/Record.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,7 @@ open class Record {
var newValueIterator = try PersistenceContainer(self).makeIterator()
return AnyIterator {
// Loop until we find a change, or exhaust columns:
while let (column, newValue) = newValueIterator.next() {
let newDbValue = newValue?.databaseValue ?? .null
while let (column, newDbValue) = newValueIterator.next() {
guard let oldRow, let oldDbValue: DatabaseValue = oldRow[column] else {
return (column, nil)
}
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Record/TableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ extension TableRecord where Self: EncodableRecord {

let container = try PersistenceContainer(db, self)
let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map {
($0, container[caseInsensitive: $0]?.databaseValue ?? .null)
($0, container.databaseValue(at: $0))
})
return RecordError.recordNotFound(
databaseTableName: databaseTableName,
Expand Down

0 comments on commit 244cafb

Please sign in to comment.