feat(vector): support VECTOR as a non-key value column in Parquet-backed primary-key tables. (#275)
diff --git a/docs/source/user_guide/data_types.rst b/docs/source/user_guide/data_types.rst
index add537a..57f0eaf 100644
--- a/docs/source/user_guide/data_types.rst
+++ b/docs/source/user_guide/data_types.rst
@@ -194,12 +194,13 @@
``SMALLINT``, ``INT``, ``BIGINT``, ``FLOAT``, or ``DOUBLE``. A VECTOR
value may be NULL, but its elements cannot be NULL.
- Paimon C++ currently supports VECTOR columns only in append-only tables
- backed by Parquet data files. They use the standard Parquet LIST
+ Paimon C++ supports VECTOR value columns in append-only and primary-key
+ tables backed by Parquet data files. They use the standard Parquet LIST
representation on disk and are restored as Arrow ``FixedSizeList``
- values on read. Primary-key tables and data-evolution tables containing
- VECTOR fields are rejected. VECTOR columns also cannot be partition or
- bucket keys. Dedicated vector storage is not included yet.
+ values on read. VECTOR is not supported in data-evolution tables. VECTOR
+ columns cannot be primary, partition, or bucket keys, nor comparator-based
+ ordering fields such as sequence and sequence-group fields. Dedicated
+ vector storage is not included yet.
Paimon C++ also reads Parquet files written by Paimon Rust or Python whose
embedded Arrow schema restores VECTOR columns as ``FixedSizeList``,
diff --git a/src/paimon/common/data/columnar/columnar_array.cpp b/src/paimon/common/data/columnar/columnar_array.cpp
index 6566aea..11a80df 100644
--- a/src/paimon/common/data/columnar/columnar_array.cpp
+++ b/src/paimon/common/data/columnar/columnar_array.cpp
@@ -67,6 +67,14 @@
}
std::shared_ptr<InternalArray> ColumnarArray::GetArray(int32_t pos) const {
+ if (array_->type_id() == arrow::Type::FIXED_SIZE_LIST) {
+ auto fixed_size_list_array = checked_cast<const arrow::FixedSizeListArray*>(array_);
+ auto fixed_size_list_type =
+ checked_pointer_cast<arrow::FixedSizeListType>(fixed_size_list_array->type());
+ auto offset = static_cast<int32_t>(fixed_size_list_array->value_offset(offset_ + pos));
+ return std::make_shared<ColumnarArray>(fixed_size_list_array->values().get(), pool_, offset,
+ fixed_size_list_type->list_size());
+ }
auto list_array = checked_cast<const arrow::ListArray*>(array_);
int32_t offset = list_array->value_offset(offset_ + pos);
int32_t length = list_array->value_length(offset_ + pos);
diff --git a/src/paimon/common/data/columnar/columnar_row.cpp b/src/paimon/common/data/columnar/columnar_row.cpp
index 5099b42..db732d9 100644
--- a/src/paimon/common/data/columnar/columnar_row.cpp
+++ b/src/paimon/common/data/columnar/columnar_row.cpp
@@ -66,6 +66,15 @@
}
std::shared_ptr<InternalArray> ColumnarRow::GetArray(int32_t pos) const {
+ if (array_vec_[pos]->type_id() == arrow::Type::FIXED_SIZE_LIST) {
+ auto fixed_size_list_array =
+ checked_cast<const arrow::FixedSizeListArray*>(array_vec_[pos]);
+ auto fixed_size_list_type =
+ checked_pointer_cast<arrow::FixedSizeListType>(fixed_size_list_array->type());
+ auto offset = static_cast<int32_t>(fixed_size_list_array->value_offset(row_id_));
+ return std::make_shared<ColumnarArray>(fixed_size_list_array->values().get(), pool_, offset,
+ fixed_size_list_type->list_size());
+ }
auto list_array = checked_cast<const arrow::ListArray*>(array_vec_[pos]);
int32_t offset = list_array->value_offset(row_id_);
int32_t length = list_array->value_length(row_id_);
diff --git a/src/paimon/common/data/columnar/columnar_row_ref.cpp b/src/paimon/common/data/columnar/columnar_row_ref.cpp
index d73e6b7..8ee635d 100644
--- a/src/paimon/common/data/columnar/columnar_row_ref.cpp
+++ b/src/paimon/common/data/columnar/columnar_row_ref.cpp
@@ -61,6 +61,15 @@
}
std::shared_ptr<InternalArray> ColumnarRowRef::GetArray(int32_t pos) const {
+ if (ctx_->array_vec[pos]->type_id() == arrow::Type::FIXED_SIZE_LIST) {
+ auto fixed_size_list_array =
+ checked_cast<const arrow::FixedSizeListArray*>(ctx_->array_vec[pos].get());
+ auto fixed_size_list_type =
+ checked_pointer_cast<arrow::FixedSizeListType>(fixed_size_list_array->type());
+ auto offset = static_cast<int32_t>(fixed_size_list_array->value_offset(row_id_));
+ return std::make_shared<ColumnarArray>(fixed_size_list_array->values().get(), ctx_->pool,
+ offset, fixed_size_list_type->list_size());
+ }
auto list_array = checked_cast<const arrow::ListArray*>(ctx_->array_vec[pos].get());
int32_t offset = list_array->value_offset(row_id_);
int32_t length = list_array->value_length(row_id_);
diff --git a/src/paimon/common/data/internal_row.cpp b/src/paimon/common/data/internal_row.cpp
index 24b6b5b..b7c011b 100644
--- a/src/paimon/common/data/internal_row.cpp
+++ b/src/paimon/common/data/internal_row.cpp
@@ -128,7 +128,8 @@
};
break;
}
- case arrow::Type::type::LIST: {
+ case arrow::Type::type::LIST:
+ case arrow::Type::type::FIXED_SIZE_LIST: {
field_getter = [field_idx](const InternalRow& row) -> VariantType {
return row.GetArray(field_idx);
};
diff --git a/src/paimon/common/data/serializer/binary_serializer_utils.cpp b/src/paimon/common/data/serializer/binary_serializer_utils.cpp
index c0248cd..d72a2b0 100644
--- a/src/paimon/common/data/serializer/binary_serializer_utils.cpp
+++ b/src/paimon/common/data/serializer/binary_serializer_utils.cpp
@@ -31,8 +31,7 @@
return binary_array;
}
auto binary_array = std::make_shared<BinaryArray>();
- auto list_type = checked_pointer_cast<arrow::ListType>(type);
- auto value_type = list_type->value_type();
+ auto value_type = type->field(0)->type();
// TODO(xinyu.lxy): reuse BinaryWriter
BinaryArrayWriter binary_writer(binary_array.get(), value->Size(),
BinaryArrayWriter::GetElementSize(value_type->id()), pool);
@@ -183,7 +182,8 @@
}
break;
}
- case arrow::Type::type::LIST: {
+ case arrow::Type::type::LIST:
+ case arrow::Type::type::FIXED_SIZE_LIST: {
auto internal_array = getter->GetArray(pos);
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<BinaryArray> binary_array,
WriteBinaryArray(internal_array, type, pool));
diff --git a/src/paimon/common/data/serializer/row_compacted_serializer.cpp b/src/paimon/common/data/serializer/row_compacted_serializer.cpp
index 411dae6..1fd618a 100644
--- a/src/paimon/common/data/serializer/row_compacted_serializer.cpp
+++ b/src/paimon/common/data/serializer/row_compacted_serializer.cpp
@@ -285,7 +285,8 @@
};
break;
}
- case arrow::Type::type::LIST: {
+ case arrow::Type::type::LIST:
+ case arrow::Type::type::FIXED_SIZE_LIST: {
field_reader = [](int32_t pos, RowReader* reader) -> Result<VariantType> {
PAIMON_ASSIGN_OR_RAISE(VariantType value, reader->ReadArray());
return value;
@@ -414,7 +415,8 @@
};
break;
}
- case arrow::Type::type::LIST: {
+ case arrow::Type::type::LIST:
+ case arrow::Type::type::FIXED_SIZE_LIST: {
field_writer = [field_type](int32_t pos, const VariantType& field,
RowWriter* writer) -> Status {
return writer->WriteArray(
diff --git a/src/paimon/core/io/key_value_in_memory_record_reader_test.cpp b/src/paimon/core/io/key_value_in_memory_record_reader_test.cpp
index 7604064..ca6f331 100644
--- a/src/paimon/core/io/key_value_in_memory_record_reader_test.cpp
+++ b/src/paimon/core/io/key_value_in_memory_record_reader_test.cpp
@@ -26,6 +26,7 @@
#include "arrow/array/array_nested.h"
#include "arrow/ipc/json_simple.h"
#include "gtest/gtest.h"
+#include "paimon/common/data/internal_array.h"
#include "paimon/common/types/data_field.h"
#include "paimon/common/types/row_kind.h"
#include "paimon/common/utils/fields_comparator.h"
@@ -349,6 +350,68 @@
ASSERT_FALSE(eof_iter);
}
+TEST_F(KeyValueInMemoryRecordReaderTest, TestSortWithVectorValues) {
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
+ auto src_type = arrow::struct_(
+ {arrow::field("pk", arrow::int32()), arrow::field("seq", arrow::int32()),
+ arrow::field("embedding", vector_type),
+ arrow::field("payload", arrow::struct_({arrow::field("embedding", vector_type)}))});
+ auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [2, 20, [0, 1, 2], [[0, 1, 2]]],
+ [1, 20, null, null],
+ [2, 10, [2, 3, 4], [null]],
+ [1, null, [3, 4, 5], [[3, 4, 5]]],
+ [2, 20, [4, 5, 6], [[4, 5, 6]]]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<FieldsComparator> key_comparator,
+ FieldsComparator::Create({DataField(0, arrow::field("pk", arrow::int32()))},
+ /*is_ascending_order=*/true));
+
+ auto check_sort = [&](const std::vector<std::string>& sequence_fields, bool ascending,
+ const std::vector<int64_t>& expected_indices) {
+ KeyValueInMemoryRecordReader reader(
+ /*last_sequence_num=*/0, src_array, std::vector<RecordBatch::RowKind>{},
+ std::vector<std::string>{"pk"}, sequence_fields, ascending, key_comparator, pool_);
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<KeyValue> results,
+ (ReadResultCollector::CollectKeyValueResult<KeyValueInMemoryRecordReader,
+ KeyValueRecordReader::Iterator>(&reader)));
+ ASSERT_EQ(expected_indices.size(), results.size());
+ for (size_t i = 0; i < results.size(); ++i) {
+ const int64_t source_index = expected_indices[i];
+ SCOPED_TRACE(source_index);
+ ASSERT_EQ(source_index, results[i].sequence_number);
+ const auto& value = results[i].value;
+ ASSERT_EQ(source_index == 1 || source_index == 3 ? 1 : 2, results[i].key->GetInt(0));
+ ASSERT_EQ(source_index == 1, value->IsNullAt(2));
+ ASSERT_EQ(source_index == 1, value->IsNullAt(3));
+ if (source_index == 1) {
+ continue;
+ }
+ const std::vector<float> expected_vector = {static_cast<float>(source_index),
+ static_cast<float>(source_index + 1),
+ static_cast<float>(source_index + 2)};
+ ASSERT_OK_AND_ASSIGN(std::vector<float> vector, value->GetArray(2)->ToFloatArray());
+ ASSERT_EQ(expected_vector, vector);
+ auto payload = value->GetRow(3, 1);
+ ASSERT_EQ(source_index == 2, payload->IsNullAt(0));
+ if (source_index != 2) {
+ ASSERT_OK_AND_ASSIGN(std::vector<float> nested_vector,
+ payload->GetArray(0)->ToFloatArray());
+ ASSERT_EQ(expected_vector, nested_vector);
+ }
+ }
+ };
+ // Equal sort keys retain input order regardless of the non-sortable value columns.
+ check_sort({}, /*ascending=*/true, {1, 3, 0, 2, 4});
+ check_sort({"seq"}, /*ascending=*/true, {3, 1, 2, 0, 4});
+ check_sort({"seq"}, /*ascending=*/false, {3, 1, 0, 4, 2});
+}
+
TEST_F(KeyValueInMemoryRecordReaderTest, TestVariantType) {
// test null, repeated key, sequence fields, out of order in variant data type
// precondition: fields[0] is key fields, fields[1] is sequence field, src_array are like:
diff --git a/src/paimon/core/io/row_to_arrow_array_converter.h b/src/paimon/core/io/row_to_arrow_array_converter.h
index 6caa9f6..336d985 100644
--- a/src/paimon/core/io/row_to_arrow_array_converter.h
+++ b/src/paimon/core/io/row_to_arrow_array_converter.h
@@ -158,6 +158,12 @@
PAIMON_RETURN_NOT_OK(Reserve(list_builder->value_builder(), idx));
break;
}
+ case arrow::Type::type::FIXED_SIZE_LIST: {
+ PAIMON_ASSIGN_OR_RAISE(auto* list_builder,
+ CastToTypedBuilder<arrow::FixedSizeListBuilder>(array_builder));
+ PAIMON_RETURN_NOT_OK(Reserve(list_builder->value_builder(), idx));
+ break;
+ }
case arrow::Type::type::MAP: {
PAIMON_ASSIGN_OR_RAISE(auto* map_builder,
CastToTypedBuilder<arrow::MapBuilder>(array_builder));
@@ -224,6 +230,11 @@
PAIMON_RETURN_NOT_OK(Accumulate(list_array->values().get(), idx));
break;
}
+ case arrow::Type::type::FIXED_SIZE_LIST: {
+ auto list_array = checked_cast<const arrow::FixedSizeListArray*>(array);
+ PAIMON_RETURN_NOT_OK(Accumulate(list_array->values().get(), idx));
+ break;
+ }
case arrow::Type::type::MAP: {
auto map_array = checked_cast<const arrow::MapArray*>(array);
PAIMON_RETURN_NOT_OK(Accumulate(map_array->keys().get(), idx));
@@ -432,6 +443,31 @@
return arrow::Status::OK();
});
}
+ case arrow::Type::type::FIXED_SIZE_LIST: {
+ PAIMON_ASSIGN_OR_RAISE(auto* list_builder,
+ CastToTypedBuilder<arrow::FixedSizeListBuilder>(array_builder));
+ std::shared_ptr<arrow::FixedSizeListType> list_type =
+ checked_pointer_cast<arrow::FixedSizeListType>(list_builder->type());
+ int32_t list_size = list_type->list_size();
+ PAIMON_ASSIGN_OR_RAISE(AppendValueFunc value_func,
+ (RowToArrowArrayConverter<T, R>::AppendField(
+ use_view, list_builder->value_builder(), reserve_count)));
+ return RowToArrowArrayConverter<T, R>::AppendValueFunc(
+ [list_builder, list_size, value_func](const DataGetters& data_getter,
+ int32_t pos) -> arrow::Status {
+ CHECK_AND_APPEND_NULL(data_getter, list_builder, pos);
+ std::shared_ptr<InternalArray> sub_array = data_getter.GetArray(pos);
+ if (!sub_array || sub_array->Size() != list_size) {
+ return arrow::Status::Invalid(
+ "VECTOR length does not match its declared dimension");
+ }
+ ARROW_RETURN_NOT_OK(list_builder->Append());
+ for (int32_t i = 0; i < list_size; ++i) {
+ ARROW_RETURN_NOT_OK(value_func(*sub_array, i));
+ }
+ return arrow::Status::OK();
+ });
+ }
case arrow::Type::type::MAP: {
PAIMON_ASSIGN_OR_RAISE(auto* map_builder,
CastToTypedBuilder<arrow::MapBuilder>(array_builder));
diff --git a/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp b/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp
index 9397ca4..22e02f1 100644
--- a/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp
+++ b/src/paimon/core/mergetree/compact/aggregate/field_aggregate_utils.cpp
@@ -73,12 +73,12 @@
Result<bool> EqualArrays(const std::shared_ptr<InternalArray>& lhs,
const std::shared_ptr<InternalArray>& rhs,
- const std::shared_ptr<arrow::ListType>& type) {
+ const std::shared_ptr<arrow::DataType>& type) {
if (!lhs || !rhs || lhs->Size() != rhs->Size()) {
return lhs == rhs;
}
for (int32_t i = 0; i < lhs->Size(); ++i) {
- PAIMON_ASSIGN_OR_RAISE(bool equal, EqualGetters(*lhs, i, *rhs, i, type->value_type()));
+ PAIMON_ASSIGN_OR_RAISE(bool equal, EqualGetters(*lhs, i, *rhs, i, type->field(0)->type()));
if (!equal) {
return false;
}
@@ -174,6 +174,7 @@
getters.GetDecimal(pos, decimal_type->precision(), decimal_type->scale()));
}
case arrow::Type::LIST:
+ case arrow::Type::FIXED_SIZE_LIST:
return VariantType(getters.GetArray(pos));
case arrow::Type::MAP:
return VariantType(getters.GetMap(pos));
@@ -229,9 +230,10 @@
DataDefine::GetVariantValue<std::shared_ptr<InternalRow>>(rhs),
checked_pointer_cast<arrow::StructType>(type));
case arrow::Type::LIST:
+ case arrow::Type::FIXED_SIZE_LIST:
return EqualArrays(DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(lhs),
DataDefine::GetVariantValue<std::shared_ptr<InternalArray>>(rhs),
- checked_pointer_cast<arrow::ListType>(type));
+ type);
case arrow::Type::MAP:
return EqualMaps(DataDefine::GetVariantValue<std::shared_ptr<InternalMap>>(lhs),
DataDefine::GetVariantValue<std::shared_ptr<InternalMap>>(rhs),
diff --git a/src/paimon/core/mergetree/compact/internal_row_equalizer.h b/src/paimon/core/mergetree/compact/internal_row_equalizer.h
index 22f15d8..a0a0b4c 100644
--- a/src/paimon/core/mergetree/compact/internal_row_equalizer.h
+++ b/src/paimon/core/mergetree/compact/internal_row_equalizer.h
@@ -144,11 +144,10 @@
.CompareTo(rhs.GetDecimal(rhs_pos, precision, scale)) == 0;
});
}
- case arrow::Type::LIST: {
- std::shared_ptr<arrow::ListType> list_type =
- checked_pointer_cast<arrow::ListType>(type);
+ case arrow::Type::LIST:
+ case arrow::Type::FIXED_SIZE_LIST: {
PAIMON_ASSIGN_OR_RAISE(ValueEqualizer element_equalizer,
- CreateValueEqualizer(list_type->value_type()));
+ CreateValueEqualizer(type->field(0)->type()));
return ValueEqualizer([element_equalizer = std::move(element_equalizer)](
const DataGetters& lhs, int32_t lhs_pos,
const DataGetters& rhs, int32_t rhs_pos) {
diff --git a/src/paimon/core/mergetree/in_memory_sort_buffer.cpp b/src/paimon/core/mergetree/in_memory_sort_buffer.cpp
index 5f53d59..272d34b 100644
--- a/src/paimon/core/mergetree/in_memory_sort_buffer.cpp
+++ b/src/paimon/core/mergetree/in_memory_sort_buffer.cpp
@@ -156,6 +156,11 @@
PAIMON_ASSIGN_OR_RAISE(int64_t value_mem, EstimateMemoryUse(list_array->values()));
return null_bits_size_in_bytes + value_mem;
}
+ case arrow::Type::type::FIXED_SIZE_LIST: {
+ auto list_array = checked_cast<const arrow::FixedSizeListArray*>(array.get());
+ PAIMON_ASSIGN_OR_RAISE(int64_t value_mem, EstimateMemoryUse(list_array->values()));
+ return null_bits_size_in_bytes + value_mem;
+ }
case arrow::Type::type::MAP: {
auto map_array = checked_cast<const arrow::MapArray*>(array.get());
PAIMON_ASSIGN_OR_RAISE(int64_t key_mem, EstimateMemoryUse(map_array->keys()));
diff --git a/src/paimon/core/schema/schema_validation.cpp b/src/paimon/core/schema/schema_validation.cpp
index 5e524a3..26d4100 100644
--- a/src/paimon/core/schema/schema_validation.cpp
+++ b/src/paimon/core/schema/schema_validation.cpp
@@ -127,6 +127,16 @@
return Status::OK();
}
+Status ValidateVectorComparatorField(const TableSchema& schema, const std::string& field_name,
+ const std::string& role) {
+ PAIMON_ASSIGN_OR_RAISE(DataField field, schema.GetField(field_name));
+ if (VectorUtils::ContainsVectorField(field.ArrowField())) {
+ return Status::Invalid(
+ fmt::format("VECTOR field '{}' cannot be used as {}.", field_name, role));
+ }
+ return Status::OK();
+}
+
} // namespace
bool SchemaValidation::IsComplexType(const std::shared_ptr<arrow::Field>& field) {
@@ -378,6 +388,8 @@
fmt::format("The sequence field group: {} can not be found in table schema.",
sequence_field_name));
}
+ PAIMON_RETURN_NOT_OK(ValidateVectorComparatorField(schema, sequence_field_name,
+ "a sequence-group ordering field"));
}
for (const auto& field : StringUtils::Split(v, Options::FIELDS_SEPARATOR)) {
@@ -445,6 +457,7 @@
PAIMON_RETURN_NOT_OK(Preconditions::CheckState(
std::find(field_names.begin(), field_names.end(), field) != field_names.end(),
fmt::format("Sequence field: '{}' cannot be found in table schema.", field)));
+ PAIMON_RETURN_NOT_OK(ValidateVectorComparatorField(schema, field, "a sequence field"));
PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> agg_func,
options.GetFieldAggFunc(field));
@@ -776,10 +789,6 @@
if (!has_vector) {
return Status::OK();
}
- if (!schema.PrimaryKeys().empty()) {
- return Status::NotImplemented(
- "VECTOR fields in primary-key tables are not implemented yet.");
- }
if (options.DataEvolutionEnabled()) {
return Status::NotImplemented(
"VECTOR fields in data-evolution tables are not implemented yet.");
diff --git a/src/paimon/core/schema/schema_validation_test.cpp b/src/paimon/core/schema/schema_validation_test.cpp
index 3e708f8..f34b25f 100644
--- a/src/paimon/core/schema/schema_validation_test.cpp
+++ b/src/paimon/core/schema/schema_validation_test.cpp
@@ -87,12 +87,54 @@
ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
"in primary key field embedding is unsupported");
+ ASSERT_OK_AND_ASSIGN(
+ table_schema, TableSchema::Create(/*schema_id=*/0, schema, /*partition_keys=*/{"embedding"},
+ /*primary_keys=*/{}, parquet_options));
+ ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
+ "in partition field embedding is unsupported");
+
+ std::map<std::string, std::string> bucket_key_options = {
+ {Options::BUCKET, "1"},
+ {Options::BUCKET_KEY, "embedding"},
+ {Options::FILE_FORMAT, "parquet"},
+ };
+ ASSERT_OK_AND_ASSIGN(table_schema,
+ TableSchema::Create(/*schema_id=*/0, schema, /*partition_keys=*/{},
+ /*primary_keys=*/{}, bucket_key_options));
+ ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
+ "Nested type cannot be in bucket-key");
+
+ std::map<std::string, std::string> sequence_field_options = {
+ {Options::BUCKET, "1"},
+ {Options::BUCKET_KEY, "id"},
+ {Options::FILE_FORMAT, "parquet"},
+ {Options::SEQUENCE_FIELD, "embedding"},
+ };
+ ASSERT_OK_AND_ASSIGN(table_schema,
+ TableSchema::Create(/*schema_id=*/0, schema, /*partition_keys=*/{},
+ /*primary_keys=*/{"id"}, sequence_field_options));
+ ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
+ "VECTOR field 'embedding' cannot be used as a sequence field.");
+
+ std::map<std::string, std::string> sequence_group_options = {
+ {Options::BUCKET, "1"},
+ {Options::BUCKET_KEY, "id"},
+ {Options::FILE_FORMAT, "parquet"},
+ {Options::MERGE_ENGINE, "partial-update"},
+ {"fields.embedding.sequence-group", "id"},
+ };
+ ASSERT_OK_AND_ASSIGN(table_schema,
+ TableSchema::Create(/*schema_id=*/0, schema, /*partition_keys=*/{},
+ /*primary_keys=*/{"id"}, sequence_group_options));
+ ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
+ "VECTOR field 'embedding' cannot be used as a sequence-group ordering "
+ "field.");
+
primary_key_options[Options::FILE_FORMAT] = "parquet";
ASSERT_OK_AND_ASSIGN(table_schema,
TableSchema::Create(/*schema_id=*/0, schema, /*partition_keys=*/{},
/*primary_keys=*/{"id"}, primary_key_options));
- ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
- "VECTOR fields in primary-key tables are not implemented yet.");
+ ASSERT_OK(SchemaValidation::ValidateTableSchema(*table_schema));
auto nested_schema = arrow::schema({
arrow::field("id", arrow::int64()),
@@ -102,8 +144,7 @@
table_schema,
TableSchema::Create(/*schema_id=*/0, nested_schema,
/*partition_keys=*/{}, /*primary_keys=*/{"id"}, primary_key_options));
- ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
- "VECTOR fields in primary-key tables are not implemented yet.");
+ ASSERT_OK(SchemaValidation::ValidateTableSchema(*table_schema));
std::map<std::string, std::string> data_evolution_options = {
{Options::BUCKET, "-1"},
@@ -117,10 +158,10 @@
/*primary_keys=*/{}, data_evolution_options));
ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
"VECTOR fields in data-evolution tables are not implemented yet.");
- ASSERT_OK_AND_ASSIGN(table_schema,
- TableSchema::Create(/*schema_id=*/0, nested_schema,
- /*partition_keys=*/{},
- /*primary_keys=*/{}, data_evolution_options));
+ ASSERT_OK_AND_ASSIGN(
+ table_schema,
+ TableSchema::Create(/*schema_id=*/0, nested_schema,
+ /*partition_keys=*/{}, /*primary_keys=*/{}, data_evolution_options));
ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
"VECTOR fields in data-evolution tables are not implemented yet.");
}
diff --git a/test/inte/realtime_write_inte_test.cpp b/test/inte/realtime_write_inte_test.cpp
index 95adf74..03e55c2 100644
--- a/test/inte/realtime_write_inte_test.cpp
+++ b/test/inte/realtime_write_inte_test.cpp
@@ -1164,6 +1164,35 @@
read_array.reset();
}
+TEST_F(RealtimeWriteInteTest, TestPkVector) {
+ options_[Options::FILE_FORMAT] = "parquet";
+ std::shared_ptr<arrow::DataType> vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
+ fields_ = {arrow::field("id", arrow::int64()), arrow::field("embedding", vector_type)};
+ schema_ = arrow::schema(fields_);
+ CreatePkTable();
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
+ CreateRealtimeWriter(realtime_context));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeUnpartitionedBatchFromJson(R"([
+ [0, 2, [2.0, 2.0, 2.0]],
+ [1, 3, null],
+ [2, 1, [1.0, 1.0, 1.0]],
+ [3, 2, [3.0, 3.0, 3.0]]
+ ])"));
+ ASSERT_OK(writer->Write(std::move(batch)));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
+ CreatePlan(realtime_context, /*predicate=*/nullptr));
+ ReadPlanWithSchemaAndCheck(plan, realtime_context, schema_,
+ R"([[0, 1, [1.0, 1.0, 1.0]],
+ [0, 2, [3.0, 3.0, 3.0]],
+ [0, 3, null]])");
+ ASSERT_OK(writer->Close());
+}
+
TEST_F(RealtimeWriteInteTest, TestPkRealtimeReadOptimizedScanUnsupported) {
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
diff --git a/test/inte/write_and_read_inte_test.cpp b/test/inte/write_and_read_inte_test.cpp
index c5d9b9d..7ce6f2e 100644
--- a/test/inte/write_and_read_inte_test.cpp
+++ b/test/inte/write_and_read_inte_test.cpp
@@ -69,6 +69,7 @@
#include "rapidjson/writer.h"
namespace paimon::test {
+
// This is a sdk end-to-end test demo that supports write, commit, scan, and read operations.
class WriteAndReadInteTest
: public ::testing::Test,
@@ -723,6 +724,189 @@
ASSERT_TRUE(success);
}
+TEST_P(WriteAndReadInteTest, TestPKVector) {
+ auto [file_format, file_system] = GetParam();
+ if (file_format != "parquet") {
+ return;
+ }
+
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
+ arrow::FieldVector fields = {
+ arrow::field("pk", arrow::int64()),
+ arrow::field("embedding", vector_type),
+ };
+ std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, file_format},
+ {Options::TARGET_FILE_SIZE, "1024"},
+ {Options::BUCKET, "1"},
+ {Options::FILE_SYSTEM, file_system},
+ };
+ if (file_system == "jindo") {
+ options = AddOptionsForJindo(options);
+ }
+ ASSERT_OK_AND_ASSIGN(
+ auto helper,
+ TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{},
+ /*primary_keys=*/{"pk"}, options, /*is_streaming_mode=*/true));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> initial_batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields), R"([[1, [1.0, 2.0, 3.0]], [2, null]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(initial_batch), /*commit_identifier=*/0,
+ /*expected_commit_messages=*/std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> update_batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields),
+ R"([[1, [4.0, 5.0, 6.0]], [3, [7.0, 8.0, 9.0]]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(update_batch), /*commit_identifier=*/1,
+ /*expected_commit_messages=*/std::nullopt));
+
+ std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar");
+ ASSERT_OK(CompactAndCommit(table_path, options, /*commit_identifier=*/2));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits,
+ helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt));
+ arrow::FieldVector result_fields = fields;
+ result_fields.insert(result_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8()));
+ ASSERT_OK_AND_ASSIGN(
+ bool success, helper->ReadAndCheckResult(
+ arrow::struct_(result_fields), data_splits,
+ R"([[0, 1, [4.0, 5.0, 6.0]], [0, 2, null], [0, 3, [7.0, 8.0, 9.0]]])"));
+ ASSERT_TRUE(success);
+}
+
+TEST_P(WriteAndReadInteTest, TestPKNestedVector) {
+ auto [file_format, file_system] = GetParam();
+ if (file_format != "parquet") {
+ return;
+ }
+
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
+ arrow::FieldVector fields = {
+ arrow::field("pk", arrow::int64()),
+ arrow::field("payload", arrow::struct_({arrow::field("embedding", vector_type),
+ arrow::field("tag", arrow::utf8())})),
+ };
+ std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, file_format},
+ {Options::TARGET_FILE_SIZE, "1024"},
+ {Options::BUCKET, "1"},
+ {Options::FILE_SYSTEM, file_system},
+ };
+ if (file_system == "jindo") {
+ options = AddOptionsForJindo(options);
+ }
+ ASSERT_OK_AND_ASSIGN(
+ auto helper,
+ TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{},
+ /*primary_keys=*/{"pk"}, options, /*is_streaming_mode=*/true));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> initial_batch,
+ TestHelper::MakeRecordBatch(
+ arrow::struct_(fields),
+ R"([[1, [[1.0, 2.0, 3.0], "initial"]], [2, [null, "null-vector"]], [3, null]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(initial_batch), /*commit_identifier=*/0,
+ /*expected_commit_messages=*/std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> update_batch,
+ TestHelper::MakeRecordBatch(
+ arrow::struct_(fields),
+ R"([[1, [[4.0, 5.0, 6.0], "updated"]], [2, [[7.0, 8.0, 9.0], null]]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(update_batch), /*commit_identifier=*/1,
+ /*expected_commit_messages=*/std::nullopt));
+
+ std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar");
+ ASSERT_OK(CompactAndCommit(table_path, options, /*commit_identifier=*/2));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits,
+ helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt));
+ arrow::FieldVector result_fields = fields;
+ result_fields.insert(result_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8()));
+ ASSERT_OK_AND_ASSIGN(bool success,
+ helper->ReadAndCheckResult(arrow::struct_(result_fields), data_splits, R"([
+ [0, 1, [[4.0, 5.0, 6.0], "updated"]],
+ [0, 2, [[7.0, 8.0, 9.0], null]],
+ [0, 3, null]
+ ])"));
+ ASSERT_TRUE(success);
+}
+
+TEST_P(WriteAndReadInteTest, TestPKVectorWithListagg) {
+ auto [file_format, file_system] = GetParam();
+ if (file_format != "parquet") {
+ return;
+ }
+
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
+ arrow::FieldVector fields = {
+ arrow::field("pk", arrow::int64()),
+ arrow::field("embedding", vector_type),
+ arrow::field("tags", arrow::utf8()),
+ };
+ std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, file_format},
+ {Options::TARGET_FILE_SIZE, "1024"},
+ {Options::BUCKET, "1"},
+ {Options::FILE_SYSTEM, file_system},
+ {Options::MERGE_ENGINE, "aggregation"},
+ {"fields.embedding.aggregate-function", "last_non_null_value"},
+ {"fields.tags.aggregate-function", "listagg"},
+ };
+ if (file_system == "jindo") {
+ options = AddOptionsForJindo(options);
+ }
+ ASSERT_OK_AND_ASSIGN(
+ auto helper,
+ TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{},
+ /*primary_keys=*/{"pk"}, options, /*is_streaming_mode=*/true));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> initial_batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields),
+ R"([[1, [1.0, 2.0, 3.0], "alpha"], [2, null, "one"]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(initial_batch), /*commit_identifier=*/0,
+ /*expected_commit_messages=*/std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> update_batch,
+ TestHelper::MakeRecordBatch(
+ arrow::struct_(fields),
+ R"([[1, [4.0, 5.0, 6.0], "beta"], [2, [7.0, 8.0, 9.0], "two"]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(update_batch), /*commit_identifier=*/1,
+ /*expected_commit_messages=*/std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> null_vector_batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields), R"([[1, null, "gamma"]])",
+ /*partition_map=*/{}, /*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(null_vector_batch), /*commit_identifier=*/2,
+ /*expected_commit_messages=*/std::nullopt));
+
+ std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar");
+ ASSERT_OK(CompactAndCommit(table_path, options, /*commit_identifier=*/3));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits,
+ helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt));
+ arrow::FieldVector result_fields = fields;
+ result_fields.insert(result_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8()));
+ ASSERT_OK_AND_ASSIGN(bool success,
+ helper->ReadAndCheckResult(arrow::struct_(result_fields), data_splits, R"([
+ [0, 1, [4.0, 5.0, 6.0], "alpha,beta,gamma"],
+ [0, 2, [7.0, 8.0, 9.0], "one,two"]
+ ])"));
+ ASSERT_TRUE(success);
+}
+
TEST_P(WriteAndReadInteTest, TestInputChangelogStreamRead) {
arrow::FieldVector fields = {
arrow::field("pk", arrow::utf8()),
diff --git a/test/inte/write_inte_test.cpp b/test/inte/write_inte_test.cpp
index 27bcea6..3c6d211 100644
--- a/test/inte/write_inte_test.cpp
+++ b/test/inte/write_inte_test.cpp
@@ -106,6 +106,7 @@
} // namespace paimon
namespace paimon::test {
+
class WriteInteTest : public testing::Test, public ::testing::WithParamInterface<std::string> {
public:
void SetUp() override {
@@ -4407,6 +4408,81 @@
ASSERT_OK(ScanAndVerifyResult(table_path, fields, expected));
}
+TEST_P(WriteInteTest, TestPkSpillableVector) {
+ auto file_format = GetParam();
+ if (file_format != "parquet") {
+ return;
+ }
+
+ auto dir = UniqueTestDirectory::Create();
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
+ arrow::FieldVector fields = {
+ arrow::field("f0", arrow::utf8()),
+ arrow::field("pt", arrow::int32()),
+ arrow::field("embedding", vector_type),
+ };
+ auto data_type = arrow::struct_(fields);
+ std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, file_format},
+ {Options::BUCKET, "1"},
+ {Options::FILE_SYSTEM, "local"},
+ {Options::WRITE_BUFFER_SIZE, "1"},
+ {Options::WRITE_BUFFER_SPILLABLE, "true"},
+ {Options::LOCAL_SORT_MAX_NUM_FILE_HANDLES, "2"},
+ {Options::WRITE_ONLY, "true"},
+ };
+ auto schema = arrow::schema(fields);
+ ::ArrowSchema c_schema;
+ ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
+ ASSERT_OK_AND_ASSIGN(auto table_path, CreateTestTable(dir->Str(), "db", "tbl", &c_schema,
+ /*partition_keys=*/{"pt"},
+ /*primary_keys=*/{"pt", "f0"}, options));
+
+ std::string tmp_dir = PathUtil::JoinPath(dir->Str(), "tmp");
+ WriteContextBuilder write_builder(table_path, "commit_user_1");
+ write_builder.WithStreamingMode(true).WithTempDirectory(tmp_dir);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context, write_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto file_store_write, FileStoreWrite::Create(std::move(write_context)));
+
+ auto write_array = [](FileStoreWrite* writer, const std::shared_ptr<arrow::Array>& array) {
+ ArrowArray c_array;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
+ auto batch = std::make_unique<RecordBatch>(std::map<std::string, std::string>{{"pt", "10"}},
+ /*bucket=*/0,
+ std::vector<RecordBatch::RowKind>{}, &c_array);
+ return writer->Write(std::move(batch));
+ };
+
+ auto batch1 =
+ arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([["Alice", 10, [1.0, 2.0, 3.0]]])")
+ .ValueOrDie();
+ auto batch2 =
+ arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([["Bob", 10, [4.0, 5.0, 6.0]]])")
+ .ValueOrDie();
+ auto batch3 =
+ arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([["Alice", 10, [7.0, 8.0, 9.0]]])")
+ .ValueOrDie();
+
+ ASSERT_OK(write_array(file_store_write.get(), batch1));
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(file_system_, tmp_dir));
+ ASSERT_OK(write_array(file_store_write.get(), batch2));
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(file_system_, tmp_dir));
+ ASSERT_OK(write_array(file_store_write.get(), batch3));
+ ASSERT_EQ(2, TestHelper::CountChannelFiles(file_system_, tmp_dir));
+
+ ASSERT_OK_AND_ASSIGN(auto commit_messages,
+ file_store_write->PrepareCommit(/*wait_compaction=*/false,
+ /*commit_identifier=*/0));
+ ASSERT_EQ(0, TestHelper::CountChannelFiles(file_system_, tmp_dir));
+ ASSERT_OK(CommitMessages(table_path, commit_messages));
+ ASSERT_OK(file_store_write->Close());
+
+ ASSERT_OK(ScanAndVerifyResult(table_path, fields,
+ R"([[0, "Alice", 10, [7.0, 8.0, 9.0]],
+ [0, "Bob", 10, [4.0, 5.0, 6.0]]])"));
+}
+
TEST_P(WriteInteTest, TestPkSpillableMultiBucketMultiRoundDataCorrectness) {
auto dir = UniqueTestDirectory::Create();
arrow::FieldVector fields = {