// // 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 result set /// /// A DuckDB result set contains the data returned from the database after a /// successful query. /// /// A result set is organized into vertical table slices called columns. Each /// column of the result set is accessible by calling the ``subscript(_:)`` /// method of the result. /// /// Elements of a column can be accessed by casting the column to the native /// Swift type that matches the underlying database column type. See ``Column`` /// for further discussion. public struct ResultSet: Sendable { /// The number of chunks in the result set public var chunkCount: DBInt { chunkStorage.chunkCount } /// The number of columns in the result set public var columnCount: DBInt { storage.columnCount } /// The total number of rows in the result set public var rowCount: DBInt { chunkStorage.totalRowCount } private let storage: ResultStorage private let chunkStorage: ChunkStorage init(connection: Connection, sql: String) throws { self.storage = try ResultStorage(connection: connection, sql: sql) self.chunkStorage = ChunkStorage(resultStorage: storage) } init(prepared: PreparedStatement) throws { self.storage = try ResultStorage(prepared: prepared) self.chunkStorage = ChunkStorage(resultStorage: storage) } /// Returns a `Void` typed column for the given column index /// /// A `Void` typed column can be cast to a column matching the underlying /// database representation using ``Column/cast(to:)-4376d``. See ``Column`` /// for further discussion. /// /// - Parameter columnIndex: the index of the column in the result set /// - Returns: a `Void` typed column public func column(at columnIndex: DBInt) -> Column { precondition(columnIndex < columnCount) return Column(result: self, columnIndex: columnIndex) { $0.unwrapNull() ? nil : () } } /// The underlying column name for the given column index /// /// - Parameter columnIndex: the index of the column in the result set /// - Returns: the name of the column public func columnName(at columnIndex: DBInt) -> String { storage.columnName(at: columnIndex) } /// The index of the given column name /// /// - Parameter columnName: the name of the column in the result set /// - Returns: the index of the column /// - Complexity: O(n) public func index(forColumnName columnName: String) -> DBInt? { for i in 0.. DatabaseType { storage.columnDataType(at: index) } func columnLogicalType(at index: DBInt) -> LogicalType { storage.columnLogicalType(at: index) } func element(forColumn columnIndex: DBInt, at index: DBInt) -> Vector.Element { var chunkIndex = DBInt.zero var chunkRowOffset = DBInt.zero while chunkIndex < chunkCount { let chunk = chunkStorage[Int(chunkIndex)] let chunkCount = chunk.count if index < chunkRowOffset + chunkCount { return chunk.withVector(at: columnIndex) { vector in vector[Int(index - chunkRowOffset)] } } else { chunkIndex += 1 chunkRowOffset += chunkCount } } preconditionFailure("item out of bounds") } } // MARK: - Collection conformance extension ResultSet: RandomAccessCollection { public typealias Element = Column public struct Iterator: IteratorProtocol { private let result: ResultSet private var position: DBInt init(result: ResultSet) { self.result = result self.position = result.startIndex } public mutating func next() -> Element? { guard position < result.endIndex else { return nil } defer { position += 1 } return .some(result[position]) } } public var startIndex: DBInt { 0 } public var endIndex: DBInt { columnCount } public subscript(position: DBInt) -> Column { column(at: position) } public func makeIterator() -> Iterator { Iterator(result: self) } public func index(after i: DBInt) -> DBInt { i + 1 } public func index(before i: DBInt) -> DBInt { i - 1 } } // MARK: - Debug Description extension ResultSet: CustomDebugStringConvertible { public var debugDescription: String { let summary = "chunks: \(chunkCount); rows: \(rowCount); columns: \(columnCount); layout:" var columns = [String]() for i in 0.." } } // MARK: - Utilities fileprivate final class ResultStorage: Sendable { let columnCount: DBInt private let ptr = UnsafeMutablePointer.allocate(capacity: 1) init(connection: Connection, sql: String) throws { let status = sql.withCString { [ptr] queryStrPtr in connection.withCConnection { duckdb_query($0, queryStrPtr, ptr) } } guard status == .success else { let error = duckdb_result_error(ptr).map(String.init(cString:)) throw DatabaseError.connectionQueryError(reason: error) } self.columnCount = duckdb_column_count(ptr) } init(prepared: PreparedStatement) throws { let status = prepared.withCPreparedStatement { [ptr] in duckdb_execute_prepared($0, ptr) } guard status == .success else { let error = duckdb_result_error(ptr).map(String.init(cString:)) throw DatabaseError.preparedStatementQueryError(reason: error) } self.columnCount = duckdb_column_count(ptr) } func columnName(at columnIndex: DBInt) -> String { String(cString: duckdb_column_name(ptr, columnIndex)) } func columnDataType(at index: DBInt) -> DatabaseType { let dataType = duckdb_column_type(ptr, index) return DatabaseType(rawValue: dataType.rawValue) } func columnLogicalType(at index: DBInt) -> LogicalType { return LogicalType { duckdb_column_logical_type(ptr, index) } } func withCResult(_ body: (duckdb_result) throws -> T) rethrows -> T { try body(ptr.pointee) } deinit { duckdb_destroy_result(ptr) ptr.deallocate() } } fileprivate final class ChunkStorage: Sendable { let chunkCount: DBInt let totalRowCount: DBInt private let chunks: [DataChunk] init(resultStorage: ResultStorage) { let chunkCount = resultStorage.withCResult { duckdb_result_chunk_count($0) } var chunks = [DataChunk]() var totalRowCount = DBInt(0) for i in 0 ..< chunkCount { let chunk = resultStorage.withCResult { DataChunk(cresult: $0, index: i) } chunks.append(chunk) totalRowCount += chunk.count } self.chunks = chunks self.chunkCount = chunkCount self.totalRowCount = totalRowCount } subscript(position: Int) -> DataChunk { chunks[position] } }