|
16 | 16 |
|
17 | 17 | #include "paimon/common/data/variant/infer_variant_shredding_schema.h" |
18 | 18 |
|
| 19 | +#include <functional> |
19 | 20 | #include <string> |
20 | 21 | #include <vector> |
21 | 22 |
|
@@ -52,6 +53,18 @@ class InferVariantShreddingSchemaTest : public ::testing::Test { |
52 | 53 | return samples; |
53 | 54 | } |
54 | 55 |
|
| 56 | + // Builds a single variant using the direct append API, which can encode types (float, binary, |
| 57 | + // uuid, decimals with a specific scale) that JSON parsing never produces. |
| 58 | + std::shared_ptr<GenericVariant> BuildVariant( |
| 59 | + const std::function<Status(VariantBuilder&)>& append) { |
| 60 | + VariantBuilder builder(/*allow_duplicate_keys=*/false); |
| 61 | + Status st = append(builder); |
| 62 | + EXPECT_TRUE(st.ok()) << st.ToString(); |
| 63 | + auto result = builder.Build(pool_); |
| 64 | + EXPECT_TRUE(result.ok()) << result.status().ToString(); |
| 65 | + return result.value(); |
| 66 | + } |
| 67 | + |
55 | 68 | protected: |
56 | 69 | std::shared_ptr<MemoryPool> pool_ = GetDefaultPool(); |
57 | 70 | InferVariantShreddingSchema infer_{/*max_schema_width=*/300, /*max_schema_depth=*/50, |
@@ -209,4 +222,149 @@ TEST_F(InferVariantShreddingSchemaTest, TemporalValuesStayUnshredded) { |
209 | 222 | ASSERT_EQ(ts_inferred, nullptr); |
210 | 223 | } |
211 | 224 |
|
| 225 | +TEST_F(InferVariantShreddingSchemaTest, MergeObjectsWithDisjointFields) { |
| 226 | + // Objects whose keys interleave exercise both single-side merge branches (field only in the |
| 227 | + // first object, and field only in the second). |
| 228 | + auto samples = Samples({R"({"a": 1, "c": 3})", R"({"b": 2, "c": 4})"}); |
| 229 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, InferColumn(infer_, samples)); |
| 230 | + ASSERT_NE(inferred, nullptr); |
| 231 | + auto expected = |
| 232 | + arrow::struct_({arrow::field("a", arrow::int64()), arrow::field("b", arrow::int64()), |
| 233 | + arrow::field("c", arrow::int64())}); |
| 234 | + ASSERT_TRUE(inferred->Equals(*expected)) << inferred->ToString(); |
| 235 | +} |
| 236 | + |
| 237 | +TEST_F(InferVariantShreddingSchemaTest, VariantNullSamplesAndFields) { |
| 238 | + // A top-level variant null (JSON `null`, not a missing sample) merges away, leaving the other |
| 239 | + // sample's inferred type. |
| 240 | + auto scalar_and_null = Samples({"1", "null"}); |
| 241 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> merged, |
| 242 | + InferColumn(infer_, scalar_and_null)); |
| 243 | + ASSERT_NE(merged, nullptr); |
| 244 | + ASSERT_TRUE(merged->Equals(*arrow::int64())) << merged->ToString(); |
| 245 | + |
| 246 | + // An object field that is always variant-null becomes an untyped variant leaf. |
| 247 | + auto object_with_null = Samples({R"({"a": null, "b": 1})"}); |
| 248 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, |
| 249 | + InferColumn(infer_, object_with_null)); |
| 250 | + ASSERT_NE(inferred, nullptr); |
| 251 | + auto expected = |
| 252 | + arrow::struct_({arrow::field("a", arrow::null()), arrow::field("b", arrow::int64())}); |
| 253 | + ASSERT_TRUE(inferred->Equals(*expected)) << inferred->ToString(); |
| 254 | +} |
| 255 | + |
| 256 | +TEST_F(InferVariantShreddingSchemaTest, ArraysMerge) { |
| 257 | + // Two arrays merge element-wise into a single typed element schema. |
| 258 | + auto samples = Samples({"[1, 2]", "[3, 4]"}); |
| 259 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, InferColumn(infer_, samples)); |
| 260 | + ASSERT_NE(inferred, nullptr); |
| 261 | + ASSERT_TRUE(inferred->Equals(*arrow::list(arrow::int64()))) << inferred->ToString(); |
| 262 | +} |
| 263 | + |
| 264 | +TEST_F(InferVariantShreddingSchemaTest, ArrayBeyondDepthLimitStaysVariant) { |
| 265 | + InferVariantShreddingSchema shallow_infer{/*max_schema_width=*/300, /*max_schema_depth=*/1, |
| 266 | + /*min_field_cardinality_ratio=*/0.1}; |
| 267 | + auto samples = Samples({R"({"arr": [1, 2]})"}); |
| 268 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, |
| 269 | + InferColumn(shallow_infer, samples)); |
| 270 | + ASSERT_NE(inferred, nullptr); |
| 271 | + // Depth 1: the nested array is beyond the limit and stays an untyped variant leaf. |
| 272 | + auto expected = arrow::struct_({arrow::field("arr", arrow::null())}); |
| 273 | + ASSERT_TRUE(inferred->Equals(*expected)) << inferred->ToString(); |
| 274 | +} |
| 275 | + |
| 276 | +TEST_F(InferVariantShreddingSchemaTest, ScalarLeafTypes) { |
| 277 | + // Float and binary leaves shred to their arrow types. |
| 278 | + std::shared_ptr<GenericVariant> float_variant = |
| 279 | + BuildVariant([](VariantBuilder& b) { return b.AppendFloat(1.5f); }); |
| 280 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> float_inferred, |
| 281 | + InferColumn(infer_, {float_variant})); |
| 282 | + ASSERT_NE(float_inferred, nullptr); |
| 283 | + ASSERT_TRUE(float_inferred->Equals(*arrow::float32())) << float_inferred->ToString(); |
| 284 | + |
| 285 | + std::shared_ptr<GenericVariant> binary_variant = BuildVariant( |
| 286 | + [](VariantBuilder& b) { return b.AppendBinary(std::string_view("\x01\x02", 2)); }); |
| 287 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> binary_inferred, |
| 288 | + InferColumn(infer_, {binary_variant})); |
| 289 | + ASSERT_NE(binary_inferred, nullptr); |
| 290 | + ASSERT_TRUE(binary_inferred->Equals(*arrow::binary())) << binary_inferred->ToString(); |
| 291 | + |
| 292 | + // A UUID has no shredding type, so the column stays unshredded. |
| 293 | + std::string uuid_bytes(16, '\0'); |
| 294 | + std::shared_ptr<GenericVariant> uuid_variant = |
| 295 | + BuildVariant([&](VariantBuilder& b) { return b.AppendUuid(uuid_bytes); }); |
| 296 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> uuid_inferred, |
| 297 | + InferColumn(infer_, {uuid_variant})); |
| 298 | + ASSERT_EQ(uuid_inferred, nullptr); |
| 299 | +} |
| 300 | + |
| 301 | +TEST_F(InferVariantShreddingSchemaTest, LargeIntegerAndDecimalMerging) { |
| 302 | + // A 19-digit long exceeds decimal(18) precision, so it stays a genuine int64 leaf. |
| 303 | + auto big_long = Samples({"1000000000000000000"}); |
| 304 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> long_inferred, |
| 305 | + InferColumn(infer_, big_long)); |
| 306 | + ASSERT_NE(long_inferred, nullptr); |
| 307 | + ASSERT_TRUE(long_inferred->Equals(*arrow::int64())) << long_inferred->ToString(); |
| 308 | + |
| 309 | + // A long (int64) merged with a fractional decimal widens via MergeDecimalWithLong. |
| 310 | + auto long_then_decimal = Samples({"1000000000000000000", "1.5"}); |
| 311 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> ld, |
| 312 | + InferColumn(infer_, long_then_decimal)); |
| 313 | + ASSERT_NE(ld, nullptr); |
| 314 | + ASSERT_TRUE(ld->Equals(*arrow::decimal128(38, 1))) << ld->ToString(); |
| 315 | + |
| 316 | + // The reversed order (decimal first, then long) hits the mirrored branch. |
| 317 | + auto decimal_then_long = Samples({"1.5", "1000000000000000000"}); |
| 318 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> dl, |
| 319 | + InferColumn(infer_, decimal_then_long)); |
| 320 | + ASSERT_NE(dl, nullptr); |
| 321 | + ASSERT_TRUE(dl->Equals(*arrow::decimal128(38, 1))) << dl->ToString(); |
| 322 | +} |
| 323 | + |
| 324 | +TEST_F(InferVariantShreddingSchemaTest, LongMergedWithIntegralDecimalStaysLong) { |
| 325 | + // A scale-0 decimal that fits in 18 digits merges with a long back to int64. |
| 326 | + std::shared_ptr<GenericVariant> integral_decimal = |
| 327 | + BuildVariant([](VariantBuilder& b) { return b.AppendDecimal(VariantDecimal{123, 0}); }); |
| 328 | + std::shared_ptr<GenericVariant> big_long = Samples({"1000000000000000000"})[0]; |
| 329 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, |
| 330 | + InferColumn(infer_, {integral_decimal, big_long})); |
| 331 | + ASSERT_NE(inferred, nullptr); |
| 332 | + ASSERT_TRUE(inferred->Equals(*arrow::int64())) << inferred->ToString(); |
| 333 | +} |
| 334 | + |
| 335 | +TEST_F(InferVariantShreddingSchemaTest, SmallFractionalDecimalPrecisionAdjusted) { |
| 336 | + // 0.0015 has more fractional digits than significant digits; precision widens up to the scale. |
| 337 | + auto samples = Samples({"0.0015"}); |
| 338 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, InferColumn(infer_, samples)); |
| 339 | + ASSERT_NE(inferred, nullptr); |
| 340 | + ASSERT_TRUE(inferred->Equals(*arrow::decimal128(18, 4))) << inferred->ToString(); |
| 341 | +} |
| 342 | + |
| 343 | +TEST_F(InferVariantShreddingSchemaTest, DecimalMergeOverflowFallsToVariant) { |
| 344 | + // A 38-digit integral decimal merged with a high-scale decimal would need precision > 38, |
| 345 | + // which decimal cannot represent, so the column stays unshredded. |
| 346 | + __int128 wide_unscaled = 0; |
| 347 | + for (int i = 0; i < 38; ++i) { |
| 348 | + wide_unscaled = wide_unscaled * 10 + 1; // 38 ones, no trailing zeros |
| 349 | + } |
| 350 | + std::shared_ptr<GenericVariant> wide_decimal = BuildVariant( |
| 351 | + [&](VariantBuilder& b) { return b.AppendDecimal(VariantDecimal{wide_unscaled, 0}); }); |
| 352 | + std::shared_ptr<GenericVariant> high_scale_decimal = |
| 353 | + BuildVariant([](VariantBuilder& b) { return b.AppendDecimal(VariantDecimal{15, 20}); }); |
| 354 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, |
| 355 | + InferColumn(infer_, {wide_decimal, high_scale_decimal})); |
| 356 | + ASSERT_EQ(inferred, nullptr); |
| 357 | +} |
| 358 | + |
| 359 | +TEST_F(InferVariantShreddingSchemaTest, ObjectWithAllRareFieldsStaysUnshredded) { |
| 360 | + InferVariantShreddingSchema strict_infer{/*max_schema_width=*/300, /*max_schema_depth=*/50, |
| 361 | + /*min_field_cardinality_ratio=*/0.6}; |
| 362 | + // Two objects with disjoint single-occurrence keys: with a 0.6 ratio every field is below the |
| 363 | + // cardinality threshold, so the object contributes no typed field and the column is dropped. |
| 364 | + auto samples = Samples({R"({"a": 1})", R"({"b": 2})"}); |
| 365 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> inferred, |
| 366 | + InferColumn(strict_infer, samples)); |
| 367 | + ASSERT_EQ(inferred, nullptr); |
| 368 | +} |
| 369 | + |
212 | 370 | } // namespace paimon::test |
0 commit comments