blob: 2d62136a3986b985b42df61bc8f6929df35699f6 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
mod types;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
use fluss as fcore;
use fluss::PartitionId;
use fluss::client::PrefixKeyLookuper;
use fluss::error::Error;
use fluss::metadata::{Column, DataType, TableInfo};
use fluss::row::{Datum, GenericRow};
use fluss::rpc::FlussError as CoreFlussError;
use fluss::rpc::message::OffsetSpec;
static RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
});
#[cxx::bridge(namespace = "fluss::ffi")]
mod ffi {
struct HashMapValue {
key: String,
value: String,
}
struct FfiConfig {
bootstrap_servers: String,
writer_request_max_size: i32,
writer_acks: String,
writer_retries: i32,
writer_batch_size: i32,
writer_dynamic_batch_size_enabled: bool,
writer_dynamic_batch_size_min: i32,
writer_bucket_no_key_assigner: String,
scanner_remote_log_prefetch_num: usize,
remote_file_download_thread_num: usize,
scanner_remote_log_read_concurrency: usize,
scanner_log_max_poll_records: usize,
scanner_log_fetch_max_bytes: i32,
scanner_log_fetch_min_bytes: i32,
scanner_log_fetch_wait_max_time_ms: i32,
scanner_log_fetch_max_bytes_for_bucket: i32,
writer_batch_timeout_ms: i64,
writer_enable_idempotence: bool,
writer_max_inflight_requests_per_bucket: usize,
writer_buffer_memory_size: usize,
writer_buffer_wait_timeout_ms: u64,
connect_timeout_ms: u64,
security_protocol: String,
security_sasl_mechanism: String,
security_sasl_username: String,
security_sasl_password: String,
lookup_queue_size: usize,
lookup_max_batch_size: usize,
lookup_batch_timeout_ms: u64,
lookup_max_inflight_requests: usize,
lookup_max_retries: i32,
}
struct FfiResult {
error_code: i32,
error_message: String,
}
struct FfiTablePath {
database_name: String,
table_name: String,
}
struct FfiColumn {
name: String,
data_type: i32,
nullable: bool,
comment: String,
precision: i32,
scale: i32,
array_nesting: i32,
array_nullability: Vec<u8>,
element_data_type: i32,
element_precision: i32,
element_scale: i32,
}
struct FfiSchema {
columns: Vec<FfiColumn>,
primary_keys: Vec<String>,
}
struct FfiTableDescriptor {
schema: FfiSchema,
partition_keys: Vec<String>,
bucket_count: i32,
bucket_keys: Vec<String>,
properties: Vec<HashMapValue>,
custom_properties: Vec<HashMapValue>,
comment: String,
}
struct FfiTableInfo {
table_id: i64,
schema_id: i32,
table_path: FfiTablePath,
created_time: i64,
modified_time: i64,
primary_keys: Vec<String>,
bucket_keys: Vec<String>,
partition_keys: Vec<String>,
num_buckets: i32,
has_primary_key: bool,
is_partitioned: bool,
properties: Vec<HashMapValue>,
custom_properties: Vec<HashMapValue>,
comment: String,
schema: FfiSchema,
}
struct FfiTableInfoResult {
result: FfiResult,
table_info: FfiTableInfo,
}
// NOTE: FfiDatum, FfiGenericRow, FfiScanRecord, FfiScanRecords, FfiScanRecordsResult
// have been replaced by opaque types below (ScanResultInner, GenericRowInner, LookupResultInner).
struct FfiArrowRecordBatch {
array_ptr: usize,
schema_ptr: usize,
table_id: i64,
partition_id: i64,
bucket_id: i32,
base_offset: i64,
}
struct FfiArrowRecordBatches {
batches: Vec<FfiArrowRecordBatch>,
}
struct FfiArrowRecordBatchesResult {
result: FfiResult,
arrow_batches: FfiArrowRecordBatches,
}
struct FfiLakeSnapshot {
snapshot_id: i64,
bucket_offsets: Vec<FfiBucketOffset>,
}
struct FfiBucketOffset {
table_id: i64,
partition_id: i64,
bucket_id: i32,
offset: i64,
}
struct FfiOffsetQuery {
offset_type: i32,
timestamp: i64,
}
struct FfiBucketInfo {
table_id: i64,
bucket_id: i32,
has_partition_id: bool,
partition_id: i64,
record_count: usize,
}
struct FfiBucketSubscription {
bucket_id: i32,
offset: i64,
}
struct FfiPartitionBucketSubscription {
partition_id: i64,
bucket_id: i32,
offset: i64,
}
struct FfiBucketOffsetPair {
bucket_id: i32,
offset: i64,
}
struct FfiListOffsetsResult {
result: FfiResult,
bucket_offsets: Vec<FfiBucketOffsetPair>,
}
// NOTE: FfiLookupResult replaced by opaque LookupResultInner below.
struct FfiLakeSnapshotResult {
result: FfiResult,
lake_snapshot: FfiLakeSnapshot,
}
struct FfiPartitionKeyValue {
key: String,
value: String,
}
struct FfiPartitionInfo {
partition_id: i64,
partition_name: String,
}
struct FfiListPartitionInfosResult {
result: FfiResult,
partition_infos: Vec<FfiPartitionInfo>,
}
struct FfiDatabaseDescriptor {
comment: String,
properties: Vec<HashMapValue>,
}
struct FfiDatabaseInfo {
database_name: String,
comment: String,
properties: Vec<HashMapValue>,
created_time: i64,
modified_time: i64,
}
struct FfiDatabaseInfoResult {
result: FfiResult,
database_info: FfiDatabaseInfo,
}
struct FfiListDatabasesResult {
result: FfiResult,
database_names: Vec<String>,
}
struct FfiListTablesResult {
result: FfiResult,
table_names: Vec<String>,
}
struct FfiBoolResult {
result: FfiResult,
value: bool,
}
struct FfiServerNode {
node_id: i32,
host: String,
port: u32,
server_type: String,
uid: String,
}
struct FfiServerNodesResult {
result: FfiResult,
server_nodes: Vec<FfiServerNode>,
}
struct FfiPtrResult {
result: FfiResult,
ptr: usize,
}
extern "Rust" {
type Connection;
type Admin;
type Table;
type AppendWriter;
type WriteResult;
type LogScanner;
type BatchScanner;
type UpsertWriter;
type Lookuper;
type PrefixLookuper;
// Opaque types for optimized FFI
type ScanResultInner;
type GenericRowInner;
type LookupResultInner;
type PrefixLookupResultInner;
type ArrayWriterInner;
type MapWriterInner;
type ValueInner;
// Connection
fn new_connection(config: &FfiConfig) -> FfiPtrResult;
unsafe fn delete_connection(conn: *mut Connection);
fn get_admin(self: &Connection) -> FfiPtrResult;
fn get_table(self: &Connection, table_path: &FfiTablePath) -> FfiPtrResult;
// Admin
unsafe fn delete_admin(admin: *mut Admin);
fn create_table(
self: &Admin,
table_path: &FfiTablePath,
descriptor: &FfiTableDescriptor,
ignore_if_exists: bool,
) -> FfiResult;
// Create a table whose columns come from an Arrow schema (C Data
// Interface pointer), supporting nested MAP/ROW columns. `descriptor`
// supplies primary keys + table-level metadata; its `schema.columns`
// are ignored in favour of `arrow_schema_ptr`.
fn create_table_arrow(
self: &Admin,
table_path: &FfiTablePath,
descriptor: &FfiTableDescriptor,
arrow_schema_ptr: usize,
ignore_if_exists: bool,
) -> FfiResult;
fn drop_table(
self: &Admin,
table_path: &FfiTablePath,
ignore_if_not_exists: bool,
) -> FfiResult;
fn get_table_info(self: &Admin, table_path: &FfiTablePath) -> FfiTableInfoResult;
fn get_latest_lake_snapshot(
self: &Admin,
table_path: &FfiTablePath,
) -> FfiLakeSnapshotResult;
fn list_offsets(
self: &Admin,
table_path: &FfiTablePath,
bucket_ids: Vec<i32>,
offset_query: &FfiOffsetQuery,
) -> FfiListOffsetsResult;
fn list_partition_offsets(
self: &Admin,
table_path: &FfiTablePath,
partition_name: String,
bucket_ids: Vec<i32>,
offset_query: &FfiOffsetQuery,
) -> FfiListOffsetsResult;
fn list_partition_infos(
self: &Admin,
table_path: &FfiTablePath,
) -> FfiListPartitionInfosResult;
fn list_partition_infos_with_spec(
self: &Admin,
table_path: &FfiTablePath,
partition_spec: Vec<FfiPartitionKeyValue>,
) -> FfiListPartitionInfosResult;
fn create_partition(
self: &Admin,
table_path: &FfiTablePath,
partition_spec: Vec<FfiPartitionKeyValue>,
ignore_if_exists: bool,
) -> FfiResult;
fn drop_partition(
self: &Admin,
table_path: &FfiTablePath,
partition_spec: Vec<FfiPartitionKeyValue>,
ignore_if_not_exists: bool,
) -> FfiResult;
fn create_database(
self: &Admin,
database_name: &str,
descriptor: &FfiDatabaseDescriptor,
ignore_if_exists: bool,
) -> FfiResult;
fn drop_database(
self: &Admin,
database_name: &str,
ignore_if_not_exists: bool,
cascade: bool,
) -> FfiResult;
fn list_databases(self: &Admin) -> FfiListDatabasesResult;
fn database_exists(self: &Admin, database_name: &str) -> FfiBoolResult;
fn get_database_info(self: &Admin, database_name: &str) -> FfiDatabaseInfoResult;
fn list_tables(self: &Admin, database_name: &str) -> FfiListTablesResult;
fn table_exists(self: &Admin, table_path: &FfiTablePath) -> FfiBoolResult;
fn get_server_nodes(self: &Admin) -> FfiServerNodesResult;
// Table
unsafe fn delete_table(table: *mut Table);
fn new_append_writer(self: &Table) -> FfiPtrResult;
fn create_scanner(self: &Table, column_indices: Vec<usize>, batch: bool) -> FfiPtrResult;
fn create_bucket_batch_scanner(
self: &Table,
column_indices: Vec<usize>,
limit: i32,
table_id: i64,
partition_id: i64,
bucket_id: i32,
) -> FfiPtrResult;
fn get_table_info_from_table(self: &Table) -> FfiTableInfo;
fn get_table_path(self: &Table) -> FfiTablePath;
fn has_primary_key(self: &Table) -> bool;
fn create_upsert_writer(self: &Table, column_indices: Vec<usize>) -> FfiPtrResult;
fn new_lookuper(self: &Table) -> FfiPtrResult;
fn new_prefix_lookuper(self: &Table, lookup_column_names: Vec<String>) -> FfiPtrResult;
// GenericRowInner — opaque row for writes
fn new_generic_row(field_count: usize) -> Box<GenericRowInner>;
fn gr_reset(self: &mut GenericRowInner);
fn gr_set_null(self: &mut GenericRowInner, idx: usize);
fn gr_set_bool(self: &mut GenericRowInner, idx: usize, val: bool);
fn gr_set_i32(self: &mut GenericRowInner, idx: usize, val: i32);
fn gr_set_i64(self: &mut GenericRowInner, idx: usize, val: i64);
fn gr_set_f32(self: &mut GenericRowInner, idx: usize, val: f32);
fn gr_set_f64(self: &mut GenericRowInner, idx: usize, val: f64);
fn gr_set_str(self: &mut GenericRowInner, idx: usize, val: &str);
fn gr_set_bytes(self: &mut GenericRowInner, idx: usize, val: &[u8]);
fn gr_set_date(self: &mut GenericRowInner, idx: usize, days: i32);
fn gr_set_time(self: &mut GenericRowInner, idx: usize, millis: i32);
fn gr_set_ts_ntz(self: &mut GenericRowInner, idx: usize, millis: i64, nanos: i32);
fn gr_set_ts_ltz(self: &mut GenericRowInner, idx: usize, millis: i64, nanos: i32);
fn gr_set_decimal_str(self: &mut GenericRowInner, idx: usize, val: &str);
fn gr_set_array(
self: &mut GenericRowInner,
idx: usize,
writer: &mut ArrayWriterInner,
) -> Result<()>;
fn gr_set_map(
self: &mut GenericRowInner,
idx: usize,
writer: &mut MapWriterInner,
) -> Result<()>;
fn gr_set_row(
self: &mut GenericRowInner,
idx: usize,
row: &mut GenericRowInner,
) -> Result<()>;
// ArrayWriterInner — opaque array builder for writes
fn new_array_writer(
size: usize,
element_leaf_type_id: i32,
precision: u32,
scale: u32,
array_nesting: u32,
) -> Result<Box<ArrayWriterInner>>;
// ROW/MAP element types: a one-field Arrow schema (the flat encoding can't carry them).
fn new_array_writer_arrow(
size: usize,
element_schema_ptr: usize,
) -> Result<Box<ArrayWriterInner>>;
fn aw_size(self: &ArrayWriterInner) -> usize;
fn aw_set_null(self: &mut ArrayWriterInner, idx: usize) -> Result<()>;
fn aw_set_bool(self: &mut ArrayWriterInner, idx: usize, val: bool) -> Result<()>;
fn aw_set_i32(self: &mut ArrayWriterInner, idx: usize, val: i32) -> Result<()>;
fn aw_set_i64(self: &mut ArrayWriterInner, idx: usize, val: i64) -> Result<()>;
fn aw_set_f32(self: &mut ArrayWriterInner, idx: usize, val: f32) -> Result<()>;
fn aw_set_f64(self: &mut ArrayWriterInner, idx: usize, val: f64) -> Result<()>;
fn aw_set_str(self: &mut ArrayWriterInner, idx: usize, val: &str) -> Result<()>;
fn aw_set_bytes(self: &mut ArrayWriterInner, idx: usize, val: &[u8]) -> Result<()>;
fn aw_set_date(self: &mut ArrayWriterInner, idx: usize, days: i32) -> Result<()>;
fn aw_set_time(self: &mut ArrayWriterInner, idx: usize, millis: i32) -> Result<()>;
fn aw_set_ts_ntz(
self: &mut ArrayWriterInner,
idx: usize,
millis: i64,
nanos: i32,
) -> Result<()>;
fn aw_set_ts_ltz(
self: &mut ArrayWriterInner,
idx: usize,
millis: i64,
nanos: i32,
) -> Result<()>;
fn aw_set_decimal_str(self: &mut ArrayWriterInner, idx: usize, val: &str) -> Result<()>;
fn aw_set_array(
self: &mut ArrayWriterInner,
idx: usize,
nested: &mut ArrayWriterInner,
) -> Result<()>;
fn aw_set_row(
self: &mut ArrayWriterInner,
idx: usize,
row: &mut GenericRowInner,
) -> Result<()>;
fn aw_set_map(
self: &mut ArrayWriterInner,
idx: usize,
map: &mut MapWriterInner,
) -> Result<()>;
// MapWriterInner — opaque map builder for writes.
#[allow(clippy::too_many_arguments)]
fn new_map_writer(
capacity: usize,
key_leaf_type_id: i32,
key_precision: u32,
key_scale: u32,
value_leaf_type_id: i32,
value_precision: u32,
value_scale: u32,
value_array_nesting: u32,
) -> Result<Box<MapWriterInner>>;
// ROW/MAP key/value types: a [key, value] Arrow schema (the flat encoding can't).
fn new_map_writer_arrow(
capacity: usize,
kv_schema_ptr: usize,
) -> Result<Box<MapWriterInner>>;
fn mw_key_bool(self: &mut MapWriterInner, val: bool) -> Result<()>;
fn mw_key_i32(self: &mut MapWriterInner, val: i32) -> Result<()>;
fn mw_key_i64(self: &mut MapWriterInner, val: i64) -> Result<()>;
fn mw_key_f32(self: &mut MapWriterInner, val: f32) -> Result<()>;
fn mw_key_f64(self: &mut MapWriterInner, val: f64) -> Result<()>;
fn mw_key_str(self: &mut MapWriterInner, val: &str) -> Result<()>;
fn mw_key_bytes(self: &mut MapWriterInner, val: &[u8]) -> Result<()>;
fn mw_key_date(self: &mut MapWriterInner, days: i32) -> Result<()>;
fn mw_key_time(self: &mut MapWriterInner, millis: i32) -> Result<()>;
fn mw_key_timestamp(self: &mut MapWriterInner, millis: i64, nanos: i32) -> Result<()>;
fn mw_key_decimal_str(self: &mut MapWriterInner, val: &str) -> Result<()>;
fn mw_value_null(self: &mut MapWriterInner) -> Result<()>;
fn mw_value_bool(self: &mut MapWriterInner, val: bool) -> Result<()>;
fn mw_value_i32(self: &mut MapWriterInner, val: i32) -> Result<()>;
fn mw_value_i64(self: &mut MapWriterInner, val: i64) -> Result<()>;
fn mw_value_f32(self: &mut MapWriterInner, val: f32) -> Result<()>;
fn mw_value_f64(self: &mut MapWriterInner, val: f64) -> Result<()>;
fn mw_value_str(self: &mut MapWriterInner, val: &str) -> Result<()>;
fn mw_value_bytes(self: &mut MapWriterInner, val: &[u8]) -> Result<()>;
fn mw_value_date(self: &mut MapWriterInner, days: i32) -> Result<()>;
fn mw_value_time(self: &mut MapWriterInner, millis: i32) -> Result<()>;
fn mw_value_timestamp(self: &mut MapWriterInner, millis: i64, nanos: i32) -> Result<()>;
fn mw_value_decimal_str(self: &mut MapWriterInner, val: &str) -> Result<()>;
fn mw_value_row(self: &mut MapWriterInner, row: &mut GenericRowInner) -> Result<()>;
fn mw_value_map(self: &mut MapWriterInner, map: &mut MapWriterInner) -> Result<()>;
fn mw_value_array(self: &mut MapWriterInner, array: &mut ArrayWriterInner) -> Result<()>;
fn mw_commit(self: &mut MapWriterInner) -> Result<()>;
// AppendWriter
unsafe fn delete_append_writer(writer: *mut AppendWriter);
fn append(self: &mut AppendWriter, row: &GenericRowInner) -> FfiPtrResult;
fn append_arrow_batch(
self: &mut AppendWriter,
array_ptr: usize,
schema_ptr: usize,
) -> FfiPtrResult;
fn flush(self: &mut AppendWriter) -> FfiResult;
// WriteResult
unsafe fn delete_write_result(wr: *mut WriteResult);
fn wait(self: &mut WriteResult) -> FfiResult;
// UpsertWriter
unsafe fn delete_upsert_writer(writer: *mut UpsertWriter);
fn upsert(self: &mut UpsertWriter, row: &GenericRowInner) -> FfiPtrResult;
fn delete_row(self: &mut UpsertWriter, row: &GenericRowInner) -> FfiPtrResult;
fn upsert_flush(self: &mut UpsertWriter) -> FfiResult;
// Lookuper
unsafe fn delete_lookuper(lookuper: *mut Lookuper);
fn lookup(self: &mut Lookuper, pk_row: &GenericRowInner) -> Box<LookupResultInner>;
// LookupResultInner accessors
fn lv_has_error(self: &LookupResultInner) -> bool;
fn lv_error_code(self: &LookupResultInner) -> i32;
fn lv_error_message(self: &LookupResultInner) -> &str;
fn lv_found(self: &LookupResultInner) -> bool;
fn lv_field_count(self: &LookupResultInner) -> usize;
fn lv_column_name(self: &LookupResultInner, field: usize) -> Result<&str>;
fn lv_column_type(self: &LookupResultInner, field: usize) -> Result<i32>;
fn lv_is_null(self: &LookupResultInner, field: usize) -> Result<bool>;
fn lv_get_bool(self: &LookupResultInner, field: usize) -> Result<bool>;
fn lv_get_i32(self: &LookupResultInner, field: usize) -> Result<i32>;
fn lv_get_i64(self: &LookupResultInner, field: usize) -> Result<i64>;
fn lv_get_f32(self: &LookupResultInner, field: usize) -> Result<f32>;
fn lv_get_f64(self: &LookupResultInner, field: usize) -> Result<f64>;
fn lv_get_str(self: &LookupResultInner, field: usize) -> Result<&str>;
fn lv_get_bytes(self: &LookupResultInner, field: usize) -> Result<&[u8]>;
fn lv_get_date_days(self: &LookupResultInner, field: usize) -> Result<i32>;
fn lv_get_time_millis(self: &LookupResultInner, field: usize) -> Result<i32>;
fn lv_get_ts_millis(self: &LookupResultInner, field: usize) -> Result<i64>;
fn lv_get_ts_nanos(self: &LookupResultInner, field: usize) -> Result<i32>;
fn lv_is_ts_ltz(self: &LookupResultInner, field: usize) -> Result<bool>;
fn lv_get_decimal_str(self: &LookupResultInner, field: usize) -> Result<String>;
// ValueInner — recursive value handle for complex column reads.
fn lv_get_value(self: &LookupResultInner, field: usize) -> Result<Box<ValueInner>>;
fn sv_get_value(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<Box<ValueInner>>;
fn v_type(self: &ValueInner) -> i32;
fn v_is_null(self: &ValueInner) -> bool;
fn v_get_bool(self: &ValueInner) -> Result<bool>;
fn v_get_i32(self: &ValueInner) -> Result<i32>;
fn v_get_i64(self: &ValueInner) -> Result<i64>;
fn v_get_f32(self: &ValueInner) -> Result<f32>;
fn v_get_f64(self: &ValueInner) -> Result<f64>;
fn v_get_str(self: &ValueInner) -> Result<String>;
fn v_get_bytes(self: &ValueInner) -> Result<Vec<u8>>;
fn v_get_date_days(self: &ValueInner) -> Result<i32>;
fn v_get_time_millis(self: &ValueInner) -> Result<i32>;
fn v_get_ts_millis(self: &ValueInner) -> Result<i64>;
fn v_get_ts_nanos(self: &ValueInner) -> Result<i32>;
fn v_get_decimal_str(self: &ValueInner) -> Result<String>;
fn v_size(self: &ValueInner) -> Result<usize>;
fn v_field_count(self: &ValueInner) -> Result<usize>;
fn v_at(self: &ValueInner, i: usize) -> Result<Box<ValueInner>>;
fn v_key_at(self: &ValueInner, i: usize) -> Result<Box<ValueInner>>;
fn v_value_at(self: &ValueInner, i: usize) -> Result<Box<ValueInner>>;
fn v_field(self: &ValueInner, i: usize) -> Result<Box<ValueInner>>;
fn v_field_by_name(self: &ValueInner, name: &str) -> Result<Box<ValueInner>>;
// PrefixLookuper
unsafe fn delete_prefix_lookuper(lookuper: *mut PrefixLookuper);
fn prefix_lookup(
self: &mut PrefixLookuper,
prefix_row: &GenericRowInner,
) -> Box<PrefixLookupResultInner>;
// PrefixLookupResultInner accessors — like LookupResultInner but indexed
// by record, since a prefix lookup returns zero-or-more rows.
fn plv_has_error(self: &PrefixLookupResultInner) -> bool;
fn plv_error_code(self: &PrefixLookupResultInner) -> i32;
fn plv_error_message(self: &PrefixLookupResultInner) -> &str;
fn plv_row_count(self: &PrefixLookupResultInner) -> usize;
fn plv_field_count(self: &PrefixLookupResultInner) -> usize;
fn plv_column_name(self: &PrefixLookupResultInner, field: usize) -> Result<&str>;
fn plv_column_type(self: &PrefixLookupResultInner, field: usize) -> Result<i32>;
fn plv_is_null(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<bool>;
fn plv_get_bool(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<bool>;
fn plv_get_i32(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<i32>;
fn plv_get_i64(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<i64>;
fn plv_get_f32(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<f32>;
fn plv_get_f64(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<f64>;
fn plv_get_str(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<&str>;
fn plv_get_bytes(self: &PrefixLookupResultInner, rec: usize, field: usize)
-> Result<&[u8]>;
fn plv_get_date_days(
self: &PrefixLookupResultInner,
rec: usize,
field: usize,
) -> Result<i32>;
fn plv_get_time_millis(
self: &PrefixLookupResultInner,
rec: usize,
field: usize,
) -> Result<i32>;
fn plv_get_ts_millis(
self: &PrefixLookupResultInner,
rec: usize,
field: usize,
) -> Result<i64>;
fn plv_get_ts_nanos(
self: &PrefixLookupResultInner,
rec: usize,
field: usize,
) -> Result<i32>;
fn plv_is_ts_ltz(self: &PrefixLookupResultInner, rec: usize, field: usize) -> Result<bool>;
fn plv_get_decimal_str(
self: &PrefixLookupResultInner,
rec: usize,
field: usize,
) -> Result<String>;
fn plv_get_value(
self: &PrefixLookupResultInner,
rec: usize,
field: usize,
) -> Result<Box<ValueInner>>;
// LogScanner
unsafe fn delete_log_scanner(scanner: *mut LogScanner);
fn subscribe(self: &LogScanner, bucket_id: i32, start_offset: i64) -> FfiResult;
fn subscribe_buckets(
self: &LogScanner,
subscriptions: Vec<FfiBucketSubscription>,
) -> FfiResult;
fn subscribe_partition(
self: &LogScanner,
partition_id: i64,
bucket_id: i32,
start_offset: i64,
) -> FfiResult;
fn subscribe_partition_buckets(
self: &LogScanner,
subscriptions: Vec<FfiPartitionBucketSubscription>,
) -> FfiResult;
fn unsubscribe(self: &LogScanner, bucket_id: i32) -> FfiResult;
fn unsubscribe_partition(self: &LogScanner, partition_id: i64, bucket_id: i32)
-> FfiResult;
fn poll(self: &LogScanner, timeout_ms: i64) -> Box<ScanResultInner>;
fn poll_record_batch(self: &LogScanner, timeout_ms: i64) -> FfiArrowRecordBatchesResult;
fn free_arrow_ffi_structures(array_ptr: usize, schema_ptr: usize);
// BatchScanner
unsafe fn delete_batch_scanner(scanner: *mut BatchScanner);
fn next_batch(self: &BatchScanner) -> FfiArrowRecordBatchesResult;
fn collect_all_batches(self: &BatchScanner) -> FfiArrowRecordBatchesResult;
// ScanResultInner accessors
fn sv_has_error(self: &ScanResultInner) -> bool;
fn sv_error_code(self: &ScanResultInner) -> i32;
fn sv_error_message(self: &ScanResultInner) -> &str;
fn sv_record_count(self: &ScanResultInner) -> usize;
fn sv_column_count(self: &ScanResultInner) -> usize;
fn sv_column_name(self: &ScanResultInner, field: usize) -> Result<&str>;
fn sv_column_type(self: &ScanResultInner, field: usize) -> Result<i32>;
fn sv_offset(self: &ScanResultInner, bucket: usize, rec: usize) -> i64;
fn sv_timestamp(self: &ScanResultInner, bucket: usize, rec: usize) -> i64;
fn sv_change_type(self: &ScanResultInner, bucket: usize, rec: usize) -> i32;
fn sv_field_count(self: &ScanResultInner) -> usize;
fn sv_is_null(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<bool>;
fn sv_get_bool(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<bool>;
fn sv_get_i32(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<i32>;
fn sv_get_i64(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<i64>;
fn sv_get_f32(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<f32>;
fn sv_get_f64(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<f64>;
fn sv_get_str(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<&str>;
fn sv_get_bytes(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<&[u8]>;
fn sv_get_date_days(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<i32>;
fn sv_get_time_millis(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<i32>;
fn sv_get_ts_millis(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<i64>;
fn sv_get_ts_nanos(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<i32>;
fn sv_is_ts_ltz(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<bool>;
fn sv_get_decimal_str(
self: &ScanResultInner,
bucket: usize,
rec: usize,
field: usize,
) -> Result<String>;
fn sv_bucket_infos(self: &ScanResultInner) -> &Vec<FfiBucketInfo>;
}
}
pub struct Connection {
inner: Arc<fcore::client::FlussConnection>,
}
pub struct Admin {
inner: Arc<fcore::client::FlussAdmin>,
}
pub struct Table {
connection: Arc<fcore::client::FlussConnection>,
metadata: Arc<fcore::client::Metadata>,
table_info: fcore::metadata::TableInfo,
table_path: fcore::metadata::TablePath,
has_pk: bool,
}
pub struct AppendWriter {
inner: fcore::client::AppendWriter,
table_info: fcore::metadata::TableInfo,
}
pub struct WriteResult {
inner: Option<fcore::client::WriteResultFuture>,
}
enum ScannerKind {
Record(fcore::client::LogScanner),
Batch(fcore::client::RecordBatchLogScanner),
}
pub struct LogScanner {
scanner: ScannerKind,
/// Fluss columns matching the projected Arrow fields (1:1 by index).
/// For non-projected scanners this is the full table schema columns.
projected_columns: Vec<fcore::metadata::Column>,
}
pub struct BatchScanner {
inner: Mutex<fcore::client::LimitBatchScanner>,
}
pub struct UpsertWriter {
inner: fcore::client::UpsertWriter,
table_info: fcore::metadata::TableInfo,
}
pub struct Lookuper {
inner: fcore::client::Lookuper,
table_info: fcore::metadata::TableInfo,
}
pub struct PrefixLookuper {
inner: PrefixKeyLookuper,
table_info: TableInfo,
/// Full-schema indices of the lookup columns, used to compact the input row.
lookup_column_indices: Vec<usize>,
}
/// Error code for client-side errors that did not originate from the server API protocol.
/// Must be non-zero so that CPP `Result::Ok()` (which checks `error_code == 0`) correctly
/// detects client-side errors as failures. The value -2 is outside the server API error
/// code range (-1 .. 57+), so it will never collide with current or future API codes.
const CLIENT_ERROR_CODE: i32 = -2;
fn ok_result() -> ffi::FfiResult {
ffi::FfiResult {
error_code: 0,
error_message: String::new(),
}
}
fn err_result(code: i32, msg: String) -> ffi::FfiResult {
ffi::FfiResult {
error_code: code,
error_message: msg,
}
}
/// Create a client-side error result (not from server API).
fn client_err(msg: String) -> ffi::FfiResult {
err_result(CLIENT_ERROR_CODE, msg)
}
fn err_from_core_error(e: &Error) -> ffi::FfiResult {
// Transport failures map to `NetworkException` (Java parity,
// retriable).
match e {
Error::FlussAPIError { api_error } => err_result(api_error.code, api_error.message.clone()),
Error::RpcError { .. } => {
err_result(CoreFlussError::NetworkException.code(), e.to_string())
}
_ => client_err(e.to_string()),
}
}
fn ok_ptr(ptr: usize) -> ffi::FfiPtrResult {
ffi::FfiPtrResult {
result: ok_result(),
ptr,
}
}
fn client_err_ptr(msg: String) -> ffi::FfiPtrResult {
ffi::FfiPtrResult {
result: client_err(msg),
ptr: 0usize,
}
}
fn err_ptr_from_core(e: &fcore::error::Error) -> ffi::FfiPtrResult {
ffi::FfiPtrResult {
result: err_from_core_error(e),
ptr: 0usize,
}
}
fn empty_arrow_batches_result(result: ffi::FfiResult) -> ffi::FfiArrowRecordBatchesResult {
ffi::FfiArrowRecordBatchesResult {
result,
arrow_batches: ffi::FfiArrowRecordBatches { batches: vec![] },
}
}
/// Wrap the result of `core_scan_batches_to_ffi` in an `FfiArrowRecordBatchesResult`.
fn arrow_batches_result(
converted: Result<ffi::FfiArrowRecordBatches, String>,
) -> ffi::FfiArrowRecordBatchesResult {
match converted {
Ok(arrow_batches) => ffi::FfiArrowRecordBatchesResult {
result: ok_result(),
arrow_batches,
},
Err(e) => empty_arrow_batches_result(client_err(e)),
}
}
// Connection implementation
fn new_connection(config: &ffi::FfiConfig) -> ffi::FfiPtrResult {
let assigner_type = match config
.writer_bucket_no_key_assigner
.parse::<fluss::config::NoKeyAssigner>()
{
Ok(v) => v,
Err(e) => return client_err_ptr(format!("Invalid bucket assigner type: {e}")),
};
let config_core = fluss::config::Config {
bootstrap_servers: config.bootstrap_servers.to_string(),
writer_request_max_size: config.writer_request_max_size,
writer_acks: config.writer_acks.to_string(),
writer_retries: config.writer_retries,
writer_batch_size: config.writer_batch_size,
writer_dynamic_batch_size_enabled: config.writer_dynamic_batch_size_enabled,
writer_dynamic_batch_size_min: config.writer_dynamic_batch_size_min,
writer_batch_timeout_ms: config.writer_batch_timeout_ms,
writer_bucket_no_key_assigner: assigner_type,
scanner_remote_log_prefetch_num: config.scanner_remote_log_prefetch_num,
remote_file_download_thread_num: config.remote_file_download_thread_num,
scanner_remote_log_read_concurrency: config.scanner_remote_log_read_concurrency,
scanner_log_max_poll_records: config.scanner_log_max_poll_records,
scanner_log_fetch_max_bytes: config.scanner_log_fetch_max_bytes,
scanner_log_fetch_min_bytes: config.scanner_log_fetch_min_bytes,
scanner_log_fetch_wait_max_time_ms: config.scanner_log_fetch_wait_max_time_ms,
scanner_log_fetch_max_bytes_for_bucket: config.scanner_log_fetch_max_bytes_for_bucket,
writer_enable_idempotence: config.writer_enable_idempotence,
writer_max_inflight_requests_per_bucket: config.writer_max_inflight_requests_per_bucket,
writer_buffer_memory_size: config.writer_buffer_memory_size,
writer_buffer_wait_timeout_ms: config.writer_buffer_wait_timeout_ms,
connect_timeout_ms: config.connect_timeout_ms,
security_protocol: config.security_protocol.to_string(),
security_sasl_mechanism: config.security_sasl_mechanism.to_string(),
security_sasl_username: config.security_sasl_username.to_string(),
security_sasl_password: config.security_sasl_password.to_string(),
lookup_queue_size: config.lookup_queue_size,
lookup_max_batch_size: config.lookup_max_batch_size,
lookup_batch_timeout_ms: config.lookup_batch_timeout_ms,
lookup_max_inflight_requests: config.lookup_max_inflight_requests,
lookup_max_retries: config.lookup_max_retries,
};
let conn = RUNTIME.block_on(async { fcore::client::FlussConnection::new(config_core).await });
match conn {
Ok(c) => {
let ptr = Box::into_raw(Box::new(Connection { inner: Arc::new(c) }));
ok_ptr(ptr as usize)
}
Err(e) => err_ptr_from_core(&e),
}
}
unsafe fn delete_connection(conn: *mut Connection) {
if !conn.is_null() {
unsafe {
drop(Box::from_raw(conn));
}
}
}
impl Connection {
fn get_admin(&self) -> ffi::FfiPtrResult {
let admin_result = self.inner.get_admin();
match admin_result {
Ok(admin) => {
let ptr = Box::into_raw(Box::new(Admin { inner: admin }));
ok_ptr(ptr as usize)
}
Err(e) => err_ptr_from_core(&e),
}
}
fn get_table(&self, table_path: &ffi::FfiTablePath) -> ffi::FfiPtrResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let table_result = RUNTIME.block_on(async { self.inner.get_table(&path).await });
match table_result {
Ok(t) => {
let ptr = Box::into_raw(Box::new(Table {
connection: self.inner.clone(),
metadata: t.metadata().clone(),
table_info: t.get_table_info().clone(),
table_path: t.table_path().clone(),
has_pk: t.has_primary_key(),
}));
ok_ptr(ptr as usize)
}
Err(e) => err_ptr_from_core(&e),
}
}
}
// Admin implementation
unsafe fn delete_admin(admin: *mut Admin) {
if !admin.is_null() {
unsafe {
drop(Box::from_raw(admin));
}
}
}
impl Admin {
fn create_table(
&self,
table_path: &ffi::FfiTablePath,
descriptor: &ffi::FfiTableDescriptor,
ignore_if_exists: bool,
) -> ffi::FfiResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let core_descriptor = match types::ffi_descriptor_to_core(descriptor) {
Ok(d) => d,
Err(e) => return client_err(e.to_string()),
};
let result = RUNTIME.block_on(async {
self.inner
.create_table(&path, &core_descriptor, ignore_if_exists)
.await
});
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn create_table_arrow(
&self,
table_path: &ffi::FfiTablePath,
descriptor: &ffi::FfiTableDescriptor,
arrow_schema_ptr: usize,
ignore_if_exists: bool,
) -> ffi::FfiResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
// Safety: C++ exports the schema via `arrow::ExportSchema` into a heap
// `FFI_ArrowSchema` whose pointer is passed here; ownership transfers.
let core_descriptor =
match unsafe { types::arrow_ffi_to_core_descriptor(arrow_schema_ptr, descriptor) } {
Ok(d) => d,
Err(e) => return client_err(e.to_string()),
};
let result = RUNTIME.block_on(async {
self.inner
.create_table(&path, &core_descriptor, ignore_if_exists)
.await
});
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn drop_table(
&self,
table_path: &ffi::FfiTablePath,
ignore_if_not_exists: bool,
) -> ffi::FfiResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let result =
RUNTIME.block_on(async { self.inner.drop_table(&path, ignore_if_not_exists).await });
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn get_table_info(&self, table_path: &ffi::FfiTablePath) -> ffi::FfiTableInfoResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let result = RUNTIME.block_on(async { self.inner.get_table_info(&path).await });
match result {
Ok(info) => ffi::FfiTableInfoResult {
result: ok_result(),
table_info: types::core_table_info_to_ffi(&info),
},
Err(e) => ffi::FfiTableInfoResult {
result: err_from_core_error(&e),
table_info: types::empty_table_info(),
},
}
}
fn get_latest_lake_snapshot(
&self,
table_path: &ffi::FfiTablePath,
) -> ffi::FfiLakeSnapshotResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let result = RUNTIME.block_on(async { self.inner.get_latest_lake_snapshot(&path).await });
match result {
Ok(snapshot) => ffi::FfiLakeSnapshotResult {
result: ok_result(),
lake_snapshot: types::core_lake_snapshot_to_ffi(&snapshot),
},
Err(e) => ffi::FfiLakeSnapshotResult {
result: err_from_core_error(&e),
lake_snapshot: ffi::FfiLakeSnapshot {
snapshot_id: -1,
bucket_offsets: vec![],
},
},
}
}
// Helper function for common list offsets functionality
fn do_list_offsets(
&self,
table_path: &ffi::FfiTablePath,
partition_name: Option<&str>,
bucket_ids: Vec<i32>,
offset_query: &ffi::FfiOffsetQuery,
) -> ffi::FfiListOffsetsResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let offset_spec = match offset_query.offset_type {
0 => OffsetSpec::Earliest,
1 => OffsetSpec::Latest,
2 => OffsetSpec::Timestamp(offset_query.timestamp),
_ => {
return ffi::FfiListOffsetsResult {
result: client_err(format!(
"Invalid offset_type: {}",
offset_query.offset_type
)),
bucket_offsets: vec![],
};
}
};
let result = RUNTIME.block_on(async {
if let Some(part_name) = partition_name {
self.inner
.list_partition_offsets(&path, part_name, &bucket_ids, offset_spec)
.await
} else {
self.inner
.list_offsets(&path, &bucket_ids, offset_spec)
.await
}
});
match result {
Ok(offsets) => {
let bucket_offsets: Vec<ffi::FfiBucketOffsetPair> = offsets
.into_iter()
.map(|(bucket_id, offset)| ffi::FfiBucketOffsetPair { bucket_id, offset })
.collect();
ffi::FfiListOffsetsResult {
result: ok_result(),
bucket_offsets,
}
}
Err(e) => ffi::FfiListOffsetsResult {
result: err_from_core_error(&e),
bucket_offsets: vec![],
},
}
}
fn list_offsets(
&self,
table_path: &ffi::FfiTablePath,
bucket_ids: Vec<i32>,
offset_query: &ffi::FfiOffsetQuery,
) -> ffi::FfiListOffsetsResult {
self.do_list_offsets(table_path, None, bucket_ids, offset_query)
}
fn list_partition_offsets(
&self,
table_path: &ffi::FfiTablePath,
partition_name: String,
bucket_ids: Vec<i32>,
offset_query: &ffi::FfiOffsetQuery,
) -> ffi::FfiListOffsetsResult {
self.do_list_offsets(table_path, Some(&partition_name), bucket_ids, offset_query)
}
fn list_partition_infos(
&self,
table_path: &ffi::FfiTablePath,
) -> ffi::FfiListPartitionInfosResult {
self.do_list_partition_infos(table_path, None)
}
fn list_partition_infos_with_spec(
&self,
table_path: &ffi::FfiTablePath,
partition_spec: Vec<ffi::FfiPartitionKeyValue>,
) -> ffi::FfiListPartitionInfosResult {
let spec_map: std::collections::HashMap<String, String> = partition_spec
.into_iter()
.map(|kv| (kv.key, kv.value))
.collect();
let spec = fcore::metadata::PartitionSpec::new(spec_map);
self.do_list_partition_infos(table_path, Some(&spec))
}
fn create_partition(
&self,
table_path: &ffi::FfiTablePath,
partition_spec: Vec<ffi::FfiPartitionKeyValue>,
ignore_if_exists: bool,
) -> ffi::FfiResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let spec_map: std::collections::HashMap<String, String> = partition_spec
.into_iter()
.map(|kv| (kv.key, kv.value))
.collect();
let partition_spec = fcore::metadata::PartitionSpec::new(spec_map);
let result = RUNTIME.block_on(async {
self.inner
.create_partition(&path, &partition_spec, ignore_if_exists)
.await
});
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn drop_partition(
&self,
table_path: &ffi::FfiTablePath,
partition_spec: Vec<ffi::FfiPartitionKeyValue>,
ignore_if_not_exists: bool,
) -> ffi::FfiResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let spec_map: std::collections::HashMap<String, String> = partition_spec
.into_iter()
.map(|kv| (kv.key, kv.value))
.collect();
let partition_spec = fcore::metadata::PartitionSpec::new(spec_map);
let result = RUNTIME.block_on(async {
self.inner
.drop_partition(&path, &partition_spec, ignore_if_not_exists)
.await
});
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn create_database(
&self,
database_name: &str,
descriptor: &ffi::FfiDatabaseDescriptor,
ignore_if_exists: bool,
) -> ffi::FfiResult {
let descriptor_opt = types::ffi_database_descriptor_to_core(descriptor);
let result = RUNTIME.block_on(async {
self.inner
.create_database(database_name, descriptor_opt.as_ref(), ignore_if_exists)
.await
});
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn drop_database(
&self,
database_name: &str,
ignore_if_not_exists: bool,
cascade: bool,
) -> ffi::FfiResult {
let result = RUNTIME.block_on(async {
self.inner
.drop_database(database_name, ignore_if_not_exists, cascade)
.await
});
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
fn list_databases(&self) -> ffi::FfiListDatabasesResult {
let result = RUNTIME.block_on(async { self.inner.list_databases().await });
match result {
Ok(names) => ffi::FfiListDatabasesResult {
result: ok_result(),
database_names: names,
},
Err(e) => ffi::FfiListDatabasesResult {
result: err_from_core_error(&e),
database_names: vec![],
},
}
}
fn database_exists(&self, database_name: &str) -> ffi::FfiBoolResult {
let result = RUNTIME.block_on(async { self.inner.database_exists(database_name).await });
match result {
Ok(exists) => ffi::FfiBoolResult {
result: ok_result(),
value: exists,
},
Err(e) => ffi::FfiBoolResult {
result: err_from_core_error(&e),
value: false,
},
}
}
fn get_database_info(&self, database_name: &str) -> ffi::FfiDatabaseInfoResult {
let result = RUNTIME.block_on(async { self.inner.get_database_info(database_name).await });
match result {
Ok(info) => ffi::FfiDatabaseInfoResult {
result: ok_result(),
database_info: types::core_database_info_to_ffi(&info),
},
Err(e) => ffi::FfiDatabaseInfoResult {
result: err_from_core_error(&e),
database_info: ffi::FfiDatabaseInfo {
database_name: String::new(),
comment: String::new(),
properties: vec![],
created_time: 0,
modified_time: 0,
},
},
}
}
fn list_tables(&self, database_name: &str) -> ffi::FfiListTablesResult {
let result = RUNTIME.block_on(async { self.inner.list_tables(database_name).await });
match result {
Ok(names) => ffi::FfiListTablesResult {
result: ok_result(),
table_names: names,
},
Err(e) => ffi::FfiListTablesResult {
result: err_from_core_error(&e),
table_names: vec![],
},
}
}
fn table_exists(&self, table_path: &ffi::FfiTablePath) -> ffi::FfiBoolResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let result = RUNTIME.block_on(async { self.inner.table_exists(&path).await });
match result {
Ok(exists) => ffi::FfiBoolResult {
result: ok_result(),
value: exists,
},
Err(e) => ffi::FfiBoolResult {
result: err_from_core_error(&e),
value: false,
},
}
}
fn do_list_partition_infos(
&self,
table_path: &ffi::FfiTablePath,
partial_partition_spec: Option<&fcore::metadata::PartitionSpec>,
) -> ffi::FfiListPartitionInfosResult {
let path = fcore::metadata::TablePath::new(
table_path.database_name.clone(),
table_path.table_name.clone(),
);
let result = RUNTIME.block_on(async {
self.inner
.list_partition_infos_with_spec(&path, partial_partition_spec)
.await
});
match result {
Ok(infos) => {
let partition_infos: Vec<ffi::FfiPartitionInfo> = infos
.into_iter()
.map(|info| ffi::FfiPartitionInfo {
partition_id: info.get_partition_id(),
partition_name: info.get_partition_name(),
})
.collect();
ffi::FfiListPartitionInfosResult {
result: ok_result(),
partition_infos,
}
}
Err(e) => ffi::FfiListPartitionInfosResult {
result: err_from_core_error(&e),
partition_infos: vec![],
},
}
}
fn get_server_nodes(&self) -> ffi::FfiServerNodesResult {
let result = RUNTIME.block_on(async { self.inner.get_server_nodes().await });
match result {
Ok(nodes) => {
let server_nodes: Vec<ffi::FfiServerNode> = nodes
.into_iter()
.map(|node| ffi::FfiServerNode {
node_id: node.id(),
host: node.host().to_string(),
port: node.port(),
server_type: node.server_type().to_string(),
uid: node.uid().to_string(),
})
.collect();
ffi::FfiServerNodesResult {
result: ok_result(),
server_nodes,
}
}
Err(e) => ffi::FfiServerNodesResult {
result: err_from_core_error(&e),
server_nodes: vec![],
},
}
}
}
// Table implementation
unsafe fn delete_table(table: *mut Table) {
if !table.is_null() {
unsafe {
drop(Box::from_raw(table));
}
}
}
impl Table {
fn fluss_table(&self) -> fcore::client::FlussTable<'_> {
fcore::client::FlussTable::new(
&self.connection,
self.metadata.clone(),
self.table_info.clone(),
)
}
fn resolve_projected_columns(
&self,
indices: &[usize],
) -> Result<Vec<fcore::metadata::Column>, String> {
let all_columns = self.table_info.get_schema().columns();
indices
.iter()
.map(|&i| {
all_columns.get(i).cloned().ok_or_else(|| {
format!(
"Invalid column index {i}: schema has {} columns",
all_columns.len()
)
})
})
.collect()
}
fn new_append_writer(&self) -> ffi::FfiPtrResult {
let _enter = RUNTIME.enter();
let table_append = match self.fluss_table().new_append() {
Ok(a) => a,
Err(e) => return err_ptr_from_core(&e),
};
let writer = match table_append.create_writer() {
Ok(w) => w,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(AppendWriter {
inner: writer,
table_info: self.table_info.clone(),
}));
ok_ptr(ptr as usize)
}
fn create_scanner(&self, column_indices: Vec<usize>, batch: bool) -> ffi::FfiPtrResult {
RUNTIME.block_on(async {
let fluss_table = self.fluss_table();
let scan = fluss_table.new_scan();
let (projected_columns, scan) = if column_indices.is_empty() {
(self.table_info.get_schema().columns().to_vec(), scan)
} else {
let cols = match self.resolve_projected_columns(&column_indices) {
Ok(c) => c,
Err(e) => return client_err_ptr(e),
};
let scan = match scan.project(&column_indices) {
Ok(s) => s,
Err(e) => return err_ptr_from_core(&e),
};
(cols, scan)
};
let scanner = if batch {
match scan.create_record_batch_log_scanner() {
Ok(s) => ScannerKind::Batch(s),
Err(e) => return err_ptr_from_core(&e),
}
} else {
match scan.create_log_scanner() {
Ok(s) => ScannerKind::Record(s),
Err(e) => return err_ptr_from_core(&e),
}
};
let ptr = Box::into_raw(Box::new(LogScanner {
scanner,
projected_columns,
}));
ok_ptr(ptr as usize)
})
}
fn create_bucket_batch_scanner(
&self,
column_indices: Vec<usize>,
limit: i32,
table_id: i64,
partition_id: i64,
bucket_id: i32,
) -> ffi::FfiPtrResult {
RUNTIME.block_on(async {
let fluss_table = self.fluss_table();
let scan = fluss_table.new_scan();
let scan = if column_indices.is_empty() {
scan
} else {
match scan.project(&column_indices) {
Ok(s) => s,
Err(e) => return err_ptr_from_core(&e),
}
};
let scan = match scan.limit(limit) {
Ok(s) => s,
Err(e) => return err_ptr_from_core(&e),
};
// A negative partition id encodes "no partition" across the FFI boundary.
let partition = (partition_id >= 0).then_some(partition_id);
let bucket =
fcore::metadata::TableBucket::new_with_partition(table_id, partition, bucket_id);
let scanner = match scan.create_bucket_batch_scanner(bucket) {
Ok(s) => s,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(BatchScanner {
inner: Mutex::new(scanner),
}));
ok_ptr(ptr as usize)
})
}
fn get_table_info_from_table(&self) -> ffi::FfiTableInfo {
types::core_table_info_to_ffi(&self.table_info)
}
fn get_table_path(&self) -> ffi::FfiTablePath {
ffi::FfiTablePath {
database_name: self.table_path.database().to_string(),
table_name: self.table_path.table().to_string(),
}
}
fn has_primary_key(&self) -> bool {
self.has_pk
}
fn create_upsert_writer(&self, column_indices: Vec<usize>) -> ffi::FfiPtrResult {
let _enter = RUNTIME.enter();
let table_upsert = match self.fluss_table().new_upsert() {
Ok(u) => u,
Err(e) => return err_ptr_from_core(&e),
};
let table_upsert = if column_indices.is_empty() {
table_upsert
} else {
match table_upsert.partial_update(Some(column_indices)) {
Ok(u) => u,
Err(e) => return err_ptr_from_core(&e),
}
};
let writer = match table_upsert.create_writer() {
Ok(w) => w,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(UpsertWriter {
inner: writer,
table_info: self.table_info.clone(),
}));
ok_ptr(ptr as usize)
}
fn new_lookuper(&self) -> ffi::FfiPtrResult {
let _enter = RUNTIME.enter();
let table_lookup = match self.fluss_table().new_lookup() {
Ok(l) => l,
Err(e) => return err_ptr_from_core(&e),
};
let lookuper = match table_lookup.create_lookuper() {
Ok(l) => l,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(Lookuper {
inner: lookuper,
table_info: self.table_info.clone(),
}));
ok_ptr(ptr as usize)
}
fn new_prefix_lookuper(&self, lookup_column_names: Vec<String>) -> ffi::FfiPtrResult {
let _enter = RUNTIME.enter();
let table_lookup = match self.fluss_table().new_lookup() {
Ok(l) => l,
Err(e) => return err_ptr_from_core(&e),
};
// `create_lookuper` validates the column names (rejecting unknown ones),
// so by the time it succeeds every name resolves to an index.
let row_type = self.table_info.row_type();
let lookup_column_indices: Vec<usize> = lookup_column_names
.iter()
.filter_map(|name| row_type.get_field_index(name))
.collect();
let lookuper = match table_lookup
.lookup_by(lookup_column_names)
.create_lookuper()
{
Ok(l) => l,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(PrefixLookuper {
inner: lookuper,
table_info: self.table_info.clone(),
lookup_column_indices,
}));
ok_ptr(ptr as usize)
}
}
// AppendWriter implementation
unsafe fn delete_append_writer(writer: *mut AppendWriter) {
if !writer.is_null() {
unsafe {
drop(Box::from_raw(writer));
}
}
}
impl AppendWriter {
fn append(&mut self, row: &GenericRowInner) -> ffi::FfiPtrResult {
let schema = self.table_info.get_schema();
let generic_row = match types::resolve_row_types(&row.row, Some(schema)) {
Ok(r) => r,
Err(e) => return client_err_ptr(e.to_string()),
};
let result_future = match self.inner.append(&generic_row) {
Ok(f) => f,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(WriteResult {
inner: Some(result_future),
}));
ok_ptr(ptr as usize)
}
fn append_arrow_batch(&mut self, array_ptr: usize, schema_ptr: usize) -> ffi::FfiPtrResult {
// Safety: C++ allocates these via `new ArrowArray/ArrowSchema` after a
// successful `ExportRecordBatch`, so both pointers are valid heap
// allocations that we take ownership of here.
let ffi_array = unsafe { *Box::from_raw(array_ptr as *mut FFI_ArrowArray) };
let ffi_schema = unsafe { Box::from_raw(schema_ptr as *mut FFI_ArrowSchema) };
// Safety: `from_ffi` requires that the array and schema conform to the
// Arrow C Data Interface, which is guaranteed by C++'s ExportRecordBatch.
let array_data = match unsafe { arrow::ffi::from_ffi(ffi_array, &ffi_schema) } {
Ok(d) => d,
Err(e) => return client_err_ptr(format!("Failed to import Arrow batch: {e}")),
};
// ffi_array is consumed by from_ffi; ffi_schema is dropped here (Box goes out of scope)
// Reconstruct RecordBatch from the imported StructArray data
let struct_array = arrow::array::StructArray::from(array_data);
let batch = arrow::record_batch::RecordBatch::from(struct_array);
let result_future = match self.inner.append_arrow_batch(batch) {
Ok(f) => f,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(WriteResult {
inner: Some(result_future),
}));
ok_ptr(ptr as usize)
}
fn flush(&mut self) -> ffi::FfiResult {
let result = RUNTIME.block_on(async { self.inner.flush().await });
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
}
unsafe fn delete_write_result(wr: *mut WriteResult) {
if !wr.is_null() {
unsafe {
drop(Box::from_raw(wr));
}
}
}
impl WriteResult {
fn wait(&mut self) -> ffi::FfiResult {
if let Some(future) = self.inner.take() {
let result = RUNTIME.block_on(future);
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
} else {
client_err("WriteResult already consumed".to_string())
}
}
}
// UpsertWriter implementation
unsafe fn delete_upsert_writer(writer: *mut UpsertWriter) {
if !writer.is_null() {
unsafe {
drop(Box::from_raw(writer));
}
}
}
impl UpsertWriter {
/// Pad row with Null to full schema width.
/// This allows callers to only set the fields they care about.
fn pad_row<'a>(&self, mut row: fcore::row::GenericRow<'a>) -> fcore::row::GenericRow<'a> {
let num_columns = self.table_info.get_schema().columns().len();
if row.values.len() < num_columns {
row.values.resize(num_columns, fcore::row::Datum::Null);
}
row
}
fn upsert(&mut self, row: &GenericRowInner) -> ffi::FfiPtrResult {
let schema = self.table_info.get_schema();
let generic_row = match types::resolve_row_types(&row.row, Some(schema)) {
Ok(r) => r,
Err(e) => return client_err_ptr(e.to_string()),
};
let generic_row = self.pad_row(generic_row);
let result_future = match self.inner.upsert(&generic_row) {
Ok(f) => f,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(WriteResult {
inner: Some(result_future),
}));
ok_ptr(ptr as usize)
}
fn delete_row(&mut self, row: &GenericRowInner) -> ffi::FfiPtrResult {
let schema = self.table_info.get_schema();
let generic_row = match types::resolve_row_types(&row.row, Some(schema)) {
Ok(r) => r,
Err(e) => return client_err_ptr(e.to_string()),
};
let generic_row = self.pad_row(generic_row);
let result_future = match self.inner.delete(&generic_row) {
Ok(f) => f,
Err(e) => return err_ptr_from_core(&e),
};
let ptr = Box::into_raw(Box::new(WriteResult {
inner: Some(result_future),
}));
ok_ptr(ptr as usize)
}
fn upsert_flush(&mut self) -> ffi::FfiResult {
let result = RUNTIME.block_on(async { self.inner.flush().await });
match result {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
}
}
// Lookuper implementation
unsafe fn delete_lookuper(lookuper: *mut Lookuper) {
if !lookuper.is_null() {
unsafe {
drop(Box::from_raw(lookuper));
}
}
}
impl Lookuper {
/// Build a dense PK-only row from a (possibly sparse) input row.
/// The user may set PK values at their full schema positions (e.g. [0, 2])
/// via name-based Set(). We compact them into [0, 1, …] to match
/// the lookup_row_type the core KeyEncoder expects.
fn dense_pk_row<'a>(&self, mut row: fcore::row::GenericRow<'a>) -> fcore::row::GenericRow<'a> {
let pk_indices = self.table_info.get_schema().primary_key_indexes();
let mut dense = fcore::row::GenericRow::new(pk_indices.len());
for (dense_idx, &schema_idx) in pk_indices.iter().enumerate() {
if schema_idx < row.values.len() {
dense.values[dense_idx] =
std::mem::replace(&mut row.values[schema_idx], fcore::row::Datum::Null);
}
}
dense
}
fn lookup(&mut self, pk_row: &GenericRowInner) -> Box<LookupResultInner> {
let schema = self.table_info.get_schema();
let generic_row = match types::resolve_row_types(&pk_row.row, Some(schema)) {
Ok(r) => self.dense_pk_row(r),
Err(e) => {
return Box::new(LookupResultInner::from_error(
CLIENT_ERROR_CODE,
e.to_string(),
));
}
};
let lookup_result = match RUNTIME.block_on(self.inner.lookup(&generic_row)) {
Ok(r) => r,
Err(e) => {
let ffi_err = err_from_core_error(&e);
return Box::new(LookupResultInner::from_error(
ffi_err.error_code,
ffi_err.error_message,
));
}
};
let columns = self.table_info.get_schema().columns().to_vec();
match lookup_result.get_single_row() {
Ok(Some(row)) => match types::compacted_row_to_owned(&row, &self.table_info) {
Ok(owned_row) => Box::new(LookupResultInner {
error: None,
found: true,
row: Some(owned_row),
columns,
}),
Err(e) => Box::new(LookupResultInner::from_error(
CLIENT_ERROR_CODE,
e.to_string(),
)),
},
Ok(None) => Box::new(LookupResultInner {
error: None,
found: false,
row: None,
columns,
}),
Err(e) => {
let ffi_err = err_from_core_error(&e);
Box::new(LookupResultInner::from_error(
ffi_err.error_code,
ffi_err.error_message,
))
}
}
}
}
// PrefixLookuper implementation
unsafe fn delete_prefix_lookuper(lookuper: *mut PrefixLookuper) {
if !lookuper.is_null() {
unsafe {
drop(Box::from_raw(lookuper));
}
}
}
impl PrefixLookuper {
/// Compact a sparse input row (prefix columns set at their schema positions)
/// into the dense, lookup-column-ordered row the core prefix encoder expects.
fn dense_prefix_row<'a>(&self, mut row: GenericRow<'a>) -> GenericRow<'a> {
let mut dense = GenericRow::new(self.lookup_column_indices.len());
for (dense_idx, &schema_idx) in self.lookup_column_indices.iter().enumerate() {
if schema_idx < row.values.len() {
dense.values[dense_idx] =
std::mem::replace(&mut row.values[schema_idx], Datum::Null);
}
}
dense
}
fn prefix_lookup(&mut self, prefix_row: &GenericRowInner) -> Box<PrefixLookupResultInner> {
let schema = self.table_info.get_schema();
let generic_row = match types::resolve_row_types(&prefix_row.row, Some(schema)) {
Ok(r) => self.dense_prefix_row(r),
Err(e) => {
return Box::new(PrefixLookupResultInner::from_error(
CLIENT_ERROR_CODE,
e.to_string(),
));
}
};
let lookup_result = match RUNTIME.block_on(self.inner.lookup(&generic_row)) {
Ok(r) => r,
Err(e) => {
let ffi_err = err_from_core_error(&e);
return Box::new(PrefixLookupResultInner::from_error(
ffi_err.error_code,
ffi_err.error_message,
));
}
};
let lookup_rows = match lookup_result.get_rows() {
Ok(rows) => rows,
Err(e) => {
let ffi_err = err_from_core_error(&e);
return Box::new(PrefixLookupResultInner::from_error(
ffi_err.error_code,
ffi_err.error_message,
));
}
};
let mut rows = Vec::with_capacity(lookup_rows.len());
for row in &lookup_rows {
match types::compacted_row_to_owned(row, &self.table_info) {
Ok(owned_row) => rows.push(owned_row),
Err(e) => {
return Box::new(PrefixLookupResultInner::from_error(
CLIENT_ERROR_CODE,
e.to_string(),
));
}
}
}
let columns = self.table_info.get_schema().columns().to_vec();
Box::new(PrefixLookupResultInner {
error: None,
rows,
columns,
})
}
}
// LogScanner implementation
unsafe fn delete_log_scanner(scanner: *mut LogScanner) {
if !scanner.is_null() {
unsafe {
drop(Box::from_raw(scanner));
}
}
}
// Helper function to free the Arrow FFI structures separately (for use after ImportRecordBatch)
pub extern "C" fn free_arrow_ffi_structures(array_ptr: usize, schema_ptr: usize) {
if array_ptr != 0 {
let _array = unsafe { Box::from_raw(array_ptr as *mut FFI_ArrowArray) };
}
if schema_ptr != 0 {
let _schema = unsafe { Box::from_raw(schema_ptr as *mut FFI_ArrowSchema) };
}
}
/// Dispatch a method call to whichever scanner variant is active.
/// Both LogScanner and RecordBatchLogScanner share the same subscribe/unsubscribe interface.
macro_rules! dispatch_scanner {
($self:expr, $method:ident($($arg:expr),*)) => {
match RUNTIME.block_on(async {
match &$self.scanner {
ScannerKind::Record(s) => s.$method($($arg),*).await,
ScannerKind::Batch(s) => s.$method($($arg),*).await,
}
}) {
Ok(_) => ok_result(),
Err(e) => err_from_core_error(&e),
}
};
}
impl LogScanner {
fn subscribe(&self, bucket_id: i32, start_offset: i64) -> ffi::FfiResult {
dispatch_scanner!(self, subscribe(bucket_id, start_offset))
}
fn subscribe_buckets(&self, subscriptions: Vec<ffi::FfiBucketSubscription>) -> ffi::FfiResult {
let bucket_offsets: HashMap<i32, i64> = subscriptions
.into_iter()
.map(|s| (s.bucket_id, s.offset))
.collect();
dispatch_scanner!(self, subscribe_buckets(&bucket_offsets))
}
fn subscribe_partition(
&self,
partition_id: PartitionId,
bucket_id: i32,
start_offset: i64,
) -> ffi::FfiResult {
dispatch_scanner!(
self,
subscribe_partition(partition_id, bucket_id, start_offset)
)
}
fn subscribe_partition_buckets(
&self,
subscriptions: Vec<ffi::FfiPartitionBucketSubscription>,
) -> ffi::FfiResult {
let offsets: HashMap<(PartitionId, i32), i64> = subscriptions
.into_iter()
.map(|s| ((s.partition_id, s.bucket_id), s.offset))
.collect();
dispatch_scanner!(self, subscribe_partition_buckets(&offsets))
}
fn unsubscribe(&self, bucket_id: i32) -> ffi::FfiResult {
dispatch_scanner!(self, unsubscribe(bucket_id))
}
fn unsubscribe_partition(&self, partition_id: PartitionId, bucket_id: i32) -> ffi::FfiResult {
dispatch_scanner!(self, unsubscribe_partition(partition_id, bucket_id))
}
fn poll(&self, timeout_ms: i64) -> Box<ScanResultInner> {
let ScannerKind::Record(ref inner) = self.scanner else {
return Box::new(ScanResultInner::from_error(
CLIENT_ERROR_CODE,
"Record-based scanner not available".to_string(),
));
};
let timeout = Duration::from_millis(timeout_ms.max(0) as u64);
let result = RUNTIME.block_on(async { inner.poll(timeout).await });
match result {
Ok(records) => {
let columns = self.projected_columns.clone();
let mut total_count = 0usize;
let mut buckets = Vec::new();
let mut bucket_infos = Vec::new();
for (table_bucket, bucket_records) in records.into_records_by_buckets() {
let count = bucket_records.len();
total_count += count;
bucket_infos.push(ffi::FfiBucketInfo {
table_id: table_bucket.table_id(),
bucket_id: table_bucket.bucket_id(),
has_partition_id: table_bucket.partition_id().is_some(),
partition_id: table_bucket.partition_id().unwrap_or(0),
record_count: count,
});
buckets.push((table_bucket, bucket_records));
}
Box::new(ScanResultInner {
error: None,
buckets,
columns,
bucket_infos,
total_count,
})
}
Err(e) => {
let ffi_err = err_from_core_error(&e);
Box::new(ScanResultInner::from_error(
ffi_err.error_code,
ffi_err.error_message,
))
}
}
}
fn poll_record_batch(&self, timeout_ms: i64) -> ffi::FfiArrowRecordBatchesResult {
let ScannerKind::Batch(ref inner_batch) = self.scanner else {
return ffi::FfiArrowRecordBatchesResult {
result: client_err("Batch-based scanner not available".to_string()),
arrow_batches: ffi::FfiArrowRecordBatches { batches: vec![] },
};
};
let timeout = Duration::from_millis(timeout_ms.max(0) as u64);
let result = RUNTIME.block_on(async { inner_batch.poll(timeout).await });
match result {
Ok(batches) => arrow_batches_result(types::core_scan_batches_to_ffi(&batches)),
Err(e) => empty_arrow_batches_result(err_from_core_error(&e)),
}
}
}
// BatchScanner implementation
unsafe fn delete_batch_scanner(scanner: *mut BatchScanner) {
if !scanner.is_null() {
unsafe {
drop(Box::from_raw(scanner));
}
}
}
impl BatchScanner {
fn next_batch(&self) -> ffi::FfiArrowRecordBatchesResult {
let mut scanner = self.inner.lock().unwrap();
match RUNTIME.block_on(scanner.next_batch()) {
Ok(Some(batch)) => arrow_batches_result(types::core_scan_batches_to_ffi(
std::slice::from_ref(&batch),
)),
Ok(None) => empty_arrow_batches_result(ok_result()),
Err(e) => empty_arrow_batches_result(err_from_core_error(&e)),
}
}
fn collect_all_batches(&self) -> ffi::FfiArrowRecordBatchesResult {
let mut scanner = self.inner.lock().unwrap();
match RUNTIME.block_on(scanner.collect_all_batches()) {
Ok(batches) => arrow_batches_result(types::core_scan_batches_to_ffi(&batches)),
Err(e) => empty_arrow_batches_result(err_from_core_error(&e)),
}
}
}
// ============================================================================
// Opaque types: GenericRowInner (write path)
// ============================================================================
pub struct GenericRowInner {
row: fcore::row::GenericRow<'static>,
}
fn new_generic_row(field_count: usize) -> Box<GenericRowInner> {
Box::new(GenericRowInner {
row: fcore::row::GenericRow::new(field_count),
})
}
impl GenericRowInner {
fn gr_reset(&mut self) {
let len = self.row.values.len();
self.row = fcore::row::GenericRow::new(len);
}
fn gr_set_null(&mut self, idx: usize) {
self.ensure_size(idx);
self.row.set_field(idx, fcore::row::Datum::Null);
}
fn gr_set_bool(&mut self, idx: usize, val: bool) {
self.ensure_size(idx);
self.row.set_field(idx, fcore::row::Datum::Bool(val));
}
fn gr_set_i32(&mut self, idx: usize, val: i32) {
self.ensure_size(idx);
self.row.set_field(idx, fcore::row::Datum::Int32(val));
}
fn gr_set_i64(&mut self, idx: usize, val: i64) {
self.ensure_size(idx);
self.row.set_field(idx, fcore::row::Datum::Int64(val));
}
fn gr_set_f32(&mut self, idx: usize, val: f32) {
self.ensure_size(idx);
self.row
.set_field(idx, fcore::row::Datum::Float32(val.into()));
}
fn gr_set_f64(&mut self, idx: usize, val: f64) {
self.ensure_size(idx);
self.row
.set_field(idx, fcore::row::Datum::Float64(val.into()));
}
fn gr_set_str(&mut self, idx: usize, val: &str) {
self.ensure_size(idx);
self.row.set_field(
idx,
fcore::row::Datum::String(std::borrow::Cow::Owned(val.to_string())),
);
}
fn gr_set_bytes(&mut self, idx: usize, val: &[u8]) {
self.ensure_size(idx);
self.row.set_field(
idx,
fcore::row::Datum::Blob(std::borrow::Cow::Owned(val.to_vec())),
);
}
fn gr_set_date(&mut self, idx: usize, days: i32) {
self.ensure_size(idx);
self.row
.set_field(idx, fcore::row::Datum::Date(fcore::row::Date::new(days)));
}
fn gr_set_time(&mut self, idx: usize, millis: i32) {
self.ensure_size(idx);
self.row
.set_field(idx, fcore::row::Datum::Time(fcore::row::Time::new(millis)));
}
fn gr_set_ts_ntz(&mut self, idx: usize, millis: i64, nanos: i32) {
self.ensure_size(idx);
// Use from_millis_nanos, falling back to millis-only on error
let ts = fcore::row::TimestampNtz::from_millis_nanos(millis, nanos)
.unwrap_or_else(|_| fcore::row::TimestampNtz::new(millis));
self.row.set_field(idx, fcore::row::Datum::TimestampNtz(ts));
}
fn gr_set_ts_ltz(&mut self, idx: usize, millis: i64, nanos: i32) {
self.ensure_size(idx);
let ts = fcore::row::TimestampLtz::from_millis_nanos(millis, nanos)
.unwrap_or_else(|_| fcore::row::TimestampLtz::new(millis));
self.row.set_field(idx, fcore::row::Datum::TimestampLtz(ts));
}
fn gr_set_decimal_str(&mut self, idx: usize, val: &str) {
self.ensure_size(idx);
// Store as string; resolve_row_types() will parse and validate against schema
self.row.set_field(
idx,
fcore::row::Datum::String(std::borrow::Cow::Owned(val.to_string())),
);
}
fn gr_set_array(&mut self, idx: usize, writer: &mut ArrayWriterInner) -> Result<(), String> {
self.ensure_size(idx);
writer.complete_if_needed()?;
let arr = writer.completed.take().ok_or_else(|| {
"ArrayWriter invariant violation: completed array missing after finalize".to_string()
})?;
self.row.set_field(idx, fcore::row::Datum::Array(arr));
Ok(())
}
fn gr_set_map(&mut self, idx: usize, writer: &mut MapWriterInner) -> Result<(), String> {
self.ensure_size(idx);
writer.complete_if_needed()?;
let map = writer.completed.take().ok_or_else(|| {
"MapWriter invariant violation: completed map missing after finalize".to_string()
})?;
self.row.set_field(idx, fcore::row::Datum::Map(map));
Ok(())
}
fn gr_set_row(&mut self, idx: usize, row: &mut GenericRowInner) -> Result<(), String> {
self.ensure_size(idx);
// Move the nested row out; the C++ side discards its writer afterwards.
let nested = std::mem::replace(&mut row.row, fcore::row::GenericRow::new(0));
self.row
.set_field(idx, fcore::row::Datum::Row(Box::new(nested)));
Ok(())
}
fn ensure_size(&mut self, idx: usize) {
if self.row.values.len() <= idx {
self.row.values.resize(idx + 1, fcore::row::Datum::Null);
}
}
}
// ============================================================================
// Shared row-reading helpers (used by both ScanResultInner and LookupResultInner)
// ============================================================================
mod row_reader {
use fcore::row::InternalRow;
use fluss as fcore;
use crate::types;
/// Get column at `field`, or error if out of bounds.
fn get_column(
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<&fcore::metadata::Column, String> {
columns.get(field).ok_or_else(|| {
format!(
"field index {field} out of range ({} columns)",
columns.len()
)
})
}
/// Validate bounds, null, and type compatibility in a single pass.
/// Returns the data type on success for callers that need to dispatch on it.
fn validate<'a>(
row: &dyn InternalRow,
columns: &'a [fcore::metadata::Column],
field: usize,
getter: &str,
allowed: impl FnOnce(&fcore::metadata::DataType) -> bool,
) -> Result<&'a fcore::metadata::DataType, String> {
let col = get_column(columns, field)?;
if row.is_null_at(field).map_err(|e| e.to_string())? {
return Err(format!("field {field} is null"));
}
let dt = col.data_type();
if !allowed(dt) {
return Err(format!(
"{getter}: column {field} has incompatible type {dt}"
));
}
Ok(dt)
}
pub fn column_type(columns: &[fcore::metadata::Column], field: usize) -> Result<i32, String> {
Ok(types::core_data_type_to_ffi(
get_column(columns, field)?.data_type(),
))
}
pub fn column_name(columns: &[fcore::metadata::Column], field: usize) -> Result<&str, String> {
Ok(get_column(columns, field)?.name())
}
pub fn is_null(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<bool, String> {
get_column(columns, field)?;
row.is_null_at(field).map_err(|e| e.to_string())
}
pub fn get_bool(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<bool, String> {
validate(row, columns, field, "get_bool", |dt| {
matches!(dt, fcore::metadata::DataType::Boolean(_))
})?;
row.get_boolean(field).map_err(|e| e.to_string())
}
pub fn get_i32(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<i32, String> {
let dt = validate(row, columns, field, "get_i32", |dt| {
matches!(
dt,
fcore::metadata::DataType::TinyInt(_)
| fcore::metadata::DataType::SmallInt(_)
| fcore::metadata::DataType::Int(_)
)
})?;
match dt {
fcore::metadata::DataType::TinyInt(_) => row
.get_byte(field)
.map(|v| v as i32)
.map_err(|e| e.to_string()),
fcore::metadata::DataType::SmallInt(_) => row
.get_short(field)
.map(|v| v as i32)
.map_err(|e| e.to_string()),
_ => row.get_int(field).map_err(|e| e.to_string()),
}
}
pub fn get_i64(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<i64, String> {
validate(row, columns, field, "get_i64", |dt| {
matches!(dt, fcore::metadata::DataType::BigInt(_))
})?;
row.get_long(field).map_err(|e| e.to_string())
}
pub fn get_f32(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<f32, String> {
validate(row, columns, field, "get_f32", |dt| {
matches!(dt, fcore::metadata::DataType::Float(_))
})?;
row.get_float(field).map_err(|e| e.to_string())
}
pub fn get_f64(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<f64, String> {
validate(row, columns, field, "get_f64", |dt| {
matches!(dt, fcore::metadata::DataType::Double(_))
})?;
row.get_double(field).map_err(|e| e.to_string())
}
pub fn get_str<'a>(
row: &'a dyn InternalRow,
columns: &'a [fcore::metadata::Column],
field: usize,
) -> Result<&'a str, String> {
let dt = validate(row, columns, field, "get_str", |dt| {
matches!(
dt,
fcore::metadata::DataType::Char(_) | fcore::metadata::DataType::String(_)
)
})?;
match dt {
fcore::metadata::DataType::Char(ct) => row
.get_char(field, ct.length() as usize)
.map_err(|e| e.to_string()),
_ => row.get_string(field).map_err(|e| e.to_string()),
}
}
pub fn get_bytes<'a>(
row: &'a dyn InternalRow,
columns: &'a [fcore::metadata::Column],
field: usize,
) -> Result<&'a [u8], String> {
let dt = validate(row, columns, field, "get_bytes", |dt| {
matches!(
dt,
fcore::metadata::DataType::Binary(_) | fcore::metadata::DataType::Bytes(_)
)
})?;
match dt {
fcore::metadata::DataType::Binary(bt) => row
.get_binary(field, bt.length())
.map_err(|e| e.to_string()),
_ => row.get_bytes(field).map_err(|e| e.to_string()),
}
}
pub fn get_date_days(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<i32, String> {
validate(row, columns, field, "get_date_days", |dt| {
matches!(dt, fcore::metadata::DataType::Date(_))
})?;
row.get_date(field)
.map(|d| d.get_inner())
.map_err(|e| e.to_string())
}
pub fn get_time_millis(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<i32, String> {
validate(row, columns, field, "get_time_millis", |dt| {
matches!(dt, fcore::metadata::DataType::Time(_))
})?;
row.get_time(field)
.map(|t| t.get_inner())
.map_err(|e| e.to_string())
}
pub fn get_ts_millis(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<i64, String> {
let dt = validate(row, columns, field, "get_ts_millis", |dt| {
matches!(
dt,
fcore::metadata::DataType::Timestamp(_)
| fcore::metadata::DataType::TimestampLTz(_)
)
})?;
match dt {
fcore::metadata::DataType::TimestampLTz(ts) => row
.get_timestamp_ltz(field, ts.precision())
.map(|v| v.get_epoch_millisecond())
.map_err(|e| e.to_string()),
fcore::metadata::DataType::Timestamp(ts) => row
.get_timestamp_ntz(field, ts.precision())
.map(|v| v.get_millisecond())
.map_err(|e| e.to_string()),
dt => Err(format!("get_ts_millis: unexpected type {dt}")),
}
}
pub fn get_ts_nanos(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<i32, String> {
let dt = validate(row, columns, field, "get_ts_nanos", |dt| {
matches!(
dt,
fcore::metadata::DataType::Timestamp(_)
| fcore::metadata::DataType::TimestampLTz(_)
)
})?;
match dt {
fcore::metadata::DataType::TimestampLTz(ts) => row
.get_timestamp_ltz(field, ts.precision())
.map(|v| v.get_nano_of_millisecond())
.map_err(|e| e.to_string()),
fcore::metadata::DataType::Timestamp(ts) => row
.get_timestamp_ntz(field, ts.precision())
.map(|v| v.get_nano_of_millisecond())
.map_err(|e| e.to_string()),
dt => Err(format!("get_ts_nanos: unexpected type {dt}")),
}
}
pub fn is_ts_ltz(columns: &[fcore::metadata::Column], field: usize) -> Result<bool, String> {
Ok(matches!(
get_column(columns, field)?.data_type(),
fcore::metadata::DataType::TimestampLTz(_)
))
}
pub fn get_decimal_str(
row: &dyn InternalRow,
columns: &[fcore::metadata::Column],
field: usize,
) -> Result<String, String> {
let dt = validate(row, columns, field, "get_decimal_str", |dt| {
matches!(dt, fcore::metadata::DataType::Decimal(_))
})?;
match dt {
fcore::metadata::DataType::Decimal(dd) => {
let decimal = row
.get_decimal(field, dd.precision() as usize, dd.scale() as usize)
.map_err(|e| e.to_string())?;
Ok(decimal.to_big_decimal().to_string())
}
dt => Err(format!("get_decimal_str: unexpected type {dt}")),
}
}
}
// ============================================================================
// array_reader — low-level accessors over an already-resolved FlussArray.
//
// `get_datum` is the single dispatch that turns any array/map element into an
// owned `Datum` for the recursive `ValueInner` handle (bounds-checked, null
// and type validated, recursing into nested ROW/MAP/ARRAY).
// ============================================================================
mod array_reader {
use super::fcore;
use fcore::metadata::DataType as DT;
use fcore::row::Datum;
fn validate_index(
arr: &fcore::row::binary_array::FlussArray,
element: usize,
op: &str,
) -> Result<(), String> {
if element < arr.size() {
Ok(())
} else {
Err(format!(
"{op}: element index out of bounds: element={element}, size={}",
arr.size()
))
}
}
pub fn is_null(
arr: &fcore::row::binary_array::FlussArray,
element: usize,
) -> Result<bool, String> {
validate_index(arr, element, "array_is_null")?;
Ok(arr.is_null_at(element))
}
pub fn get_datum(
arr: &fcore::row::binary_array::FlussArray,
elem_type: &fcore::metadata::DataType,
element: usize,
) -> Result<fcore::row::Datum<'static>, String> {
if is_null(arr, element)? {
return Ok(Datum::Null);
}
let map_err = |e: fcore::error::Error| e.to_string();
Ok(match elem_type {
DT::Boolean(_) => Datum::Bool(arr.get_boolean(element).map_err(map_err)?),
DT::TinyInt(_) => Datum::Int8(arr.get_byte(element).map_err(map_err)?),
DT::SmallInt(_) => Datum::Int16(arr.get_short(element).map_err(map_err)?),
DT::Int(_) => Datum::Int32(arr.get_int(element).map_err(map_err)?),
DT::BigInt(_) => Datum::Int64(arr.get_long(element).map_err(map_err)?),
DT::Float(_) => Datum::Float32(arr.get_float(element).map_err(map_err)?.into()),
DT::Double(_) => Datum::Float64(arr.get_double(element).map_err(map_err)?.into()),
DT::String(_) | DT::Char(_) => Datum::String(std::borrow::Cow::Owned(
arr.get_string(element).map_err(map_err)?.to_string(),
)),
DT::Bytes(_) | DT::Binary(_) => Datum::Blob(std::borrow::Cow::Owned(
arr.get_binary(element).map_err(map_err)?.to_vec(),
)),
DT::Decimal(d) => Datum::Decimal(
arr.get_decimal(element, d.precision(), d.scale())
.map_err(map_err)?,
),
DT::Date(_) => Datum::Date(arr.get_date(element).map_err(map_err)?),
DT::Time(_) => Datum::Time(arr.get_time(element).map_err(map_err)?),
DT::Timestamp(t) => Datum::TimestampNtz(
arr.get_timestamp_ntz(element, t.precision())
.map_err(map_err)?,
),
DT::TimestampLTz(t) => Datum::TimestampLtz(
arr.get_timestamp_ltz(element, t.precision())
.map_err(map_err)?,
),
DT::Array(_) => Datum::Array(arr.get_array(element).map_err(map_err)?),
DT::Map(mt) => Datum::Map(
arr.get_map(element, mt.key_type(), mt.value_type())
.map_err(map_err)?,
),
DT::Row(rt) => {
let cr = arr.get_row(element, rt).map_err(map_err)?;
let cols: Vec<fcore::metadata::Column> = rt
.fields()
.iter()
.map(|f| fcore::metadata::Column::new(f.name(), f.data_type().clone()))
.collect();
Datum::Row(Box::new(
crate::types::internal_row_to_owned_generic(&cr, &cols)
.map_err(|e| e.to_string())?,
))
}
})
}
}
// ============================================================================
// Opaque types: ScanResultInner (scan read path)
// ============================================================================
pub struct ScanResultInner {
error: Option<(i32, String)>,
buckets: Vec<(fcore::metadata::TableBucket, Vec<fcore::record::ScanRecord>)>,
columns: Vec<fcore::metadata::Column>,
bucket_infos: Vec<ffi::FfiBucketInfo>,
total_count: usize,
}
impl ScanResultInner {
fn from_error(code: i32, msg: String) -> Self {
Self {
error: Some((code, msg)),
buckets: Vec::new(),
columns: Vec::new(),
bucket_infos: Vec::new(),
total_count: 0,
}
}
fn resolve(&self, bucket: usize, rec: usize) -> &fcore::record::ScanRecord {
&self.buckets[bucket].1[rec]
}
fn sv_has_error(&self) -> bool {
self.error.is_some()
}
fn sv_error_code(&self) -> i32 {
self.error.as_ref().map_or(0, |e| e.0)
}
fn sv_error_message(&self) -> &str {
self.error.as_ref().map_or("", |e| e.1.as_str())
}
fn sv_record_count(&self) -> usize {
self.total_count
}
fn sv_column_count(&self) -> usize {
self.columns.len()
}
fn sv_column_name(&self, field: usize) -> Result<&str, String> {
row_reader::column_name(&self.columns, field)
}
fn sv_column_type(&self, field: usize) -> Result<i32, String> {
row_reader::column_type(&self.columns, field)
}
fn sv_offset(&self, bucket: usize, rec: usize) -> i64 {
self.resolve(bucket, rec).offset()
}
fn sv_timestamp(&self, bucket: usize, rec: usize) -> i64 {
self.resolve(bucket, rec).timestamp()
}
fn sv_change_type(&self, bucket: usize, rec: usize) -> i32 {
self.resolve(bucket, rec).change_type().to_byte_value() as i32
}
fn sv_field_count(&self) -> usize {
self.columns.len()
}
// Field accessors — C++ validates bounds in BucketRecords/RecordAt, validate() checks field.
fn sv_is_null(&self, bucket: usize, rec: usize, field: usize) -> Result<bool, String> {
row_reader::is_null(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_bool(&self, bucket: usize, rec: usize, field: usize) -> Result<bool, String> {
row_reader::get_bool(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_i32(&self, bucket: usize, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_i32(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_i64(&self, bucket: usize, rec: usize, field: usize) -> Result<i64, String> {
row_reader::get_i64(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_f32(&self, bucket: usize, rec: usize, field: usize) -> Result<f32, String> {
row_reader::get_f32(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_f64(&self, bucket: usize, rec: usize, field: usize) -> Result<f64, String> {
row_reader::get_f64(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_str(&self, bucket: usize, rec: usize, field: usize) -> Result<&str, String> {
row_reader::get_str(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_bytes(&self, bucket: usize, rec: usize, field: usize) -> Result<&[u8], String> {
row_reader::get_bytes(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_date_days(&self, bucket: usize, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_date_days(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_time_millis(&self, bucket: usize, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_time_millis(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_ts_millis(&self, bucket: usize, rec: usize, field: usize) -> Result<i64, String> {
row_reader::get_ts_millis(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_ts_nanos(&self, bucket: usize, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_ts_nanos(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_is_ts_ltz(&self, _bucket: usize, _rec: usize, field: usize) -> Result<bool, String> {
row_reader::is_ts_ltz(&self.columns, field)
}
fn sv_get_decimal_str(
&self,
bucket: usize,
rec: usize,
field: usize,
) -> Result<String, String> {
row_reader::get_decimal_str(self.resolve(bucket, rec).row(), &self.columns, field)
}
fn sv_get_value(
&self,
bucket: usize,
rec: usize,
field: usize,
) -> Result<Box<ValueInner>, String> {
// Scan rows are borrowed Arrow cursors; materialize only the requested
// field's owned datum (not the whole row). Top-level scalar scan reads
// keep their zero-copy index fast-path.
let datum = crate::types::field_to_owned_datum(
self.resolve(bucket, rec).row(),
&self.columns,
field,
)
.map_err(|e| e.to_string())?;
let data_type = self
.columns
.get(field)
.ok_or_else(|| format!("field index {field} out of range"))?
.data_type()
.clone();
Ok(Box::new(ValueInner { datum, data_type }))
}
fn sv_bucket_infos(&self) -> &Vec<ffi::FfiBucketInfo> {
&self.bucket_infos
}
}
// ============================================================================
// Opaque types: LookupResultInner (lookup read path)
// ============================================================================
pub struct LookupResultInner {
error: Option<(i32, String)>,
found: bool,
row: Option<fcore::row::GenericRow<'static>>,
columns: Vec<fcore::metadata::Column>,
}
impl LookupResultInner {
fn from_error(code: i32, msg: String) -> Self {
Self {
error: Some((code, msg)),
found: false,
row: None,
columns: Vec::new(),
}
}
fn lv_has_error(&self) -> bool {
self.error.is_some()
}
fn lv_error_code(&self) -> i32 {
self.error.as_ref().map_or(0, |e| e.0)
}
fn lv_error_message(&self) -> &str {
self.error.as_ref().map_or("", |e| e.1.as_str())
}
fn lv_found(&self) -> bool {
self.found
}
fn lv_field_count(&self) -> usize {
self.columns.len()
}
fn lv_column_type(&self, field: usize) -> Result<i32, String> {
row_reader::column_type(&self.columns, field)
}
fn lv_column_name(&self, field: usize) -> Result<&str, String> {
row_reader::column_name(&self.columns, field)
}
fn lv_row(&self) -> Result<&fcore::row::GenericRow<'static>, String> {
self.row
.as_ref()
.ok_or_else(|| "no row available (not found or error)".to_string())
}
// Field accessors — delegate to shared row_reader helpers.
fn lv_is_null(&self, field: usize) -> Result<bool, String> {
let r = self.lv_row()?;
row_reader::is_null(r, &self.columns, field)
}
fn lv_get_bool(&self, field: usize) -> Result<bool, String> {
let r = self.lv_row()?;
row_reader::get_bool(r, &self.columns, field)
}
fn lv_get_i32(&self, field: usize) -> Result<i32, String> {
let r = self.lv_row()?;
row_reader::get_i32(r, &self.columns, field)
}
fn lv_get_i64(&self, field: usize) -> Result<i64, String> {
let r = self.lv_row()?;
row_reader::get_i64(r, &self.columns, field)
}
fn lv_get_f32(&self, field: usize) -> Result<f32, String> {
let r = self.lv_row()?;
row_reader::get_f32(r, &self.columns, field)
}
fn lv_get_f64(&self, field: usize) -> Result<f64, String> {
let r = self.lv_row()?;
row_reader::get_f64(r, &self.columns, field)
}
fn lv_get_str(&self, field: usize) -> Result<&str, String> {
let r = self.lv_row()?;
row_reader::get_str(r, &self.columns, field)
}
fn lv_get_bytes(&self, field: usize) -> Result<&[u8], String> {
let r = self.lv_row()?;
row_reader::get_bytes(r, &self.columns, field)
}
fn lv_get_date_days(&self, field: usize) -> Result<i32, String> {
let r = self.lv_row()?;
row_reader::get_date_days(r, &self.columns, field)
}
fn lv_get_time_millis(&self, field: usize) -> Result<i32, String> {
let r = self.lv_row()?;
row_reader::get_time_millis(r, &self.columns, field)
}
fn lv_get_ts_millis(&self, field: usize) -> Result<i64, String> {
let r = self.lv_row()?;
row_reader::get_ts_millis(r, &self.columns, field)
}
fn lv_get_ts_nanos(&self, field: usize) -> Result<i32, String> {
let r = self.lv_row()?;
row_reader::get_ts_nanos(r, &self.columns, field)
}
fn lv_is_ts_ltz(&self, field: usize) -> Result<bool, String> {
row_reader::is_ts_ltz(&self.columns, field)
}
fn lv_get_decimal_str(&self, field: usize) -> Result<String, String> {
let r = self.lv_row()?;
row_reader::get_decimal_str(r, &self.columns, field)
}
fn lv_get_value(&self, field: usize) -> Result<Box<ValueInner>, String> {
value_from_owned_row(self.lv_row()?, &self.columns, field)
}
}
// ============================================================================
// Opaque types: PrefixLookupResultInner (prefix lookup read path)
//
// Like LookupResultInner but holds 0..n rows; accessors take a record index.
// ============================================================================
pub struct PrefixLookupResultInner {
error: Option<(i32, String)>,
rows: Vec<GenericRow<'static>>,
columns: Vec<Column>,
}
impl PrefixLookupResultInner {
fn from_error(code: i32, msg: String) -> Self {
Self {
error: Some((code, msg)),
rows: Vec::new(),
columns: Vec::new(),
}
}
fn plv_has_error(&self) -> bool {
self.error.is_some()
}
fn plv_error_code(&self) -> i32 {
self.error.as_ref().map_or(0, |e| e.0)
}
fn plv_error_message(&self) -> &str {
self.error.as_ref().map_or("", |e| e.1.as_str())
}
fn plv_row_count(&self) -> usize {
self.rows.len()
}
fn plv_field_count(&self) -> usize {
self.columns.len()
}
fn plv_column_type(&self, field: usize) -> Result<i32, String> {
row_reader::column_type(&self.columns, field)
}
fn plv_column_name(&self, field: usize) -> Result<&str, String> {
row_reader::column_name(&self.columns, field)
}
fn plv_row(&self, rec: usize) -> Result<&GenericRow<'static>, String> {
self.rows
.get(rec)
.ok_or_else(|| format!("record index {rec} out of range ({} rows)", self.rows.len()))
}
// Field accessors — delegate to shared row_reader helpers.
fn plv_is_null(&self, rec: usize, field: usize) -> Result<bool, String> {
row_reader::is_null(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_bool(&self, rec: usize, field: usize) -> Result<bool, String> {
row_reader::get_bool(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_i32(&self, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_i32(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_i64(&self, rec: usize, field: usize) -> Result<i64, String> {
row_reader::get_i64(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_f32(&self, rec: usize, field: usize) -> Result<f32, String> {
row_reader::get_f32(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_f64(&self, rec: usize, field: usize) -> Result<f64, String> {
row_reader::get_f64(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_str(&self, rec: usize, field: usize) -> Result<&str, String> {
row_reader::get_str(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_bytes(&self, rec: usize, field: usize) -> Result<&[u8], String> {
row_reader::get_bytes(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_date_days(&self, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_date_days(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_time_millis(&self, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_time_millis(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_ts_millis(&self, rec: usize, field: usize) -> Result<i64, String> {
row_reader::get_ts_millis(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_ts_nanos(&self, rec: usize, field: usize) -> Result<i32, String> {
row_reader::get_ts_nanos(self.plv_row(rec)?, &self.columns, field)
}
fn plv_is_ts_ltz(&self, _rec: usize, field: usize) -> Result<bool, String> {
row_reader::is_ts_ltz(&self.columns, field)
}
fn plv_get_decimal_str(&self, rec: usize, field: usize) -> Result<String, String> {
row_reader::get_decimal_str(self.plv_row(rec)?, &self.columns, field)
}
fn plv_get_value(&self, rec: usize, field: usize) -> Result<Box<ValueInner>, String> {
value_from_owned_row(self.plv_row(rec)?, &self.columns, field)
}
}
// ============================================================================
// ValueInner — recursive value handle for complex reads. Holds an owned Datum
// + its DataType: leaf getters match on the datum, navigators (v_at/v_key_at/
// v_value_at/v_field) descend into a child node.
// ============================================================================
pub struct ValueInner {
datum: fcore::row::Datum<'static>,
data_type: fcore::metadata::DataType,
}
/// Builds a `ValueInner` from an owned row; shared by the lookup and prefix-lookup paths.
fn value_from_owned_row(
row: &GenericRow<'static>,
columns: &[Column],
field: usize,
) -> Result<Box<ValueInner>, String> {
let datum = row
.values
.get(field)
.ok_or_else(|| format!("field index {field} out of range"))?
.clone();
let data_type = columns
.get(field)
.ok_or_else(|| format!("field index {field} out of range"))?
.data_type()
.clone();
Ok(Box::new(ValueInner { datum, data_type }))
}
impl ValueInner {
fn type_err(&self, expected: &str) -> String {
format!("value of type {} is not {expected}", self.data_type)
}
fn v_type(&self) -> i32 {
crate::types::core_data_type_to_ffi(&self.data_type)
}
fn v_is_null(&self) -> bool {
matches!(self.datum, fcore::row::Datum::Null)
}
fn v_get_bool(&self) -> Result<bool, String> {
match &self.datum {
fcore::row::Datum::Bool(v) => Ok(*v),
_ => Err(self.type_err("BOOLEAN")),
}
}
fn v_get_i32(&self) -> Result<i32, String> {
match &self.datum {
fcore::row::Datum::Int8(v) => Ok(i32::from(*v)),
fcore::row::Datum::Int16(v) => Ok(i32::from(*v)),
fcore::row::Datum::Int32(v) => Ok(*v),
_ => Err(self.type_err("INT")),
}
}
fn v_get_i64(&self) -> Result<i64, String> {
match &self.datum {
fcore::row::Datum::Int64(v) => Ok(*v),
_ => Err(self.type_err("BIGINT")),
}
}
fn v_get_f32(&self) -> Result<f32, String> {
match &self.datum {
fcore::row::Datum::Float32(v) => Ok(v.0),
_ => Err(self.type_err("FLOAT")),
}
}
fn v_get_f64(&self) -> Result<f64, String> {
match &self.datum {
fcore::row::Datum::Float64(v) => Ok(v.0),
_ => Err(self.type_err("DOUBLE")),
}
}
fn v_get_str(&self) -> Result<String, String> {
match &self.datum {
fcore::row::Datum::String(v) => Ok(v.to_string()),
_ => Err(self.type_err("STRING")),
}
}
fn v_get_bytes(&self) -> Result<Vec<u8>, String> {
match &self.datum {
fcore::row::Datum::Blob(v) => Ok(v.to_vec()),
_ => Err(self.type_err("BYTES")),
}
}
fn v_get_date_days(&self) -> Result<i32, String> {
match &self.datum {
fcore::row::Datum::Date(d) => Ok(d.get_inner()),
_ => Err(self.type_err("DATE")),
}
}
fn v_get_time_millis(&self) -> Result<i32, String> {
match &self.datum {
fcore::row::Datum::Time(t) => Ok(t.get_inner()),
_ => Err(self.type_err("TIME")),
}
}
fn v_get_ts_millis(&self) -> Result<i64, String> {
match &self.datum {
fcore::row::Datum::TimestampNtz(t) => Ok(t.get_millisecond()),
fcore::row::Datum::TimestampLtz(t) => Ok(t.get_epoch_millisecond()),
_ => Err(self.type_err("TIMESTAMP")),
}
}
fn v_get_ts_nanos(&self) -> Result<i32, String> {
match &self.datum {
fcore::row::Datum::TimestampNtz(t) => Ok(t.get_nano_of_millisecond()),
fcore::row::Datum::TimestampLtz(t) => Ok(t.get_nano_of_millisecond()),
_ => Err(self.type_err("TIMESTAMP")),
}
}
fn v_get_decimal_str(&self) -> Result<String, String> {
match &self.datum {
fcore::row::Datum::Decimal(d) => Ok(d.to_big_decimal().to_string()),
_ => Err(self.type_err("DECIMAL")),
}
}
fn v_size(&self) -> Result<usize, String> {
match &self.datum {
fcore::row::Datum::Array(a) => Ok(a.size()),
fcore::row::Datum::Map(m) => Ok(m.size()),
_ => Err(self.type_err("ARRAY/MAP")),
}
}
fn v_field_count(&self) -> Result<usize, String> {
match &self.datum {
fcore::row::Datum::Row(r) => Ok(r.values.len()),
_ => Err(self.type_err("ROW")),
}
}
fn v_at(&self, i: usize) -> Result<Box<ValueInner>, String> {
match &self.datum {
fcore::row::Datum::Array(a) => {
let elem_type = match &self.data_type {
fcore::metadata::DataType::Array(at) => at.get_element_type().clone(),
_ => return Err("internal: ARRAY datum without ARRAY type".to_string()),
};
let datum = array_reader::get_datum(a, &elem_type, i)?;
Ok(Box::new(ValueInner {
datum,
data_type: elem_type,
}))
}
_ => Err(self.type_err("ARRAY")),
}
}
fn v_key_at(&self, i: usize) -> Result<Box<ValueInner>, String> {
match &self.datum {
fcore::row::Datum::Map(m) => {
let kt = m.key_type().clone();
let datum = array_reader::get_datum(m.key_array(), &kt, i)?;
Ok(Box::new(ValueInner {
datum,
data_type: kt,
}))
}
_ => Err(self.type_err("MAP")),
}
}
fn v_value_at(&self, i: usize) -> Result<Box<ValueInner>, String> {
match &self.datum {
fcore::row::Datum::Map(m) => {
let vt = m.value_type().clone();
let datum = array_reader::get_datum(m.value_array(), &vt, i)?;
Ok(Box::new(ValueInner {
datum,
data_type: vt,
}))
}
_ => Err(self.type_err("MAP")),
}
}
fn v_field(&self, i: usize) -> Result<Box<ValueInner>, String> {
match &self.datum {
fcore::row::Datum::Row(r) => {
let ft = match &self.data_type {
fcore::metadata::DataType::Row(rt) => rt
.fields()
.get(i)
.ok_or_else(|| format!("field index {i} out of range"))?
.data_type()
.clone(),
_ => return Err("internal: ROW datum without ROW type".to_string()),
};
let datum = r
.values
.get(i)
.ok_or_else(|| format!("field index {i} out of range"))?
.clone();
Ok(Box::new(ValueInner {
datum,
data_type: ft,
}))
}
_ => Err(self.type_err("ROW")),
}
}
fn v_field_by_name(&self, name: &str) -> Result<Box<ValueInner>, String> {
match &self.data_type {
fcore::metadata::DataType::Row(rt) => {
let idx = rt
.fields()
.iter()
.position(|f| f.name() == name)
.ok_or_else(|| format!("no field named '{name}' in {}", self.data_type))?;
self.v_field(idx)
}
_ => Err(self.type_err("ROW")),
}
}
}
// ============================================================================
// Opaque types: ArrayWriterInner (array builder for writes)
// ============================================================================
pub struct ArrayWriterInner {
writer: Option<fcore::row::binary_array::FlussArrayWriter>,
completed: Option<fcore::row::binary_array::FlussArray>,
element_type: fcore::metadata::DataType,
num_elements: usize,
}
// ============================================================================
// MapWriterInner — opaque map builder (mirrors ArrayWriterInner)
// ============================================================================
pub struct MapWriterInner {
writer: Option<fcore::row::binary_map::FlussMapWriter>,
completed: Option<fcore::row::binary_map::FlussMap>,
key_type: fcore::metadata::DataType,
value_type: fcore::metadata::DataType,
pending_key: Option<fcore::row::Datum<'static>>,
pending_value: Option<fcore::row::Datum<'static>>,
}
fn ts_ntz_from(millis: i64, nanos: i32) -> fcore::row::TimestampNtz {
fcore::row::TimestampNtz::from_millis_nanos(millis, nanos)
.unwrap_or_else(|_| fcore::row::TimestampNtz::new(millis))
}
fn ts_ltz_from(millis: i64, nanos: i32) -> fcore::row::TimestampLtz {
fcore::row::TimestampLtz::from_millis_nanos(millis, nanos)
.unwrap_or_else(|_| fcore::row::TimestampLtz::new(millis))
}
/// Parse a decimal string into a `Datum::Decimal` matching `dt`'s precision and
/// scale. Used by the map key/value decimal setters, which carry no schema.
fn parse_decimal_datum(
dt: &fcore::metadata::DataType,
val: &str,
) -> Result<fcore::row::Datum<'static>, String> {
match dt {
fcore::metadata::DataType::Decimal(d) => {
let bd = bigdecimal::BigDecimal::from_str(val).map_err(|e| e.to_string())?;
let dec = fcore::row::Decimal::from_big_decimal(bd, d.precision(), d.scale())
.map_err(|e| e.to_string())?;
Ok(fcore::row::Datum::Decimal(dec))
}
other => Err(format!(
"decimal setter used on non-DECIMAL map type {other}"
)),
}
}
fn timestamp_datum(
dt: &fcore::metadata::DataType,
millis: i64,
nanos: i32,
) -> Result<fcore::row::Datum<'static>, String> {
match dt {
fcore::metadata::DataType::Timestamp(_) => {
Ok(fcore::row::Datum::TimestampNtz(ts_ntz_from(millis, nanos)))
}
fcore::metadata::DataType::TimestampLTz(_) => {
Ok(fcore::row::Datum::TimestampLtz(ts_ltz_from(millis, nanos)))
}
other => Err(format!(
"timestamp setter used on non-TIMESTAMP map type {other}"
)),
}
}
/// Narrow a C++ `int32` into the `Datum` a map key/value expects, erroring on
/// overflow. Core needs `Int8`/`Int16` for TINYINT/SMALLINT; a widened `Int32`
/// is rejected. Mirrors `ArrayWriterInner::aw_set_i32`.
fn int_datum(dt: &DataType, val: i32, role: &str) -> Result<Datum<'static>, String> {
match dt {
DataType::TinyInt(_) => i8::try_from(val)
.map(Datum::Int8)
.map_err(|_| format!("Value {val} does not fit TINYINT map {role}")),
DataType::SmallInt(_) => i16::try_from(val)
.map(Datum::Int16)
.map_err(|_| format!("Value {val} does not fit SMALLINT map {role}")),
_ => Ok(Datum::Int32(val)),
}
}
#[allow(clippy::too_many_arguments)]
fn new_map_writer(
capacity: usize,
key_leaf_type_id: i32,
key_precision: u32,
key_scale: u32,
value_leaf_type_id: i32,
value_precision: u32,
value_scale: u32,
value_array_nesting: u32,
) -> Result<Box<MapWriterInner>, String> {
let key_type = types::element_type_from_ffi(key_leaf_type_id, key_precision, key_scale, 0)
.map_err(|e| e.to_string())?;
let value_type = types::element_type_from_ffi(
value_leaf_type_id,
value_precision,
value_scale,
value_array_nesting,
)
.map_err(|e| e.to_string())?;
let writer = fcore::row::binary_map::FlussMapWriter::new(capacity, &key_type, &value_type);
Ok(Box::new(MapWriterInner {
writer: Some(writer),
completed: None,
key_type,
value_type,
pending_key: None,
pending_value: None,
}))
}
fn new_map_writer_arrow(
capacity: usize,
kv_schema_ptr: usize,
) -> Result<Box<MapWriterInner>, String> {
let mut types =
unsafe { types::arrow_ffi_to_data_types(kv_schema_ptr) }.map_err(|e| e.to_string())?;
if types.len() != 2 {
return Err(format!(
"map writer Arrow schema must have exactly 2 fields (key, value), got {}",
types.len()
));
}
let value_type = types.swap_remove(1);
let key_type = types.swap_remove(0);
let writer = fcore::row::binary_map::FlussMapWriter::new(capacity, &key_type, &value_type);
Ok(Box::new(MapWriterInner {
writer: Some(writer),
completed: None,
key_type,
value_type,
pending_key: None,
pending_value: None,
}))
}
impl MapWriterInner {
fn writer_mut(&mut self) -> Result<&mut fcore::row::binary_map::FlussMapWriter, String> {
self.writer
.as_mut()
.ok_or_else(|| "MapWriter is already finalized".to_string())
}
fn mw_key_bool(&mut self, val: bool) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Bool(val));
Ok(())
}
fn mw_key_i32(&mut self, val: i32) -> Result<(), String> {
self.pending_key = Some(int_datum(&self.key_type, val, "key")?);
Ok(())
}
fn mw_key_i64(&mut self, val: i64) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Int64(val));
Ok(())
}
fn mw_key_f32(&mut self, val: f32) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Float32(val.into()));
Ok(())
}
fn mw_key_f64(&mut self, val: f64) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Float64(val.into()));
Ok(())
}
fn mw_key_str(&mut self, val: &str) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::String(std::borrow::Cow::Owned(
val.to_string(),
)));
Ok(())
}
fn mw_key_bytes(&mut self, val: &[u8]) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Blob(std::borrow::Cow::Owned(
val.to_vec(),
)));
Ok(())
}
fn mw_key_date(&mut self, days: i32) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Date(fcore::row::Date::new(days)));
Ok(())
}
fn mw_key_time(&mut self, millis: i32) -> Result<(), String> {
self.pending_key = Some(fcore::row::Datum::Time(fcore::row::Time::new(millis)));
Ok(())
}
fn mw_key_timestamp(&mut self, millis: i64, nanos: i32) -> Result<(), String> {
self.pending_key = Some(timestamp_datum(&self.key_type, millis, nanos)?);
Ok(())
}
fn mw_key_decimal_str(&mut self, val: &str) -> Result<(), String> {
self.pending_key = Some(parse_decimal_datum(&self.key_type, val)?);
Ok(())
}
fn mw_value_null(&mut self) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Null);
Ok(())
}
fn mw_value_bool(&mut self, val: bool) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Bool(val));
Ok(())
}
fn mw_value_i32(&mut self, val: i32) -> Result<(), String> {
self.pending_value = Some(int_datum(&self.value_type, val, "value")?);
Ok(())
}
fn mw_value_i64(&mut self, val: i64) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Int64(val));
Ok(())
}
fn mw_value_f32(&mut self, val: f32) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Float32(val.into()));
Ok(())
}
fn mw_value_f64(&mut self, val: f64) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Float64(val.into()));
Ok(())
}
fn mw_value_str(&mut self, val: &str) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::String(std::borrow::Cow::Owned(
val.to_string(),
)));
Ok(())
}
fn mw_value_bytes(&mut self, val: &[u8]) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Blob(std::borrow::Cow::Owned(
val.to_vec(),
)));
Ok(())
}
fn mw_value_date(&mut self, days: i32) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Date(fcore::row::Date::new(days)));
Ok(())
}
fn mw_value_time(&mut self, millis: i32) -> Result<(), String> {
self.pending_value = Some(fcore::row::Datum::Time(fcore::row::Time::new(millis)));
Ok(())
}
fn mw_value_timestamp(&mut self, millis: i64, nanos: i32) -> Result<(), String> {
self.pending_value = Some(timestamp_datum(&self.value_type, millis, nanos)?);
Ok(())
}
fn mw_value_decimal_str(&mut self, val: &str) -> Result<(), String> {
self.pending_value = Some(parse_decimal_datum(&self.value_type, val)?);
Ok(())
}
fn mw_value_row(&mut self, row: &mut GenericRowInner) -> Result<(), String> {
let nested = std::mem::replace(&mut row.row, fcore::row::GenericRow::new(0));
self.pending_value = Some(fcore::row::Datum::Row(Box::new(nested)));
Ok(())
}
fn mw_value_map(&mut self, map: &mut MapWriterInner) -> Result<(), String> {
map.complete_if_needed()?;
let completed = map
.completed
.take()
.ok_or_else(|| "MapWriter invariant violation: nested map missing".to_string())?;
self.pending_value = Some(fcore::row::Datum::Map(completed));
Ok(())
}
fn mw_value_array(&mut self, array: &mut ArrayWriterInner) -> Result<(), String> {
array.complete_if_needed()?;
let completed = array
.completed
.take()
.ok_or_else(|| "ArrayWriter invariant violation: nested array missing".to_string())?;
self.pending_value = Some(fcore::row::Datum::Array(completed));
Ok(())
}
/// Commit the pending key/value as one map entry. Keys cannot be null;
/// an unset value defaults to null.
fn mw_commit(&mut self) -> Result<(), String> {
let key = self
.pending_key
.take()
.ok_or_else(|| "MapWriter: entry key not set before commit".to_string())?;
let value = self.pending_value.take().unwrap_or(fcore::row::Datum::Null);
self.writer_mut()?
.write_entry(key, value)
.map_err(|e| e.to_string())
}
fn complete_if_needed(&mut self) -> Result<(), String> {
if self.completed.is_none() {
let w = self
.writer
.take()
.ok_or_else(|| "MapWriter has already been finalized".to_string())?;
self.completed = Some(w.complete().map_err(|e| e.to_string())?);
}
Ok(())
}
}
fn new_array_writer(
size: usize,
element_leaf_type_id: i32,
precision: u32,
scale: u32,
array_nesting: u32,
) -> Result<Box<ArrayWriterInner>, String> {
let element_type =
types::element_type_from_ffi(element_leaf_type_id, precision, scale, array_nesting)
.map_err(|e| e.to_string())?;
let writer = fcore::row::binary_array::FlussArrayWriter::new(size, &element_type);
Ok(Box::new(ArrayWriterInner {
writer: Some(writer),
completed: None,
element_type,
num_elements: size,
}))
}
fn new_array_writer_arrow(
size: usize,
element_schema_ptr: usize,
) -> Result<Box<ArrayWriterInner>, String> {
let mut types =
unsafe { types::arrow_ffi_to_data_types(element_schema_ptr) }.map_err(|e| e.to_string())?;
let element_type = if types.is_empty() {
return Err("array element Arrow schema must have one field".to_string());
} else {
types.swap_remove(0)
};
let writer = fcore::row::binary_array::FlussArrayWriter::new(size, &element_type);
Ok(Box::new(ArrayWriterInner {
writer: Some(writer),
completed: None,
element_type,
num_elements: size,
}))
}
impl ArrayWriterInner {
fn writer_mut(&mut self) -> Result<&mut fcore::row::binary_array::FlussArrayWriter, String> {
self.writer
.as_mut()
.ok_or_else(|| "ArrayWriter is already finalized".to_string())
}
fn validate_index(&self, idx: usize) -> Result<(), String> {
if idx < self.num_elements {
Ok(())
} else {
Err(format!(
"ArrayWriter index out of bounds: idx={idx}, size={}",
self.num_elements
))
}
}
fn complete_if_needed(&mut self) -> Result<(), String> {
if self.completed.is_none() {
let w = self
.writer
.take()
.ok_or_else(|| "ArrayWriter has already been finalized".to_string())?;
self.completed = Some(w.complete().map_err(|e| e.to_string())?);
}
Ok(())
}
/// Checks writer liveness first, then the element index. Returning the
/// clearest finalization error before a bounds error keeps diagnostics
/// aligned with the caller's intent when a writer is misused after
/// completion.
fn ensure_writable(&self, idx: usize) -> Result<(), String> {
if self.writer.is_none() {
return Err("ArrayWriter is already finalized".to_string());
}
self.validate_index(idx)
}
fn aw_size(&self) -> usize {
self.num_elements
}
fn aw_set_null(&mut self, idx: usize) -> Result<(), String> {
self.ensure_writable(idx)?;
self.writer_mut()?.set_null_at(idx);
Ok(())
}
fn aw_set_bool(&mut self, idx: usize, val: bool) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Boolean(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected BOOLEAN element, got {}",
self.element_type
));
}
self.writer_mut()?.write_boolean(idx, val);
Ok(())
}
fn aw_set_i32(&mut self, idx: usize, val: i32) -> Result<(), String> {
self.ensure_writable(idx)?;
match &self.element_type {
fcore::metadata::DataType::TinyInt(_) => {
let v = i8::try_from(val)
.map_err(|_| format!("Value {val} does not fit TINYINT element"))?;
self.writer_mut()?.write_byte(idx, v);
}
fcore::metadata::DataType::SmallInt(_) => {
let v = i16::try_from(val)
.map_err(|_| format!("Value {val} does not fit SMALLINT element"))?;
self.writer_mut()?.write_short(idx, v);
}
fcore::metadata::DataType::Int(_) => {
self.writer_mut()?.write_int(idx, val);
}
_ => {
return Err(format!(
"ArrayWriter type mismatch: expected TINYINT/SMALLINT/INT element, got {}",
self.element_type
));
}
}
Ok(())
}
fn aw_set_i64(&mut self, idx: usize, val: i64) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::BigInt(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected BIGINT element, got {}",
self.element_type
));
}
self.writer_mut()?.write_long(idx, val);
Ok(())
}
fn aw_set_f32(&mut self, idx: usize, val: f32) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Float(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected FLOAT element, got {}",
self.element_type
));
}
self.writer_mut()?.write_float(idx, val);
Ok(())
}
fn aw_set_f64(&mut self, idx: usize, val: f64) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Double(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected DOUBLE element, got {}",
self.element_type
));
}
self.writer_mut()?.write_double(idx, val);
Ok(())
}
fn aw_set_str(&mut self, idx: usize, val: &str) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(
self.element_type,
fcore::metadata::DataType::String(_) | fcore::metadata::DataType::Char(_)
) {
return Err(format!(
"ArrayWriter type mismatch: expected STRING/CHAR element, got {}",
self.element_type
));
}
self.writer_mut()?.write_string(idx, val);
Ok(())
}
fn aw_set_bytes(&mut self, idx: usize, val: &[u8]) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(
self.element_type,
fcore::metadata::DataType::Bytes(_) | fcore::metadata::DataType::Binary(_)
) {
return Err(format!(
"ArrayWriter type mismatch: expected BYTES/BINARY element, got {}",
self.element_type
));
}
self.writer_mut()?.write_binary_bytes(idx, val);
Ok(())
}
fn aw_set_date(&mut self, idx: usize, days: i32) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Date(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected DATE element, got {}",
self.element_type
));
}
self.writer_mut()?
.write_date(idx, fcore::row::Date::new(days));
Ok(())
}
fn aw_set_time(&mut self, idx: usize, millis: i32) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Time(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected TIME element, got {}",
self.element_type
));
}
self.writer_mut()?
.write_time(idx, fcore::row::Time::new(millis));
Ok(())
}
fn aw_set_ts_ntz(&mut self, idx: usize, millis: i64, nanos: i32) -> Result<(), String> {
self.ensure_writable(idx)?;
let precision = match &self.element_type {
fcore::metadata::DataType::Timestamp(ts) => ts.precision(),
_ => {
return Err(format!(
"ArrayWriter type mismatch: expected TIMESTAMP element, got {}",
self.element_type
));
}
};
let ts = fcore::row::TimestampNtz::from_millis_nanos(millis, nanos)
.map_err(|e| e.to_string())?;
self.writer_mut()?.write_timestamp_ntz(idx, &ts, precision);
Ok(())
}
fn aw_set_ts_ltz(&mut self, idx: usize, millis: i64, nanos: i32) -> Result<(), String> {
self.ensure_writable(idx)?;
let precision = match &self.element_type {
fcore::metadata::DataType::TimestampLTz(ts) => ts.precision(),
_ => {
return Err(format!(
"ArrayWriter type mismatch: expected TIMESTAMP_LTZ element, got {}",
self.element_type
));
}
};
let ts = fcore::row::TimestampLtz::from_millis_nanos(millis, nanos)
.map_err(|e| e.to_string())?;
self.writer_mut()?.write_timestamp_ltz(idx, &ts, precision);
Ok(())
}
fn aw_set_decimal_str(&mut self, idx: usize, val: &str) -> Result<(), String> {
self.ensure_writable(idx)?;
let (precision, scale) = match &self.element_type {
fcore::metadata::DataType::Decimal(d) => (d.precision(), d.scale()),
_ => {
return Err(format!(
"ArrayWriter type mismatch: expected DECIMAL element, got {}",
self.element_type
));
}
};
let bd = bigdecimal::BigDecimal::from_str(val).map_err(|e| e.to_string())?;
let decimal = fcore::row::Decimal::from_big_decimal(bd, precision, scale)
.map_err(|e| e.to_string())?;
self.writer_mut()?.write_decimal(idx, &decimal, precision);
Ok(())
}
fn aw_set_array(&mut self, idx: usize, nested: &mut ArrayWriterInner) -> Result<(), String> {
self.ensure_writable(idx)?;
let expected_inner = match &self.element_type {
fcore::metadata::DataType::Array(at) => at.get_element_type(),
_ => {
return Err(format!(
"ArrayWriter type mismatch: expected ARRAY element, got {}",
self.element_type
));
}
};
if !structurally_compatible(expected_inner, &nested.element_type) {
return Err(format!(
"Nested ArrayWriter type mismatch: expected nested element type {}, got {}",
expected_inner, nested.element_type
));
}
nested.complete_if_needed()?;
let arr = nested.completed.as_ref().ok_or_else(|| {
"ArrayWriter invariant violation: nested completed array missing after finalize"
.to_string()
})?;
self.writer_mut()?.write_array(idx, arr);
Ok(())
}
fn aw_set_row(&mut self, idx: usize, row: &mut GenericRowInner) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Row(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected ROW element, got {}",
self.element_type
));
}
self.writer_mut()?
.write_row(idx, &row.row)
.map_err(|e| e.to_string())
}
fn aw_set_map(&mut self, idx: usize, map: &mut MapWriterInner) -> Result<(), String> {
self.ensure_writable(idx)?;
if !matches!(self.element_type, fcore::metadata::DataType::Map(_)) {
return Err(format!(
"ArrayWriter type mismatch: expected MAP element, got {}",
self.element_type
));
}
map.complete_if_needed()?;
let completed = map.completed.as_ref().ok_or_else(|| {
"ArrayWriter invariant violation: nested completed map missing after finalize"
.to_string()
})?;
self.writer_mut()?.write_map(idx, completed);
Ok(())
}
}
/// Structural type equivalence that ignores nullability flags but preserves
/// variant and precision/scale semantics. Used to compare ArrayWriter element
/// types on the binding boundary. Nullability is ignored in structural comparison
/// because the Rust-side element type is always reconstructed as nullable
/// (encoding doesn't depend on it).
fn structurally_compatible(a: &fcore::metadata::DataType, b: &fcore::metadata::DataType) -> bool {
match (a, b) {
(DataType::Boolean(_), DataType::Boolean(_))
| (DataType::TinyInt(_), DataType::TinyInt(_))
| (DataType::SmallInt(_), DataType::SmallInt(_))
| (DataType::Int(_), DataType::Int(_))
| (DataType::BigInt(_), DataType::BigInt(_))
| (DataType::Float(_), DataType::Float(_))
| (DataType::Double(_), DataType::Double(_))
| (DataType::String(_), DataType::String(_))
| (DataType::Bytes(_), DataType::Bytes(_))
| (DataType::Date(_), DataType::Date(_))
| (DataType::Time(_), DataType::Time(_)) => true,
(DataType::Timestamp(x), DataType::Timestamp(y)) => x.precision() == y.precision(),
(DataType::TimestampLTz(x), DataType::TimestampLTz(y)) => x.precision() == y.precision(),
(DataType::Char(x), DataType::Char(y)) => x.length() == y.length(),
(DataType::Binary(x), DataType::Binary(y)) => x.length() == y.length(),
(DataType::Decimal(x), DataType::Decimal(y)) => {
x.precision() == y.precision() && x.scale() == y.scale()
}
(DataType::Array(x), DataType::Array(y)) => {
structurally_compatible(x.get_element_type(), y.get_element_type())
}
_ => false,
}
}