This is an automated email from the ASF dual-hosted git repository.
fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git
The following commit(s) were added to refs/heads/main by this push:
new c4d6c49de [cpp] Encode CreateTable column types losslessly (#3603)
c4d6c49de is described below
commit c4d6c49de798635e0d47406faafdcd927a878e74
Author: Anton Borisov <[email protected]>
AuthorDate: Thu Jul 9 17:51:37 2026 +0100
[cpp] Encode CreateTable column types losslessly (#3603)
---
fluss-rust/bindings/cpp/include/fluss.hpp | 21 +-
fluss-rust/bindings/cpp/src/admin.cpp | 30 --
fluss-rust/bindings/cpp/src/ffi_converter.hpp | 259 +++++++-------
fluss-rust/bindings/cpp/src/lib.rs | 72 ++--
fluss-rust/bindings/cpp/src/type_lowering.hpp | 15 +-
fluss-rust/bindings/cpp/src/types.rs | 384 ++++++++-------------
.../bindings/cpp/test/test_ffi_converter.cpp | 339 +++++++++---------
fluss-rust/bindings/cpp/test/test_kv_table.cpp | 300 ++++++++--------
fluss-rust/bindings/cpp/test/test_log_table.cpp | 279 +++++++++------
.../website/docs/user-guide/cpp/api-reference.md | 1 -
.../website/docs/user-guide/cpp/data-types.md | 9 +-
11 files changed, 807 insertions(+), 902 deletions(-)
diff --git a/fluss-rust/bindings/cpp/include/fluss.hpp
b/fluss-rust/bindings/cpp/include/fluss.hpp
index 9e727d6d3..935cf7085 100644
--- a/fluss-rust/bindings/cpp/include/fluss.hpp
+++ b/fluss-rust/bindings/cpp/include/fluss.hpp
@@ -304,7 +304,7 @@ class DataType {
static DataType String() { return DataType(TypeId::String); }
static DataType Bytes() { return DataType(TypeId::Bytes); }
static DataType Date() { return DataType(TypeId::Date); }
- static DataType Time() { return DataType(TypeId::Time); }
+ static DataType Time(int32_t precision = 0) { return
DataType(TypeId::Time, precision, 0); }
static DataType Timestamp(int32_t precision = 6) {
return DataType(TypeId::Timestamp, precision, 0);
}
@@ -434,10 +434,6 @@ struct Column {
struct Schema {
std::vector<Column> columns;
std::vector<std::string> primary_keys;
- /// When set (via FromArrow), the table's column types come from this Arrow
- /// schema instead of `columns` — the only way to express nested MAP/ROW
- /// columns. `columns` stays empty in that case.
- std::shared_ptr<arrow::Schema> arrow_schema;
class Builder {
public:
@@ -459,15 +455,6 @@ struct Schema {
};
static Builder NewBuilder() { return Builder(); }
-
- /// Build a Schema whose column types come from an Arrow schema. Use this
- /// for tables with nested MAP/ROW columns (`arrow::map()`,
`arrow::struct_()`);
- /// `Admin::CreateTable` routes Arrow-backed schemas through the C Data
- /// Interface automatically.
- static Schema FromArrow(std::shared_ptr<arrow::Schema> arrow_schema,
- std::vector<std::string> primary_keys = {}) {
- return Schema{{}, std::move(primary_keys), std::move(arrow_schema)};
- }
};
struct TableDescriptor {
@@ -1457,9 +1444,9 @@ class Admin {
bool Available() const;
- /// Creates a table. For nested MAP/ROW columns, build `descriptor`'s
schema
- /// via `Schema::FromArrow(...)` — `CreateTable` routes Arrow-backed
schemas
- /// through the C Data Interface automatically.
+ /// Creates a table. Column types — including nested ARRAY/MAP/ROW built
+ /// with `DataType::Array`/`Map`/`Row` — are carried to the server exactly
+ /// as declared (precision, scale, length, nullability, and field names).
Result CreateTable(const TablePath& table_path, const TableDescriptor&
descriptor,
bool ignore_if_exists = false);
diff --git a/fluss-rust/bindings/cpp/src/admin.cpp
b/fluss-rust/bindings/cpp/src/admin.cpp
index 4ebc6f450..2459f41cb 100644
--- a/fluss-rust/bindings/cpp/src/admin.cpp
+++ b/fluss-rust/bindings/cpp/src/admin.cpp
@@ -60,36 +60,6 @@ Result Admin::CreateTable(const TablePath& table_path, const
TableDescriptor& de
}
auto ffi_path = utils::to_ffi_table_path(table_path);
-
- // A MAP/ROW column can't go through the flat FFI encoding, so the schema
is
- // sent over Arrow instead (explicit via FromArrow, or lowered from native
- // columns). Rust derives the columns from it, so the flat columns are
dropped
- // here; primary keys / metadata still come from the descriptor.
- std::shared_ptr<arrow::Schema> arrow_schema =
descriptor.schema.arrow_schema;
- if (!arrow_schema) {
- for (const auto& col : descriptor.schema.columns) {
- if (detail::is_compound(col.data_type)) {
- arrow_schema =
detail::columns_to_arrow_schema(descriptor.schema.columns);
- break;
- }
- }
- }
-
- if (arrow_schema) {
- TableDescriptor arrow_desc = descriptor;
- arrow_desc.schema.columns.clear();
- auto ffi_desc = utils::to_ffi_table_descriptor(arrow_desc);
- size_t schema_ptr = 0;
- try {
- schema_ptr = detail::export_arrow_schema(*arrow_schema);
- } catch (const std::exception& e) {
- return utils::make_client_error(e.what());
- }
- auto ffi_result =
- admin_->create_table_arrow(ffi_path, ffi_desc, schema_ptr,
ignore_if_exists);
- return utils::from_ffi_result(ffi_result);
- }
-
auto ffi_desc = utils::to_ffi_table_descriptor(descriptor);
auto ffi_result = admin_->create_table(ffi_path, ffi_desc,
ignore_if_exists);
return utils::from_ffi_result(ffi_result);
diff --git a/fluss-rust/bindings/cpp/src/ffi_converter.hpp
b/fluss-rust/bindings/cpp/src/ffi_converter.hpp
index 89a701828..584749675 100644
--- a/fluss-rust/bindings/cpp/src/ffi_converter.hpp
+++ b/fluss-rust/bindings/cpp/src/ffi_converter.hpp
@@ -28,77 +28,159 @@
namespace fluss {
namespace utils {
-/// Compact FFI representation of a (possibly nested) array type.
-///
-/// `nesting` counts the number of ARRAY wrappers stripped to reach the leaf
-/// element type. `leaf_type`/`leaf_precision`/`leaf_scale` describe that leaf
-/// scalar. A non-array input produces a zero-initialised value (nesting == 0).
-/// `array_nullability` has `nesting + 1` entries: one per ARRAY wrapper
-/// (outermost first) plus a trailing entry for the leaf scalar's nullability.
-///
-/// Using a flat representation — rather than serialising a recursive
-/// `DataType` — keeps the cxx bridge contract small while preserving schema
-/// fidelity across the FFI boundary when paired with rebuild_array_type().
+/// Flattened view of an `ARRAY<ARRAY<...<scalar>>>` element type, used by the
+/// data-writer path: `nesting` counts the ARRAY wrappers stripped to reach the
+/// leaf scalar, which `leaf_*` describe. A non-array input yields `nesting ==
0`
+/// and callers use the type's own id/precision/scale.
struct FlattenedArrayType {
int32_t nesting{0};
int32_t leaf_type{0};
int32_t leaf_precision{0};
int32_t leaf_scale{0};
- std::vector<uint8_t> array_nullability;
};
-/// Flattens an `ARRAY<ARRAY<...<leaf>>>` DataType into a FlattenedArrayType.
-///
-/// Contract:
-/// - If `data_type` is not an ARRAY, returns a zero-valued
FlattenedArrayType
-/// and callers must use the column's own `id/precision/scale` instead.
-/// - If `data_type` is an ARRAY but has a null element_type() chain (which
-/// should only happen on malformed input), returns a zero-valued result to
-/// signal the caller to reject the schema.
-/// - Otherwise, `nesting >= 1`, array_nullability has `nesting + 1` entries
-/// (last = leaf scalar nullability), and leaf_* describe the innermost
scalar.
inline FlattenedArrayType flatten_array_type(const DataType& data_type) {
FlattenedArrayType out;
if (data_type.id() != TypeId::Array) {
return out;
}
-
const DataType* current = &data_type;
while (current && current->id() == TypeId::Array) {
out.nesting += 1;
- out.array_nullability.push_back(current->nullable() ? 1 : 0);
current = current->element_type();
}
if (!current) {
return FlattenedArrayType{};
}
-
out.leaf_type = static_cast<int32_t>(current->id());
out.leaf_precision = current->precision();
out.leaf_scale = current->scale();
- out.array_nullability.push_back(current->nullable() ? 1 : 0);
return out;
}
-/// Inverse of flatten_array_type: rebuilds an `ARRAY<ARRAY<...<leaf>>>` type
-/// from the compact flat form. Requires `flat.nesting >= 1`; callers handle
-/// the `nesting == 0` case by using a plain scalar DataType directly.
-/// `array_nullability` must have `nesting + 1` entries (last = leaf).
-inline DataType rebuild_array_type(const FlattenedArrayType& flat) {
- bool leaf_nullable = (static_cast<size_t>(flat.nesting) <
flat.array_nullability.size())
- ?
(flat.array_nullability[static_cast<size_t>(flat.nesting)] != 0)
- : true;
- DataType dt(static_cast<TypeId>(flat.leaf_type), flat.leaf_precision,
flat.leaf_scale,
- leaf_nullable);
- for (int32_t i = flat.nesting - 1; i >= 0; --i) {
- bool nullable = (static_cast<size_t>(i) <
flat.array_nullability.size())
- ? (flat.array_nullability[static_cast<size_t>(i)]
!= 0)
- : true;
- auto arr = DataType::Array(std::move(dt));
- if (!nullable) {
- arr = arr.NotNull();
+/// Serialize a type tree into a preorder node arena (see `FfiTypeNode`): the
+/// node itself, then ARRAY's element / MAP's key+value / ROW's field subtrees.
+/// `field_name` is set only for ROW field nodes. Inverse of
`nodes_to_data_type`.
+inline void data_type_to_nodes(const DataType& dt, const std::string&
field_name,
+ rust::Vec<ffi::FfiTypeNode>& out) {
+ ffi::FfiTypeNode node;
+ node.type_id = static_cast<int32_t>(dt.id());
+ node.nullable = dt.nullable();
+ node.precision = dt.precision();
+ node.scale = dt.scale();
+ node.field_name = rust::String(field_name);
+ switch (dt.id()) {
+ case TypeId::Array:
+ node.child_count = 1;
+ break;
+ case TypeId::Map:
+ node.child_count = 2;
+ break;
+ case TypeId::Row:
+ node.child_count = static_cast<uint32_t>(dt.field_count());
+ break;
+ default:
+ node.child_count = 0;
+ break;
+ }
+ out.push_back(std::move(node));
+
+ switch (dt.id()) {
+ case TypeId::Array:
+ data_type_to_nodes(*dt.element_type(), "", out);
+ break;
+ case TypeId::Map:
+ data_type_to_nodes(*dt.key_type(), "", out);
+ data_type_to_nodes(*dt.value_type(), "", out);
+ break;
+ case TypeId::Row:
+ for (size_t i = 0; i < dt.field_count(); ++i) {
+ data_type_to_nodes(*dt.field_type(i), dt.field_name(i), out);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+/// Builds a scalar DataType from a leaf node, restoring
precision/scale/length.
+inline DataType scalar_node_to_data_type(const ffi::FfiTypeNode& node) {
+ switch (static_cast<TypeId>(node.type_id)) {
+ case TypeId::Boolean:
+ return DataType::Boolean();
+ case TypeId::TinyInt:
+ return DataType::TinyInt();
+ case TypeId::SmallInt:
+ return DataType::SmallInt();
+ case TypeId::Int:
+ return DataType::Int();
+ case TypeId::BigInt:
+ return DataType::BigInt();
+ case TypeId::Float:
+ return DataType::Float();
+ case TypeId::Double:
+ return DataType::Double();
+ case TypeId::String:
+ return DataType::String();
+ case TypeId::Bytes:
+ return DataType::Bytes();
+ case TypeId::Date:
+ return DataType::Date();
+ case TypeId::Time:
+ return DataType::Time(node.precision);
+ case TypeId::Timestamp:
+ return DataType::Timestamp(node.precision);
+ case TypeId::TimestampLtz:
+ return DataType::TimestampLtz(node.precision);
+ case TypeId::Decimal:
+ return DataType::Decimal(node.precision, node.scale);
+ case TypeId::Char:
+ return DataType::Char(node.precision);
+ case TypeId::Binary:
+ return DataType::Binary(node.precision);
+ default:
+ throw std::runtime_error("Unknown scalar type id in type tree: " +
+ std::to_string(node.type_id));
+ }
+}
+
+/// Inverse of `data_type_to_nodes`: reconstruct one type from the arena,
+/// advancing `cursor` past the nodes it consumes.
+inline DataType nodes_to_data_type(const rust::Vec<ffi::FfiTypeNode>& nodes,
size_t& cursor) {
+ if (cursor >= nodes.size()) {
+ throw std::runtime_error("type tree ended before all nodes were read");
+ }
+ const ffi::FfiTypeNode& node = nodes[cursor++];
+ DataType dt = [&]() -> DataType {
+ switch (static_cast<TypeId>(node.type_id)) {
+ case TypeId::Array: {
+ DataType element = nodes_to_data_type(nodes, cursor);
+ return DataType::Array(std::move(element));
+ }
+ case TypeId::Map: {
+ DataType key = nodes_to_data_type(nodes, cursor);
+ DataType value = nodes_to_data_type(nodes, cursor);
+ return DataType::Map(std::move(key), std::move(value));
+ }
+ case TypeId::Row: {
+ std::vector<std::pair<std::string, DataType>> fields;
+ fields.reserve(node.child_count);
+ for (uint32_t i = 0; i < node.child_count; ++i) {
+ if (cursor >= nodes.size()) {
+ throw std::runtime_error("ROW field missing from type
tree");
+ }
+ std::string fname(nodes[cursor].field_name);
+ DataType ftype = nodes_to_data_type(nodes, cursor);
+ fields.emplace_back(std::move(fname), std::move(ftype));
+ }
+ return DataType::Row(std::move(fields));
+ }
+ default:
+ return scalar_node_to_data_type(node);
}
- dt = std::move(arr);
+ }();
+ if (!node.nullable) {
+ dt = dt.NotNull();
}
return dt;
}
@@ -168,25 +250,8 @@ inline ffi::FfiConfig to_ffi_config(const Configuration&
config) {
inline ffi::FfiColumn to_ffi_column(const Column& col) {
ffi::FfiColumn ffi_col;
ffi_col.name = rust::String(col.name);
- ffi_col.data_type = static_cast<int32_t>(col.data_type.id());
- ffi_col.nullable = col.data_type.nullable();
ffi_col.comment = rust::String(col.comment);
- ffi_col.precision = col.data_type.precision();
- ffi_col.scale = col.data_type.scale();
- auto flat = flatten_array_type(col.data_type);
- ffi_col.array_nesting = flat.nesting;
- for (auto nullable : flat.array_nullability) {
- ffi_col.array_nullability.push_back(nullable);
- }
- if (flat.nesting > 0 && flat.leaf_type != 0) {
- ffi_col.element_data_type = flat.leaf_type;
- ffi_col.element_precision = flat.leaf_precision;
- ffi_col.element_scale = flat.leaf_scale;
- } else {
- ffi_col.element_data_type = 0;
- ffi_col.element_precision = 0;
- ffi_col.element_scale = 0;
- }
+ data_type_to_nodes(col.data_type, "", ffi_col.type_nodes);
return ffi_col;
}
@@ -251,72 +316,12 @@ inline ffi::FfiTableDescriptor
to_ffi_table_descriptor(const TableDescriptor& de
}
inline Column from_ffi_column(const ffi::FfiColumn& ffi_col) {
- auto type_id = static_cast<TypeId>(ffi_col.data_type);
- if (type_id == TypeId::Array) {
- auto is_supported_leaf_type = [](int32_t leaf_type) {
- switch (static_cast<TypeId>(leaf_type)) {
- case TypeId::Boolean:
- case TypeId::TinyInt:
- case TypeId::SmallInt:
- case TypeId::Int:
- case TypeId::BigInt:
- case TypeId::Float:
- case TypeId::Double:
- case TypeId::String:
- case TypeId::Bytes:
- case TypeId::Date:
- case TypeId::Time:
- case TypeId::Timestamp:
- case TypeId::TimestampLtz:
- case TypeId::Decimal:
- case TypeId::Char:
- case TypeId::Binary:
- return true;
- default:
- return false;
- }
- };
- // ROW/MAP element schema can't pass through the flat FFI column; give
the
- // array a non-null element of the right kind so element_type() is
safe to deref.
- auto element_id = static_cast<TypeId>(ffi_col.element_data_type);
- if (element_id == TypeId::Map || element_id == TypeId::Row) {
- return Column{std::string(ffi_col.name),
DataType::Array(DataType(element_id)),
- std::string(ffi_col.comment)};
- }
- if (ffi_col.element_data_type == 0) {
- throw std::runtime_error("Malformed ARRAY column '" +
std::string(ffi_col.name) +
- "': missing element_data_type");
- }
- if (ffi_col.array_nesting < 0) {
- throw std::runtime_error("Malformed ARRAY column '" +
std::string(ffi_col.name) +
- "': array_nesting must be non-negative");
- }
- if (ffi_col.element_data_type == static_cast<int32_t>(TypeId::Array)) {
- throw std::runtime_error("Malformed ARRAY column '" +
std::string(ffi_col.name) +
- "': leaf element_data_type cannot be
ARRAY");
- }
- if (!is_supported_leaf_type(ffi_col.element_data_type)) {
- throw std::runtime_error("Malformed ARRAY column '" +
std::string(ffi_col.name) +
- "': unsupported leaf element_data_type " +
-
std::to_string(ffi_col.element_data_type));
- }
-
- int32_t nesting = ffi_col.array_nesting > 0 ? ffi_col.array_nesting :
1;
- std::vector<uint8_t> array_nullability;
- for (auto nullable : ffi_col.array_nullability) {
- array_nullability.push_back(nullable);
- }
- auto dt = rebuild_array_type(
- FlattenedArrayType{
- nesting,
- ffi_col.element_data_type,
- ffi_col.element_precision,
- ffi_col.element_scale,
- std::move(array_nullability),
- });
- return Column{std::string(ffi_col.name), std::move(dt),
std::string(ffi_col.comment)};
+ size_t cursor = 0;
+ DataType dt = nodes_to_data_type(ffi_col.type_nodes, cursor);
+ if (cursor != ffi_col.type_nodes.size()) {
+ throw std::runtime_error("Column '" + std::string(ffi_col.name) +
+ "': type tree has trailing nodes");
}
- DataType dt(type_id, ffi_col.precision, ffi_col.scale, ffi_col.nullable);
return Column{std::string(ffi_col.name), std::move(dt),
std::string(ffi_col.comment)};
}
diff --git a/fluss-rust/bindings/cpp/src/lib.rs
b/fluss-rust/bindings/cpp/src/lib.rs
index 2d62136a3..3b59f950d 100644
--- a/fluss-rust/bindings/cpp/src/lib.rs
+++ b/fluss-rust/bindings/cpp/src/lib.rs
@@ -90,18 +90,29 @@ mod ffi {
table_name: String,
}
- struct FfiColumn {
- name: String,
- data_type: i32,
+ // One node of a type tree, serialized in preorder. A column's type is the
+ // sequence of nodes starting at its root; ARRAY is followed by its element
+ // subtree, MAP by its key then value subtrees, ROW by its `child_count`
+ // field subtrees (each field node carries its `field_name`). This carries
+ // the full recursive type losslessly — exact precision/scale/length,
+ // nullability at every level, and ROW field names.
+ struct FfiTypeNode {
+ type_id: i32,
nullable: bool,
- comment: String,
+ // Decimal/Timestamp/TimestampLtz precision, or Char/Binary length; 0
otherwise.
precision: i32,
+ // Decimal scale; 0 otherwise.
scale: i32,
- array_nesting: i32,
- array_nullability: Vec<u8>,
- element_data_type: i32,
- element_precision: i32,
- element_scale: i32,
+ // Field name when this node is a ROW field; empty otherwise.
+ field_name: String,
+ // Immediate children: 0 scalar, 1 ARRAY, 2 MAP, N ROW fields.
+ child_count: u32,
+ }
+
+ struct FfiColumn {
+ name: String,
+ comment: String,
+ type_nodes: Vec<FfiTypeNode>,
}
struct FfiSchema {
@@ -317,17 +328,6 @@ mod ffi {
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,
@@ -1078,38 +1078,6 @@ impl Admin {
}
}
- 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,
diff --git a/fluss-rust/bindings/cpp/src/type_lowering.hpp
b/fluss-rust/bindings/cpp/src/type_lowering.hpp
index 98e676889..3608a39b5 100644
--- a/fluss-rust/bindings/cpp/src/type_lowering.hpp
+++ b/fluss-rust/bindings/cpp/src/type_lowering.hpp
@@ -103,8 +103,9 @@ inline std::shared_ptr<arrow::DataType> to_arrow_type(const
DataType& dt) {
}
}
-/// True if the type is (or nests) a MAP/ROW the flat FFI column can't express,
-/// so it must route through Arrow. Array-of-scalar stays on the flat path.
+/// True if the type is (or nests) a MAP/ROW. The data-writer path uses this to
+/// route compound element/key/value types through Arrow; array-of-scalar uses
+/// the flat writer path.
inline bool is_compound(const DataType& dt) {
switch (dt.id()) {
case TypeId::Map:
@@ -119,16 +120,6 @@ inline bool is_compound(const DataType& dt) {
}
}
-inline std::shared_ptr<arrow::Schema> columns_to_arrow_schema(const
std::vector<Column>& columns) {
- std::vector<std::shared_ptr<arrow::Field>> fields;
- fields.reserve(columns.size());
- for (const auto& col : columns) {
- fields.push_back(
- arrow::field(col.name, to_arrow_type(col.data_type),
col.data_type.nullable()));
- }
- return arrow::schema(std::move(fields));
-}
-
/// Exports an Arrow schema to a heap FFI_ArrowSchema; the Rust bridge takes
/// ownership of the returned pointer. Throws on failure.
inline size_t export_arrow_schema(const arrow::Schema& schema) {
diff --git a/fluss-rust/bindings/cpp/src/types.rs
b/fluss-rust/bindings/cpp/src/types.rs
index 22a4adef6..baf2d7d7d 100644
--- a/fluss-rust/bindings/cpp/src/types.rs
+++ b/fluss-rust/bindings/cpp/src/types.rs
@@ -19,6 +19,9 @@ use crate::ffi;
use anyhow::{Result, anyhow};
use arrow::array::Array;
use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
+use fcore::metadata::{
+ ArrayType, Column, DataField, DataType, DataTypes, DecimalType, MapType,
RowType,
+};
use fcore::row::Datum;
use fluss as fcore;
use std::borrow::Cow;
@@ -44,229 +47,186 @@ pub const DATA_TYPE_ARRAY: i32 = 17;
pub const DATA_TYPE_MAP: i32 = 18;
pub const DATA_TYPE_ROW: i32 = 19;
-/// Separates scalar and array type specs so each variant only carries
-/// the fields it actually needs — no zeroed-out placeholders.
-enum FfiDataTypeSpec {
- Scalar {
- data_type: i32,
- precision: u32,
- scale: u32,
- nullable: bool,
- },
- Array {
- element_data_type: i32,
- element_precision: u32,
- element_scale: u32,
- array_nesting: u32,
- /// `nesting` entries for each ARRAY wrapper (outermost first) plus
- /// one trailing entry for the leaf scalar. Length = `nesting + 1`.
- array_nullability: Vec<u8>,
- },
+fn ffi_column_to_core_data_type(col: &ffi::FfiColumn) -> Result<DataType> {
+ let mut cursor = 0usize;
+ let dt = nodes_to_data_type(&col.type_nodes, &mut cursor)?;
+ if cursor != col.type_nodes.len() {
+ return Err(anyhow!(
+ "Column '{}': type tree has {} trailing nodes",
+ col.name,
+ col.type_nodes.len() - cursor
+ ));
+ }
+ Ok(dt)
}
-fn ffi_column_to_core_data_type(col: &ffi::FfiColumn) ->
Result<fcore::metadata::DataType> {
- if col.data_type == DATA_TYPE_ARRAY {
- ffi_data_type_to_core(FfiDataTypeSpec::Array {
- element_data_type: col.element_data_type,
- element_precision: col.element_precision as u32,
- element_scale: col.element_scale as u32,
- array_nesting: col.array_nesting.max(0) as u32,
- array_nullability: col.array_nullability.clone(),
- })
- } else {
- ffi_data_type_to_core(FfiDataTypeSpec::Scalar {
- data_type: col.data_type,
- precision: col.precision as u32,
- scale: col.scale as u32,
- nullable: col.nullable,
- })
+/// Reconstruct one type from a preorder node arena, advancing `cursor` past
+/// the consumed nodes. Mirrors the C++ `data_type_to_nodes` encoder.
+fn nodes_to_data_type(nodes: &[ffi::FfiTypeNode], cursor: &mut usize) ->
Result<DataType> {
+ let node = nodes
+ .get(*cursor)
+ .ok_or_else(|| anyhow!("type tree ended before all nodes were read"))?;
+ *cursor += 1;
+
+ if node.precision < 0 || node.scale < 0 {
+ return Err(anyhow!(
+ "type node precision and scale must be non-negative"
+ ));
}
+ let precision = node.precision as u32;
+ let scale = node.scale as u32;
+
+ let dt = match node.type_id {
+ DATA_TYPE_ARRAY => {
+ let element = nodes_to_data_type(nodes, cursor)?;
+ DataType::Array(ArrayType::with_nullable(node.nullable, element))
+ }
+ DATA_TYPE_MAP => {
+ let key = nodes_to_data_type(nodes, cursor)?;
+ let value = nodes_to_data_type(nodes, cursor)?;
+ DataType::Map(MapType::with_nullable(node.nullable, key, value))
+ }
+ DATA_TYPE_ROW => {
+ let mut fields = Vec::with_capacity(node.child_count as usize);
+ for _ in 0..node.child_count {
+ let field_name = nodes
+ .get(*cursor)
+ .ok_or_else(|| anyhow!("ROW field missing from type
tree"))?
+ .field_name
+ .clone();
+ let field_type = nodes_to_data_type(nodes, cursor)?;
+ // C++ ROW columns carry no per-field description.
+ fields.push(DataField::new(field_name, field_type, None));
+ }
+ DataType::Row(RowType::with_nullable(node.nullable, fields))
+ }
+ scalar => return scalar_to_core(scalar, precision, scale,
node.nullable),
+ };
+ Ok(dt)
}
-fn type_precision_scale(dt: &fcore::metadata::DataType) -> (i32, i32) {
+fn type_precision_scale(dt: &DataType) -> (i32, i32) {
match dt {
- fcore::metadata::DataType::Decimal(d) => (d.precision() as i32,
d.scale() as i32),
- fcore::metadata::DataType::Timestamp(ts) => (ts.precision() as i32, 0),
- fcore::metadata::DataType::TimestampLTz(ts) => (ts.precision() as i32,
0),
- fcore::metadata::DataType::Char(ch) => (ch.length() as i32, 0),
- fcore::metadata::DataType::Binary(bin) => (bin.length() as i32, 0),
+ DataType::Decimal(d) => (d.precision() as i32, d.scale() as i32),
+ DataType::Time(t) => (t.precision() as i32, 0),
+ DataType::Timestamp(ts) => (ts.precision() as i32, 0),
+ DataType::TimestampLTz(ts) => (ts.precision() as i32, 0),
+ DataType::Char(ch) => (ch.length() as i32, 0),
+ DataType::Binary(bin) => (bin.length() as i32, 0),
_ => (0, 0),
}
}
-struct FlattenedLeafType {
- nesting: i32,
- leaf_type: i32,
- leaf_precision: i32,
- leaf_scale: i32,
- /// `nesting` entries for ARRAY wrappers (outermost first) plus one
- /// trailing entry for the leaf scalar. Length = `nesting + 1`.
- array_nullability: Vec<u8>,
+fn scalar_to_core(data_type: i32, precision: u32, scale: u32, nullable: bool)
-> Result<DataType> {
+ let dt = match data_type {
+ DATA_TYPE_BOOLEAN => DataTypes::boolean(),
+ DATA_TYPE_TINYINT => DataTypes::tinyint(),
+ DATA_TYPE_SMALLINT => DataTypes::smallint(),
+ DATA_TYPE_INT => DataTypes::int(),
+ DATA_TYPE_BIGINT => DataTypes::bigint(),
+ DATA_TYPE_FLOAT => DataTypes::float(),
+ DATA_TYPE_DOUBLE => DataTypes::double(),
+ DATA_TYPE_STRING => DataTypes::string(),
+ DATA_TYPE_BYTES => DataTypes::bytes(),
+ DATA_TYPE_DATE => DataTypes::date(),
+ DATA_TYPE_TIME => DataTypes::time_with_precision(precision),
+ DATA_TYPE_TIMESTAMP => DataTypes::timestamp_with_precision(precision),
+ DATA_TYPE_TIMESTAMP_LTZ =>
DataTypes::timestamp_ltz_with_precision(precision),
+ DATA_TYPE_DECIMAL => DataType::Decimal(DecimalType::new(precision,
scale)?),
+ DATA_TYPE_CHAR => DataTypes::char(precision),
+ DATA_TYPE_BINARY => DataTypes::binary(precision as usize),
+ _ => return Err(anyhow!("Unknown data type: {}", data_type)),
+ };
+ if nullable {
+ Ok(dt)
+ } else {
+ Ok(dt.as_non_nullable())
+ }
}
-fn flatten_array_leaf_type(dt: &fcore::metadata::DataType) ->
Result<FlattenedLeafType> {
- let mut nesting = 0_i32;
- let mut leaf = dt;
- let mut array_nullability = Vec::new();
- while let fcore::metadata::DataType::Array(at) = leaf {
- nesting += 1;
- array_nullability.push(u8::from(leaf.is_nullable()));
- leaf = at.get_element_type();
- }
- if nesting == 0 {
- return Err(anyhow!("Expected ARRAY data type, got {dt}"));
- }
- let leaf_type = core_data_type_to_ffi(leaf);
- if leaf_type == 0 {
- return Err(anyhow!(
- "Unsupported ARRAY leaf type for C++ bindings: {leaf}"
- ));
+/// Serialize a type tree to a preorder node arena. Mirrors the C++
+/// `nodes_to_data_type` decoder; `field_name` is set only for ROW fields.
+fn core_data_type_to_nodes(dt: &DataType, field_name: &str, out: &mut
Vec<ffi::FfiTypeNode>) {
+ let (precision, scale) = type_precision_scale(dt);
+ let child_count = match dt {
+ DataType::Array(_) => 1,
+ DataType::Map(_) => 2,
+ DataType::Row(rt) => rt.fields().len() as u32,
+ _ => 0,
+ };
+ out.push(ffi::FfiTypeNode {
+ type_id: core_data_type_to_ffi(dt),
+ nullable: dt.is_nullable(),
+ precision,
+ scale,
+ field_name: field_name.to_string(),
+ child_count,
+ });
+ match dt {
+ DataType::Array(at) => core_data_type_to_nodes(at.get_element_type(),
"", out),
+ DataType::Map(mt) => {
+ core_data_type_to_nodes(mt.key_type(), "", out);
+ core_data_type_to_nodes(mt.value_type(), "", out);
+ }
+ DataType::Row(rt) => {
+ for field in rt.fields() {
+ core_data_type_to_nodes(field.data_type(), field.name(), out);
+ }
+ }
+ _ => {}
}
- array_nullability.push(u8::from(leaf.is_nullable()));
- let (leaf_precision, leaf_scale) = type_precision_scale(leaf);
- Ok(FlattenedLeafType {
- nesting,
- leaf_type,
- leaf_precision,
- leaf_scale,
- array_nullability,
- })
}
+/// Build a nested `ARRAY<…<scalar>>` from a flat leaf description. Used by the
+/// data-writer path (`element_type_from_ffi`), which carries array-of-scalar
+/// element types without a full node arena.
fn build_array_type_from_leaf(
element_data_type: i32,
element_precision: u32,
element_scale: u32,
array_nesting: u32,
- array_nullability: &[u8],
-) -> Result<fcore::metadata::DataType> {
+) -> Result<DataType> {
if array_nesting == 0 {
return Err(anyhow!("ARRAY nesting must be >= 1"));
}
- let leaf_nullable = array_nullability
- .get(array_nesting as usize)
- .map(|v| *v != 0)
- .unwrap_or(true);
- let mut dt = ffi_data_type_to_core(FfiDataTypeSpec::Scalar {
- data_type: element_data_type,
- precision: element_precision,
- scale: element_scale,
- nullable: leaf_nullable,
- })?;
- for i in (0..array_nesting).rev() {
- let nullable = array_nullability
- .get(i as usize)
- .map(|v| *v != 0)
- .unwrap_or(true);
- dt =
fcore::metadata::DataType::Array(fcore::metadata::ArrayType::with_nullable(
- nullable, dt,
- ));
+ let mut dt = scalar_to_core(element_data_type, element_precision,
element_scale, true)?;
+ for _ in 0..array_nesting {
+ dt = DataType::Array(ArrayType::new(dt));
}
Ok(dt)
}
-fn ffi_data_type_to_core(spec: FfiDataTypeSpec) ->
Result<fcore::metadata::DataType> {
- match spec {
- FfiDataTypeSpec::Scalar {
- data_type,
- precision,
- scale,
- nullable,
- } => {
- let dt = match data_type {
- DATA_TYPE_BOOLEAN => fcore::metadata::DataTypes::boolean(),
- DATA_TYPE_TINYINT => fcore::metadata::DataTypes::tinyint(),
- DATA_TYPE_SMALLINT => fcore::metadata::DataTypes::smallint(),
- DATA_TYPE_INT => fcore::metadata::DataTypes::int(),
- DATA_TYPE_BIGINT => fcore::metadata::DataTypes::bigint(),
- DATA_TYPE_FLOAT => fcore::metadata::DataTypes::float(),
- DATA_TYPE_DOUBLE => fcore::metadata::DataTypes::double(),
- DATA_TYPE_STRING => fcore::metadata::DataTypes::string(),
- DATA_TYPE_BYTES => fcore::metadata::DataTypes::bytes(),
- DATA_TYPE_DATE => fcore::metadata::DataTypes::date(),
- DATA_TYPE_TIME => fcore::metadata::DataTypes::time(),
- DATA_TYPE_TIMESTAMP => {
-
fcore::metadata::DataTypes::timestamp_with_precision(precision)
- }
- DATA_TYPE_TIMESTAMP_LTZ => {
-
fcore::metadata::DataTypes::timestamp_ltz_with_precision(precision)
- }
- DATA_TYPE_DECIMAL => {
- let dt = fcore::metadata::DecimalType::new(precision,
scale)?;
- fcore::metadata::DataType::Decimal(dt)
- }
- DATA_TYPE_CHAR => fcore::metadata::DataTypes::char(precision),
- DATA_TYPE_BINARY =>
fcore::metadata::DataTypes::binary(precision as usize),
- _ => return Err(anyhow!("Unknown data type: {}", data_type)),
- };
- if nullable {
- Ok(dt)
- } else {
- Ok(dt.as_non_nullable())
- }
- }
- FfiDataTypeSpec::Array {
- element_data_type,
- element_precision,
- element_scale,
- array_nesting,
- ref array_nullability,
- } => build_array_type_from_leaf(
- element_data_type,
- element_precision,
- element_scale,
- array_nesting,
- array_nullability,
- ),
- }
-}
-
-pub fn core_data_type_to_ffi(dt: &fcore::metadata::DataType) -> i32 {
+pub fn core_data_type_to_ffi(dt: &DataType) -> i32 {
match dt {
- fcore::metadata::DataType::Boolean(_) => DATA_TYPE_BOOLEAN,
- fcore::metadata::DataType::TinyInt(_) => DATA_TYPE_TINYINT,
- fcore::metadata::DataType::SmallInt(_) => DATA_TYPE_SMALLINT,
- fcore::metadata::DataType::Int(_) => DATA_TYPE_INT,
- fcore::metadata::DataType::BigInt(_) => DATA_TYPE_BIGINT,
- fcore::metadata::DataType::Float(_) => DATA_TYPE_FLOAT,
- fcore::metadata::DataType::Double(_) => DATA_TYPE_DOUBLE,
- fcore::metadata::DataType::String(_) => DATA_TYPE_STRING,
- fcore::metadata::DataType::Bytes(_) => DATA_TYPE_BYTES,
- fcore::metadata::DataType::Date(_) => DATA_TYPE_DATE,
- fcore::metadata::DataType::Time(_) => DATA_TYPE_TIME,
- fcore::metadata::DataType::Timestamp(_) => DATA_TYPE_TIMESTAMP,
- fcore::metadata::DataType::TimestampLTz(_) => DATA_TYPE_TIMESTAMP_LTZ,
- fcore::metadata::DataType::Decimal(_) => DATA_TYPE_DECIMAL,
- fcore::metadata::DataType::Char(_) => DATA_TYPE_CHAR,
- fcore::metadata::DataType::Binary(_) => DATA_TYPE_BINARY,
- fcore::metadata::DataType::Array(_) => DATA_TYPE_ARRAY,
- fcore::metadata::DataType::Map(_) => DATA_TYPE_MAP,
- fcore::metadata::DataType::Row(_) => DATA_TYPE_ROW,
+ DataType::Boolean(_) => DATA_TYPE_BOOLEAN,
+ DataType::TinyInt(_) => DATA_TYPE_TINYINT,
+ DataType::SmallInt(_) => DATA_TYPE_SMALLINT,
+ DataType::Int(_) => DATA_TYPE_INT,
+ DataType::BigInt(_) => DATA_TYPE_BIGINT,
+ DataType::Float(_) => DATA_TYPE_FLOAT,
+ DataType::Double(_) => DATA_TYPE_DOUBLE,
+ DataType::String(_) => DATA_TYPE_STRING,
+ DataType::Bytes(_) => DATA_TYPE_BYTES,
+ DataType::Date(_) => DATA_TYPE_DATE,
+ DataType::Time(_) => DATA_TYPE_TIME,
+ DataType::Timestamp(_) => DATA_TYPE_TIMESTAMP,
+ DataType::TimestampLTz(_) => DATA_TYPE_TIMESTAMP_LTZ,
+ DataType::Decimal(_) => DATA_TYPE_DECIMAL,
+ DataType::Char(_) => DATA_TYPE_CHAR,
+ DataType::Binary(_) => DATA_TYPE_BINARY,
+ DataType::Array(_) => DATA_TYPE_ARRAY,
+ DataType::Map(_) => DATA_TYPE_MAP,
+ DataType::Row(_) => DATA_TYPE_ROW,
}
}
-fn core_column_to_ffi(col: &fcore::metadata::Column) -> ffi::FfiColumn {
- let (precision, scale) = type_precision_scale(col.data_type());
-
- let flat = match col.data_type() {
- fcore::metadata::DataType::Array(_) =>
flatten_array_leaf_type(col.data_type()).ok(),
- _ => None,
- };
-
+fn core_column_to_ffi(col: &Column) -> ffi::FfiColumn {
+ let mut type_nodes = Vec::new();
+ core_data_type_to_nodes(col.data_type(), "", &mut type_nodes);
ffi::FfiColumn {
name: col.name().to_string(),
- data_type: core_data_type_to_ffi(col.data_type()),
- nullable: col.data_type().is_nullable(),
comment: col.comment().unwrap_or("").to_string(),
- precision,
- scale,
- array_nesting: flat.as_ref().map_or(0, |f| f.nesting),
- array_nullability: flat
- .as_ref()
- .map_or_else(Vec::new, |f| f.array_nullability.clone()),
- element_data_type: flat.as_ref().map_or(0, |f| f.leaf_type),
- element_precision: flat.as_ref().map_or(0, |f| f.leaf_precision),
- element_scale: flat.as_ref().map_or(0, |f| f.leaf_scale),
+ type_nodes,
}
}
@@ -276,12 +236,6 @@ pub fn ffi_descriptor_to_core(
let mut schema_builder = fcore::metadata::Schema::builder();
for col in &descriptor.schema.columns {
- if col.precision < 0 || col.scale < 0 || col.array_nesting < 0 {
- return Err(anyhow!(
- "Column '{}': precision, scale, and array_nesting must be
non-negative",
- col.name
- ));
- }
let dt = ffi_column_to_core_data_type(col)?;
schema_builder = schema_builder.column(&col.name, dt);
if !col.comment.is_empty() {
@@ -298,7 +252,7 @@ pub fn ffi_descriptor_to_core(
/// Assemble a core `TableDescriptor` from a pre-built `Schema` plus the
/// descriptor's table-level metadata (partition/bucket keys, properties,
-/// comment). Shared by the flat-column and Arrow-schema create-table paths.
+/// comment).
fn build_descriptor(
schema: fcore::metadata::Schema,
descriptor: &ffi::FfiTableDescriptor,
@@ -336,34 +290,6 @@ fn build_descriptor(
Ok(builder.build()?)
}
-/// Build a core `TableDescriptor` whose columns come from an Arrow schema
-/// imported over the C Data Interface. Lets C++ define nested MAP/ROW columns
-/// the flat `FfiColumn` encoding can't express, reusing core's canonical
-/// `from_arrow_field` converter rather than a second conversion copy.
-///
-/// # Safety
-/// `schema_ptr` must be a valid `FFI_ArrowSchema` heap pointer exported by C++
-/// (e.g. via `arrow::ExportSchema`); ownership is taken and released here.
-pub unsafe fn arrow_ffi_to_core_descriptor(
- schema_ptr: usize,
- descriptor: &ffi::FfiTableDescriptor,
-) -> Result<fcore::metadata::TableDescriptor> {
- let ffi_schema = unsafe { Box::from_raw(schema_ptr as *mut
arrow::ffi::FFI_ArrowSchema) };
- let arrow_schema = arrow::datatypes::Schema::try_from(ffi_schema.as_ref())
- .map_err(|e| anyhow!("Failed to import Arrow schema: {e}"))?;
-
- let mut schema_builder = fcore::metadata::Schema::builder();
- for field in arrow_schema.fields() {
- let dt = fcore::record::from_arrow_field(field.as_ref())?;
- schema_builder = schema_builder.column(field.name(), dt);
- }
- if !descriptor.schema.primary_keys.is_empty() {
- schema_builder =
schema_builder.primary_key(descriptor.schema.primary_keys.clone());
- }
-
- build_descriptor(schema_builder.build()?, descriptor)
-}
-
/// Import a heap `FFI_ArrowSchema` (exported by C++) and return its fields as
/// Fluss DataTypes. Lets ArrayWriter/MapWriter carry ROW/MAP element and
/// key/value types that the flat FFI encoding cannot express.
@@ -474,15 +400,9 @@ pub fn element_type_from_ffi(
array_nesting: u32,
) -> Result<fcore::metadata::DataType> {
if array_nesting == 0 {
- ffi_data_type_to_core(FfiDataTypeSpec::Scalar {
- data_type: leaf_dt,
- precision,
- scale,
- nullable: true,
- })
+ scalar_to_core(leaf_dt, precision, scale, true)
} else {
- let array_nullability = vec![1u8; (array_nesting + 1) as usize];
- build_array_type_from_leaf(leaf_dt, precision, scale, array_nesting,
&array_nullability)
+ build_array_type_from_leaf(leaf_dt, precision, scale, array_nesting)
}
}
diff --git a/fluss-rust/bindings/cpp/test/test_ffi_converter.cpp
b/fluss-rust/bindings/cpp/test/test_ffi_converter.cpp
index 2078bdabb..b0e4e2508 100644
--- a/fluss-rust/bindings/cpp/test/test_ffi_converter.cpp
+++ b/fluss-rust/bindings/cpp/test/test_ffi_converter.cpp
@@ -18,223 +18,226 @@
*/
#include <gtest/gtest.h>
+
#include <stdexcept>
#include "ffi_converter.hpp"
namespace {
-fluss::ffi::FfiColumn MakeArrayColumn(int32_t nesting, int32_t element_type,
- bool nullable = true, bool leaf_nullable
= true,
- std::vector<uint8_t>
per_level_nullability = {}) {
- fluss::ffi::FfiColumn col;
- col.name = rust::String("bad_array");
- col.data_type = static_cast<int32_t>(fluss::TypeId::Array);
- col.nullable = nullable;
- col.comment = rust::String("");
- col.precision = 0;
- col.scale = 0;
- col.array_nesting = nesting;
- if (!per_level_nullability.empty()) {
- for (auto v : per_level_nullability) {
- col.array_nullability.push_back(v);
- }
- } else {
- for (int32_t i = 0; i < nesting; ++i) {
- col.array_nullability.push_back((i == 0 ? nullable : true) ? 1 :
0);
- }
- col.array_nullability.push_back(leaf_nullable ? 1 : 0);
- }
- col.element_data_type = element_type;
- col.element_precision = 0;
- col.element_scale = 0;
- return col;
-}
-
-fluss::ffi::FfiColumn MakeScalarColumn(const char* name, fluss::TypeId type_id,
- bool nullable = true, int32_t precision
= 0,
- int32_t scale = 0) {
- fluss::ffi::FfiColumn col;
- col.name = rust::String(name);
- col.data_type = static_cast<int32_t>(type_id);
- col.nullable = nullable;
- col.comment = rust::String("");
- col.precision = precision;
- col.scale = scale;
- col.array_nesting = 0;
- col.element_data_type = 0;
- col.element_precision = 0;
- col.element_scale = 0;
- return col;
-}
-
-} // namespace
+using fluss::Column;
+using fluss::DataType;
+using fluss::TypeId;
-TEST(FfiConverterTest, RejectsArrayWithoutElementType) {
- auto col = MakeArrayColumn(1, 0);
- EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
+// Encode a column to the FFI node arena and decode it back, exercising the
+// full create-table / get-table-info type transport.
+Column RoundTrip(const Column& col) {
+ auto ffi_col = fluss::utils::to_ffi_column(col);
+ return fluss::utils::from_ffi_column(ffi_col);
}
-TEST(FfiConverterTest, RejectsArrayWithArrayLeafType) {
- auto col = MakeArrayColumn(2, static_cast<int32_t>(fluss::TypeId::Array));
- EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
+fluss::ffi::FfiTypeNode Node(int32_t type_id, uint32_t child_count = 0, bool
nullable = true,
+ int32_t precision = 0, int32_t scale = 0,
+ const char* field_name = "") {
+ fluss::ffi::FfiTypeNode n;
+ n.type_id = type_id;
+ n.nullable = nullable;
+ n.precision = precision;
+ n.scale = scale;
+ n.field_name = rust::String(field_name);
+ n.child_count = child_count;
+ return n;
}
-TEST(FfiConverterTest, RejectsArrayWithUnknownLeafType) {
- auto col = MakeArrayColumn(1, 999);
- EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
-}
+} // namespace
-TEST(FfiConverterTest, SupportsLegacyOneLevelArrayMetadata) {
- auto col = MakeArrayColumn(0, static_cast<int32_t>(fluss::TypeId::Int));
- auto converted = fluss::utils::from_ffi_column(col);
- EXPECT_EQ(converted.data_type.id(), fluss::TypeId::Array);
- ASSERT_NE(converted.data_type.element_type(), nullptr);
- EXPECT_EQ(converted.data_type.element_type()->id(), fluss::TypeId::Int);
-}
+// --- DataType value semantics ---
-// --- Nullability tests ---
-
-TEST(DataTypeTest, DefaultNullable) {
- auto dt = fluss::DataType::Int();
- EXPECT_TRUE(dt.nullable());
-}
+TEST(DataTypeTest, DefaultNullable) { EXPECT_TRUE(DataType::Int().nullable());
}
TEST(DataTypeTest, NotNullMethod) {
- auto dt = fluss::DataType::Int().NotNull();
+ auto dt = DataType::Int().NotNull();
EXPECT_FALSE(dt.nullable());
- EXPECT_EQ(dt.id(), fluss::TypeId::Int);
+ EXPECT_EQ(dt.id(), TypeId::Int);
}
TEST(DataTypeTest, NotNullPreservesPrecisionScale) {
- auto dt = fluss::DataType::Decimal(10, 2).NotNull();
+ auto dt = DataType::Decimal(10, 2).NotNull();
EXPECT_FALSE(dt.nullable());
EXPECT_EQ(dt.precision(), 10);
EXPECT_EQ(dt.scale(), 2);
}
-TEST(DataTypeTest, ArrayElementNullability) {
- auto dt = fluss::DataType::Array(fluss::DataType::Int().NotNull());
- EXPECT_TRUE(dt.nullable());
- ASSERT_NE(dt.element_type(), nullptr);
- EXPECT_FALSE(dt.element_type()->nullable());
-}
+// --- Node-arena round trips ---
-TEST(DataTypeTest, NotNullArrayNullableElement) {
- auto dt = fluss::DataType::Array(fluss::DataType::Int()).NotNull();
- EXPECT_FALSE(dt.nullable());
- ASSERT_NE(dt.element_type(), nullptr);
- EXPECT_TRUE(dt.element_type()->nullable());
+TEST(FfiConverterTest, ScalarRoundTrip) {
+ Column col{"id", DataType::Int(), "primary id"};
+ auto back = RoundTrip(col);
+ EXPECT_EQ(back.name, "id");
+ EXPECT_EQ(back.comment, "primary id");
+ EXPECT_EQ(back.data_type.id(), TypeId::Int);
+ EXPECT_TRUE(back.data_type.nullable());
}
-TEST(DataTypeTest, NotNullArrayNotNullElement) {
- auto dt =
fluss::DataType::Array(fluss::DataType::Int().NotNull()).NotNull();
- EXPECT_FALSE(dt.nullable());
- ASSERT_NE(dt.element_type(), nullptr);
- EXPECT_FALSE(dt.element_type()->nullable());
+TEST(FfiConverterTest, ScalarNotNullRoundTrip) {
+ auto back = RoundTrip(Column{"id", DataType::Int().NotNull(), ""});
+ EXPECT_EQ(back.data_type.id(), TypeId::Int);
+ EXPECT_FALSE(back.data_type.nullable());
}
-TEST(FfiConverterTest, ScalarNullableRoundTrip) {
- fluss::Column col{"id", fluss::DataType::Int(), ""};
- auto ffi_col = fluss::utils::to_ffi_column(col);
- EXPECT_TRUE(ffi_col.nullable);
- auto back = fluss::utils::from_ffi_column(ffi_col);
- EXPECT_TRUE(back.data_type.nullable());
+TEST(FfiConverterTest, DecimalPrecisionScalePreserved) {
+ auto back = RoundTrip(Column{"amount", DataType::Decimal(18, 4), ""});
+ EXPECT_EQ(back.data_type.id(), TypeId::Decimal);
+ EXPECT_EQ(back.data_type.precision(), 18);
+ EXPECT_EQ(back.data_type.scale(), 4);
}
-TEST(FfiConverterTest, ScalarNotNullRoundTrip) {
- fluss::Column col{"id", fluss::DataType::Int().NotNull(), ""};
- auto ffi_col = fluss::utils::to_ffi_column(col);
- EXPECT_FALSE(ffi_col.nullable);
- auto back = fluss::utils::from_ffi_column(ffi_col);
- EXPECT_FALSE(back.data_type.nullable());
+// Non-canonical precisions are exactly what the old Arrow-schema path
quantized;
+// the node arena must preserve them.
+TEST(FfiConverterTest, TimestampPrecisionPreserved) {
+ auto back = RoundTrip(Column{"ts", DataType::Timestamp(2), ""});
+ EXPECT_EQ(back.data_type.id(), TypeId::Timestamp);
+ EXPECT_EQ(back.data_type.precision(), 2);
+
+ auto ltz = RoundTrip(Column{"ts_ltz", DataType::TimestampLtz(9), ""});
+ EXPECT_EQ(ltz.data_type.id(), TypeId::TimestampLtz);
+ EXPECT_EQ(ltz.data_type.precision(), 9);
}
-TEST(FfiConverterTest, ArrayNotNullElementRoundTrip) {
- fluss::Column col{"tags",
fluss::DataType::Array(fluss::DataType::String().NotNull()), ""};
- auto ffi_col = fluss::utils::to_ffi_column(col);
- EXPECT_TRUE(ffi_col.nullable);
- ASSERT_EQ(ffi_col.array_nullability.size(), 2u);
- EXPECT_EQ(ffi_col.array_nullability[1], 0);
- auto back = fluss::utils::from_ffi_column(ffi_col);
- EXPECT_TRUE(back.data_type.nullable());
- ASSERT_NE(back.data_type.element_type(), nullptr);
- EXPECT_FALSE(back.data_type.element_type()->nullable());
+TEST(FfiConverterTest, TimePrecisionPreserved) {
+ auto back = RoundTrip(Column{"t", DataType::Time(3), ""});
+ EXPECT_EQ(back.data_type.id(), TypeId::Time);
+ EXPECT_EQ(back.data_type.precision(), 3);
}
-TEST(FfiConverterTest, NotNullArrayNullableElementRoundTrip) {
- fluss::Column col{"ids",
fluss::DataType::Array(fluss::DataType::Int()).NotNull(), ""};
- auto ffi_col = fluss::utils::to_ffi_column(col);
- EXPECT_FALSE(ffi_col.nullable);
- ASSERT_EQ(ffi_col.array_nullability.size(), 2u);
- EXPECT_EQ(ffi_col.array_nullability[1], 1);
- auto back = fluss::utils::from_ffi_column(ffi_col);
- EXPECT_FALSE(back.data_type.nullable());
- ASSERT_NE(back.data_type.element_type(), nullptr);
- EXPECT_TRUE(back.data_type.element_type()->nullable());
+TEST(FfiConverterTest, CharBinaryLengthPreserved) {
+ auto ch = RoundTrip(Column{"code", DataType::Char(12), ""});
+ EXPECT_EQ(ch.data_type.id(), TypeId::Char);
+ EXPECT_EQ(ch.data_type.precision(), 12);
+
+ auto bin = RoundTrip(Column{"hash", DataType::Binary(32), ""});
+ EXPECT_EQ(bin.data_type.id(), TypeId::Binary);
+ EXPECT_EQ(bin.data_type.precision(), 32);
}
-TEST(FfiConverterTest, NotNullArrayNotNullElementRoundTrip) {
- fluss::Column col{
- "strict_ids",
- fluss::DataType::Array(fluss::DataType::Int().NotNull()).NotNull(),
- "",
- };
- auto ffi_col = fluss::utils::to_ffi_column(col);
- EXPECT_FALSE(ffi_col.nullable);
- ASSERT_EQ(ffi_col.array_nullability.size(), 2u);
- EXPECT_EQ(ffi_col.array_nullability[1], 0);
- auto back = fluss::utils::from_ffi_column(ffi_col);
- EXPECT_FALSE(back.data_type.nullable());
+TEST(FfiConverterTest, ArrayRoundTrip) {
+ auto back = RoundTrip(Column{"tags", DataType::Array(DataType::String()),
""});
+ EXPECT_EQ(back.data_type.id(), TypeId::Array);
ASSERT_NE(back.data_type.element_type(), nullptr);
- EXPECT_FALSE(back.data_type.element_type()->nullable());
+ EXPECT_EQ(back.data_type.element_type()->id(), TypeId::String);
}
-TEST(FfiConverterTest, NestedArrayIntermediateNullabilityRoundTrip) {
- fluss::Column col{
- "nested",
-
fluss::DataType::Array(fluss::DataType::Array(fluss::DataType::Int()).NotNull()),
- "",
- };
- auto ffi_col = fluss::utils::to_ffi_column(col);
- auto back = fluss::utils::from_ffi_column(ffi_col);
-
+TEST(FfiConverterTest, NestedArrayPerLevelNullabilityRoundTrip) {
+ // array<array<int NOT NULL> NOT NULL> (outer nullable)
+ Column col{"nested",
+
DataType::Array(DataType::Array(DataType::Int().NotNull()).NotNull()), ""};
+ auto back = RoundTrip(col);
EXPECT_TRUE(back.data_type.nullable());
ASSERT_NE(back.data_type.element_type(), nullptr);
EXPECT_FALSE(back.data_type.element_type()->nullable());
ASSERT_NE(back.data_type.element_type()->element_type(), nullptr);
- EXPECT_TRUE(back.data_type.element_type()->element_type()->nullable());
+ EXPECT_EQ(back.data_type.element_type()->element_type()->id(),
TypeId::Int);
+ EXPECT_FALSE(back.data_type.element_type()->element_type()->nullable());
}
-TEST(FfiConverterTest, NestedArrayAllLevelsNullabilityRoundTrip) {
- fluss::Column col{
- "strict_nested",
- fluss::DataType::Array(
- fluss::DataType::Array(fluss::DataType::Int().NotNull()).NotNull())
- .NotNull(),
- "",
- };
- auto ffi_col = fluss::utils::to_ffi_column(col);
- auto back = fluss::utils::from_ffi_column(ffi_col);
+TEST(FfiConverterTest, MapRoundTrip) {
+ Column col{"attrs", DataType::Map(DataType::String(),
DataType::Int().NotNull()), ""};
+ auto back = RoundTrip(col);
+ EXPECT_EQ(back.data_type.id(), TypeId::Map);
+ ASSERT_NE(back.data_type.key_type(), nullptr);
+ ASSERT_NE(back.data_type.value_type(), nullptr);
+ EXPECT_EQ(back.data_type.key_type()->id(), TypeId::String);
+ EXPECT_EQ(back.data_type.value_type()->id(), TypeId::Int);
+ EXPECT_FALSE(back.data_type.value_type()->nullable());
+}
+
+TEST(FfiConverterTest, RowRoundTrip) {
+ Column col{"profile",
+ DataType::Row({{"age", DataType::Int().NotNull()}, {"city",
DataType::String()}}),
+ "user profile"};
+ auto back = RoundTrip(col);
+ EXPECT_EQ(back.comment, "user profile");
+ EXPECT_EQ(back.data_type.id(), TypeId::Row);
+ ASSERT_EQ(back.data_type.field_count(), 2u);
+ EXPECT_EQ(back.data_type.field_name(0), "age");
+ ASSERT_NE(back.data_type.field_type(0), nullptr);
+ EXPECT_EQ(back.data_type.field_type(0)->id(), TypeId::Int);
+ EXPECT_FALSE(back.data_type.field_type(0)->nullable());
+ EXPECT_EQ(back.data_type.field_name(1), "city");
+ EXPECT_EQ(back.data_type.field_type(1)->id(), TypeId::String);
+ EXPECT_TRUE(back.data_type.field_type(1)->nullable());
+}
+
+// array<map<string, row<amount: decimal(18,4), ts: timestamp(3)>>> — every
kind
+// of nesting plus precision-bearing leaves, round-tripped exactly.
+TEST(FfiConverterTest, DeeplyNestedRoundTrip) {
+ auto row = DataType::Row({{"amount", DataType::Decimal(18, 4)}, {"ts",
DataType::Timestamp(3)}});
+ Column col{"events", DataType::Array(DataType::Map(DataType::String(),
std::move(row))), ""};
+ auto back = RoundTrip(col);
+
+ ASSERT_EQ(back.data_type.id(), TypeId::Array);
+ const DataType* map = back.data_type.element_type();
+ ASSERT_NE(map, nullptr);
+ ASSERT_EQ(map->id(), TypeId::Map);
+ EXPECT_EQ(map->key_type()->id(), TypeId::String);
+ const DataType* inner = map->value_type();
+ ASSERT_NE(inner, nullptr);
+ ASSERT_EQ(inner->id(), TypeId::Row);
+ ASSERT_EQ(inner->field_count(), 2u);
+ EXPECT_EQ(inner->field_name(0), "amount");
+ EXPECT_EQ(inner->field_type(0)->precision(), 18);
+ EXPECT_EQ(inner->field_type(0)->scale(), 4);
+ EXPECT_EQ(inner->field_name(1), "ts");
+ EXPECT_EQ(inner->field_type(1)->precision(), 3);
+}
+
+TEST(FfiConverterTest, NodeArenaShape) {
+ EXPECT_EQ(fluss::utils::to_ffi_column(Column{"a", DataType::Int(),
""}).type_nodes.size(), 1u);
+ EXPECT_EQ(
+ fluss::utils::to_ffi_column(Column{"a",
DataType::Array(DataType::Int()), ""}).type_nodes.size(),
+ 2u);
+ EXPECT_EQ(fluss::utils::to_ffi_column(
+ Column{"a", DataType::Map(DataType::String(),
DataType::Int()), ""})
+ .type_nodes.size(),
+ 3u);
+ EXPECT_EQ(fluss::utils::to_ffi_column(
+ Column{"a", DataType::Row({{"x", DataType::Int()}, {"y",
DataType::String()}}), ""})
+ .type_nodes.size(),
+ 3u);
+}
+
+// --- Decoder rejects malformed arenas ---
+
+TEST(FfiConverterTest, RejectsEmptyTypeTree) {
+ fluss::ffi::FfiColumn col;
+ col.name = rust::String("broken");
+ col.comment = rust::String("");
+ EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
+}
- EXPECT_FALSE(back.data_type.nullable());
- ASSERT_NE(back.data_type.element_type(), nullptr);
- EXPECT_FALSE(back.data_type.element_type()->nullable());
- ASSERT_NE(back.data_type.element_type()->element_type(), nullptr);
- EXPECT_FALSE(back.data_type.element_type()->element_type()->nullable());
+TEST(FfiConverterTest, RejectsUnknownTypeId) {
+ fluss::ffi::FfiColumn col;
+ col.name = rust::String("broken");
+ col.comment = rust::String("");
+ col.type_nodes.push_back(Node(999));
+ EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
}
-TEST(FfiConverterTest, FfiColumnNonNullableScalarReconstructed) {
- auto col = MakeScalarColumn("id", fluss::TypeId::Int, false);
- auto converted = fluss::utils::from_ffi_column(col);
- EXPECT_FALSE(converted.data_type.nullable());
- EXPECT_EQ(converted.data_type.id(), fluss::TypeId::Int);
+TEST(FfiConverterTest, RejectsTrailingNodes) {
+ fluss::ffi::FfiColumn col;
+ col.name = rust::String("broken");
+ col.comment = rust::String("");
+ col.type_nodes.push_back(Node(static_cast<int32_t>(TypeId::Int)));
+ col.type_nodes.push_back(Node(static_cast<int32_t>(TypeId::Int)));
+ EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
}
-TEST(FfiConverterTest, FfiColumnNonNullableArrayReconstructed) {
- auto col = MakeArrayColumn(1, static_cast<int32_t>(fluss::TypeId::String),
false, false);
- auto converted = fluss::utils::from_ffi_column(col);
- EXPECT_FALSE(converted.data_type.nullable());
- ASSERT_NE(converted.data_type.element_type(), nullptr);
- EXPECT_FALSE(converted.data_type.element_type()->nullable());
+TEST(FfiConverterTest, RejectsRowMissingField) {
+ fluss::ffi::FfiColumn col;
+ col.name = rust::String("broken");
+ col.comment = rust::String("");
+ // ROW claims two fields but only one follows.
+ col.type_nodes.push_back(Node(static_cast<int32_t>(TypeId::Row),
/*child_count=*/2));
+ col.type_nodes.push_back(Node(static_cast<int32_t>(TypeId::Int), 0, true,
0, 0, "x"));
+ EXPECT_THROW((void)fluss::utils::from_ffi_column(col), std::runtime_error);
}
diff --git a/fluss-rust/bindings/cpp/test/test_kv_table.cpp
b/fluss-rust/bindings/cpp/test/test_kv_table.cpp
index b7902149b..20c7fb623 100644
--- a/fluss-rust/bindings/cpp/test/test_kv_table.cpp
+++ b/fluss-rust/bindings/cpp/test/test_kv_table.cpp
@@ -23,6 +23,8 @@
#include "test_utils.h"
+using fluss::DataType;
+
class KvTableTest : public ::testing::Test {
protected:
fluss::Admin& admin() { return
fluss_test::FlussTestEnvironment::Instance()->GetAdmin(); }
@@ -39,9 +41,9 @@ TEST_F(KvTableTest, UpsertDeleteAndLookup) {
fluss::TablePath table_path("fluss", "test_upsert_and_lookup_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
- .AddColumn("age", fluss::DataType::BigInt())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
+ .AddColumn("age", DataType::BigInt())
.SetPrimaryKeys({"id"})
.Build();
@@ -164,8 +166,8 @@ TEST_F(KvTableTest, LimitScan) {
fluss::TablePath table_path("fluss", "test_limit_scan_pk_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
.SetPrimaryKeys({"id"})
.Build();
@@ -234,9 +236,9 @@ TEST_F(KvTableTest, LookupWithNestedArray) {
fluss::TablePath table_path("fluss", "test_lookup_nested_array_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
+ .AddColumn("id", DataType::Int())
.AddColumn("matrix",
-
fluss::DataType::Array(fluss::DataType::Array(fluss::DataType::Int())))
+
DataType::Array(DataType::Array(DataType::Int())))
.SetPrimaryKeys({"id"})
.Build();
@@ -258,15 +260,15 @@ TEST_F(KvTableTest, LookupWithNestedArray) {
auto row = table.NewRow();
row.Set("id", 1);
- fluss::ArrayWriter inner1(2, fluss::DataType::Int());
+ fluss::ArrayWriter inner1(2, DataType::Int());
inner1.SetInt32(0, 11);
inner1.SetInt32(1, 12);
- fluss::ArrayWriter inner2(2, fluss::DataType::Int());
+ fluss::ArrayWriter inner2(2, DataType::Int());
inner2.SetInt32(0, 21);
inner2.SetInt32(1, 22);
- fluss::ArrayWriter outer(2,
fluss::DataType::Array(fluss::DataType::Int()));
+ fluss::ArrayWriter outer(2, DataType::Array(DataType::Int()));
outer.SetArray(0, std::move(inner1));
outer.SetArray(1, std::move(inner2));
row.Set("matrix", std::move(outer));
@@ -308,45 +310,47 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
fluss::TablePath table_path("fluss", "test_lookup_complex_matrix_cpp");
- auto row_seq_label = arrow::struct_(
- {arrow::field("seq", arrow::int32()), arrow::field("label",
arrow::utf8())});
-
- auto arrow_schema = arrow::schema({
- arrow::field("id", arrow::int32()),
- arrow::field("m_str_int", arrow::map(arrow::utf8(), arrow::int32())),
- arrow::field("m_str_row", arrow::map(arrow::utf8(), row_seq_label)),
- arrow::field("m_str_map",
- arrow::map(arrow::utf8(), arrow::map(arrow::utf8(),
arrow::int32()))),
- arrow::field("m_str_arr", arrow::map(arrow::utf8(),
arrow::list(arrow::int32()))),
- arrow::field("arr_map", arrow::list(arrow::map(arrow::utf8(),
arrow::int32()))),
- arrow::field("arr_row", arrow::list(row_seq_label)),
- arrow::field("r_deep", arrow::struct_({arrow::field(
- "inner", arrow::struct_({arrow::field("n",
arrow::int32())}))})),
- arrow::field("r_with_arr",
- arrow::struct_({arrow::field("f_int", arrow::int32()),
- arrow::field("f_arr",
arrow::list(arrow::int32()))})),
- // row_rich: every scalar type + an array field in one ROW.
- arrow::field("r_rich",
- arrow::struct_({
- arrow::field("f_bool", arrow::boolean()),
- arrow::field("f_int", arrow::int32()),
- arrow::field("f_long", arrow::int64()),
- arrow::field("f_float", arrow::float32()),
- arrow::field("f_double", arrow::float64()),
- arrow::field("f_str", arrow::utf8()),
- arrow::field("f_bytes", arrow::binary()),
- arrow::field("f_decimal", arrow::decimal128(10, 2)),
- arrow::field("f_date", arrow::date32()),
- arrow::field("f_time",
arrow::time32(arrow::TimeUnit::MILLI)),
- arrow::field("f_ts_ntz",
arrow::timestamp(arrow::TimeUnit::MICRO)),
- arrow::field("f_ts_ltz",
arrow::timestamp(arrow::TimeUnit::MICRO, "UTC")),
- arrow::field("f_binary", arrow::fixed_size_binary(4)),
- arrow::field("f_arr", arrow::list(arrow::int32())),
- })),
- arrow::field("m_str_tiny", arrow::map(arrow::utf8(), arrow::int8())),
- arrow::field("arr_small", arrow::list(arrow::int16())),
- });
- auto schema = fluss::Schema::FromArrow(arrow_schema, {"id"});
+ auto row_seq_label = DataType::Row({{"seq", DataType::Int()}, {"label",
DataType::String()}});
+
+ auto schema =
+ fluss::Schema::NewBuilder()
+ .AddColumn("id", DataType::Int())
+ .AddColumn("m_str_int", DataType::Map(DataType::String(),
DataType::Int()))
+ .AddColumn("m_str_row", DataType::Map(DataType::String(),
row_seq_label))
+ .AddColumn("m_str_map",
+ DataType::Map(DataType::String(),
+ DataType::Map(DataType::String(),
DataType::Int())))
+ .AddColumn("m_str_arr",
+ DataType::Map(DataType::String(),
DataType::Array(DataType::Int())))
+ .AddColumn("arr_map",
+ DataType::Array(DataType::Map(DataType::String(),
DataType::Int())))
+ .AddColumn("arr_row", DataType::Array(row_seq_label))
+ .AddColumn("r_deep",
+ DataType::Row({{"inner", DataType::Row({{"n",
DataType::Int()}})}}))
+ .AddColumn("r_with_arr",
+ DataType::Row({{"f_int", DataType::Int()},
+ {"f_arr",
DataType::Array(DataType::Int())}}))
+ // r_rich: every scalar type + an array field in one ROW.
+ .AddColumn("r_rich", DataType::Row({
+ {"f_bool", DataType::Boolean()},
+ {"f_int", DataType::Int()},
+ {"f_long", DataType::BigInt()},
+ {"f_float", DataType::Float()},
+ {"f_double", DataType::Double()},
+ {"f_str", DataType::String()},
+ {"f_bytes", DataType::Bytes()},
+ {"f_decimal", DataType::Decimal(10, 2)},
+ {"f_date", DataType::Date()},
+ {"f_time", DataType::Time()},
+ {"f_ts_ntz", DataType::Timestamp(6)},
+ {"f_ts_ltz", DataType::TimestampLtz(6)},
+ {"f_binary", DataType::Binary(4)},
+ {"f_arr",
DataType::Array(DataType::Int())},
+ }))
+ .AddColumn("m_str_tiny", DataType::Map(DataType::String(),
DataType::TinyInt()))
+ .AddColumn("arr_small", DataType::Array(DataType::SmallInt()))
+ .SetPrimaryKeys({"id"})
+ .Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -366,7 +370,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
// map<string,int> — second entry has a NULL value.
{
- fluss::MapWriter m(2, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter m(2, DataType::String(), DataType::Int());
m.SetKeyString("a");
m.SetValueInt32(1);
m.Commit();
@@ -375,9 +379,9 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
m.Commit();
row.Set("m_str_int", std::move(m));
}
- // map<string, row<seq,label>> — value is a ROW, so the Arrow ctor.
+ // map<string, row<seq,label>>
{
- fluss::MapWriter m(1, arrow::utf8(), row_seq_label);
+ fluss::MapWriter m(1, DataType::String(), row_seq_label);
m.SetKeyString("k");
fluss::GenericRow v(2);
v.SetInt32(0, 7);
@@ -390,7 +394,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
{
fluss::MapWriter m(1, arrow::utf8(), arrow::map(arrow::utf8(),
arrow::int32()));
m.SetKeyString("k");
- fluss::MapWriter inner(1, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter inner(1, DataType::String(), DataType::Int());
inner.SetKeyString("x");
inner.SetValueInt32(9);
inner.Commit();
@@ -400,10 +404,10 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
}
// map<string, array<int>> — value array fits the flat ctor.
{
- fluss::MapWriter m(1, fluss::DataType::String(),
- fluss::DataType::Array(fluss::DataType::Int()));
+ fluss::MapWriter m(1, DataType::String(),
+ DataType::Array(DataType::Int()));
m.SetKeyString("k");
- fluss::ArrayWriter v(2, fluss::DataType::Int());
+ fluss::ArrayWriter v(2, DataType::Int());
v.SetInt32(0, 10);
v.SetInt32(1, 20);
m.SetValueArray(std::move(v));
@@ -413,7 +417,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
// array<map<string,int>> — element is a MAP, so the Arrow ctor.
{
fluss::ArrayWriter a(1, arrow::map(arrow::utf8(), arrow::int32()));
- fluss::MapWriter e(1, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter e(1, DataType::String(), DataType::Int());
e.SetKeyString("p");
e.SetValueInt32(5);
e.Commit();
@@ -445,7 +449,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
{
fluss::GenericRow r(2);
r.SetInt32(0, 100);
- fluss::ArrayWriter arr(3, fluss::DataType::Int());
+ fluss::ArrayWriter arr(3, DataType::Int());
arr.SetInt32(0, 1);
arr.SetInt32(1, 2);
arr.SetInt32(2, 3);
@@ -468,7 +472,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
rr.SetTimestampNtz(10, fluss::Timestamp{1769163227123LL, 456000});
rr.SetTimestampLtz(11, fluss::Timestamp{1769163227456LL, 0});
rr.SetBytes(12, {1, 2, 3, 4});
- fluss::ArrayWriter farr(3, fluss::DataType::Int());
+ fluss::ArrayWriter farr(3, DataType::Int());
farr.SetInt32(0, 7);
farr.SetNull(1);
farr.SetInt32(2, 11);
@@ -477,7 +481,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
}
// map<string,tinyint>
{
- fluss::MapWriter m(2, fluss::DataType::String(),
fluss::DataType::TinyInt());
+ fluss::MapWriter m(2, DataType::String(), DataType::TinyInt());
m.SetKeyString("lo");
m.SetValueInt32(-128);
m.Commit();
@@ -488,7 +492,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
}
// array<smallint>
{
- fluss::ArrayWriter a(2, fluss::DataType::SmallInt());
+ fluss::ArrayWriter a(2, DataType::SmallInt());
a.SetInt32(0, 1000);
a.SetInt32(1, -2000);
row.Set("arr_small", std::move(a));
@@ -616,7 +620,7 @@ TEST_F(KvTableTest, LookupComplexTypesMatrix) {
// Row 2 (id=2) — every compound column NULL.
{
- const int column_count = arrow_schema->num_fields();
+ const int column_count = static_cast<int>(schema.columns.size());
auto row = table.NewRow();
row.SetInt32(0, 2);
for (int i = 1; i < column_count; ++i) {
@@ -647,13 +651,13 @@ TEST_F(KvTableTest, MapWithTimestampValuesNtzAndLtz) {
fluss::TablePath table_path("fluss", "test_map_timestamp_values_cpp");
- auto arrow_schema = arrow::schema({
- arrow::field("id", arrow::int32()),
- arrow::field("mn", arrow::map(arrow::utf8(),
arrow::timestamp(arrow::TimeUnit::MICRO))),
- arrow::field("ml",
- arrow::map(arrow::utf8(),
arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"))),
- });
- auto schema = fluss::Schema::FromArrow(arrow_schema, {"id"});
+ auto schema =
+ fluss::Schema::NewBuilder()
+ .AddColumn("id", DataType::Int())
+ .AddColumn("mn", DataType::Map(DataType::String(),
DataType::Timestamp(6)))
+ .AddColumn("ml", DataType::Map(DataType::String(),
DataType::TimestampLtz(6)))
+ .SetPrimaryKeys({"id"})
+ .Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -716,16 +720,16 @@ TEST_F(KvTableTest, NativeNestedBuilderNoArrow) {
fluss::TablePath table_path("fluss", "test_native_nested_builder_cpp");
// array<row<seq: int, attrs: map<string, int>>>
- auto event = fluss::DataType::Row({
- {"seq", fluss::DataType::Int()},
- {"attrs", fluss::DataType::Map(fluss::DataType::String(),
fluss::DataType::Int())},
+ auto event = DataType::Row({
+ {"seq", DataType::Int()},
+ {"attrs", DataType::Map(DataType::String(), DataType::Int())},
});
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("events", fluss::DataType::Array(event))
- .AddColumn("profile", fluss::DataType::Row({
- {"name",
fluss::DataType::String()},
- {"score",
fluss::DataType::Double()},
+ .AddColumn("id", DataType::Int())
+ .AddColumn("events", DataType::Array(event))
+ .AddColumn("profile", DataType::Row({
+ {"name", DataType::String()},
+ {"score", DataType::Double()},
}))
.SetPrimaryKeys({"id"})
.Build();
@@ -750,8 +754,8 @@ TEST_F(KvTableTest, NativeNestedBuilderNoArrow) {
for (int i = 0; i < 2; i++) {
fluss::GenericRow ev(2);
ev.SetInt32(0, i);
- fluss::MapWriter attrs(static_cast<size_t>(i + 1),
fluss::DataType::String(),
- fluss::DataType::Int());
+ fluss::MapWriter attrs(static_cast<size_t>(i + 1),
DataType::String(),
+ DataType::Int());
attrs.SetKeyString("a");
attrs.SetValueInt32(i * 10);
attrs.Commit();
@@ -805,16 +809,17 @@ TEST_F(KvTableTest, ComplexTypesPartialUpdate) {
fluss::TablePath table_path("fluss", "test_complex_partial_update_cpp");
- auto arrow_schema = arrow::schema({
- arrow::field("id", arrow::int32()),
- arrow::field("name", arrow::utf8()),
- arrow::field("score", arrow::int64()),
- arrow::field("nested", arrow::struct_({arrow::field("seq",
arrow::int32()),
- arrow::field("label",
arrow::utf8())})),
- arrow::field("attrs", arrow::map(arrow::utf8(), arrow::int32())),
- arrow::field("tags", arrow::list(arrow::utf8())),
- });
- auto schema = fluss::Schema::FromArrow(arrow_schema, {"id"});
+ auto schema =
+ fluss::Schema::NewBuilder()
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
+ .AddColumn("score", DataType::BigInt())
+ .AddColumn("nested", DataType::Row({{"seq", DataType::Int()},
+ {"label",
DataType::String()}}))
+ .AddColumn("attrs", DataType::Map(DataType::String(),
DataType::Int()))
+ .AddColumn("tags", DataType::Array(DataType::String()))
+ .SetPrimaryKeys({"id"})
+ .Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -837,7 +842,7 @@ TEST_F(KvTableTest, ComplexTypesPartialUpdate) {
nested.SetInt32(0, 10);
nested.SetString(1, "alpha");
row.SetRow(3, std::move(nested));
- fluss::MapWriter attrs(2, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter attrs(2, DataType::String(), DataType::Int());
attrs.SetKeyString("a");
attrs.SetValueInt32(1);
attrs.Commit();
@@ -845,7 +850,7 @@ TEST_F(KvTableTest, ComplexTypesPartialUpdate) {
attrs.SetValueInt32(2);
attrs.Commit();
row.SetMap(4, std::move(attrs));
- fluss::ArrayWriter tags(2, fluss::DataType::String());
+ fluss::ArrayWriter tags(2, DataType::String());
tags.SetString(0, "x");
tags.SetString(1, "y");
row.SetArray(5, std::move(tags));
@@ -925,14 +930,15 @@ TEST_F(KvTableTest, PartitionedComplexTypes) {
fluss::TablePath table_path("fluss", "test_partitioned_complex_cpp");
- auto arrow_schema = arrow::schema({
- arrow::field("region", arrow::utf8()),
- arrow::field("user_id", arrow::int32()),
- arrow::field("nested", arrow::struct_({arrow::field("seq",
arrow::int32()),
- arrow::field("label",
arrow::utf8())})),
- arrow::field("attrs", arrow::map(arrow::utf8(), arrow::int32())),
- });
- auto schema = fluss::Schema::FromArrow(arrow_schema, {"region",
"user_id"});
+ auto schema =
+ fluss::Schema::NewBuilder()
+ .AddColumn("region", DataType::String())
+ .AddColumn("user_id", DataType::Int())
+ .AddColumn("nested", DataType::Row({{"seq", DataType::Int()},
+ {"label",
DataType::String()}}))
+ .AddColumn("attrs", DataType::Map(DataType::String(),
DataType::Int()))
+ .SetPrimaryKeys({"region", "user_id"})
+ .Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -964,7 +970,7 @@ TEST_F(KvTableTest, PartitionedComplexTypes) {
nested.SetInt32(0, d.seq);
nested.SetString(1, d.label);
row.SetRow(2, std::move(nested));
- fluss::MapWriter attrs(1, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter attrs(1, DataType::String(), DataType::Int());
attrs.SetKeyString(d.label);
attrs.SetValueInt32(d.seq);
attrs.Commit();
@@ -1002,8 +1008,8 @@ TEST_F(KvTableTest, LookupArrayValidationErrors) {
fluss::TablePath table_path("fluss",
"test_lookup_array_validation_errors_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("vals",
fluss::DataType::Array(fluss::DataType::Int()))
+ .AddColumn("id", DataType::Int())
+ .AddColumn("vals", DataType::Array(DataType::Int()))
.SetPrimaryKeys({"id"})
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -1020,7 +1026,7 @@ TEST_F(KvTableTest, LookupArrayValidationErrors) {
auto row = table.NewRow();
row.Set("id", 1);
- fluss::ArrayWriter vals(2, fluss::DataType::Int());
+ fluss::ArrayWriter vals(2, DataType::Int());
vals.SetInt32(0, 99);
vals.SetNull(1);
row.Set("vals", std::move(vals));
@@ -1070,9 +1076,9 @@ TEST_F(KvTableTest, CompositePrimaryKeys) {
fluss::TablePath table_path("fluss", "test_composite_pk_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("region", fluss::DataType::String())
- .AddColumn("score", fluss::DataType::BigInt())
- .AddColumn("user_id", fluss::DataType::Int())
+ .AddColumn("region", DataType::String())
+ .AddColumn("score", DataType::BigInt())
+ .AddColumn("user_id", DataType::Int())
.SetPrimaryKeys({"region", "user_id"})
.Build();
@@ -1167,10 +1173,10 @@ TEST_F(KvTableTest, PrefixLookupByBucketKey) {
// Bucket key (a, b) is a strict prefix of the PK (a, b, c), enabling
prefix lookup on (a, b).
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("a", fluss::DataType::Int())
- .AddColumn("b", fluss::DataType::String())
- .AddColumn("c", fluss::DataType::BigInt())
- .AddColumn("d", fluss::DataType::String())
+ .AddColumn("a", DataType::Int())
+ .AddColumn("b", DataType::String())
+ .AddColumn("c", DataType::BigInt())
+ .AddColumn("d", DataType::String())
.SetPrimaryKeys({"a", "b", "c"})
.Build();
@@ -1265,9 +1271,9 @@ TEST_F(KvTableTest, PrefixLookupValidationErrors) {
fluss::TablePath table_path("fluss", "test_prefix_lookup_validation_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("a", fluss::DataType::Int())
- .AddColumn("b", fluss::DataType::String())
- .AddColumn("c", fluss::DataType::BigInt())
+ .AddColumn("a", DataType::Int())
+ .AddColumn("b", DataType::String())
+ .AddColumn("c", DataType::BigInt())
.SetPrimaryKeys({"a", "b", "c"})
.Build();
@@ -1311,11 +1317,11 @@ TEST_F(KvTableTest, PrefixLookupPartitioned) {
// Partitioned by region; bucket key (a, b) is a prefix of the PK minus
the partition column.
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("region", fluss::DataType::String())
- .AddColumn("a", fluss::DataType::Int())
- .AddColumn("b", fluss::DataType::String())
- .AddColumn("c", fluss::DataType::BigInt())
- .AddColumn("d", fluss::DataType::String())
+ .AddColumn("region", DataType::String())
+ .AddColumn("a", DataType::Int())
+ .AddColumn("b", DataType::String())
+ .AddColumn("c", DataType::BigInt())
+ .AddColumn("d", DataType::String())
.SetPrimaryKeys({"region", "a", "b", "c"})
.Build();
@@ -1415,10 +1421,10 @@ TEST_F(KvTableTest, PartialUpdate) {
fluss::TablePath table_path("fluss", "test_partial_update_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
- .AddColumn("age", fluss::DataType::BigInt())
- .AddColumn("score", fluss::DataType::BigInt())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
+ .AddColumn("age", DataType::BigInt())
+ .AddColumn("score", DataType::BigInt())
.SetPrimaryKeys({"id"})
.Build();
@@ -1505,10 +1511,10 @@ TEST_F(KvTableTest, PartialUpdateByIndex) {
fluss::TablePath table_path("fluss", "test_partial_update_by_index_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
- .AddColumn("age", fluss::DataType::BigInt())
- .AddColumn("score", fluss::DataType::BigInt())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
+ .AddColumn("age", DataType::BigInt())
+ .AddColumn("score", DataType::BigInt())
.SetPrimaryKeys({"id"})
.Build();
@@ -1596,10 +1602,10 @@ TEST_F(KvTableTest, PartitionedTableUpsertAndLookup) {
// Create a partitioned KV table with region as partition key
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("region", fluss::DataType::String())
- .AddColumn("user_id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
- .AddColumn("score", fluss::DataType::BigInt())
+ .AddColumn("region", DataType::String())
+ .AddColumn("user_id", DataType::Int())
+ .AddColumn("name", DataType::String())
+ .AddColumn("score", DataType::BigInt())
.SetPrimaryKeys({"region", "user_id"})
.Build();
@@ -1738,23 +1744,23 @@ TEST_F(KvTableTest, AllSupportedDatatypes) {
// Create a table with all supported datatypes
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("pk_int", fluss::DataType::Int())
- .AddColumn("col_boolean", fluss::DataType::Boolean())
- .AddColumn("col_tinyint", fluss::DataType::TinyInt())
- .AddColumn("col_smallint", fluss::DataType::SmallInt())
- .AddColumn("col_int", fluss::DataType::Int())
- .AddColumn("col_bigint", fluss::DataType::BigInt())
- .AddColumn("col_float", fluss::DataType::Float())
- .AddColumn("col_double", fluss::DataType::Double())
- .AddColumn("col_char", fluss::DataType::Char(10))
- .AddColumn("col_string", fluss::DataType::String())
- .AddColumn("col_decimal", fluss::DataType::Decimal(10,
2))
- .AddColumn("col_date", fluss::DataType::Date())
- .AddColumn("col_time", fluss::DataType::Time())
- .AddColumn("col_timestamp", fluss::DataType::Timestamp())
- .AddColumn("col_timestamp_ltz",
fluss::DataType::TimestampLtz())
- .AddColumn("col_bytes", fluss::DataType::Bytes())
- .AddColumn("col_binary", fluss::DataType::Binary(20))
+ .AddColumn("pk_int", DataType::Int())
+ .AddColumn("col_boolean", DataType::Boolean())
+ .AddColumn("col_tinyint", DataType::TinyInt())
+ .AddColumn("col_smallint", DataType::SmallInt())
+ .AddColumn("col_int", DataType::Int())
+ .AddColumn("col_bigint", DataType::BigInt())
+ .AddColumn("col_float", DataType::Float())
+ .AddColumn("col_double", DataType::Double())
+ .AddColumn("col_char", DataType::Char(10))
+ .AddColumn("col_string", DataType::String())
+ .AddColumn("col_decimal", DataType::Decimal(10, 2))
+ .AddColumn("col_date", DataType::Date())
+ .AddColumn("col_time", DataType::Time())
+ .AddColumn("col_timestamp", DataType::Timestamp())
+ .AddColumn("col_timestamp_ltz", DataType::TimestampLtz())
+ .AddColumn("col_bytes", DataType::Bytes())
+ .AddColumn("col_binary", DataType::Binary(20))
.SetPrimaryKeys({"pk_int"})
.Build();
diff --git a/fluss-rust/bindings/cpp/test/test_log_table.cpp
b/fluss-rust/bindings/cpp/test/test_log_table.cpp
index 9078b27e8..ef29d9c93 100644
--- a/fluss-rust/bindings/cpp/test/test_log_table.cpp
+++ b/fluss-rust/bindings/cpp/test/test_log_table.cpp
@@ -29,6 +29,8 @@
#include "test_utils.h"
+using fluss::DataType;
+
class LogTableTest : public ::testing::Test {
protected:
fluss::Admin& admin() { return
fluss_test::FlussTestEnvironment::Instance()->GetAdmin(); }
@@ -45,8 +47,8 @@ TEST_F(LogTableTest, AppendRecordBatchAndScan) {
fluss::TablePath table_path("fluss",
"test_append_record_batch_and_scan_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("c1", fluss::DataType::Int())
- .AddColumn("c2", fluss::DataType::String())
+ .AddColumn("c1", DataType::Int())
+ .AddColumn("c2", DataType::String())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -178,8 +180,8 @@ TEST_F(LogTableTest, LimitScan) {
fluss::TablePath table_path("fluss", "test_limit_scan_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("c1", fluss::DataType::Int())
- .AddColumn("c2", fluss::DataType::String())
+ .AddColumn("c1", DataType::Int())
+ .AddColumn("c2", DataType::String())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -253,9 +255,9 @@ TEST_F(LogTableTest, LimitScanProjection) {
fluss::TablePath table_path("fluss", "test_limit_scan_projection_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("c1", fluss::DataType::Int())
- .AddColumn("c2", fluss::DataType::String())
- .AddColumn("c3", fluss::DataType::BigInt())
+ .AddColumn("c1", DataType::Int())
+ .AddColumn("c2", DataType::String())
+ .AddColumn("c3", DataType::BigInt())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -319,7 +321,7 @@ TEST_F(LogTableTest, LimitScanErrors) {
fluss::TablePath table_path("fluss", "test_limit_scan_errors_cpp");
- auto schema = fluss::Schema::NewBuilder().AddColumn("c1",
fluss::DataType::Int()).Build();
+ auto schema = fluss::Schema::NewBuilder().AddColumn("c1",
DataType::Int()).Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
.SetBucketCount(1)
@@ -384,8 +386,8 @@ TEST_F(LogTableTest, ListOffsets) {
fluss::TablePath table_path("fluss", "test_list_offsets_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -476,9 +478,9 @@ TEST_F(LogTableTest, TestProject) {
fluss::TablePath table_path("fluss", "test_project_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("col_a", fluss::DataType::Int())
- .AddColumn("col_b", fluss::DataType::String())
- .AddColumn("col_c", fluss::DataType::Int())
+ .AddColumn("col_a", DataType::Int())
+ .AddColumn("col_b", DataType::String())
+ .AddColumn("col_c", DataType::Int())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -591,8 +593,8 @@ TEST_F(LogTableTest, TestPollBatches) {
fluss::TablePath table_path("fluss", "test_poll_batches_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -711,22 +713,22 @@ TEST_F(LogTableTest, AllSupportedDatatypes) {
// Create a log table with all supported datatypes
auto schema =
fluss::Schema::NewBuilder()
- .AddColumn("col_tinyint", fluss::DataType::TinyInt())
- .AddColumn("col_smallint", fluss::DataType::SmallInt())
- .AddColumn("col_int", fluss::DataType::Int())
- .AddColumn("col_bigint", fluss::DataType::BigInt())
- .AddColumn("col_float", fluss::DataType::Float())
- .AddColumn("col_double", fluss::DataType::Double())
- .AddColumn("col_boolean", fluss::DataType::Boolean())
- .AddColumn("col_char", fluss::DataType::Char(10))
- .AddColumn("col_string", fluss::DataType::String())
- .AddColumn("col_decimal", fluss::DataType::Decimal(10, 2))
- .AddColumn("col_date", fluss::DataType::Date())
- .AddColumn("col_time", fluss::DataType::Time())
- .AddColumn("col_timestamp", fluss::DataType::Timestamp())
- .AddColumn("col_timestamp_ltz", fluss::DataType::TimestampLtz())
- .AddColumn("col_bytes", fluss::DataType::Bytes())
- .AddColumn("col_binary", fluss::DataType::Binary(4))
+ .AddColumn("col_tinyint", DataType::TinyInt())
+ .AddColumn("col_smallint", DataType::SmallInt())
+ .AddColumn("col_int", DataType::Int())
+ .AddColumn("col_bigint", DataType::BigInt())
+ .AddColumn("col_float", DataType::Float())
+ .AddColumn("col_double", DataType::Double())
+ .AddColumn("col_boolean", DataType::Boolean())
+ .AddColumn("col_char", DataType::Char(10))
+ .AddColumn("col_string", DataType::String())
+ .AddColumn("col_decimal", DataType::Decimal(10, 2))
+ .AddColumn("col_date", DataType::Date())
+ .AddColumn("col_time", DataType::Time())
+ .AddColumn("col_timestamp", DataType::Timestamp())
+ .AddColumn("col_timestamp_ltz", DataType::TimestampLtz())
+ .AddColumn("col_bytes", DataType::Bytes())
+ .AddColumn("col_binary", DataType::Binary(4))
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -862,9 +864,9 @@ TEST_F(LogTableTest, PartitionedTableAppendScan) {
// Create a partitioned log table
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("region", fluss::DataType::String())
- .AddColumn("value", fluss::DataType::BigInt())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("region", DataType::String())
+ .AddColumn("value", DataType::BigInt())
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -1049,9 +1051,9 @@ TEST_F(LogTableTest, AppendAndScanWithArray) {
fluss::TablePath table_path("fluss", "test_append_scan_with_array_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("tags",
fluss::DataType::Array(fluss::DataType::String()))
- .AddColumn("scores",
fluss::DataType::Array(fluss::DataType::Int()))
+ .AddColumn("id", DataType::Int())
+ .AddColumn("tags", DataType::Array(DataType::String()))
+ .AddColumn("scores", DataType::Array(DataType::Int()))
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -1084,12 +1086,12 @@ TEST_F(LogTableTest, AppendAndScanWithArray) {
auto row = table.NewRow();
row.Set("id", 1);
- fluss::ArrayWriter tags(2, fluss::DataType::String());
+ fluss::ArrayWriter tags(2, DataType::String());
tags.SetString(0, "hello");
tags.SetString(1, "world");
row.SetArray(1, std::move(tags));
- fluss::ArrayWriter scores(3, fluss::DataType::Int());
+ fluss::ArrayWriter scores(3, DataType::Int());
scores.SetInt32(0, 10);
scores.SetInt32(1, 20);
scores.SetInt32(2, 30);
@@ -1101,11 +1103,11 @@ TEST_F(LogTableTest, AppendAndScanWithArray) {
auto row = table.NewRow();
row.Set("id", 2);
- fluss::ArrayWriter tags(1, fluss::DataType::String());
+ fluss::ArrayWriter tags(1, DataType::String());
tags.SetNull(0);
row.SetArray(1, std::move(tags));
- fluss::ArrayWriter scores(0, fluss::DataType::Int());
+ fluss::ArrayWriter scores(0, DataType::Int());
row.SetArray(2, std::move(scores));
ASSERT_OK(append_writer.Append(row));
@@ -1178,14 +1180,13 @@ TEST_F(LogTableTest, AppendAndScanWithMapAndRow) {
fluss::TablePath table_path("fluss", "test_append_scan_map_row_cpp");
- // MAP / ROW columns can't be built with the flat schema builder.
- auto arrow_schema = arrow::schema({
- arrow::field("id", arrow::int32()),
- arrow::field("attrs", arrow::map(arrow::utf8(), arrow::int32())),
- arrow::field("nested", arrow::struct_({arrow::field("seq",
arrow::int32()),
- arrow::field("label",
arrow::utf8())})),
- });
- auto schema = fluss::Schema::FromArrow(arrow_schema);
+ auto schema =
+ fluss::Schema::NewBuilder()
+ .AddColumn("id", DataType::Int())
+ .AddColumn("attrs", DataType::Map(DataType::String(),
DataType::Int()))
+ .AddColumn("nested", DataType::Row({{"seq", DataType::Int()},
+ {"label",
DataType::String()}}))
+ .Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -1203,7 +1204,7 @@ TEST_F(LogTableTest, AppendAndScanWithMapAndRow) {
auto row = table.NewRow();
row.Set("id", 1);
- fluss::MapWriter attrs(2, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter attrs(2, DataType::String(), DataType::Int());
attrs.SetKeyString("a");
attrs.SetValueInt32(1);
attrs.Commit();
@@ -1267,15 +1268,15 @@ TEST_F(LogTableTest, ProjectionWithCompoundTypes) {
fluss::TablePath table_path("fluss", "test_log_projection_compound_cpp");
- auto arrow_schema = arrow::schema({
- arrow::field("id", arrow::int32()),
- arrow::field("nested", arrow::struct_({arrow::field("seq",
arrow::int32()),
- arrow::field("label",
arrow::utf8())})),
- arrow::field("attrs", arrow::map(arrow::utf8(), arrow::int32())),
- arrow::field("tags", arrow::list(arrow::utf8())),
- arrow::field("extra", arrow::utf8()),
- });
- auto schema = fluss::Schema::FromArrow(arrow_schema);
+ auto schema =
+ fluss::Schema::NewBuilder()
+ .AddColumn("id", DataType::Int())
+ .AddColumn("nested", DataType::Row({{"seq", DataType::Int()},
+ {"label",
DataType::String()}}))
+ .AddColumn("attrs", DataType::Map(DataType::String(),
DataType::Int()))
+ .AddColumn("tags", DataType::Array(DataType::String()))
+ .AddColumn("extra", DataType::String())
+ .Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -1296,7 +1297,7 @@ TEST_F(LogTableTest, ProjectionWithCompoundTypes) {
nested.SetInt32(0, 42);
nested.SetString(1, "hello");
row.SetRow(1, std::move(nested));
- fluss::MapWriter attrs(2, fluss::DataType::String(),
fluss::DataType::Int());
+ fluss::MapWriter attrs(2, DataType::String(), DataType::Int());
attrs.SetKeyString("x");
attrs.SetValueInt32(1);
attrs.Commit();
@@ -1304,7 +1305,7 @@ TEST_F(LogTableTest, ProjectionWithCompoundTypes) {
attrs.SetValueInt32(2);
attrs.Commit();
row.SetMap(2, std::move(attrs));
- fluss::ArrayWriter tags(2, fluss::DataType::String());
+ fluss::ArrayWriter tags(2, DataType::String());
tags.SetString(0, "alpha");
tags.SetString(1, "beta");
row.SetArray(3, std::move(tags));
@@ -1365,9 +1366,9 @@ TEST_F(LogTableTest, AppendAndScanWithNestedArray) {
auto schema =
fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
+ .AddColumn("id", DataType::Int())
.AddColumn("matrix",
-
fluss::DataType::Array(fluss::DataType::Array(fluss::DataType::Int())))
+ DataType::Array(DataType::Array(DataType::Int())))
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -1389,15 +1390,15 @@ TEST_F(LogTableTest, AppendAndScanWithNestedArray) {
auto row = table.NewRow();
row.Set("id", 1);
- fluss::ArrayWriter inner1(2, fluss::DataType::Int());
+ fluss::ArrayWriter inner1(2, DataType::Int());
inner1.SetInt32(0, 1);
inner1.SetInt32(1, 2);
- fluss::ArrayWriter inner2(2, fluss::DataType::Int());
+ fluss::ArrayWriter inner2(2, DataType::Int());
inner2.SetInt32(0, 3);
inner2.SetInt32(1, 4);
- fluss::ArrayWriter outer(2,
fluss::DataType::Array(fluss::DataType::Int()));
+ fluss::ArrayWriter outer(2, DataType::Array(DataType::Int()));
outer.SetArray(0, std::move(inner1));
outer.SetArray(1, std::move(inner2));
@@ -1460,12 +1461,12 @@ TEST_F(LogTableTest, AppendAndScanWithArrayRichTypes) {
auto schema =
fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("arr_bytes",
fluss::DataType::Array(fluss::DataType::Bytes()))
- .AddColumn("arr_date",
fluss::DataType::Array(fluss::DataType::Date()))
- .AddColumn("arr_time",
fluss::DataType::Array(fluss::DataType::Time()))
- .AddColumn("arr_ts",
fluss::DataType::Array(fluss::DataType::Timestamp(6)))
- .AddColumn("arr_decimal",
fluss::DataType::Array(fluss::DataType::Decimal(10, 2)))
+ .AddColumn("id", DataType::Int())
+ .AddColumn("arr_bytes", DataType::Array(DataType::Bytes()))
+ .AddColumn("arr_date", DataType::Array(DataType::Date()))
+ .AddColumn("arr_time", DataType::Array(DataType::Time()))
+ .AddColumn("arr_ts", DataType::Array(DataType::Timestamp(6)))
+ .AddColumn("arr_decimal", DataType::Array(DataType::Decimal(10,
2)))
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -1485,28 +1486,28 @@ TEST_F(LogTableTest, AppendAndScanWithArrayRichTypes) {
auto row = table.NewRow();
row.Set("id", 1);
- fluss::ArrayWriter arr_bytes(2, fluss::DataType::Bytes());
+ fluss::ArrayWriter arr_bytes(2, DataType::Bytes());
arr_bytes.SetBytes(0, std::vector<uint8_t>{0x10, 0x20, 0x30});
arr_bytes.SetNull(1);
row.SetArray(1, std::move(arr_bytes));
- fluss::ArrayWriter arr_date(2, fluss::DataType::Date());
+ fluss::ArrayWriter arr_date(2, DataType::Date());
auto d0 = fluss::Date::FromDays(20000);
arr_date.SetDate(0, d0);
arr_date.SetNull(1);
row.SetArray(2, std::move(arr_date));
- fluss::ArrayWriter arr_time(1, fluss::DataType::Time());
+ fluss::ArrayWriter arr_time(1, DataType::Time());
auto t0 = fluss::Time::FromMillis(3600000);
arr_time.SetTime(0, t0);
row.SetArray(3, std::move(arr_time));
- fluss::ArrayWriter arr_ts(1, fluss::DataType::Timestamp(6));
+ fluss::ArrayWriter arr_ts(1, DataType::Timestamp(6));
auto ts0 = fluss::Timestamp::FromMillisNanos(1769163227123, 456000);
arr_ts.SetTimestampNtz(0, ts0);
row.SetArray(4, std::move(arr_ts));
- fluss::ArrayWriter arr_decimal(2, fluss::DataType::Decimal(10, 2));
+ fluss::ArrayWriter arr_decimal(2, DataType::Decimal(10, 2));
arr_decimal.SetDecimal(0, "123.45");
arr_decimal.SetNull(1);
row.SetArray(5, std::move(arr_decimal));
@@ -1566,7 +1567,7 @@ TEST_F(LogTableTest, AppendAndScanWithArrayRichTypes) {
TEST_F(LogTableTest, ArrayApiValidationErrors) {
// Type mismatch setter should fail through FFI Result propagation.
{
- fluss::ArrayWriter bool_array(1, fluss::DataType::Boolean());
+ fluss::ArrayWriter bool_array(1, DataType::Boolean());
bool threw = false;
try {
bool_array.SetInt32(0, 42);
@@ -1581,8 +1582,8 @@ TEST_F(LogTableTest, ArrayApiValidationErrors) {
fluss::TablePath table_path("fluss",
"test_array_api_validation_errors_cpp");
auto schema = fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("vals",
fluss::DataType::Array(fluss::DataType::Int()))
+ .AddColumn("id", DataType::Int())
+ .AddColumn("vals", DataType::Array(DataType::Int()))
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
@@ -1598,7 +1599,7 @@ TEST_F(LogTableTest, ArrayApiValidationErrors) {
ASSERT_OK(table.NewAppend().CreateWriter(append_writer));
auto row = table.NewRow();
row.Set("id", 1);
- fluss::ArrayWriter vals(2, fluss::DataType::Int());
+ fluss::ArrayWriter vals(2, DataType::Int());
vals.SetInt32(0, 7);
vals.SetNull(1);
row.SetArray(1, std::move(vals));
@@ -1659,13 +1660,13 @@ TEST_F(LogTableTest,
AppendAndScanWithArrayEncodingEdgeCases) {
auto schema =
fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("arr_long_str",
fluss::DataType::Array(fluss::DataType::String()))
- .AddColumn("arr_big_decimal",
fluss::DataType::Array(fluss::DataType::Decimal(22, 5)))
- .AddColumn("arr_ts_nano",
fluss::DataType::Array(fluss::DataType::Timestamp(9)))
- .AddColumn("arr_float",
fluss::DataType::Array(fluss::DataType::Float()))
- .AddColumn("arr_double",
fluss::DataType::Array(fluss::DataType::Double()))
- .AddColumn("arr_binary",
fluss::DataType::Array(fluss::DataType::Binary(4)))
+ .AddColumn("id", DataType::Int())
+ .AddColumn("arr_long_str", DataType::Array(DataType::String()))
+ .AddColumn("arr_big_decimal",
DataType::Array(DataType::Decimal(22, 5)))
+ .AddColumn("arr_ts_nano", DataType::Array(DataType::Timestamp(9)))
+ .AddColumn("arr_float", DataType::Array(DataType::Float()))
+ .AddColumn("arr_double", DataType::Array(DataType::Double()))
+ .AddColumn("arr_binary", DataType::Array(DataType::Binary(4)))
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
@@ -1686,38 +1687,38 @@ TEST_F(LogTableTest,
AppendAndScanWithArrayEncodingEdgeCases) {
row.Set("id", 1);
// >= 8 bytes forces the heap-pointer variable-length path (threshold:
7)
- fluss::ArrayWriter arr_long_str(2, fluss::DataType::String());
+ fluss::ArrayWriter arr_long_str(2, DataType::String());
arr_long_str.SetString(0, "abcdefgh");
arr_long_str.SetString(1, "this is a much longer string that
definitely exceeds inline");
row.SetArray(1, std::move(arr_long_str));
// precision > 18 forces non-compact decimal encoding
- fluss::ArrayWriter arr_big_decimal(2, fluss::DataType::Decimal(22, 5));
+ fluss::ArrayWriter arr_big_decimal(2, DataType::Decimal(22, 5));
arr_big_decimal.SetDecimal(0, "12345678901234567.12345");
arr_big_decimal.SetDecimal(1, "-99999999999999999.99999");
row.SetArray(2, std::move(arr_big_decimal));
// precision > 3 forces non-compact timestamp (millis +
nanos-of-millis)
- fluss::ArrayWriter arr_ts_nano(1, fluss::DataType::Timestamp(9));
+ fluss::ArrayWriter arr_ts_nano(1, DataType::Timestamp(9));
auto ts_nano = fluss::Timestamp::FromMillisNanos(1769163227123,
456789);
arr_ts_nano.SetTimestampNtz(0, ts_nano);
row.SetArray(3, std::move(arr_ts_nano));
// IEEE 754 special values: NaN, +Infinity, -Infinity
- fluss::ArrayWriter arr_float(3, fluss::DataType::Float());
+ fluss::ArrayWriter arr_float(3, DataType::Float());
arr_float.SetFloat32(0, std::numeric_limits<float>::quiet_NaN());
arr_float.SetFloat32(1, std::numeric_limits<float>::infinity());
arr_float.SetFloat32(2, -std::numeric_limits<float>::infinity());
row.SetArray(4, std::move(arr_float));
- fluss::ArrayWriter arr_double(3, fluss::DataType::Double());
+ fluss::ArrayWriter arr_double(3, DataType::Double());
arr_double.SetFloat64(0, std::numeric_limits<double>::quiet_NaN());
arr_double.SetFloat64(1, std::numeric_limits<double>::infinity());
arr_double.SetFloat64(2, -std::numeric_limits<double>::infinity());
row.SetArray(5, std::move(arr_double));
// Fixed-length binary
- fluss::ArrayWriter arr_binary(2, fluss::DataType::Binary(4));
+ fluss::ArrayWriter arr_binary(2, DataType::Binary(4));
arr_binary.SetBytes(0, std::vector<uint8_t>{0xDE, 0xAD, 0xBE, 0xEF});
arr_binary.SetNull(1);
row.SetArray(6, std::move(arr_binary));
@@ -1795,7 +1796,7 @@ TEST_F(LogTableTest,
AppendAndScanWithArrayEncodingEdgeCases) {
TEST_F(LogTableTest, ArrayWriterOverflowDetection) {
// SetInt32 on TINYINT array must throw when value overflows i8 range
(-128..127)
{
- fluss::ArrayWriter tinyint_arr(1, fluss::DataType::TinyInt());
+ fluss::ArrayWriter tinyint_arr(1, DataType::TinyInt());
EXPECT_EQ(tinyint_arr.Size(), 1u);
bool threw = false;
try {
@@ -1810,7 +1811,7 @@ TEST_F(LogTableTest, ArrayWriterOverflowDetection) {
// SetInt32 on SMALLINT array must throw when value overflows i16 range
(-32768..32767)
{
- fluss::ArrayWriter smallint_arr(1, fluss::DataType::SmallInt());
+ fluss::ArrayWriter smallint_arr(1, DataType::SmallInt());
bool threw = false;
try {
smallint_arr.SetInt32(0, 40000);
@@ -1824,7 +1825,7 @@ TEST_F(LogTableTest, ArrayWriterOverflowDetection) {
// Negative overflow: -200 doesn't fit TINYINT
{
- fluss::ArrayWriter tinyint_arr(1, fluss::DataType::TinyInt());
+ fluss::ArrayWriter tinyint_arr(1, DataType::TinyInt());
bool threw = false;
try {
tinyint_arr.SetInt32(0, -200);
@@ -1836,15 +1837,15 @@ TEST_F(LogTableTest, ArrayWriterOverflowDetection) {
// Values within range must succeed
{
- fluss::ArrayWriter tinyint_arr(1, fluss::DataType::TinyInt());
+ fluss::ArrayWriter tinyint_arr(1, DataType::TinyInt());
EXPECT_NO_THROW(tinyint_arr.SetInt32(0, 127));
}
{
- fluss::ArrayWriter tinyint_arr(1, fluss::DataType::TinyInt());
+ fluss::ArrayWriter tinyint_arr(1, DataType::TinyInt());
EXPECT_NO_THROW(tinyint_arr.SetInt32(0, -128));
}
{
- fluss::ArrayWriter smallint_arr(1, fluss::DataType::SmallInt());
+ fluss::ArrayWriter smallint_arr(1, DataType::SmallInt());
EXPECT_NO_THROW(smallint_arr.SetInt32(0, 32767));
}
}
@@ -1855,7 +1856,7 @@ TEST_F(LogTableTest, MapWriterOverflowDetection) {
// TINYINT map value overflowing i8 (-128..127) must throw.
{
- fluss::MapWriter m(1, fluss::DataType::String(),
fluss::DataType::TinyInt());
+ fluss::MapWriter m(1, DataType::String(), DataType::TinyInt());
bool threw = false;
try {
m.SetValueInt32(1000);
@@ -1868,7 +1869,7 @@ TEST_F(LogTableTest, MapWriterOverflowDetection) {
// SMALLINT map value overflowing i16 must throw.
{
- fluss::MapWriter m(1, fluss::DataType::String(),
fluss::DataType::SmallInt());
+ fluss::MapWriter m(1, DataType::String(), DataType::SmallInt());
bool threw = false;
try {
m.SetValueInt32(40000);
@@ -1881,7 +1882,7 @@ TEST_F(LogTableTest, MapWriterOverflowDetection) {
// Keys are checked the same way: a TINYINT key out of range throws.
{
- fluss::MapWriter m(1, fluss::DataType::TinyInt(),
fluss::DataType::Int());
+ fluss::MapWriter m(1, DataType::TinyInt(), DataType::Int());
bool threw = false;
try {
m.SetKeyInt32(-200);
@@ -1894,7 +1895,7 @@ TEST_F(LogTableTest, MapWriterOverflowDetection) {
// In-range key and value must succeed.
{
- fluss::MapWriter m(1, fluss::DataType::TinyInt(),
fluss::DataType::SmallInt());
+ fluss::MapWriter m(1, DataType::TinyInt(), DataType::SmallInt());
EXPECT_NO_THROW(m.SetKeyInt32(127));
EXPECT_NO_THROW(m.SetValueInt32(32767));
}
@@ -1908,13 +1909,13 @@ TEST_F(LogTableTest, NullabilityPreservedInTableInfo) {
auto schema =
fluss::Schema::NewBuilder()
- .AddColumn("id", fluss::DataType::Int())
- .AddColumn("name", fluss::DataType::String())
- .AddColumn("tags",
fluss::DataType::Array(fluss::DataType::String().NotNull()))
- .AddColumn("ids",
fluss::DataType::Array(fluss::DataType::Int()).NotNull())
+ .AddColumn("id", DataType::Int())
+ .AddColumn("name", DataType::String())
+ .AddColumn("tags", DataType::Array(DataType::String().NotNull()))
+ .AddColumn("ids", DataType::Array(DataType::Int()).NotNull())
.AddColumn("nested",
- fluss::DataType::Array(
-
fluss::DataType::Array(fluss::DataType::Int()).NotNull()))
+ DataType::Array(
+ DataType::Array(DataType::Int()).NotNull()))
.SetPrimaryKeys({"id"})
.Build();
@@ -1962,3 +1963,61 @@ TEST_F(LogTableTest, NullabilityPreservedInTableInfo) {
ASSERT_OK(adm.DropTable(table_path, false));
}
+
+// Precision, scale, length, and column comments survive CreateTable ->
GetTableInfo.
+TEST_F(LogTableTest, ScalarTypeMetadataPreservedInTableInfo) {
+ auto& adm = admin();
+ auto& conn = connection();
+
+ fluss::TablePath table_path("fluss",
"test_scalar_type_metadata_table_info_cpp");
+
+ auto schema = fluss::Schema::NewBuilder()
+ .AddColumn("id", DataType::Int(), "primary id")
+ .AddColumn("code", DataType::Char(12))
+ .AddColumn("hash", DataType::Binary(32))
+ .AddColumn("amount", DataType::Decimal(18, 4), "money
amount")
+ .AddColumn("event_time", DataType::Time(3))
+ .AddColumn("event_ts", DataType::Timestamp(9))
+ .Build();
+
+ auto table_descriptor = fluss::TableDescriptor::NewBuilder()
+ .SetSchema(schema)
+ .SetProperty("table.replication.factor", "1")
+ .Build();
+
+ fluss_test::CreateTable(adm, table_path, table_descriptor);
+
+ fluss::Table table;
+ ASSERT_OK(conn.GetTable(table_path, table));
+ auto info = table.GetTableInfo();
+
+ ASSERT_EQ(info.schema.columns.size(), 6u);
+
+ // column comment
+ EXPECT_EQ(info.schema.columns[0].name, "id");
+ EXPECT_EQ(info.schema.columns[0].comment, "primary id");
+
+ // CHAR length
+ EXPECT_EQ(info.schema.columns[1].data_type.id(), fluss::TypeId::Char);
+ EXPECT_EQ(info.schema.columns[1].data_type.precision(), 12);
+
+ // BINARY length
+ EXPECT_EQ(info.schema.columns[2].data_type.id(), fluss::TypeId::Binary);
+ EXPECT_EQ(info.schema.columns[2].data_type.precision(), 32);
+
+ // DECIMAL precision + scale, and comment
+ EXPECT_EQ(info.schema.columns[3].data_type.id(), fluss::TypeId::Decimal);
+ EXPECT_EQ(info.schema.columns[3].data_type.precision(), 18);
+ EXPECT_EQ(info.schema.columns[3].data_type.scale(), 4);
+ EXPECT_EQ(info.schema.columns[3].comment, "money amount");
+
+ // TIME precision
+ EXPECT_EQ(info.schema.columns[4].data_type.id(), fluss::TypeId::Time);
+ EXPECT_EQ(info.schema.columns[4].data_type.precision(), 3);
+
+ // TIMESTAMP precision
+ EXPECT_EQ(info.schema.columns[5].data_type.id(), fluss::TypeId::Timestamp);
+ EXPECT_EQ(info.schema.columns[5].data_type.precision(), 9);
+
+ ASSERT_OK(adm.DropTable(table_path, false));
+}
diff --git a/fluss-rust/website/docs/user-guide/cpp/api-reference.md
b/fluss-rust/website/docs/user-guide/cpp/api-reference.md
index c5640a898..621eb7ace 100644
--- a/fluss-rust/website/docs/user-guide/cpp/api-reference.md
+++ b/fluss-rust/website/docs/user-guide/cpp/api-reference.md
@@ -461,7 +461,6 @@ Read-only result of a prefix lookup — zero or more matched
rows. Each row is a
| Method | Description |
|-----------------------------------|-----------------------------|
| `NewBuilder() -> Schema::Builder` | Create a new schema builder |
-| `FromArrow(std::shared_ptr<arrow::Schema> schema, std::vector<std::string>
primary_keys = {}) -> Schema` | Build a schema from an existing Arrow schema
(escape hatch; prefer `DataType::Map` / `DataType::Row`) |
## `Schema::Builder`
diff --git a/fluss-rust/website/docs/user-guide/cpp/data-types.md
b/fluss-rust/website/docs/user-guide/cpp/data-types.md
index 9b8ab0c6c..106db41aa 100644
--- a/fluss-rust/website/docs/user-guide/cpp/data-types.md
+++ b/fluss-rust/website/docs/user-guide/cpp/data-types.md
@@ -85,12 +85,9 @@ auto schema = fluss::Schema::NewBuilder()
.Build();
```
-:::note Arrow escape hatch
-If you already have an `arrow::Schema`, pass it directly with
-`fluss::Schema::FromArrow(arrow_schema, /*primary_keys=*/{"id"})`. It's
-equivalent — the native factories above lower to the same Arrow types
-internally, without pulling Arrow into your code.
-:::
+Column types are sent to the server exactly as declared: precision, scale,
+length, per-level nullability, and `ROW` field names all round-trip losslessly
+through `CreateTable` and `GetTableInfo`.
## GenericRow Setters