Skip to content

Commit

Permalink
Merge pull request #25 from dannynorth/keys-exist
Browse files Browse the repository at this point in the history
Add bulk key existence checking
  • Loading branch information
mergesort authored Oct 20, 2023
2 parents a415bc2 + 6f15e37 commit b955bce
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 10 deletions.
30 changes: 20 additions & 10 deletions Sources/Bodega/DiskStorageEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public actor DiskStorageEngine: StorageEngine {
/// - data: The `Data` being stored to disk.
/// - key: A ``CacheKey`` for matching `Data` to a location on disk.
public func write(_ data: Data, key: CacheKey) throws {
let fileURL = self.concatenatedPath(key: key.value)
let fileURL = self.concatenatedPath(key: key)
let folderURL = fileURL.deletingLastPathComponent()

if !Self.directoryExists(atURL: folderURL) {
Expand All @@ -56,7 +56,7 @@ public actor DiskStorageEngine: StorageEngine {
/// - key: A ``CacheKey`` for matching `Data` to a location on disk.
/// - Returns: The `Data` stored on disk if it exists, nil if there is no `Data` stored for the `CacheKey`.
public func read(key: CacheKey) -> Data? {
return try? Data(contentsOf: self.concatenatedPath(key: key.value))
return try? Data(contentsOf: self.concatenatedPath(key: key))
}

/// Reads `Data` from disk based on the associated array of ``CacheKey``s provided as a parameter
Expand Down Expand Up @@ -109,7 +109,7 @@ public actor DiskStorageEngine: StorageEngine {
/// - key: A ``CacheKey`` for matching `Data` to a location on disk.
public func remove(key: CacheKey) throws {
do {
try FileManager.default.removeItem(at: self.concatenatedPath(key: key.value))
try FileManager.default.removeItem(at: self.concatenatedPath(key: key))
} catch CocoaError.fileNoSuchFile {
// No-op, we treat deleting a non-existent file/folder as a successful removal rather than throwing
} catch {
Expand All @@ -129,10 +129,13 @@ public actor DiskStorageEngine: StorageEngine {
}

/// Checks whether a value with a key is persisted.
/// - Parameter key: The key to for existence.
///
/// This implementation provides `O(1)` checking for the key's existence.
/// - Parameter key: The key to check for existence.
/// - Returns: If the key exists the function returns true, false if it does not.
public func keyExists(_ key: CacheKey) -> Bool {
self.allKeys().contains(key)
let fileURL = self.concatenatedPath(key: key)
return Self.fileExists(atURL: fileURL)
}

/// Iterates through a directory to find the total number of `Data` items.
Expand All @@ -159,7 +162,7 @@ public actor DiskStorageEngine: StorageEngine {
/// - key: A ``CacheKey`` for matching `Data` to a location on disk.
/// - Returns: The creation date of the `Data` on disk if it exists, nil if there is no `Data` stored for the `CacheKey`.
public func createdAt(key: CacheKey) -> Date? {
return try? self.concatenatedPath(key: key.value)
return try? self.concatenatedPath(key: key)
.resourceValues(forKeys: [.creationDateKey]).creationDate
}

Expand All @@ -168,7 +171,7 @@ public actor DiskStorageEngine: StorageEngine {
/// - key: A ``CacheKey`` for matching `Data` to a location on disk.
/// - Returns: The updatedAt date of the `Data` on disk if it exists, nil if there is no `Data` stored for the ``CacheKey``.
public func updatedAt(key: CacheKey) -> Date? {
return try? self.concatenatedPath(key: key.value)
return try? self.concatenatedPath(key: key)
.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate
}

Expand All @@ -177,7 +180,7 @@ public actor DiskStorageEngine: StorageEngine {
/// - key: A ``CacheKey`` for matching `Data` to a location on disk.
/// - Returns: The last access date of the `Data` on disk if it exists, nil if there is no `Data` stored for the ``CacheKey``.
public func lastAccessed(key: CacheKey) -> Date? {
return try? self.concatenatedPath(key: key.value)
return try? self.concatenatedPath(key: key)
.resourceValues(forKeys: [.contentAccessDateKey]).contentAccessDate
}
}
Expand All @@ -197,8 +200,15 @@ private extension DiskStorageEngine {

return FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
}

static func fileExists(atURL url: URL) -> Bool {
var isDirectory: ObjCBool = true

let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
return exists == true && isDirectory.boolValue == false
}

func concatenatedPath(key: String) -> URL {
return self.directory.url.appendingPathComponent(key)
func concatenatedPath(key: CacheKey) -> URL {
return self.directory.url.appendingPathComponent(key.value)
}
}
18 changes: 18 additions & 0 deletions Sources/Bodega/SQLiteStorageEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ public actor SQLiteStorageEngine: StorageEngine {
return false
}
}

/// Filters the provided keys to return only the ones that exist in the engine
/// - Parameter keys: The list of keys to check for existence.
/// - Returns: An array of keys that exist. This value is always a subset of the `keys` passed in.
public func keysExist(_ keys: [CacheKey]) async -> [CacheKey] {
do {
let rawKeys = keys.map(\.rawValue)

let query = Self.storageTable
.select(Self.expressions.keyRow)
.filter(rawKeys.contains(Self.expressions.keyRow))

return try self.connection.prepare(query)
.map({ CacheKey(verbatim: $0[Self.expressions.keyRow]) })
} catch {
return []
}
}

/// Iterates through the database to find the total number of `Data` items.
/// - Returns: The file/key count.
Expand Down
16 changes: 16 additions & 0 deletions Sources/Bodega/StorageEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public protocol StorageEngine: Actor {
func removeAllData() async throws

func keyExists(_ key: CacheKey) async -> Bool
func keysExist(_ keys: [CacheKey]) async -> [CacheKey]
func keyCount() async -> Int
func allKeys() async -> [CacheKey]

Expand Down Expand Up @@ -86,4 +87,19 @@ extension StorageEngine {
let allKeys = await self.allKeys()
return await self.readDataAndKeys(keys: allKeys)
}

/// Filters the provided keys to return only the ones that exist in the engine
/// - Parameter keys: The list of keys to check for existence.
/// - Returns: An array of keys that exist. This value is always a subset of the `keys` passed in.
public func keysExist(_ keys: [CacheKey]) async -> [CacheKey] {
let allKeys = await self.allKeys()
let keySet = Set(allKeys)
return keys.filter { keySet.contains($0) }
}

/// Read the number of keys located in the ``StorageEngine``.
/// - Returns: The number of keys located in the ``StorageEngine``
public func keyCount() async -> Int {
return await self.allKeys().count
}
}
10 changes: 10 additions & 0 deletions Tests/BodegaTests/DiskStorageEngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,16 @@ final class DiskStorageEngineTests: XCTestCase {
let cacheKeyExistsAfterRemovingData = await storage.keyExists(Self.testCacheKey)
XCTAssertFalse(cacheKeyExistsAfterRemovingData)
}

func testKeysExist() async throws {
let allKeys = Self.storedKeysAndData.map(\.key) + [Self.testCacheKey]
let noKeysExistBeforeAddingData = await storage.keysExist(allKeys)
XCTAssertTrue(noKeysExistBeforeAddingData.isEmpty)

try await storage.write(Self.testData, key: Self.testCacheKey)
let someKeysExist = await storage.keysExist(allKeys)
XCTAssertEqual(someKeysExist, [Self.testCacheKey])
}

func testAllKeys() async throws {
try await self.writeItemsToDisk(count: 10)
Expand Down
10 changes: 10 additions & 0 deletions Tests/BodegaTests/SQLiteStorageEngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ final class SQLiteStorageEngineTests: XCTestCase {
let cacheKeyExistsAfterRemovingData = await storage.keyExists(Self.testCacheKey)
XCTAssertFalse(cacheKeyExistsAfterRemovingData)
}

func testKeysExist() async throws {
let allKeys = Self.storedKeysAndData.map(\.key) + [Self.testCacheKey]
let noKeysExistBeforeAddingData = await storage.keysExist(allKeys)
XCTAssertTrue(noKeysExistBeforeAddingData.isEmpty)

try await storage.write(Self.testData, key: Self.testCacheKey)
let someKeysExist = await storage.keysExist(allKeys)
XCTAssertEqual(someKeysExist, [Self.testCacheKey])
}

func testAllKeys() async throws {
try await self.writeItemsToDatabase(count: 10)
Expand Down

0 comments on commit b955bce

Please sign in to comment.