433 lines
17 KiB
Swift
433 lines
17 KiB
Swift
//
|
|
// 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 that efficiently appends data to a DuckDB table
|
|
///
|
|
/// Appenders are the most efficient way of loading data into DuckDB from within
|
|
/// Swift, and are recommended for fast data loading. The appender is much
|
|
/// faster than using prepared statements or individual INSERT INTO statements.
|
|
///
|
|
/// Appends are made in row-wise format. For every column an `append(_:)` call
|
|
/// should be made after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// The following example shows two rows being appended to a table with a two
|
|
/// column layout.
|
|
///
|
|
/// ```swift
|
|
/// do {
|
|
/// let database = try Database(store: .inMemory)
|
|
/// let connection = try database.connect()
|
|
/// try connection.execute("CREATE TABLE people(id INTEGER, name VARCHAR)")
|
|
/// let appender = try Appender(connection: connection, table: "people")
|
|
/// // add first row
|
|
/// try appender.append(Int32(1))
|
|
/// try appender.append("Mark")
|
|
/// try appender.endRow()
|
|
/// // add second row
|
|
/// try appender.append(Int32(2))
|
|
/// try appender.append("Hannes")
|
|
/// try appender.endRow()
|
|
/// // flush rows to table
|
|
/// try appender.flush()
|
|
/// }
|
|
/// catch {
|
|
/// // handle error
|
|
/// }
|
|
/// ```
|
|
public final class Appender {
|
|
|
|
private let connection: Connection
|
|
private let ptr = UnsafeMutablePointer<duckdb_appender?>.allocate(capacity: 1)
|
|
|
|
/// Creates a new appender
|
|
///
|
|
/// Instantiates an ``Appender`` through which table rows can be efficiently
|
|
/// added
|
|
///
|
|
/// - Parameter connection: the connection through which rows should be
|
|
/// appended
|
|
/// - Parameter schema: the database schema (defaults to `main`)
|
|
/// - Parameter table: the table to append to
|
|
/// - Throws: ``DatabaseError/appenderFailedToInitialize(reason:)`` if the
|
|
/// appender failed to instantiate
|
|
public init(connection: Connection, schema: String? = nil, table: String) throws {
|
|
self.connection = connection
|
|
let status = schema.withOptionalCString { schemaStrPtr in
|
|
table.withCString { tableStrPtr in
|
|
connection.withCConnection { duckdb_appender_create($0, schemaStrPtr, tableStrPtr, ptr) }
|
|
}
|
|
}
|
|
guard .success == status else {
|
|
throw DatabaseError.appenderFailedToInitialize(reason: appenderError())
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
try? flush()
|
|
duckdb_appender_destroy(ptr)
|
|
ptr.deallocate()
|
|
}
|
|
|
|
/// Signals the end of the current row
|
|
///
|
|
/// After all columns for a row have been appended, ``endRow()`` must be
|
|
/// called to indicate that the row is ready to be added to the database.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToEndRow(reason:)``
|
|
/// if the row could not be completed in its current state
|
|
public func endRow() throws {
|
|
let status = duckdb_appender_end_row(ptr.pointee)
|
|
guard .success == status else {
|
|
throw DatabaseError.appenderFailedToEndRow(reason: appenderError())
|
|
}
|
|
}
|
|
|
|
/// Flushes pending rows to the database
|
|
///
|
|
/// To optimize performance, the appender writes rows to the database in
|
|
/// batches. Use `flush()` to immediately write any rows that are pending
|
|
/// insertion.
|
|
///
|
|
/// - Throws: ``DatabaseError/appenderFailedToFlush(reason:)`` if the pending
|
|
/// rows failed to be written to the database
|
|
public func flush() throws {
|
|
let status = duckdb_appender_flush(ptr.pointee)
|
|
guard .success == status else {
|
|
throw DatabaseError.appenderFailedToFlush(reason: appenderError())
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension Appender {
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Bool?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_bool(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Int8?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_int8(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Int16?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_int16(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Int32?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_int32(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Int64?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_int64(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: IntHuge?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_hugeint(ptr.pointee, .init(value)) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: UIntHuge?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_uhugeint(ptr.pointee, .init(value)) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: UInt8?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_uint8(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: UInt16?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_uint16(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: UInt32?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_uint32(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: UInt64?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_uint64(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Float?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_float(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Double?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_double(ptr.pointee, value) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Date?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_date(ptr.pointee, .init(date: value)) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Time?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand { duckdb_append_time(ptr.pointee, .init(time: value)) }
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Timestamp?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand {
|
|
duckdb_append_timestamp(ptr.pointee, .init(timestamp: value))
|
|
}
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Interval?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand {
|
|
duckdb_append_interval(ptr.pointee, .init(interval: value))
|
|
}
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: String?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
let data = value.data(using: .utf8)!
|
|
try withThrowingCommand {
|
|
data.withUnsafeBytes { dataPtr in
|
|
duckdb_append_varchar_length(ptr.pointee, dataPtr.baseAddress, .init(dataPtr.count))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Appends a value for the current row of the given type
|
|
///
|
|
/// Appends are made in row-wise format. For every column, an `append(_:)`
|
|
/// call should be made, after which the row should be finished by calling
|
|
/// ``endRow()``.
|
|
///
|
|
/// - Parameter value: the value to append
|
|
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
|
|
/// if a value of this type was not expected in the appender's current state
|
|
func append(_ value: Data?) throws {
|
|
guard let value = try unwrapValueOrAppendNull(value) else { return }
|
|
try withThrowingCommand {
|
|
value.withUnsafeBytes { dataPtr in
|
|
duckdb_append_blob( ptr.pointee, dataPtr.baseAddress, .init(dataPtr.count)) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Appender {
|
|
|
|
func appendNullValue() throws {
|
|
try withThrowingCommand { duckdb_append_null(ptr.pointee) }
|
|
}
|
|
|
|
func unwrapValueOrAppendNull<T>(_ value: T?) throws -> T? {
|
|
guard let value else {
|
|
try appendNullValue()
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func withThrowingCommand(_ body: () throws -> duckdb_state) throws {
|
|
let state = try body()
|
|
guard state == .success else {
|
|
throw DatabaseError.appenderFailedToAppendItem(reason: appenderError())
|
|
}
|
|
}
|
|
|
|
func appenderError() -> String? {
|
|
duckdb_appender_error(ptr.pointee).map(String.init(cString:))
|
|
}
|
|
}
|