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 /// 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 key–value 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 token–value 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 token—either indexed or named—is 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 } } } }