Skip to content

Commit

Permalink
Correcting SQLiteStorageEngine assumptions, adding keyExists, and upd…
Browse files Browse the repository at this point in the history
…ating SQLite/Objectstorage tests

- Adding keyExists method to StorageEngine.
- Removing primaryKey constraint from keyRow so it can be overwritten with new writes.
- Changing ObjectStorage creationDate to createdAt.
- Adding SQLiteStorageEngine tests.
- Adding tests for ObjectStorage's ability to integrate both StorageEngines.
- Updating many tests along the way.
  • Loading branch information
mergesort committed Jul 16, 2022
1 parent 60de735 commit 6ea3668
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 105 deletions.
7 changes: 7 additions & 0 deletions Sources/Bodega/DiskStorageEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ public actor DiskStorageEngine: StorageEngine {
return self.allKeys().count
}

/// Checks whether a value with a key is persisted.
/// - Parameter key: The key to 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)
}

/// Iterates through a `directory` to find all of the keys.
/// - Returns: An array of the keys contained in a directory.
public func allKeys() -> [CacheKey] {
Expand Down
3 changes: 1 addition & 2 deletions Sources/Bodega/ObjectStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,13 @@ public actor ObjectStorage {
/// - Parameters:
/// - key: A `CacheKey` for matching an `Object`.
/// - Returns: The creation date of the `Object` if it exists, nil if there is no `Object` stored for the `CacheKey`.
public func creationDate(forKey key: CacheKey, subdirectory: String? = nil) async -> Date? {
public func createdAt(forKey key: CacheKey) async -> Date? {
return await storage.createdAt(key: key)
}

/// Returns the modification date for the object represented by the `CacheKey`, if it exists.
/// - Parameters:
/// - key: A `CacheKey` for matching an `Object`.
/// - subdirectory: An optional subdirectory the caller can read from.
/// - Returns: The modification date of the object if it exists, nil if there is no object stored for the `CacheKey`.
public func updatedAt(forKey key: CacheKey) async -> Date? {
return await storage.updatedAt(key: key)
Expand Down
45 changes: 36 additions & 9 deletions Sources/Bodega/SQLiteStorageEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public actor SQLiteStorageEngine: StorageEngine {
self.connection = try Connection(directory.url.appendingPathExtension("sqlite3").absoluteString)

try self.connection.run(Self.storageTable.create(ifNotExists: true) { table in
table.column(Self.expressions.keyRow, primaryKey: true)
table.column(Self.expressions.keyRow)
table.column(Self.expressions.dataRow)
table.column(Self.expressions.createdAtRow, defaultValue: Date())
table.column(Self.expressions.updatedAtRow, defaultValue: Date())
Expand All @@ -62,14 +62,21 @@ public actor SQLiteStorageEngine: StorageEngine {
/// - data: The `Data` being stored to disk.
/// - key: A `CacheKey` for matching `Data`.
public func write(_ data: Data, key: CacheKey) throws {
try self.connection.run(
Self.storageTable.insert(
or: .replace,
Self.expressions.keyRow <- key.rawValue,
Self.expressions.dataRow <- data,
Self.expressions.updatedAtRow <- Date()
let values = [
Self.expressions.keyRow <- key.rawValue,
Self.expressions.dataRow <- data,
Self.expressions.updatedAtRow <- Date()
]

if self.keyExists(key) {
try self.connection.run(
Self.storageTable.update(values)
)
)
} else {
try self.connection.run(
Self.storageTable.insert(values)
)
}
}

/// Writes an array of `Data` items to the database with their associated `CacheKey` from the tuple.
Expand Down Expand Up @@ -199,12 +206,32 @@ public actor SQLiteStorageEngine: StorageEngine {
/// - Returns: The file/key count.
public func keyCount() -> Int {
do {
return try self.connection.scalar(Self.storageTable.select(Self.expressions.keyRow.distinct.count))
return try self.connection.scalar(
Self.storageTable.select(
Self.expressions.keyRow.distinct.count
)
)
} catch {
return 0
}
}

/// Checks whether a value with a key is persisted.
/// - Parameter key: The key to for existence.
/// - Returns: If the key exists the function returns true, false if it does not.
public func keyExists(_ key: CacheKey) -> Bool {
do {
let query = Self.storageTable
.select(Self.expressions.keyRow)
.filter(Self.expressions.keyRow == key.rawValue)
.limit(1)

return try self.connection.pluck(query)?[Self.expressions.keyRow] != nil
} catch {
return false
}
}

/// Iterates through the database to find all of the keys.
/// - Returns: An array of the keys contained in a directory.
public func allKeys() -> [CacheKey] {
Expand Down
1 change: 1 addition & 0 deletions Sources/Bodega/StorageEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public protocol StorageEngine: Actor {
func removeAllData() throws

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

func createdAt(key: CacheKey) -> Date?
Expand Down
83 changes: 42 additions & 41 deletions Tests/BodegaTests/DiskStorageEngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ final class DiskStorageEngineTests: XCTestCase {
XCTAssertEqual(overwrittenKeyCount, 10)
}

func testKeyExists() async throws {
let cacheKeyExistsBeforeAddingData = await storage.keyExists(Self.testCacheKey)
XCTAssertFalse(cacheKeyExistsBeforeAddingData)

try await storage.write(Self.testData, key: Self.testCacheKey)
let cacheKeyExistsAfterAddingData = await storage.keyExists(Self.testCacheKey)
XCTAssertTrue(cacheKeyExistsAfterAddingData)

try await storage.remove(key: Self.testCacheKey)
let cacheKeyExistsAfterRemovingData = await storage.keyExists(Self.testCacheKey)
XCTAssertFalse(cacheKeyExistsAfterRemovingData)
}

func testAllKeys() async throws {
try await self.writeItemsToDisk(count: 10)
let allKeys = await storage.allKeys().sorted(by: { $0.value < $1.value })
Expand All @@ -207,64 +220,52 @@ final class DiskStorageEngineTests: XCTestCase {
XCTAssertEqual(allKeys.count, 10)
}

func testCreationDate() async throws {
// Make sure the modificationDate is nil if the key hasn't been stored
var modificationDate = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNil(modificationDate)
// Unlike most StorageEngines when a DiskStorage rewrites data the createdAt changes.
// Since we are overwriting a file, a new file is created, with a new createdAt.
func testCreatedAtDate() async throws {
// Make sure the createdAt is nil if the key hasn't been stored
let initialCreatedAt = await storage.createdAt(key: Self.testCacheKey)
XCTAssertNil(initialCreatedAt)

// Make sure the modification date is in the right range if it has been stored
var dateBefore = Date()
// Make sure the createdAt is in the right range if it has been stored
try await storage.write(Self.testData, key: Self.testCacheKey)
var dateAfter = Date()
modificationDate = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNotNil(modificationDate)
XCTAssertLessThanOrEqual(dateBefore, modificationDate!)
XCTAssertLessThanOrEqual(modificationDate!, dateAfter)
let firstWriteDate = await storage.createdAt(key: Self.testCacheKey)

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the modification date is updated when the data is re-written
dateBefore = Date()
// Make sure the createdAt date is not updated when the data is re-written
try await storage.write(Self.testData, key: Self.testCacheKey)
dateAfter = Date()
modificationDate = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNotNil(modificationDate)
XCTAssertLessThanOrEqual(dateBefore, modificationDate!)
XCTAssertLessThanOrEqual(modificationDate!, dateAfter)
let secondWriteDate = await storage.createdAt(key: Self.testCacheKey)

// DiskStorageEngine will overwrite the original data so unlike other engines
// a new `createdAt` will be generated on write.
XCTAssertNotEqual(firstWriteDate, secondWriteDate)
}

func testUpdatedAtDate() async throws {
// Make sure the modificationDate is nil if the key hasn't been stored
var modificationDate = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNil(modificationDate)

// Make sure the modification date is in the right range if it has been stored
var dateBefore = Date()
// Make sure the updatedAt is nil if the key hasn't been stored
let initialUpdatedAt = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNil(initialUpdatedAt)

// Make sure the updatedAt is in the right range if it has been stored
try await storage.write(Self.testData, key: Self.testCacheKey)
var dateAfter = Date()
modificationDate = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNotNil(modificationDate)
XCTAssertLessThanOrEqual(dateBefore, modificationDate!)
XCTAssertLessThanOrEqual(modificationDate!, dateAfter)

let firstWriteDate = await storage.updatedAt(key: Self.testCacheKey)

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the modification date is updated when the data is re-written
dateBefore = Date()

// Make sure the updatedAt date is updated when the data is re-written
try await storage.write(Self.testData, key: Self.testCacheKey)
dateAfter = Date()
modificationDate = await storage.updatedAt(key: Self.testCacheKey)
XCTAssertNotNil(modificationDate)
XCTAssertLessThanOrEqual(dateBefore, modificationDate!)
XCTAssertLessThanOrEqual(modificationDate!, dateAfter)
let secondWriteDate = await storage.updatedAt(key: Self.testCacheKey)

XCTAssertNotEqual(firstWriteDate, secondWriteDate)
}

func testLastAccessDate() async throws {
// Make sure the accessDate is nil if the key hasn't been stored
// Make sure lastAccessed is nil if the key hasn't been stored
var accessDate = await storage.lastAccessed(key: Self.testCacheKey)
XCTAssertNil(accessDate)

// Make sure the access date is in the right range if it has been stored
// Make sure lastAccessed is in the right range if it has been stored
var dateBefore = Date()
try await storage.write(Self.testData, key: Self.testCacheKey)
var dateAfter = Date()
Expand All @@ -275,7 +276,7 @@ final class DiskStorageEngineTests: XCTestCase {

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the access date is updated when the data is read
// Make sure lastAccessed is updated when the data is read
dateBefore = Date()
let data = await storage.read(key: Self.testCacheKey)
dateAfter = Date()
Expand Down
140 changes: 87 additions & 53 deletions Tests/BodegaTests/ObjectStorageTests.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,90 @@
import XCTest
@testable import Bodega

final class ObjectStorageTests: XCTestCase {
// Testing an ObjectStorage instance that's backed by a SQLiteStorageEngine
final class SQLiteStorageEngineBackedObjectStorageTests: ObjectStorageTests {

private var storage: ObjectStorage!
override func setUp() async throws {
storage = ObjectStorage(
storage: SQLiteStorageEngine(directory: .temporary(appendingPath: "SQLiteTests"))!
)

try await storage.removeAllObjects()
}

// Like most StorageEngines when a SQLiteStorageEngine rewrites data the createdAt does not change.
// This is in contrast to DiskStorage where we are overwriting a file
// and a new file with a new createdAt is created.
func testCreatedAtDate() async throws {
// Make sure the createdAt is nil if the key hasn't been stored
let initialCreatedAt = await storage.createdAt(forKey: Self.testCacheKey)
XCTAssertNil(initialCreatedAt)

private var diskBackedObjectStorage: ObjectStorage!
private var sqliteBackedObjectStorage: ObjectStorage!
// Make sure the createdAt is in the right range if it has been stored
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
let firstWriteDate = await storage.createdAt(forKey: Self.testCacheKey)

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the createdAt date is not updated when the data is re-written
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
let secondWriteDate = await storage.createdAt(forKey: Self.testCacheKey)

XCTAssertEqual(firstWriteDate, secondWriteDate)
}

}

// Testing an ObjectStorage instance that's backed by a DiskStorageEngine
final class DiskStorageEngineBackedObjectStorageTests: ObjectStorageTests {

override func setUp() async throws {
let diskStorage = DiskStorageEngine(directory: .temporary(appendingPath: "FileSystemTests"))
let sqliteStorage = SQLiteStorageEngine(directory: .temporary(appendingPath: "SQLiteTests"))!
storage = ObjectStorage(
storage: DiskStorageEngine(directory: .temporary(appendingPath: "DiskStorageTests"))
)

try await storage.removeAllObjects()
}

// Unlike most StorageEngines when a DiskStorage rewrites data the createdAt changes.
// Since we are overwriting a file, a new file with a new createdAt is created.
func testCreatedAtDate() async throws {
// Make sure the createdAt is nil if the key hasn't been stored
let initialCreatedAt = await storage.createdAt(forKey: Self.testCacheKey)
XCTAssertNil(initialCreatedAt)

// Remove this soon
storage = ObjectStorage(storage: diskStorage)
// Make sure the createdAt is in the right range if it has been stored
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
let firstWriteDate = await storage.createdAt(forKey: Self.testCacheKey)

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the createdAt date is not updated when the data is re-written
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
let secondWriteDate = await storage.createdAt(forKey: Self.testCacheKey)

// DiskStorageEngine will overwrite the original data so unlike other engines
// a new `createdAt` will be generated on write.
XCTAssertNotEqual(firstWriteDate, secondWriteDate)
}

}

class ObjectStorageTests: XCTestCase {

diskBackedObjectStorage = ObjectStorage(storage: diskStorage)
sqliteBackedObjectStorage = ObjectStorage(storage: sqliteStorage)
fileprivate var storage: ObjectStorage!

// You should run SQLiteStorageEngineBackedObjectStorageTests and DiskStorageEngineBackedObjectStorageTests
// but not ObjectStorageTests since it's only here for the purpose of shared code.
// Since this can run on it's own, instead what we do is pick one of the two storages at random
// and let the tests run, since they should pass anyhow as long as the storages work.
override func setUp() async throws {
let diskStorageEngine = DiskStorageEngine(directory: .temporary(appendingPath: "FileSystemTests"))
let sqliteStorageEngine = SQLiteStorageEngine(directory: .temporary(appendingPath: "SQLiteTests"))!

storage = ObjectStorage(
storage: [diskStorageEngine, sqliteStorageEngine].randomElement()!
)

try await storage.removeAllObjects()
}
Expand Down Expand Up @@ -217,56 +285,22 @@ final class ObjectStorageTests: XCTestCase {
XCTAssertEqual(allKeys.count, 10)
}

func testCreationDate() async throws {
// Make sure the creationDate is nil if the key hasn't been stored
var creationDate = await storage.creationDate(forKey: Self.testCacheKey)
XCTAssertNil(creationDate)
func testUpdatedAtDate() async throws {
// Make sure the updatedAt is nil if the key hasn't been stored
let initialUpdatedAt = await storage.updatedAt(forKey: Self.testCacheKey)
XCTAssertNil(initialUpdatedAt)

// Make sure the modification date is in the right range if it has been stored
var dateBefore = Date()
// Make sure the updatedAt is in the right range if it has been stored
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
var dateAfter = Date()
creationDate = await storage.creationDate(forKey: Self.testCacheKey)
XCTAssertNotNil(creationDate)
XCTAssertLessThanOrEqual(dateBefore, creationDate!)
XCTAssertLessThanOrEqual(creationDate!, dateAfter)
let firstWriteDate = await storage.updatedAt(forKey: Self.testCacheKey)

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the creationDate date is updated when the data is re-written
dateBefore = Date()
// Make sure the updatedAt date is updated when the data is re-written
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
dateAfter = Date()
creationDate = await storage.creationDate(forKey: Self.testCacheKey)
XCTAssertNotNil(creationDate)
XCTAssertLessThanOrEqual(dateBefore, creationDate!)
XCTAssertLessThanOrEqual(creationDate!, dateAfter)
}
let secondWriteDate = await storage.updatedAt(forKey: Self.testCacheKey)

func testUpdatedAtDate() async throws {
// Make sure the modificationDate is nil if the key hasn't been stored
var modificationDate = await storage.updatedAt(forKey: Self.testCacheKey)
XCTAssertNil(modificationDate)

// Make sure the modification date is in the right range if it has been stored
var dateBefore = Date()
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
var dateAfter = Date()
modificationDate = await storage.updatedAt(forKey: Self.testCacheKey)
XCTAssertNotNil(modificationDate)
XCTAssertLessThanOrEqual(dateBefore, modificationDate!)
XCTAssertLessThanOrEqual(modificationDate!, dateAfter)

try await Task.sleep(nanoseconds: 1_000_000)

// Make sure the modification date is updated when the data is re-written
dateBefore = Date()
try await storage.store(Self.testObject, forKey: Self.testCacheKey)
dateAfter = Date()
modificationDate = await storage.updatedAt(forKey: Self.testCacheKey)
XCTAssertNotNil(modificationDate)
XCTAssertLessThanOrEqual(dateBefore, modificationDate!)
XCTAssertLessThanOrEqual(modificationDate!, dateAfter)
XCTAssertNotEqual(firstWriteDate, secondWriteDate)
}

}
Expand Down
Loading

0 comments on commit 6ea3668

Please sign in to comment.