///|
// ============================================================================
// Arrow results on canonical typed vectors (issue #62)
// ============================================================================
//
// `Connection::query_arrow` previously re-encoded every column: as JSON on
// the JS backends and as a custom packed-byte format on native. The result
// is now eagerly materialized into the canonical `VectorChunk` typed-vector
// model shared with `Connection::query_chunks`, so no JSON round-trip or
// byte re-encoding is involved on any backend. Int64/UInt64 columns keep
// full precision, and per-row NULL masks come from the same validity layout
// used by every other result path.
//
// `ArrowResult` stays an opaque handle: `close` releases the backend-native
// result, and `to_chunks`/`get_schema`/`close` report `Closed` afterwards.
// The legacy per-column `get_column_*` accessors are kept as deprecated
// forwarders in `deprecated.mbt`; new code should use `to_chunks` and the
// `Vector` accessors (`ints`, `int64s`, `uint64s`, `doubles`, `strings`,
// `decimals`, `list_parts`, `value_at`, `is_null`, ...).
///|
/// One Arrow field of a result schema.
pub struct ArrowField {
name : String
nullable : Bool
type_id : String
}
///|
/// Result schema: one `ArrowField` per column, in column order.
pub struct ArrowSchemaInfo {
fields : Array[ArrowField]
}
///|
/// Materialized `query_arrow` result. Opaque to callers; the canonical
/// `VectorChunk`s are exposed via `to_chunks`, and the backend-native handle
/// is released by `close`.
pub struct ArrowResult {
priv mut handle : ArrowHandle?
priv columns : Array[String]
priv column_types : Array[ColumnType]
priv nullables : FixedArray[Bool]
priv chunks : Array[VectorChunk]
priv mut closed : Bool
}
///|
/// Number of result columns.
pub fn ArrowResult::column_count(self : ArrowResult) -> Int {
self.columns.length()
}
///|
/// Total rows across all materialized chunks.
pub fn ArrowResult::row_count(self : ArrowResult) -> Int {
let mut rows = 0
for chunk in self.chunks {
rows = rows + chunk.row_count()
} nobreak {
()
}
rows
}
///|
/// The canonical typed-vector representation of the whole result.
///
/// `Err(Closed)` once `close` has released the result.
pub fn ArrowResult::to_chunks(
self : ArrowResult,
) -> Result[Array[VectorChunk], DuckDBError] {
if self.closed {
Err(DuckDBError::Closed(resource="arrow result"))
} else {
Ok(self.chunks)
}
}
///|
/// Result schema: column names, nullability, and a flat type id per field.
/// `type_id` keeps the legacy primitive names ("bool"/"int32"/"int64"/
/// "double"/"string") and covers the wider `ColumnType` set ("uint32",
/// "uint64", "decimal", "date", "timestamp", "interval", "hugeint", "uuid",
/// "list", "struct", "map", ...). `Err(Closed)` once closed.
pub fn ArrowResult::get_schema(
self : ArrowResult,
) -> Result[ArrowSchemaInfo, DuckDBError] {
if self.closed {
Err(DuckDBError::Closed(resource="arrow result"))
} else {
let fields = Array::makei(self.columns.length(), fn(col) {
{
name: self.columns[col],
nullable: if col < self.nullables.length() {
self.nullables[col]
} else {
true
},
type_id: arrow_field_type_id(self.column_types[col]),
}
})
Ok({ fields, })
}
}
///|
/// Flat type-id string for a `ColumnType` (see `get_schema`).
fn arrow_field_type_id(logical_type : ColumnType) -> String {
match logical_type {
ColumnType::Boolean => "bool"
ColumnType::TinyInt | ColumnType::SmallInt | ColumnType::Integer => "int32"
ColumnType::BigInt => "int64"
ColumnType::UTinyInt | ColumnType::USmallInt | ColumnType::UInteger =>
"uint32"
ColumnType::UBigInt => "uint64"
ColumnType::Float | ColumnType::Double => "double"
ColumnType::Varchar | ColumnType::Enum | ColumnType::StringLiteral =>
"string"
ColumnType::Blob => "blob"
ColumnType::Date => "date"
ColumnType::Time | ColumnType::TimeNs | ColumnType::TimeTz => "time"
ColumnType::Timestamp
| ColumnType::TimestampS
| ColumnType::TimestampMs
| ColumnType::TimestampNs
| ColumnType::TimestampTz => "timestamp"
ColumnType::Interval => "interval"
ColumnType::Decimal => "decimal"
ColumnType::HugeInt | ColumnType::UHugeInt => "hugeint"
ColumnType::Uuid => "uuid"
ColumnType::List | ColumnType::Array => "list"
ColumnType::Struct => "struct"
ColumnType::Map => "map"
ColumnType::Union => "union"
ColumnType::Bit => "bit"
_ => "string"
}
}
///|
/// Release the backend-native result. Idempotent contract: the first call
/// frees the handle; calling `close` again reports `Closed` instead of
/// touching released resources.
pub fn ArrowResult::close(
self : ArrowResult,
on_done~ : (Result[Unit, DuckDBError]) -> Unit,
) -> Unit {
if self.closed {
on_done(Err(DuckDBError::Closed(resource="arrow result")))
return
}
match self.handle {
Some(handle) => arrow_result_close_handle(handle)
None => ()
}
self.handle = None
self.closed = true
on_done(Ok(()))
}