// // DuckDBFastReader.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import Foundation // MARK: - DuckDB C API bridge // // duckdb-swift's per-element access scans every chunk on every read — // O(rows × chunks), which turns a 10M-row load into minutes. The C API // underneath is statically linked into this binary, and its POINTER-BASED // entry points expose materialized results as contiguous column arrays. // Only functions taking pointer arguments are declared (never by-value // duckdb_result), so there is no struct-ABI risk. @_silgen_name("duckdb_open") private func c_open( _ path: UnsafePointer?, _ out: UnsafeMutablePointer ) -> Int32 @_silgen_name("duckdb_close") private func c_close(_ database: UnsafeMutablePointer) @_silgen_name("duckdb_connect") private func c_connect( _ database: OpaquePointer?, _ out: UnsafeMutablePointer ) -> Int32 @_silgen_name("duckdb_disconnect") private func c_disconnect(_ connection: UnsafeMutablePointer) @_silgen_name("duckdb_query") private func c_query( _ connection: OpaquePointer?, _ sql: UnsafePointer?, _ outResult: UnsafeMutableRawPointer? ) -> Int32 @_silgen_name("duckdb_destroy_result") private func c_destroyResult(_ result: UnsafeMutableRawPointer?) @_silgen_name("duckdb_result_error") private func c_resultError(_ result: UnsafeMutableRawPointer?) -> UnsafePointer? @_silgen_name("duckdb_column_count") private func c_columnCount(_ result: UnsafeMutableRawPointer?) -> UInt64 @_silgen_name("duckdb_row_count") private func c_rowCount(_ result: UnsafeMutableRawPointer?) -> UInt64 @_silgen_name("duckdb_column_name") private func c_columnName( _ result: UnsafeMutableRawPointer?, _ column: UInt64 ) -> UnsafePointer? @_silgen_name("duckdb_column_data") private func c_columnData( _ result: UnsafeMutableRawPointer?, _ column: UInt64 ) -> UnsafeMutableRawPointer? @_silgen_name("duckdb_nullmask_data") private func c_nullmaskData( _ result: UnsafeMutableRawPointer?, _ column: UInt64 ) -> UnsafeMutablePointer? @_silgen_name("duckdb_value_varchar") private func c_valueVarchar( _ result: UnsafeMutableRawPointer?, _ column: UInt64, _ row: UInt64 ) -> UnsafeMutablePointer? @_silgen_name("duckdb_free") private func c_free(_ pointer: UnsafeMutableRawPointer?) // MARK: - Fast reader /// Bulk column extraction through the DuckDB C API. Owns a private /// in-memory instance (loads read files directly, so no state is shared /// with the store's duckdb-swift database). The caller pre-casts every /// column to DOUBLE or VARCHAR in SQL, so extraction is a memcpy for /// numerics and one call per row for strings. final class DuckDBFastReader { struct ColumnSpec { let name: String let isNumeric: Bool } /// Opaque duckdb_result storage — the struct is ~48 bytes; 512 gives /// generous headroom across duckdb versions. private static let resultBlobSize = 512 private var database: OpaquePointer? private var connection: OpaquePointer? init() throws(ZQDataError) { guard c_open(nil, &database) == 0 else { throw ZQDataError("fast reader: could not open DuckDB") } guard c_connect(database, &connection) == 0 else { var db = database c_close(&db) throw ZQDataError("fast reader: could not connect") } } deinit { var conn = connection c_disconnect(&conn) var db = database c_close(&db) } /// Runs `sql` (whose SELECT list must already match `columns`' order /// and DOUBLE/VARCHAR types) and materializes a data frame. func read(sql: String, columns: [ColumnSpec]) throws(ZQDataError) -> ZQDataFrame { let result = UnsafeMutableRawPointer.allocate( byteCount: Self.resultBlobSize, alignment: 16 ) result.initializeMemory(as: UInt8.self, repeating: 0, count: Self.resultBlobSize) defer { c_destroyResult(result) result.deallocate() } guard c_query(connection, sql, result) == 0 else { let message = c_resultError(result).map { String(cString: $0) } ?? "unknown error" throw ZQDataError("query failed: \(message)") } let rowCount = Int(c_rowCount(result)) let columnCount = Int(c_columnCount(result)) guard columnCount == columns.count else { throw ZQDataError( "fast reader: expected \(columns.count) columns, got \(columnCount)" ) } var frameColumns: [ZQColumn] = [] frameColumns.reserveCapacity(columnCount) for (index, spec) in columns.enumerated() { let column = UInt64(index) if spec.isNumeric { var values = [Double](repeating: .nan, count: rowCount) var missing = [Bool](repeating: false, count: rowCount) if rowCount > 0 { guard let data = c_columnData(result, column) else { throw ZQDataError("fast reader: no data for '\(spec.name)'") } let doubles = data.assumingMemoryBound(to: Double.self) values.withUnsafeMutableBufferPointer { buffer in buffer.baseAddress!.update(from: doubles, count: rowCount) } if let nulls = c_nullmaskData(result, column) { missing.withUnsafeMutableBufferPointer { buffer in buffer.baseAddress!.update(from: nulls, count: rowCount) } } for i in 0.. 0 { let nulls = c_nullmaskData(result, column) for row in 0..