// // DuckDB // https://github.com/duckdb/duckdb-swift // // Copyright © 2018-2024 Stichting DuckDB Foundation // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. @_implementationOnly import Cduckdb import Foundation /// An object representing a DuckDB prepared statement /// /// A prepared statement is a parameterized query. The query is prepared with /// question marks (`?`) or dollar symbols (`$1`) indicating the parameters of /// the query. Values can then be bound to these parameters, after which the /// prepared statement can be executed using those parameters. A single query /// can be prepared once and executed many times. /// /// Prepared statements are useful to: /// /// - Easily supply parameters to functions while avoiding string /// concatenation/SQL injection attacks. /// - Speed up queries that will be executed many times with different /// parameters. /// /// The following example creates a prepared statement that allows parameters /// to be bound in two positions within a 'select' statement. The prepared /// statement is finally executed by calling ``PreparedStatement/execute()``. /// /// ```swift /// let connection: Connection = ... /// let statement = try PreparedStatement( /// connection: connection, /// query: "SELECT $1 from $2" /// ) /// try statement.bind("last_name") /// try statement.bind("employees") /// // executes 'SELECT last_name from employees' /// let result = try statement.execute() /// ``` public final class PreparedStatement { /// The number of parameters to which values can be bound public var parameterCount: Int { Int(duckdb_nparams(ptr.pointee)) } private let connection: Connection private let ptr = UnsafeMutablePointer.allocate(capacity: 1) /// Creates a new prepared statement for a given connection and query /// /// The query is prepared with question marks (`?`) or dollar symbols (`$1`) /// indicating the parameters of the query. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter connection: the connection on which the prepared stement will /// execute /// - Parameter query: the parameterized query /// - Throws: ``DatabaseError/preparedStatementFailedToInitialize(reason:)`` /// if there is a problem with the query or connection public init(connection: Connection, query: String) throws { self.connection = connection let status = query.withCString { queryStPtr in connection.withCConnection { duckdb_prepare($0, queryStPtr, ptr) } } guard .success == status else { throw DatabaseError.preparedStatementFailedToInitialize(reason: preparedStatementError()) } } deinit { duckdb_destroy_prepare(ptr) ptr.deallocate() } /// Executes the prepared statement /// /// Issues the parameterized query to the database using the values previously /// bound via the `bind(_:at:)` set of functions. /// /// - Throws: ``DatabaseError/preparedStatementQueryError(reason:)`` public func execute() throws -> ResultSet { try ResultSet(prepared: self) } func parameterType(at index: Int) -> DatabaseType { let cparamtype = duckdb_param_type(ptr.pointee, DBInt(index)) return cparamtype.asTypeID } func clearBindings() { duckdb_clear_bindings(ptr.pointee) } func withCPreparedStatement(_ body: (duckdb_prepared_statement?) throws -> T) rethrows -> T { try body(ptr.pointee) } } public extension PreparedStatement { /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Bool?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_boolean(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Int8?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_int8(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Int16?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_int16(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Int32?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_int32(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Int64?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_int64(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: IntHuge?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_hugeint(ptr.pointee, .init(index), .init(value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: UIntHuge?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_uhugeint(ptr.pointee, .init(index), .init(value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Decimal?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_decimal(ptr.pointee, .init(index), try .init(value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: UInt8?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_uint8(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: UInt16?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_uint16(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: UInt32?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_uint32(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: UInt64?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_uint64(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Float?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_float(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Double?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_double(ptr.pointee, .init(index), value) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Date?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_date(ptr.pointee, .init(index), .init(date: value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Time?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_time(ptr.pointee, .init(index), .init(time: value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Timestamp?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_timestamp(ptr.pointee, .init(index), .init(timestamp: value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Interval?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { duckdb_bind_interval(ptr.pointee, .init(index), .init(interval: value)) } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: String?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } let data = value.data(using: .utf8)! try withThrowingCommand { data.withUnsafeBytes { dataPtr in duckdb_bind_varchar_length( ptr.pointee, .init(index), dataPtr.baseAddress, .init(dataPtr.count)) } } } /// Binds a value of the given type at the specified parameter index /// /// Sets the value that will be used for the next call to ``execute()``. /// /// - Important: Prepared statement parameters use one-based indexing /// - Parameter value: the value to bind /// - Parameter index: the one-based parameter index /// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)`` /// if there is a type-mismatch between the value being bound and the /// underlying column type func bind(_ value: Data?, at index: Int) throws { guard let value = try unwrapValueOrBindNull(value, at: index) else { return } try withThrowingCommand { value.withUnsafeBytes { dataPtr in duckdb_bind_blob( ptr.pointee, .init(index), dataPtr.baseAddress, .init(dataPtr.count)) } } } } private extension PreparedStatement { func bindNullValue(at index: Int) throws { try withThrowingCommand { duckdb_bind_null(ptr.pointee, .init(index)) } } func unwrapValueOrBindNull(_ value: T?, at index: Int) throws -> T? { guard let value else { try bindNullValue(at: index) return nil } return value } func withThrowingCommand(_ body: () throws -> duckdb_state) throws { let state = try body() guard state == .success else { throw DatabaseError.preparedStatementFailedToBindParameter(reason: preparedStatementError()) } } func preparedStatementError() -> String? { duckdb_prepare_error(ptr.pointee).map(String.init(cString:)) } }