@@ -158,7 +158,7 @@ class LuminaGlobalIndexTest : public ::testing::Test {
158158 return struct_array;
159159 }
160160
161- private :
161+ protected :
162162 std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
163163 std::shared_ptr<FileSystem> fs_ = std::make_shared<LocalFileSystem>();
164164 std::map<std::string, std::string> options_ = {{" lumina.index.dimension" , " 4" },
@@ -470,4 +470,201 @@ TEST_F(LuminaGlobalIndexTest, TestHighCardinalityAndMultiThreadSearch) {
470470 }
471471}
472472
473+ TEST_F (LuminaGlobalIndexTest, TestWriteWithNullRows) {
474+ auto test_root_dir = paimon::test::UniqueTestDirectory::Create ();
475+ ASSERT_TRUE (test_root_dir);
476+ std::string test_root = test_root_dir->Str ();
477+
478+ // Array with null at row 1 (middle): rows 0,2,3 are valid, row 1 is null
479+ // This should split into two segments: [0,0] and [2,3]
480+ std::shared_ptr<arrow::Array> array_with_null =
481+ arrow::ipc::internal::json::ArrayFromJSON (data_type_,
482+ R"( [
483+ [[0.0, 0.0, 0.0, 0.0]],
484+ [null],
485+ [[1.0, 0.0, 1.0, 0.0]],
486+ [[1.0, 1.0, 1.0, 1.0]]
487+ ])" )
488+ .ValueOrDie ();
489+
490+ ASSERT_OK_AND_ASSIGN (
491+ auto meta, WriteGlobalIndex (test_root, data_type_, options_, array_with_null, Range (0 , 3 )));
492+ ASSERT_OK_AND_ASSIGN (auto reader,
493+ CreateGlobalIndexReader (test_root, data_type_, options_, meta));
494+ {
495+ // Search should return ids 0, 2, 3 (skipping null row 1)
496+ ASSERT_OK_AND_ASSIGN (
497+ auto scored_result,
498+ reader->VisitVectorSearch (std::make_shared<VectorSearch>(
499+ /* field_name=*/ " f0" , /* limit=*/ 4 , query_, /* filter=*/ nullptr ,
500+ /* predicate=*/ nullptr , /* distance_type=*/ std::nullopt , /* options=*/ options_)));
501+ // Only 3 vectors indexed (row 1 is null), so limit=4 returns 3
502+ CheckResult (scored_result, {3l , 2l , 0l }, {0 .01f , 2 .21f , 4 .21f });
503+ }
504+ }
505+
506+ TEST_F (LuminaGlobalIndexTest, TestWriteWithMultipleNullSegments) {
507+ auto test_root_dir = paimon::test::UniqueTestDirectory::Create ();
508+ ASSERT_TRUE (test_root_dir);
509+ std::string test_root = test_root_dir->Str ();
510+
511+ // Nulls at rows 0, 2, 5: valid rows are 1, 3, 4
512+ // Splits into segments: [1,1], [3,4]
513+ std::shared_ptr<arrow::Array> array_with_nulls =
514+ arrow::ipc::internal::json::ArrayFromJSON (data_type_,
515+ R"( [
516+ [null],
517+ [[0.0, 1.0, 0.0, 1.0]],
518+ [null],
519+ [[1.0, 0.0, 1.0, 0.0]],
520+ [[1.0, 1.0, 1.0, 1.0]],
521+ [null]
522+ ])" )
523+ .ValueOrDie ();
524+
525+ ASSERT_OK_AND_ASSIGN (auto meta, WriteGlobalIndex (test_root, data_type_, options_,
526+ array_with_nulls, Range (0 , 5 )));
527+ ASSERT_OK_AND_ASSIGN (auto reader,
528+ CreateGlobalIndexReader (test_root, data_type_, options_, meta));
529+ {
530+ ASSERT_OK_AND_ASSIGN (
531+ auto scored_result,
532+ reader->VisitVectorSearch (std::make_shared<VectorSearch>(
533+ /* field_name=*/ " f0" , /* limit=*/ 4 , query_, /* filter=*/ nullptr ,
534+ /* predicate=*/ nullptr , /* distance_type=*/ std::nullopt , /* options=*/ options_)));
535+ // Only 3 vectors indexed at ids 1, 3, 4
536+ CheckResult (scored_result, {4l , 1l , 3l }, {0 .01f , 2 .01f , 2 .21f });
537+ }
538+ }
539+
540+ TEST_F (LuminaGlobalIndexTest, TestWriteWithAllNullRows) {
541+ auto test_root_dir = paimon::test::UniqueTestDirectory::Create ();
542+ ASSERT_TRUE (test_root_dir);
543+ std::string test_root = test_root_dir->Str ();
544+
545+ // All rows are null — no vectors to index
546+ std::shared_ptr<arrow::Array> all_null_array =
547+ arrow::ipc::internal::json::ArrayFromJSON (data_type_,
548+ R"( [
549+ [null],
550+ [null],
551+ [null]
552+ ])" )
553+ .ValueOrDie ();
554+
555+ auto global_index = std::make_shared<LuminaGlobalIndex>(options_);
556+ auto path_factory = std::make_shared<FakeIndexPathFactory>(test_root);
557+ auto file_writer = std::make_shared<GlobalIndexFileManager>(fs_, path_factory);
558+
559+ ASSERT_OK_AND_ASSIGN (
560+ std::shared_ptr<GlobalIndexWriter> global_writer,
561+ global_index->CreateWriter (" f0" , CreateArrowSchema (data_type_).get (), file_writer, pool_));
562+
563+ ArrowArray c_array;
564+ ASSERT_TRUE (arrow::ExportArray (*all_null_array, &c_array).ok ());
565+ std::vector<int64_t > row_ids = {0 , 1 , 2 };
566+ ASSERT_OK (global_writer->AddBatch (&c_array, std::move (row_ids)));
567+ // Finish with zero indexed vectors — returns empty metas
568+ ASSERT_OK_AND_ASSIGN (auto result_metas, global_writer->Finish ());
569+ ASSERT_TRUE (result_metas.empty ());
570+ }
571+
572+ TEST_F (LuminaGlobalIndexTest, TestWriteWithNullAndFilter) {
573+ auto test_root_dir = paimon::test::UniqueTestDirectory::Create ();
574+ ASSERT_TRUE (test_root_dir);
575+ std::string test_root = test_root_dir->Str ();
576+
577+ // Null at row 2: valid rows are 0, 1, 3
578+ std::shared_ptr<arrow::Array> array_with_null =
579+ arrow::ipc::internal::json::ArrayFromJSON (data_type_,
580+ R"( [
581+ [[0.0, 0.0, 0.0, 0.0]],
582+ [[0.0, 1.0, 0.0, 1.0]],
583+ [null],
584+ [[1.0, 1.0, 1.0, 1.0]]
585+ ])" )
586+ .ValueOrDie ();
587+
588+ ASSERT_OK_AND_ASSIGN (
589+ auto meta, WriteGlobalIndex (test_root, data_type_, options_, array_with_null, Range (0 , 3 )));
590+ ASSERT_OK_AND_ASSIGN (auto reader,
591+ CreateGlobalIndexReader (test_root, data_type_, options_, meta));
592+ {
593+ // Filter: only allow ids < 3 (filters out id=3), so only ids 0, 1 remain
594+ auto filter = [](int64_t id) -> bool { return id < 3 ; };
595+ ASSERT_OK_AND_ASSIGN (
596+ auto scored_result,
597+ reader->VisitVectorSearch (std::make_shared<VectorSearch>(
598+ /* field_name=*/ " f0" , /* limit=*/ 4 , query_, filter,
599+ /* predicate=*/ nullptr , /* distance_type=*/ std::nullopt , /* options=*/ options_)));
600+ CheckResult (scored_result, {1l , 0l }, {2 .01f , 4 .21f });
601+ }
602+ }
603+
604+ TEST_F (LuminaGlobalIndexTest, TestWriteWithNullAcrossMultipleBatches) {
605+ auto test_root_dir = paimon::test::UniqueTestDirectory::Create ();
606+ ASSERT_TRUE (test_root_dir);
607+ std::string test_root = test_root_dir->Str ();
608+
609+ // Batch 1: rows 0-2, null at row 1 → indexed ids: {0, 2}
610+ std::shared_ptr<arrow::Array> batch1 = arrow::ipc::internal::json::ArrayFromJSON (data_type_,
611+ R"( [
612+ [[0.0, 0.0, 0.0, 0.0]],
613+ [null],
614+ [[1.0, 0.0, 1.0, 0.0]]
615+ ])" )
616+ .ValueOrDie ();
617+
618+ // Batch 2: rows 3-5, null at row 3 → indexed ids: {4, 5}
619+ std::shared_ptr<arrow::Array> batch2 = arrow::ipc::internal::json::ArrayFromJSON (data_type_,
620+ R"( [
621+ [null],
622+ [[1.0, 1.0, 1.0, 1.0]],
623+ [[0.0, 1.0, 0.0, 1.0]]
624+ ])" )
625+ .ValueOrDie ();
626+
627+ auto global_index = std::make_shared<LuminaGlobalIndex>(options_);
628+ auto path_factory = std::make_shared<FakeIndexPathFactory>(test_root);
629+ auto file_writer = std::make_shared<GlobalIndexFileManager>(fs_, path_factory);
630+
631+ ASSERT_OK_AND_ASSIGN (
632+ std::shared_ptr<GlobalIndexWriter> global_writer,
633+ global_index->CreateWriter (" f0" , CreateArrowSchema (data_type_).get (), file_writer, pool_));
634+
635+ // AddBatch 1: row_ids {0, 1, 2}
636+ {
637+ ArrowArray c_array;
638+ ASSERT_TRUE (arrow::ExportArray (*batch1, &c_array).ok ());
639+ std::vector<int64_t > row_ids = {0 , 1 , 2 };
640+ ASSERT_OK (global_writer->AddBatch (&c_array, std::move (row_ids)));
641+ }
642+ // AddBatch 2: row_ids {3, 4, 5}
643+ {
644+ ArrowArray c_array;
645+ ASSERT_TRUE (arrow::ExportArray (*batch2, &c_array).ok ());
646+ std::vector<int64_t > row_ids = {3 , 4 , 5 };
647+ ASSERT_OK (global_writer->AddBatch (&c_array, std::move (row_ids)));
648+ }
649+
650+ ASSERT_OK_AND_ASSIGN (auto result_metas, global_writer->Finish ());
651+ ASSERT_EQ (result_metas.size (), 1 );
652+
653+ ASSERT_OK_AND_ASSIGN (auto reader,
654+ CreateGlobalIndexReader (test_root, data_type_, options_, result_metas[0 ]));
655+ {
656+ // Search all: should return ids {0, 2, 4, 5}, never {1, 3}
657+ ASSERT_OK_AND_ASSIGN (
658+ auto scored_result,
659+ reader->VisitVectorSearch (std::make_shared<VectorSearch>(
660+ /* field_name=*/ " f0" , /* limit=*/ 10 , query_, /* filter=*/ nullptr ,
661+ /* predicate=*/ nullptr , /* distance_type=*/ std::nullopt , /* options=*/ options_)));
662+ // id 0: [0,0,0,0] → L2 dist to [1,1,1,1.1] = 4.21
663+ // id 2: [1,0,1,0] → L2 dist = 2.21
664+ // id 4: [1,1,1,1] → L2 dist = 0.01
665+ // id 5: [0,1,0,1] → L2 dist = 2.01
666+ CheckResult (scored_result, {4l , 5l , 2l , 0l }, {0 .01f , 2 .01f , 2 .21f , 4 .21f });
667+ }
668+ }
669+
473670} // namespace paimon::lumina::test
0 commit comments