| // 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, |
| } |
| } |