Files
data-lite-core/Sources/DataLiteCore/Classes/Statement+Arguments.swift

414 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import OrderedCollections
extension Statement {
/// A structure representing a set of arguments used in database statements.
///
/// `Arguments` provides a convenient way to manage and pass parameters to database queries.
/// It supports both indexed and named tokens, allowing flexibility in specifying parameters.
///
/// ## Argument Tokens
///
/// A "token" in this context refers to a placeholder in the SQL statement for a value that is provided at runtime.
/// There are two types of tokens:
///
/// - Indexed Tokens: Represented by numerical indices (`?NNNN`, `?`).
/// These placeholders correspond to specific parameter positions.
/// - Named Tokens: Represented by string names (`:AAAA`, `@AAAA`, `$AAAA`).
/// These placeholders are identified by unique names.
///
/// More information on SQLite parameters can be found [here](https://www.sqlite.org/lang_expr.html#varparam).
/// The `Arguments` structure supports indexed (?) and named (:AAAA) forms of tokens.
///
/// ## Creating Arguments
///
/// You can initialize `Arguments` using arrays or dictionaries:
///
/// - **Indexed Arguments**: Initialize with an array of values or use an array literal.
/// ```swift
/// let args: Statement.Arguments = ["John", 30]
/// ```
/// - **Named Arguments**: Initialize with a dictionary of named values or use a dictionary literal.
/// ```swift
/// let args: Statement.Arguments = ["name": "John", "age": 30]
/// ```
///
/// ## Combining Arguments
///
/// You can combine two sets of `Arguments` using the ``merge(with:using:)-23pzs``or
/// ``merged(with:using:)-23p3q``methods. These methods allow you to define how to resolve
/// conflicts when the same parameter token exists in both argument sets.
///
/// ```swift
/// var base: Statement.Arguments = ["name": "Alice"]
/// let update: Statement.Arguments = ["name": "Bob", "age": 30]
///
/// base.merge(with: update) { token, current, new in
/// return .replace
/// }
/// ```
///
/// Alternatively, you can create a new merged instance without modifying the original:
///
/// ```swift
/// let merged = base.merged(with: update) { token, current, new in
/// return .ignore
/// }
/// ```
///
/// Conflict resolution is controlled by the closure you provide, which receives the token, the current value,
/// and the new value. It returns a value of type ``ConflictResolution``, specifying how to handle the
/// conflict.. This ensures that merging is performed explicitly and predictably, avoiding accidental overwrites.
///
/// - Important: Although mixing parameter styles is technically allowed, it is generally not recommended.
/// For clarity and maintainability, you should consistently use either indexed or named parameters
/// throughout a query. Mixing styles may lead to confusion or hard-to-diagnose bugs in more complex queries.
///
/// ## Topics
///
/// ### Subtypes
///
/// - ``Token``
/// - ``ConflictResolution``
///
/// ### Type Aliases
///
/// - ``Resolver``
/// - ``Elements``
/// - ``RawValue``
/// - ``Index``
/// - ``Element``
///
/// ### Initializers
///
/// - ``init()``
/// - ``init(_:)-1v7s``
/// - ``init(_:)-bfj9``
/// - ``init(arrayLiteral:)``
/// - ``init(dictionaryLiteral:)``
///
/// ### Instance Properties
///
/// - ``tokens``
/// - ``count``
/// - ``isEmpty``
/// - ``startIndex``
/// - ``endIndex``
/// - ``description``
///
/// ### Instance Methods
///
/// - ``index(after:)``
/// - ``contains(_:)``
/// - ``merged(with:using:)-23p3q``
/// - ``merged(with:using:)-89krm``
/// - ``merge(with:using:)-23pzs``
/// - ``merge(with:using:)-4r21o``
///
/// ### Subscripts
///
/// - ``subscript(_:)``
public struct Arguments: Collection, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral, CustomStringConvertible {
/// Represents a token used in database statements, either indexed or named.
///
/// Tokens are used to identify placeholders for values in SQL statements.
/// They can either be indexed, represented by an integer index, or named, represented by a string name.
public enum Token: Hashable {
/// Represents an indexed token with a numerical index.
case indexed(index: Int)
/// Represents a named token with a string name.
case named(name: String)
}
/// A strategy for resolving conflicts when merging two sets of arguments.
///
/// When two `Arguments` instances contain the same token, a `ConflictResolution` value
/// determines how the conflict should be handled.
public enum ConflictResolution {
/// Keeps the current value and ignores the new one.
case ignore
/// Replaces the current value with the new one.
case replace
}
/// A closure used to resolve conflicts when merging two sets of arguments.
///
/// This closure is invoked when both argument sets contain the same token.
/// It determines whether to keep the existing value or replace it with the new one.
///
/// - Parameters:
/// - token: The conflicting parameter token.
/// - current: The value currently associated with the token.
/// - new: The new value from the other argument set.
/// - Returns: A strategy indicating how to resolve the conflict.
public typealias Resolver = (
_ token: Token,
_ current: SQLiteRawValue,
_ new: SQLiteRawValue
) -> ConflictResolution
/// The underlying storage for `Arguments`, mapping tokens to their raw values while preserving order.
///
/// Keys are tokens (either indexed or named), and values are the corresponding SQLite-compatible values.
public typealias Elements = OrderedDictionary<Token, SQLiteRawValue>
/// The value type used in the underlying elements dictionary.
///
/// This represents a SQLite-compatible raw value, such as a string, number, or null.
public typealias RawValue = Elements.Value
/// The index type used to traverse the arguments collection.
public typealias Index = Elements.Index
/// A keyvalue pair representing an argument token and its associated value.
public typealias Element = (token: Token, value: RawValue)
// MARK: - Private Properties
private var elements: Elements
// MARK: - Public Properties
/// The starting index of the arguments collection, which is always zero.
///
/// This property represents the initial position in the arguments collection.
/// Since the elements are indexed starting from zero, it consistently returns zero,
/// allowing predictable forward iteration.
///
/// - Complexity: `O(1)`
public var startIndex: Index {
0
}
/// The ending index of the arguments collection, equal to the number of elements.
///
/// This property marks the position one past the last element in the collection.
/// It returns the total number of arguments and defines the upper bound for iteration
/// over tokens and their associated values.
///
/// - Complexity: `O(1)`
public var endIndex: Index {
elements.count
}
/// A Boolean value indicating whether the arguments collection is empty.
///
/// Returns `true` if the collection contains no arguments; otherwise, returns `false`.
///
/// - Complexity: `O(1)`
public var isEmpty: Bool {
elements.isEmpty
}
/// The number of arguments in the collection.
///
/// This property reflects the total number of tokenvalue pairs
/// currently stored in the arguments set.
///
/// - Complexity: `O(1)`
public var count: Int {
elements.count
}
/// A textual representation of the arguments collection.
///
/// The description includes all tokens and their associated values
/// in the order they appear in the collection. This is useful for debugging.
///
/// - Complexity: `O(n)`
public var description: String {
elements.description
}
/// An array of all tokens present in the arguments collection.
///
/// The tokens are returned in insertion order and include both
/// indexed and named forms, depending on how the arguments were constructed.
///
/// - Complexity: `O(1)`
public var tokens: [Token] {
elements.keys.elements
}
// MARK: - Inits
/// Initializes an empty `Arguments`.
///
/// - Complexity: `O(1)`
public init() {
self.elements = [:]
}
/// Initializes `Arguments` with an array of values.
///
/// - Parameter elements: An array of `SQLiteRawBindable` values.
///
/// - Complexity: `O(n)`, where `n` is the number of elements in the input array.
public init(_ elements: [SQLiteRawBindable?]) {
self.elements = .init(
uniqueKeysWithValues: elements.enumerated().map { offset, value in
(.indexed(index: offset + 1), value?.sqliteRawValue ?? .null)
}
)
}
/// Initializes `Arguments` with a dictionary of named values.
///
/// - Parameter elements: A dictionary mapping names to `SQLiteRawBindable` values.
///
/// - Complexity: `O(n)`, where `n` is the number of elements in the input dictionary.
public init(_ elements: [String: SQLiteRawBindable?]) {
self.elements = .init(
uniqueKeysWithValues: elements.map { name, value in
(.named(name: name), value?.sqliteRawValue ?? .null)
}
)
}
/// Initializes `Arguments` from an array literal.
///
/// This initializer enables array literal syntax for positional (indexed) arguments.
///
/// ```swift
/// let args: Statement.Arguments = ["Alice", 42]
/// ```
///
/// Each value is bound to a token of the form `?1`, `?2`, etc., based on its position.
///
/// - Complexity: `O(n)`, where `n` is the number of elements.
public init(arrayLiteral elements: SQLiteRawBindable?...) {
self.elements = .init(
uniqueKeysWithValues: elements.enumerated().map { offset, value in
(.indexed(index: offset + 1), value?.sqliteRawValue ?? .null)
}
)
}
/// Initializes `Arguments` from a dictionary literal.
///
/// This initializer enables dictionary literal syntax for named arguments.
///
/// ```swift
/// let args: Statement.Arguments = ["name": "Alice", "age": 42]
/// ```
///
/// Each key becomes a named token (`:name`, `:age`, etc.).
///
/// - Complexity: `O(n)`, where `n` is the number of elements.
public init(dictionaryLiteral elements: (String, SQLiteRawBindable?)...) {
self.elements = .init(
uniqueKeysWithValues: elements.map { name, value in
(.named(name: name), value?.sqliteRawValue ?? .null)
}
)
}
// MARK: - Subscripts
/// Accesses the element at the specified position.
///
/// This subscript returns the `(token, value)` pair located at the given index
/// in the arguments collection. The order of elements reflects their insertion order.
///
/// - Parameter index: The position of the element to access.
/// - Returns: A tuple containing the token and its associated value.
///
/// - Complexity: `O(1)`
public subscript(index: Index) -> Element {
let element = elements.elements[index]
return (element.key, element.value)
}
// MARK: - Methods
/// Returns the position immediately after the given index.
///
/// Use this method to advance an index when iterating over the arguments collection.
///
/// - Parameter i: A valid index of the collection.
/// - Returns: The index value immediately following `i`.
///
/// - Complexity: `O(1)`
public func index(after i: Index) -> Index {
i + 1
}
/// Returns a Boolean value indicating whether the specified token exists in the arguments.
///
/// Use this method to check whether a tokeneither indexed or namedis present in the collection.
///
/// - Parameter token: The token to search for in the arguments.
/// - Returns: `true` if the token exists in the collection; otherwise, `false`.
///
/// - Complexity: On average, the complexity is `O(1)`.
public func contains(_ token: Token) -> Bool {
elements.keys.contains(token)
}
/// Merges the contents of another `Arguments` instance into this one using a custom resolver.
///
/// For each token present in `other`, the method either inserts the new value
/// or resolves conflicts when the token already exists in the current collection.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into this one.
/// - resolve: A closure that determines how to resolve conflicts between existing and new values.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public mutating func merge(with other: Self, using resolve: Resolver) {
for (token, newValue) in other.elements {
if let index = elements.index(forKey: token) {
let currentValue = elements.values[index]
switch resolve(token, currentValue, newValue) {
case .ignore: continue
case .replace: elements[token] = newValue
}
} else {
elements[token] = newValue
}
}
}
/// Merges the contents of another `Arguments` instance into this one using a fixed conflict resolution strategy.
///
/// This variant applies the same resolution strategy to all conflicts without requiring a custom closure.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into this one.
/// - resolution: A fixed strategy to apply when a token conflict occurs.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public mutating func merge(with other: Self, using resolution: ConflictResolution) {
merge(with: other) { _, _, _ in resolution }
}
/// Returns a new `Arguments` instance by merging the contents of another one using a custom resolver.
///
/// This method creates a copy of the current arguments and merges `other` into it.
/// For each conflicting token, the provided resolver determines whether to keep the existing value
/// or replace it with the new one.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into the copy.
/// - resolve: A closure that determines how to resolve conflicts between existing and new values.
/// - Returns: A new `Arguments` instance containing the merged values.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public func merged(with other: Self, using resolve: Resolver) -> Self {
var copy = self
copy.merge(with: other, using: resolve)
return copy
}
/// Returns a new `Arguments` instance by merging the contents of another one using a fixed strategy.
///
/// This variant uses the same resolution strategy for all conflicts without requiring a custom closure.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into the copy.
/// - resolution: A fixed strategy to apply when a token conflict occurs.
/// - Returns: A new `Arguments` instance containing the merged values.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public func merged(with other: Self, using resolution: ConflictResolution) -> Self {
merged(with: other) { _, _, _ in resolution }
}
}
}