// // DuckDBStore.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import DuckDB import Foundation /// DuckDB-backed data store (CLAUDE.md §6). File loading goes through /// DuckDB readers (`read_parquet`, `read_csv`) and query results are /// materialized into `ZQDataFrame` columns: every numeric database type /// widens to Float64, text stays as strings. public actor ZQDataStore { private let database: Database private let connection: Connection public init() throws { self.database = try Database(store: .inMemory) self.connection = try database.connect() } // MARK: - Loading /// Loads a dataset file into a data frame. Format is inferred from the /// file extension: .parquet, .csv, .tsv, .json, .arrow. public func load(contentsOf url: URL) throws -> ZQDataFrame { let path = url.path guard FileManager.default.fileExists(atPath: path) else { throw ZQDataError("file not found: \(path)") } // Stata files use the native reader (CLAUDE.md §6). if ["dta"].contains(url.pathExtension.lowercased()) { return try DTAReader.read(contentsOf: url) } let escaped = path.replacingOccurrences(of: "'", with: "''") let reader: String switch url.pathExtension.lowercased() { case "parquet", "pq": reader = "read_parquet('\(escaped)')" case "csv", "tsv", "txt": reader = "read_csv('\(escaped)')" case "json", "ndjson", "jsonl": reader = "read_json('\(escaped)')" case "arrow", "feather", "ipc": reader = "read_ipc('\(escaped)')" case let ext: throw ZQDataError("unsupported file format '.\(ext)'") } return try dataFrame(fromQuery: "SELECT * FROM \(reader)") } /// Runs an arbitrary SQL query and materializes the result. /// /// Two-phase: a `LIMIT 0` probe through duckdb-swift discovers names /// and types, then the full query — with every column cast to DOUBLE /// or VARCHAR in SQL — extracts through the C-API bulk reader. /// duckdb-swift's per-element access rescans every chunk per read /// (O(rows × chunks)); the bulk path is a straight memcpy per numeric /// column, ~200× faster at 10M rows. public func dataFrame(fromQuery sql: String) throws -> ZQDataFrame { let probe: ResultSet do { probe = try connection.query("SELECT * FROM (\(sql)) __q LIMIT 0") } catch { throw ZQDataError("query failed: \(error)") } var specs: [DuckDBFastReader.ColumnSpec] = [] var selections: [String] = [] for index in 0..