DataRaft swift package
This commit is contained in:
53
Sources/DataRaft/Protocols/DatabaseServiceKeyProvider.swift
Normal file
53
Sources/DataRaft/Protocols/DatabaseServiceKeyProvider.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
/// A protocol for supplying encryption keys to `DatabaseService` instances.
|
||||
///
|
||||
/// `DatabaseServiceKeyProvider` allows database services to delegate the responsibility of
|
||||
/// retrieving, managing, and applying encryption keys. This enables separation of concerns
|
||||
/// and allows for advanced strategies such as per-user key derivation, secure hardware-backed
|
||||
/// storage, or biometric access control.
|
||||
///
|
||||
/// When assigned to a `DatabaseService`, the provider is queried automatically whenever a
|
||||
/// connection is created or re-established (e.g., during service initialization or reconnect).
|
||||
///
|
||||
/// You can also implement error handling or diagnostics via the optional
|
||||
/// ``databaseService(_:didReceive:)`` method.
|
||||
///
|
||||
/// - Tip: You may throw from ``databaseServiceKey(_:)`` to indicate that the key is temporarily
|
||||
/// unavailable or access is denied.
|
||||
public protocol DatabaseServiceKeyProvider: AnyObject {
|
||||
/// Returns the encryption key to be applied to the given database service.
|
||||
///
|
||||
/// This method is invoked by the `DatabaseService` during initialization or reconnection
|
||||
/// to retrieve the encryption key that should be applied to the new connection.
|
||||
///
|
||||
/// Implementations may return a static key, derive it from metadata, or load it from
|
||||
/// secure storage. If the key is unavailable (e.g., user not authenticated, system locked),
|
||||
/// this method may throw to indicate failure.
|
||||
///
|
||||
/// - Parameter service: The requesting database service.
|
||||
/// - Returns: A `Connection.Key` representing the encryption key.
|
||||
/// - Throws: Any error indicating that the key cannot be retrieved.
|
||||
func databaseServiceKey(_ service: DatabaseService) throws -> Connection.Key
|
||||
|
||||
/// Notifies the provider that the database service encountered an error while applying a key.
|
||||
///
|
||||
/// This method is called when the service fails to retrieve or apply the encryption key.
|
||||
/// You can use it to report diagnostics, attempt recovery, or update internal state.
|
||||
///
|
||||
/// The default implementation is a no-op.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - service: The database service reporting the error.
|
||||
/// - error: The error encountered during key retrieval or application.
|
||||
func databaseService(_ service: DatabaseService, didReceive error: Error)
|
||||
}
|
||||
|
||||
public extension DatabaseServiceKeyProvider {
|
||||
/// Default no-op implementation of error handling callback.
|
||||
///
|
||||
/// This allows conforming types to ignore the error reporting mechanism
|
||||
/// if they do not need to respond to key failures.
|
||||
func databaseService(_ service: DatabaseService, didReceive error: Error) {}
|
||||
}
|
||||
55
Sources/DataRaft/Protocols/DatabaseServiceProtocol.swift
Normal file
55
Sources/DataRaft/Protocols/DatabaseServiceProtocol.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
/// A protocol that defines a common interface for working with a database connection.
|
||||
///
|
||||
/// Conforming types provide methods for executing closures with a live `Connection`, optionally
|
||||
/// wrapped in transactions. These closures are guaranteed to execute in a thread-safe and
|
||||
/// serialized manner. Implementations may also support reconnecting and managing encryption keys.
|
||||
public protocol DatabaseServiceProtocol: AnyObject {
|
||||
/// A closure that performs a database operation using an active connection.
|
||||
///
|
||||
/// The `Perform<T>` alias defines the signature for a database operation block
|
||||
/// that receives a live `Connection` and either returns a result or throws an error.
|
||||
/// It is commonly used to express atomic units of work in ``perform(_:)`` or
|
||||
/// ``perform(in:closure:)`` calls.
|
||||
///
|
||||
/// - Parameter T: The result type returned by the closure.
|
||||
/// - Returns: A value of type `T` produced by the closure.
|
||||
/// - Throws: Any error that occurs during execution of the database operation.
|
||||
typealias Perform<T> = (Connection) throws -> T
|
||||
|
||||
/// The object responsible for providing encryption keys for the database connection.
|
||||
///
|
||||
/// When assigned, the key provider will be queried for a new key and applied to the current
|
||||
/// connection, if available.
|
||||
var keyProvider: DatabaseServiceKeyProvider? { get set }
|
||||
|
||||
/// Re-establishes the database connection using the stored provider.
|
||||
///
|
||||
/// If a `keyProvider` is set, the returned connection will attempt to apply a new key.
|
||||
///
|
||||
/// - Throws: Any error that occurs during connection creation or key application.
|
||||
func reconnect() throws
|
||||
|
||||
/// Executes the given closure with a live connection.
|
||||
///
|
||||
/// - Parameter closure: The operation to execute.
|
||||
/// - Returns: The result produced by the closure.
|
||||
/// - Throws: Any error thrown during execution.
|
||||
func perform<T>(_ closure: Perform<T>) rethrows -> T
|
||||
|
||||
/// Executes the given closure within a transaction.
|
||||
///
|
||||
/// If no transaction is active, a new one is started and committed or rolled back as needed.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - transaction: The transaction type to begin.
|
||||
/// - closure: The operation to execute within the transaction.
|
||||
/// - Returns: The result produced by the closure.
|
||||
/// - Throws: Any error thrown by the closure or transaction.
|
||||
func perform<T>(
|
||||
in transaction: TransactionType,
|
||||
closure: Perform<T>
|
||||
) rethrows -> T
|
||||
}
|
||||
17
Sources/DataRaft/Protocols/RowDatabaseServiceProtocol.swift
Normal file
17
Sources/DataRaft/Protocols/RowDatabaseServiceProtocol.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
import DataLiteCoder
|
||||
|
||||
/// A protocol for database services that support row encoding and decoding.
|
||||
///
|
||||
/// Conforming types provide `RowEncoder` and `RowDecoder` instances for serializing
|
||||
/// and deserializing model types to and from SQLite row representations.
|
||||
///
|
||||
/// This enables strongly typed, reusable, and safe access to database records
|
||||
/// using Swift's `Codable` system.
|
||||
public protocol RowDatabaseServiceProtocol: DatabaseServiceProtocol {
|
||||
/// The encoder used to serialize values into database rows.
|
||||
var encoder: RowEncoder { get }
|
||||
|
||||
/// The decoder used to deserialize database rows into typed models.
|
||||
var decoder: RowDecoder { get }
|
||||
}
|
||||
33
Sources/DataRaft/Protocols/VersionRepresentable.swift
Normal file
33
Sources/DataRaft/Protocols/VersionRepresentable.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
|
||||
/// A constraint that defines the requirements for a type used as a database schema version.
|
||||
///
|
||||
/// This type alias specifies the minimal set of capabilities a version type must have
|
||||
/// to participate in schema migrations. Conforming types must be:
|
||||
///
|
||||
/// - `Equatable`: to check whether two versions are equal
|
||||
/// - `Comparable`: to compare versions and determine ordering
|
||||
/// - `Hashable`: to use versions as dictionary keys or in sets
|
||||
/// - `Sendable`: to ensure safe use in concurrent contexts
|
||||
///
|
||||
/// Use this alias as a base constraint when defining custom version types
|
||||
/// for use with ``VersionStorage``.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct SemanticVersion: VersionRepresentable {
|
||||
/// let major: Int
|
||||
/// let minor: Int
|
||||
/// let patch: Int
|
||||
///
|
||||
/// static func < (lhs: Self, rhs: Self) -> Bool {
|
||||
/// if lhs.major != rhs.major {
|
||||
/// return lhs.major < rhs.major
|
||||
/// }
|
||||
/// if lhs.minor != rhs.minor {
|
||||
/// return lhs.minor < rhs.minor
|
||||
/// }
|
||||
/// return lhs.patch < rhs.patch
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
public typealias VersionRepresentable = Equatable & Comparable & Hashable & Sendable
|
||||
140
Sources/DataRaft/Protocols/VersionStorage.swift
Normal file
140
Sources/DataRaft/Protocols/VersionStorage.swift
Normal file
@@ -0,0 +1,140 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
/// A protocol that defines how the database version is stored and retrieved.
|
||||
///
|
||||
/// This protocol decouples the concept of version representation from
|
||||
/// the way the version is stored. It enables flexible implementations
|
||||
/// that can store version values in different forms and places.
|
||||
///
|
||||
/// The associated `Version` type determines how the version is represented
|
||||
/// (e.g. as an integer, a semantic string, or a structured object), while the
|
||||
/// conforming type defines how that version is persisted.
|
||||
///
|
||||
/// Use this protocol to implement custom strategies for version tracking:
|
||||
/// - Store an integer version in SQLite's `user_version` field.
|
||||
/// - Store a string in a dedicated metadata table.
|
||||
/// - Store structured data in a JSON column.
|
||||
///
|
||||
/// To define your own versioning mechanism, implement `VersionStorage`
|
||||
/// and choose a `Version` type that conforms to ``VersionRepresentable``.
|
||||
///
|
||||
/// You can implement this protocol to define a custom way of storing the version
|
||||
/// of a database schema. For example, the version could be a string stored in a metadata table.
|
||||
///
|
||||
/// Below is an example of a simple implementation that stores the version string
|
||||
/// in a table named `schema_version`.
|
||||
///
|
||||
/// ```swift
|
||||
/// final class StringVersionStorage: VersionStorage {
|
||||
/// typealias Version = String
|
||||
///
|
||||
/// func prepare(_ connection: Connection) throws {
|
||||
/// let script: SQLScript = """
|
||||
/// CREATE TABLE IF NOT EXISTS schema_version (
|
||||
/// version TEXT NOT NULL
|
||||
/// );
|
||||
///
|
||||
/// INSERT INTO schema_version (version)
|
||||
/// SELECT '0.0.0'
|
||||
/// WHERE NOT EXISTS (SELECT 1 FROM schema_version);
|
||||
/// """
|
||||
/// try connection.execute(sql: script)
|
||||
/// }
|
||||
///
|
||||
/// func getVersion(_ connection: Connection) throws -> Version {
|
||||
/// let query = "SELECT version FROM schema_version LIMIT 1"
|
||||
/// let stmt = try connection.prepare(sql: query)
|
||||
/// guard try stmt.step(), let value: Version = stmt.columnValue(at: 0) else {
|
||||
/// throw DatabaseError.message("Missing version in schema_version table.")
|
||||
/// }
|
||||
/// return value
|
||||
/// }
|
||||
///
|
||||
/// func setVersion(_ connection: Connection, _ version: Version) throws {
|
||||
/// let query = "UPDATE schema_version SET version = ?"
|
||||
/// let stmt = try connection.prepare(sql: query)
|
||||
/// try stmt.bind(version, at: 0)
|
||||
/// try stmt.step()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This implementation works as follows:
|
||||
///
|
||||
/// - `prepare(_:)` creates the `schema_version` table if it does not exist, and ensures that it
|
||||
/// contains exactly one row with an initial version value (`"0.0.0"`).
|
||||
///
|
||||
/// - `getVersion(_:)` reads the current version string from the single row in the table.
|
||||
/// If the row is missing, it throws an error.
|
||||
///
|
||||
/// - `setVersion(_:_:)` updates the version string in that row. A `WHERE` clause is not necessary
|
||||
/// because the table always contains exactly one row.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Associated Types
|
||||
///
|
||||
/// - ``Version``
|
||||
///
|
||||
/// ### Instance Methods
|
||||
///
|
||||
/// - ``prepare(_:)``
|
||||
/// - ``getVersion(_:)``
|
||||
/// - ``setVersion(_:_:)``
|
||||
public protocol VersionStorage {
|
||||
/// A type representing the database schema version.
|
||||
associatedtype Version: VersionRepresentable
|
||||
|
||||
/// Prepares the storage mechanism for tracking the schema version.
|
||||
///
|
||||
/// This method is called before any version operations. Use it to create required tables
|
||||
/// or metadata structures needed for version management.
|
||||
///
|
||||
/// - Important: This method is executed within an active migration transaction.
|
||||
/// Do not issue `BEGIN` or `COMMIT` manually. If this method throws an error,
|
||||
/// the entire migration process will be aborted and rolled back.
|
||||
///
|
||||
/// - Parameter connection: The database connection used for schema preparation.
|
||||
/// - Throws: An error if preparation fails.
|
||||
func prepare(_ connection: Connection) throws
|
||||
|
||||
/// Returns the current schema version stored in the database.
|
||||
///
|
||||
/// This method must return a valid version previously stored by the migration system.
|
||||
///
|
||||
/// - Important: This method is executed within an active migration transaction.
|
||||
/// Do not issue `BEGIN` or `COMMIT` manually. If this method throws an error,
|
||||
/// the entire migration process will be aborted and rolled back.
|
||||
///
|
||||
/// - Parameter connection: The database connection used to fetch the version.
|
||||
/// - Returns: The version currently stored in the database.
|
||||
/// - Throws: An error if reading fails or the version is missing.
|
||||
func getVersion(_ connection: Connection) throws -> Version
|
||||
|
||||
/// Stores the given version as the current schema version.
|
||||
///
|
||||
/// This method is called at the end of the migration process to persist
|
||||
/// the final schema version after all migration steps have completed successfully.
|
||||
///
|
||||
/// - Important: This method is executed within an active migration transaction.
|
||||
/// Do not issue `BEGIN` or `COMMIT` manually. If this method throws an error,
|
||||
/// the entire migration process will be aborted and rolled back.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - connection: The database connection used to write the version.
|
||||
/// - version: The version to store.
|
||||
/// - Throws: An error if writing fails.
|
||||
func setVersion(_ connection: Connection, _ version: Version) throws
|
||||
}
|
||||
|
||||
public extension VersionStorage {
|
||||
/// A default implementation that performs no preparation.
|
||||
///
|
||||
/// Override this method if your storage implementation requires any setup,
|
||||
/// such as creating a version table or inserting an initial value.
|
||||
///
|
||||
/// If you override this method and it throws an error, the migration process
|
||||
/// will be aborted and rolled back.
|
||||
func prepare(_ connection: Connection) throws {}
|
||||
}
|
||||
Reference in New Issue
Block a user