Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
54 changes: 43 additions & 11 deletions Sources/PostgreSQL/Codable/PostgreSQLRowDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,48 @@ struct PostgreSQLRowDecoder {
let codingPath: [CodingKey] = []
let row: [PostgreSQLColumn: PostgreSQLData]
let tableOID: UInt32
let allKeys: [Key]
var allKeys: [Key] {
// Unlikely to be called (mostly present for protocol conformance), so we don't need to cache this property.
return row.keys
.compactMap { col in
if tableOID == 0 || col.tableOID == tableOID || col.tableOID == 0 {
return col.name
} else {
return nil
}
}.compactMap(Key.init(stringValue:))
}

init(row: [PostgreSQLColumn: PostgreSQLData], tableOID: UInt32) {
self.row = row
self.tableOID = tableOID
self.allKeys = row.keys.compactMap { col in
if tableOID == 0 || col.tableOID == tableOID || col.tableOID == 0 {
return col.name
} else {
return nil
}
}.compactMap(Key.init(stringValue:))
}

private func data(for key: Key) -> PostgreSQLData? {
let columnName = key.stringValue
var column = PostgreSQLColumn(tableOID: self.tableOID, name: columnName)
// First, check for an exact (tableOID, columnName) match.
if let data = row[column] { return data }

if self.tableOID != 0 {
// No column with our exact table OID; check for a (0, columnName) match instead.
column.tableOID = 0
return row[column]
} else {
// No (0, columnName) match; check via (slow!) linear search for _any_ matching column name,
// regardless of tableOID.
// Note: This path is hit in `PostgreSQLConnection.tableNames`, but luckily the `PGClass` only has
// two keys, so the performance impact of linear search is acceptable there.
return row.firstValue(tableOID: tableOID, name: columnName)
}
}

func contains(_ key: Key) -> Bool {
return allKeys.contains { $0.stringValue == key.stringValue }
return data(for: key) != nil
}

func decodeNil(forKey key: Key) throws -> Bool {
guard let data = row.firstValue(tableOID: tableOID, name: key.stringValue) else {
guard let data = data(for: key) else {
return true
}
switch data.storage {
Expand All @@ -76,12 +98,22 @@ struct PostgreSQLRowDecoder {
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
guard let data = row.firstValue(tableOID: tableOID, name: key.stringValue) else {
guard let data = data(for: key) else {
throw DecodingError.valueNotFound(T.self, .init(codingPath: codingPath + [key], debugDescription: "Could not decode \(T.self)."))
}
return try PostgreSQLDataDecoder().decode(T.self, from: data)
}

// This specialization avoids two dictionary lookups (caused by calls to `contains` and `decodeNil`) present in
// the default implementation of `decodeIfPresent`.
func decodeIfPresent<T>(_ type: T.Type, forKey key: Key) throws -> T? where T : Decodable {
guard let data = data(for: key) else { return nil }
switch data.storage {
case .null: return nil
default: return try PostgreSQLDataDecoder().decode(T.self, from: data)
}
}

func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
fatalError()
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/PostgreSQL/Column/PostgreSQLColumn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ extension PostgreSQLColumn: CustomStringConvertible {

extension Dictionary where Key == PostgreSQLColumn {
/// Accesses the _first_ value from this dictionary with a matching field name.
///
/// - Note: This performs a linear search over the dictionary and thus is fairly slow.
public func firstValue(tableOID: UInt32 = 0, name: String) -> Value? {
for (column, data) in self {
if (tableOID == 0 || column.tableOID == 0 || column.tableOID == tableOID) && column.name == name {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
private struct PGClass: PostgreSQLTable {
static let sqlTableIdentifierString = "pg_class"
var oid: UInt32
var relname: String
}

extension PostgreSQLConnection {
/// Caches table OID to string name associations.
public struct TableNameCache {
/// Stores table names. [OID: Name]
private let tableNames: [UInt32: String]
/// Stores table OIDs. [Name: OID]
/// Used to accelerate the Name -> OID lookup.
private let tableOIDs: [String: UInt32]

/// Fetches the table name for a given table OID. Returns `nil` if no table with that OID is known.
///
Expand All @@ -19,17 +28,19 @@ extension PostgreSQLConnection {
/// - name: Table name.
/// - returns: Table OID.
public func tableOID(name: String) -> UInt32? {
for (key, val) in tableNames {
if val == name {
return key
}
}
return nil
return tableOIDs[name]
}

/// Creates a new cache.
init(_ tableNames: [UInt32: String]) {
fileprivate init(_ tableClasses: [PGClass]) {
var tableNames: [UInt32: String] = [:]
var tableOIDs: [String: UInt32] = [:]
for tableClass in tableClasses {
tableNames[tableClass.oid] = tableClass.relname
tableOIDs[tableClass.relname] = tableClass.oid
}
self.tableNames = tableNames
self.tableOIDs = tableOIDs
}
}

Expand All @@ -42,18 +53,9 @@ extension PostgreSQLConnection {
if let existing = tableNameCache, !refresh {
return future(existing)
} else {
struct PGClass: PostgreSQLTable {
static let sqlTableIdentifierString = "pg_class"
var oid: UInt32
var relname: String
}
return select().column("oid").column("relname").from(PGClass.self).all().map { rows in
var cache: [UInt32: String] = [:]
let rows = try rows.map { try self.decode(PGClass.self, from: $0, table: nil) }
for row in rows {
cache[row.oid] = row.relname
}
let new = TableNameCache(cache)
let new = TableNameCache(rows)
self.tableNameCache = new
return new
}
Expand Down