/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <algorithm>
#include <cstdint>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "arrow/api.h"
#include "arrow/c/bridge.h"
#include "fmt/format.h"
#include "gtest/gtest.h"
#include "paimon/common/data/variant/generic_variant.h"
#include "paimon/common/data/variant/variant_shredding_utils.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/factories/io_hook.h"
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/core/io/data_file_meta.h"
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/data/variant.h"
#include "paimon/defs.h"
#include "paimon/format/file_format_factory.h"
#include "paimon/format/reader_builder.h"
#include "paimon/fs/file_system.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/reader/file_batch_reader.h"
#include "paimon/record_batch.h"
#include "paimon/table/source/startup_mode.h"
#include "paimon/testing/utils/io_exception_helper.h"
#include "paimon/testing/utils/read_result_collector.h"
#include "paimon/testing/utils/test_helper.h"
#include "paimon/testing/utils/testharness.h"
#include "paimon/testing/utils/variant_test_data.h"

namespace paimon::test {

// End-to-end tests for tables with a VARIANT column: create, write, commit, scan and read.
class VariantTableInteTest : public ::testing::Test {
 public:
    void SetUp() override {
        dir_ = UniqueTestDirectory::Create();
        ASSERT_TRUE(dir_);
        test_dir_ = dir_->Str();
        pool_ = GetDefaultPool();
        fields_ = {arrow::field("id", arrow::int32()), VariantTypeUtils::ToArrowField("v")};
        schema_ = arrow::schema(fields_);
    }

    void TearDown() override {
        dir_.reset();
    }

    std::shared_ptr<arrow::StructArray> BuildArray(const std::vector<const char*>& jsons,
                                                   int32_t id_offset = 0) {
        auto result =
            VariantTestData::BuildVariantBatch(fields_[0], fields_[1], jsons, pool_, id_offset);
        EXPECT_TRUE(result.ok()) << result.status().ToString();
        return std::move(result).value();
    }

    Result<std::unique_ptr<RecordBatch>> MakeBatch(
        const std::shared_ptr<arrow::StructArray>& array) {
        ::ArrowArray arrow_array;
        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &arrow_array));
        RecordBatchBuilder batch_builder(&arrow_array);
        return batch_builder.SetPartition({}).SetBucket(0).SetRowKinds({}).Finish();
    }

    // Reads all rows back and checks the variant column renders to `expected_jsons` (nullptr
    // means a null variant). The read result carries a leading `_VALUE_KIND` column.
    void ReadAndCheck(TestHelper* helper, const std::vector<std::shared_ptr<Split>>& splits,
                      const std::vector<int32_t>& expected_ids,
                      const std::vector<const char*>& expected_jsons) {
        ASSERT_OK_AND_ASSIGN(auto result, helper->ReadResult(splits));
        ASSERT_EQ(result->num_chunks(), 1);
        auto result_struct = std::static_pointer_cast<arrow::StructArray>(result->chunk(0));
        ASSERT_EQ(result_struct->length(), static_cast<int64_t>(expected_jsons.size()));
        auto struct_type = std::static_pointer_cast<arrow::StructType>(result_struct->type());
        int32_t id_index = struct_type->GetFieldIndex("id");
        int32_t variant_index = struct_type->GetFieldIndex("v");
        ASSERT_GE(id_index, 0);
        ASSERT_GE(variant_index, 0);
        auto id_column =
            std::static_pointer_cast<arrow::Int32Array>(result_struct->field(id_index));
        auto variant_column =
            std::static_pointer_cast<arrow::StructArray>(result_struct->field(variant_index));
        auto value_column = std::static_pointer_cast<arrow::BinaryArray>(variant_column->field(0));
        auto metadata_column =
            std::static_pointer_cast<arrow::BinaryArray>(variant_column->field(1));
        for (size_t i = 0; i < expected_jsons.size(); ++i) {
            SCOPED_TRACE("row " + std::to_string(i));
            ASSERT_EQ(id_column->Value(i), expected_ids[i]);
            if (expected_jsons[i] == nullptr) {
                ASSERT_TRUE(variant_column->IsNull(i));
                continue;
            }
            ASSERT_FALSE(variant_column->IsNull(i));
            ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant,
                                 GenericVariant::Create(value_column->GetView(i),
                                                        metadata_column->GetView(i), pool_));
            ASSERT_OK_AND_ASSIGN(std::string actual_json, variant->ToJson());
            ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> expected,
                                 GenericVariant::FromJson(expected_jsons[i], pool_));
            ASSERT_OK_AND_ASSIGN(std::string expected_json, expected->ToJson());
            ASSERT_EQ(actual_json, expected_json);
        }
    }

    // Builds a variant-access projection field via the public builder.
    std::shared_ptr<arrow::Field> BuildAccessField(
        const std::vector<std::pair<std::shared_ptr<arrow::DataType>, std::string>>& accesses,
        const std::string& field_name) {
        VariantAccessBuilder builder;
        for (const auto& [type, path] : accesses) {
            auto target = std::make_unique<ArrowSchema>();
            EXPECT_TRUE(arrow::ExportField(arrow::Field("t", type), target.get()).ok());
            EXPECT_OK(builder.AddField(target.get(), path, /*fail_on_error=*/false));
        }
        auto c_field = builder.Build(field_name);
        EXPECT_TRUE(c_field.ok()) << c_field.status().ToString();
        auto imported = arrow::ImportField(c_field.value().get());
        EXPECT_TRUE(imported.ok()) << imported.status().ToString();
        return imported.ValueOrDie();
    }

    // Reads `splits` back with `read_schema` projected and returns the single result chunk.
    void ReadWithSchema(TestHelper* helper, const std::vector<std::shared_ptr<Split>>& splits,
                        const std::shared_ptr<arrow::Schema>& read_schema,
                        std::shared_ptr<arrow::StructArray>* result_struct) {
        auto c_read_schema = std::make_unique<ArrowSchema>();
        ASSERT_TRUE(arrow::ExportSchema(*read_schema, c_read_schema.get()).ok());
        ASSERT_OK_AND_ASSIGN(auto result, helper->ReadResult(splits, std::move(c_read_schema)));
        ASSERT_EQ(result->num_chunks(), 1);
        *result_struct = std::static_pointer_cast<arrow::StructArray>(result->chunk(0));
    }

    Result<std::shared_ptr<arrow::Schema>> ReadDataFileSchema(
        const std::string& bucket_path, const std::shared_ptr<DataFileMeta>& file,
        const std::map<std::string, std::string>& options) const {
        std::string file_path = PathUtil::JoinPath(bucket_path, file->file_name);
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream> unique_input_stream,
                               dir_->GetFileSystem()->Open(file_path));
        std::shared_ptr<InputStream> input_stream(std::move(unique_input_stream));
        PAIMON_ASSIGN_OR_RAISE(std::string format, file->FileFormat());
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileFormat> file_format,
                               FileFormatFactory::Get(format, options));
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReaderBuilder> reader_builder,
                               file_format->CreateReaderBuilder(/*batch_size=*/10));
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileBatchReader> reader,
                               reader_builder->Build(input_stream));
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ArrowSchema> c_file_schema, reader->GetFileSchema());
        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> file_schema,
                                          arrow::ImportSchema(c_file_schema.get()));
        return file_schema;
    }

    std::map<std::string, std::string> AdaptiveInferenceOptions(int64_t target_file_row_num,
                                                                int32_t initial_sample_rows,
                                                                int32_t adaptive_sample_rows,
                                                                double admission_ratio,
                                                                double retention_ratio) const {
        return {
            {Options::MANIFEST_FORMAT, "avro"},
            {Options::FILE_FORMAT, "parquet"},
            {Options::BUCKET, "-1"},
            {Options::WRITE_ONLY, "true"},
            {Options::TARGET_FILE_ROW_NUM, std::to_string(target_file_row_num)},
            {Options::VARIANT_INFER_SHREDDING_SCHEMA, "true"},
            {Options::VARIANT_SHREDDING_INFERENCE_MODE, "adaptive"},
            {Options::VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW, std::to_string(initial_sample_rows)},
            {Options::VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW,
             std::to_string(adaptive_sample_rows)},
            {Options::VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO,
             fmt::format("{}", admission_ratio)},
            {Options::VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO,
             fmt::format("{}", retention_ratio)},
        };
    }

 protected:
    std::string test_dir_;
    std::unique_ptr<UniqueTestDirectory> dir_;
    std::shared_ptr<MemoryPool> pool_;
    arrow::FieldVector fields_;
    std::shared_ptr<arrow::Schema> schema_;
};

TEST_F(VariantTableInteTest, TestAppendTable) {
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "-1"},
    };
    ASSERT_OK_AND_ASSIGN(
        auto helper, TestHelper::Create(test_dir_, schema_, /*partition_keys=*/{},
                                        /*primary_keys=*/{}, options, /*is_streaming_mode=*/false));
    // The document set covers deep object/array alternation, escaped and unicode strings,
    // wide integers, decimals, exponent doubles and empty containers.
    std::vector<const char*> jsons = {
        R"({"age": 35, "city": "Hangzhou"})",
        nullptr,
        "[1, \"two\", 3.5, null, true]",
        "{\"nested\": {\"x\": [1, 2]}, \"s\": \"中文\"}",
        R"({
            "user": {
                "id": 9007199254740993,
                "name": "张三 \"quoted\" \\ / \b\f\n\r\t",
                "tags": ["a", 1, 2.5, true, null, {"deep": [[1, [2, [3, [4]]]]]}],
                "address": {
                    "city": "Hangzhou",
                    "geo": {"lat": 30.274085, "lng": 120.15507, "alt": -1.5e-3},
                    "history": [
                        {"year": 2020, "city": "Beijing"},
                        {"year": 2021, "city": "Shanghai", "note": null}
                    ]
                },
                "balance": 12345678901234567890.123456789,
                "scores": [0.1, -0.0, 1e100, -1e-100]
            },
            "empty_object": {},
            "empty_array": [],
            "flags": [true, false, null]
        })",
        R"([{"a": [{"b": {"c": [null, {"d": 1}]}}]}, [], {}, "end"])",
        R"({"unicode": "😀"})",
    };
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeBatch(BuildArray(jsons)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs,
                         helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
                                                /*expected_commit_messages=*/std::nullopt));
    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));
    ReadAndCheck(helper.get(), splits, {0, 1, 2, 3, 4, 5, 6}, jsons);
}

TEST_F(VariantTableInteTest, TestAdaptiveInferenceAcrossRollingFiles) {
    std::map<std::string, std::string> options =
        AdaptiveInferenceOptions(/*target_file_row_num=*/10, /*initial_sample_rows=*/10,
                                 /*adaptive_sample_rows=*/10, /*admission_ratio=*/0.4,
                                 /*retention_ratio=*/0.2);
    ASSERT_OK_AND_ASSIGN(
        std::unique_ptr<TestHelper> helper,
        TestHelper::Create(test_dir_, schema_, /*partition_keys=*/{}, /*primary_keys=*/{}, options,
                           /*is_streaming_mode=*/false));

    std::vector<std::string> json_storage;
    json_storage.reserve(30);
    for (int32_t id = 0; id < 10; ++id) {
        json_storage.push_back(id < 5 ? fmt::format(R"({{"legacy":"value","stable":{}}})", id)
                                      : fmt::format(R"({{"stable":{}}})", id));
    }
    for (int32_t id = 10; id < 20; ++id) {
        json_storage.push_back(id < 19 ? fmt::format(R"({{"emerging":true,"stable":{}}})", id)
                                       : fmt::format(R"({{"stable":{}}})", id));
    }
    for (int32_t id = 20; id < 30; ++id) {
        json_storage.push_back(fmt::format(R"({{"stable":{}}})", id));
    }
    std::vector<const char*> jsons;
    jsons.reserve(json_storage.size());
    for (const std::string& json : json_storage) {
        jsons.push_back(json.c_str());
    }

    std::vector<std::unique_ptr<RecordBatch>> batches;
    for (int32_t file_index = 0; file_index < 3; ++file_index) {
        auto begin = jsons.begin() + file_index * 10;
        std::vector<const char*> file_jsons(begin, begin + 10);
        ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
                             MakeBatch(BuildArray(file_jsons, /*id_offset=*/file_index * 10)));
        batches.push_back(std::move(batch));
    }
    ASSERT_OK(helper->WriteAndCommit(std::move(batches), /*commit_identifier=*/0,
                                     /*expected_commit_messages=*/std::nullopt));

    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));
    ASSERT_EQ(1, splits.size());
    auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(splits[0]);
    ASSERT_NE(data_split, nullptr);
    std::vector<std::shared_ptr<DataFileMeta>> files = data_split->DataFiles();
    std::sort(
        files.begin(), files.end(),
        [](const std::shared_ptr<DataFileMeta>& left, const std::shared_ptr<DataFileMeta>& right) {
            return left->min_sequence_number < right->min_sequence_number;
        });
    ASSERT_EQ(3, files.size());

    std::vector<std::shared_ptr<arrow::DataType>> expected_variant_types;
    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> first_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("legacy", arrow::utf8()), arrow::field("stable", arrow::int64())})));
    expected_variant_types.push_back(first_type);
    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> second_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("emerging", arrow::boolean()), arrow::field("legacy", arrow::utf8()),
             arrow::field("stable", arrow::int64())})));
    expected_variant_types.push_back(second_type);
    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> third_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("emerging", arrow::boolean()), arrow::field("stable", arrow::int64())})));
    expected_variant_types.push_back(third_type);

    for (size_t i = 0; i < files.size(); ++i) {
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Schema> file_schema,
                             ReadDataFileSchema(data_split->BucketPath(), files[i], options));
        std::shared_ptr<arrow::Field> variant_field = file_schema->GetFieldByName("v");
        ASSERT_NE(variant_field, nullptr);
        ASSERT_TRUE(variant_field->type()->Equals(*expected_variant_types[i]))
            << "file=" << files[i]->file_name << ", actual=" << variant_field->type()->ToString()
            << ", expected=" << expected_variant_types[i]->ToString();
    }

    std::vector<int32_t> expected_ids;
    expected_ids.reserve(30);
    for (int32_t id = 0; id < 30; ++id) {
        expected_ids.push_back(id);
    }
    ReadAndCheck(helper.get(), splits, expected_ids, jsons);
}

TEST_F(VariantTableInteTest, TestAdaptiveInferenceWithMultipleVariantFields) {
    std::shared_ptr<arrow::Field> left_field = VariantTypeUtils::ToArrowField("left_payload");
    std::shared_ptr<arrow::Field> right_field = VariantTypeUtils::ToArrowField("right_payload");
    std::shared_ptr<arrow::Schema> table_schema =
        arrow::schema({arrow::field("id", arrow::int32()), left_field, right_field});
    std::map<std::string, std::string> options =
        AdaptiveInferenceOptions(/*target_file_row_num=*/2, /*initial_sample_rows=*/2,
                                 /*adaptive_sample_rows=*/2, /*admission_ratio=*/0.4,
                                 /*retention_ratio=*/0.2);
    ASSERT_OK_AND_ASSIGN(
        std::unique_ptr<TestHelper> helper,
        TestHelper::Create(test_dir_, table_schema, /*partition_keys=*/{},
                           /*primary_keys=*/{}, options, /*is_streaming_mode=*/false));

    std::vector<const char*> left_jsons = {
        R"({"legacy":"a","stable":1})",
        R"({"legacy":"b","stable":2})",
        R"({"emerging":true,"stable":3})",
        R"({"emerging":false,"stable":4})",
    };
    std::vector<const char*> right_jsons = {
        R"({"sparse":"x","stable":"a"})",
        R"({"stable":"b"})",
        R"({"stable":"c"})",
        R"({"emerging":true,"stable":"d"})",
    };
    std::vector<std::unique_ptr<RecordBatch>> batches;
    for (int32_t file_index = 0; file_index < 2; ++file_index) {
        auto left_begin = left_jsons.begin() + file_index * 2;
        std::vector<const char*> file_left_jsons(left_begin, left_begin + 2);
        auto right_begin = right_jsons.begin() + file_index * 2;
        std::vector<const char*> file_right_jsons(right_begin, right_begin + 2);
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> left,
                             VariantTestData::BuildVariantBatch(table_schema->field(0), left_field,
                                                                file_left_jsons, pool_,
                                                                /*id_offset=*/file_index * 2 + 1));
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> right,
                             VariantTestData::BuildVariantBatch(table_schema->field(0), right_field,
                                                                file_right_jsons, pool_,
                                                                /*id_offset=*/file_index * 2 + 1));
        std::shared_ptr<arrow::StructArray> rows =
            arrow::StructArray::Make({left->field(0), left->field(1), right->field(1)},
                                     table_schema->fields())
                .ValueOrDie();
        ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeBatch(rows));
        batches.push_back(std::move(batch));
    }
    ASSERT_OK(helper->WriteAndCommit(std::move(batches), /*commit_identifier=*/0,
                                     /*expected_commit_messages=*/std::nullopt));

    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));
    ASSERT_EQ(1, splits.size());
    std::shared_ptr<DataSplitImpl> data_split = std::dynamic_pointer_cast<DataSplitImpl>(splits[0]);
    ASSERT_NE(data_split, nullptr);
    std::vector<std::shared_ptr<DataFileMeta>> files = data_split->DataFiles();
    std::sort(
        files.begin(), files.end(),
        [](const std::shared_ptr<DataFileMeta>& left, const std::shared_ptr<DataFileMeta>& right) {
            return left->min_sequence_number < right->min_sequence_number;
        });
    ASSERT_EQ(2, files.size());

    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> first_left_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("legacy", arrow::utf8()), arrow::field("stable", arrow::int64())})));
    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> second_left_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("emerging", arrow::boolean()), arrow::field("legacy", arrow::utf8()),
             arrow::field("stable", arrow::int64())})));
    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> right_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("sparse", arrow::utf8()), arrow::field("stable", arrow::utf8())})));
    std::vector<std::shared_ptr<arrow::DataType>> expected_left_types = {first_left_type,
                                                                         second_left_type};
    for (size_t i = 0; i < files.size(); ++i) {
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Schema> file_schema,
                             ReadDataFileSchema(data_split->BucketPath(), files[i], options));
        std::shared_ptr<arrow::Field> file_left = file_schema->GetFieldByName("left_payload");
        std::shared_ptr<arrow::Field> file_right = file_schema->GetFieldByName("right_payload");
        ASSERT_NE(file_left, nullptr);
        ASSERT_NE(file_right, nullptr);
        ASSERT_TRUE(file_left->type()->Equals(*expected_left_types[i]))
            << "file=" << files[i]->file_name << ", actual=" << file_left->type()->ToString()
            << ", expected=" << expected_left_types[i]->ToString();
        ASSERT_TRUE(file_right->type()->Equals(*right_type))
            << "file=" << files[i]->file_name << ", actual=" << file_right->type()->ToString()
            << ", expected=" << right_type->ToString();
    }

    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result, helper->ReadResult(splits));
    std::map<int32_t, std::string> actual;
    for (const std::shared_ptr<arrow::Array>& chunk : result->chunks()) {
        auto rows = std::static_pointer_cast<arrow::StructArray>(chunk);
        auto row_type = std::static_pointer_cast<arrow::StructType>(rows->type());
        auto ids =
            std::static_pointer_cast<arrow::Int32Array>(rows->field(row_type->GetFieldIndex("id")));
        auto left = std::static_pointer_cast<arrow::StructArray>(
            rows->field(row_type->GetFieldIndex("left_payload")));
        auto right = std::static_pointer_cast<arrow::StructArray>(
            rows->field(row_type->GetFieldIndex("right_payload")));
        auto left_values = std::static_pointer_cast<arrow::BinaryArray>(left->field(0));
        auto left_metadata = std::static_pointer_cast<arrow::BinaryArray>(left->field(1));
        auto right_values = std::static_pointer_cast<arrow::BinaryArray>(right->field(0));
        auto right_metadata = std::static_pointer_cast<arrow::BinaryArray>(right->field(1));
        for (int64_t i = 0; i < rows->length(); ++i) {
            ASSERT_OK_AND_ASSIGN(
                std::shared_ptr<GenericVariant> left_variant,
                GenericVariant::Create(left_values->GetView(i), left_metadata->GetView(i), pool_));
            ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> right_variant,
                                 GenericVariant::Create(right_values->GetView(i),
                                                        right_metadata->GetView(i), pool_));
            ASSERT_OK_AND_ASSIGN(std::string left_json, left_variant->ToJson());
            ASSERT_OK_AND_ASSIGN(std::string right_json, right_variant->ToJson());
            actual.emplace(ids->Value(i), left_json + "|" + right_json);
        }
    }
    ASSERT_EQ(4, actual.size());
    for (int32_t id = 1; id <= 4; ++id) {
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> expected_left,
                             GenericVariant::FromJson(left_jsons[id - 1], pool_));
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> expected_right,
                             GenericVariant::FromJson(right_jsons[id - 1], pool_));
        ASSERT_OK_AND_ASSIGN(std::string expected_left_json, expected_left->ToJson());
        ASSERT_OK_AND_ASSIGN(std::string expected_right_json, expected_right->ToJson());
        ASSERT_EQ(expected_left_json + "|" + expected_right_json, actual[id]);
    }
}

TEST_F(VariantTableInteTest, TestAdaptiveInferenceWithNestedVariant) {
    std::shared_ptr<arrow::Field> nested_variant = VariantTypeUtils::ToArrowField("payload");
    std::shared_ptr<arrow::Field> nested_field = arrow::field(
        "nested", arrow::struct_({arrow::field("label", arrow::utf8()), nested_variant}));
    std::shared_ptr<arrow::Schema> table_schema =
        arrow::schema({arrow::field("id", arrow::int32()), nested_field});
    std::map<std::string, std::string> options =
        AdaptiveInferenceOptions(/*target_file_row_num=*/2, /*initial_sample_rows=*/2,
                                 /*adaptive_sample_rows=*/2, /*admission_ratio=*/0.4,
                                 /*retention_ratio=*/0.2);
    ASSERT_OK_AND_ASSIGN(
        std::unique_ptr<TestHelper> helper,
        TestHelper::Create(test_dir_, table_schema, /*partition_keys=*/{},
                           /*primary_keys=*/{}, options, /*is_streaming_mode=*/false));

    std::vector<const char*> labels = {"first", "second", "third", "fourth"};
    std::vector<const char*> jsons = {
        R"({"legacy":"a","stable":1})",
        R"({"legacy":"b","stable":2})",
        R"({"emerging":true,"stable":3})",
        R"({"emerging":false,"stable":4})",
    };
    std::vector<std::unique_ptr<RecordBatch>> batches;
    for (int32_t file_index = 0; file_index < 2; ++file_index) {
        auto json_begin = jsons.begin() + file_index * 2;
        std::vector<const char*> file_jsons(json_begin, json_begin + 2);
        ASSERT_OK_AND_ASSIGN(
            std::shared_ptr<arrow::StructArray> variants,
            VariantTestData::BuildVariantBatch(table_schema->field(0), nested_variant, file_jsons,
                                               pool_, /*id_offset=*/file_index * 2 + 1));
        arrow::StringBuilder label_builder;
        for (int32_t i = file_index * 2; i < file_index * 2 + 2; ++i) {
            arrow::Status status = label_builder.Append(labels[i]);
            ASSERT_TRUE(status.ok()) << status.ToString();
        }
        std::shared_ptr<arrow::Array> label_array = label_builder.Finish().ValueOrDie();
        std::shared_ptr<arrow::StructArray> nested =
            arrow::StructArray::Make({label_array, variants->field(1)},
                                     nested_field->type()->fields())
                .ValueOrDie();
        std::shared_ptr<arrow::StructArray> rows =
            arrow::StructArray::Make({variants->field(0), nested}, table_schema->fields())
                .ValueOrDie();
        ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeBatch(rows));
        batches.push_back(std::move(batch));
    }
    ASSERT_OK(helper->WriteAndCommit(std::move(batches), /*commit_identifier=*/0,
                                     /*expected_commit_messages=*/std::nullopt));

    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));
    ASSERT_EQ(1, splits.size());
    std::shared_ptr<DataSplitImpl> data_split = std::dynamic_pointer_cast<DataSplitImpl>(splits[0]);
    ASSERT_NE(data_split, nullptr);
    std::vector<std::shared_ptr<DataFileMeta>> files = data_split->DataFiles();
    std::sort(
        files.begin(), files.end(),
        [](const std::shared_ptr<DataFileMeta>& left, const std::shared_ptr<DataFileMeta>& right) {
            return left->min_sequence_number < right->min_sequence_number;
        });
    ASSERT_EQ(2, files.size());

    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> first_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("legacy", arrow::utf8()), arrow::field("stable", arrow::int64())})));
    ASSERT_OK_AND_ASSIGN(
        std::shared_ptr<arrow::DataType> second_type,
        VariantShreddingUtils::VariantShreddingSchema(arrow::struct_(
            {arrow::field("emerging", arrow::boolean()), arrow::field("legacy", arrow::utf8()),
             arrow::field("stable", arrow::int64())})));
    std::vector<std::shared_ptr<arrow::DataType>> expected_types = {first_type, second_type};
    for (size_t i = 0; i < files.size(); ++i) {
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Schema> file_schema,
                             ReadDataFileSchema(data_split->BucketPath(), files[i], options));
        std::shared_ptr<arrow::Field> file_nested = file_schema->GetFieldByName("nested");
        ASSERT_NE(file_nested, nullptr);
        auto file_nested_type = std::static_pointer_cast<arrow::StructType>(file_nested->type());
        std::shared_ptr<arrow::Field> file_variant = file_nested_type->GetFieldByName("payload");
        ASSERT_NE(file_variant, nullptr);
        ASSERT_TRUE(file_variant->type()->Equals(*expected_types[i]))
            << "file=" << files[i]->file_name << ", actual=" << file_variant->type()->ToString()
            << ", expected=" << expected_types[i]->ToString();
    }

    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result, helper->ReadResult(splits));
    std::map<int32_t, std::string> actual;
    for (const std::shared_ptr<arrow::Array>& chunk : result->chunks()) {
        auto rows = std::static_pointer_cast<arrow::StructArray>(chunk);
        auto row_type = std::static_pointer_cast<arrow::StructType>(rows->type());
        auto ids =
            std::static_pointer_cast<arrow::Int32Array>(rows->field(row_type->GetFieldIndex("id")));
        auto nested = std::static_pointer_cast<arrow::StructArray>(
            rows->field(row_type->GetFieldIndex("nested")));
        auto nested_type = std::static_pointer_cast<arrow::StructType>(nested->type());
        auto label_column = std::static_pointer_cast<arrow::StringArray>(
            nested->field(nested_type->GetFieldIndex("label")));
        auto variant = std::static_pointer_cast<arrow::StructArray>(
            nested->field(nested_type->GetFieldIndex("payload")));
        auto value_column = std::static_pointer_cast<arrow::BinaryArray>(variant->field(0));
        auto metadata_column = std::static_pointer_cast<arrow::BinaryArray>(variant->field(1));
        for (int64_t i = 0; i < rows->length(); ++i) {
            ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> value,
                                 GenericVariant::Create(value_column->GetView(i),
                                                        metadata_column->GetView(i), pool_));
            ASSERT_OK_AND_ASSIGN(std::string json, value->ToJson());
            actual.emplace(ids->Value(i), label_column->GetString(i) + "|" + json);
        }
    }
    ASSERT_EQ(4, actual.size());
    for (int32_t id = 1; id <= 4; ++id) {
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> expected,
                             GenericVariant::FromJson(jsons[id - 1], pool_));
        ASSERT_OK_AND_ASSIGN(std::string expected_json, expected->ToJson());
        ASSERT_EQ(std::string(labels[id - 1]) + "|" + expected_json, actual[id]);
    }
}

TEST_F(VariantTableInteTest, TestPrimaryKeyTable) {
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "1"},
    };
    ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, schema_, /*partition_keys=*/{},
                                                         /*primary_keys=*/{"id"}, options,
                                                         /*is_streaming_mode=*/true));
    ASSERT_OK_AND_ASSIGN(
        std::unique_ptr<RecordBatch> batch_1,
        MakeBatch(BuildArray({"{\"a\": 1}", "{\"b\": 2}", nullptr}, /*id_offset=*/0)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs_1,
                         helper->WriteAndCommit(std::move(batch_1), /*commit_identifier=*/0,
                                                /*expected_commit_messages=*/std::nullopt));
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_2,
                         MakeBatch(BuildArray({"{\"b\": \"updated\"}", "[42]"}, /*id_offset=*/1)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs_2,
                         helper->WriteAndCommit(std::move(batch_2), /*commit_identifier=*/1,
                                                /*expected_commit_messages=*/std::nullopt));
    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt));
    // The second batch overwrites ids 1 and 2, so the merged view holds three rows.
    ReadAndCheck(helper.get(), splits, {0, 1, 2}, {"{\"a\": 1}", R"({"b": "updated"})", "[42]"});
}

TEST_F(VariantTableInteTest, TestVariantAccessRead) {
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "-1"},
    };
    ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, schema_, /*partition_keys=*/{},
                                                         /*primary_keys=*/{}, options,
                                                         /*is_streaming_mode=*/false));
    std::vector<const char*> jsons = {R"({"age": 35, "city": "Chicago"})",
                                      R"({"age": 25, "other": "Hello"})", nullptr};
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeBatch(BuildArray(jsons)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs,
                         helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
                                                /*expected_commit_messages=*/std::nullopt));
    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));

    auto access_field =
        BuildAccessField({{arrow::int64(), "$.age"}, {arrow::utf8(), "$.other"}}, "v");
    auto read_schema = arrow::schema({fields_[0], access_field});
    std::shared_ptr<arrow::StructArray> result_struct;
    ReadWithSchema(helper.get(), splits, read_schema, &result_struct);
    ASSERT_EQ(result_struct->length(), 3);
    auto struct_type = std::static_pointer_cast<arrow::StructType>(result_struct->type());
    auto v_column = std::static_pointer_cast<arrow::StructArray>(
        result_struct->field(struct_type->GetFieldIndex("v")));
    const auto& age = static_cast<const arrow::Int64Array&>(*v_column->field(0));
    const auto& other = static_cast<const arrow::StringArray&>(*v_column->field(1));
    ASSERT_EQ(age.Value(0), 35);
    ASSERT_EQ(age.Value(1), 25);
    ASSERT_TRUE(v_column->IsNull(2));
    ASSERT_TRUE(other.IsNull(0));
    ASSERT_EQ(other.GetString(1), "Hello");
}

// The two tests below read a variant nested inside a ROW and inside an ARRAY column as a
// variant-access projection. Unlike the format-level tests they go through the whole table read
// path, where the read schema is resolved against the table schema before the read plans see it.
TEST_F(VariantTableInteTest, TestNestedRowVariantAccessRead) {
    // Table: [id, s: ROW<nv: VARIANT, t: STRING>]
    auto struct_field = arrow::field("s", arrow::struct_({VariantTypeUtils::ToArrowField("nv"),
                                                          arrow::field("t", arrow::utf8())}));
    auto table_schema = arrow::schema({fields_[0], struct_field});
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "-1"},
    };
    ASSERT_OK_AND_ASSIGN(auto helper,
                         TestHelper::Create(test_dir_, table_schema, /*partition_keys=*/{},
                                            /*primary_keys=*/{}, options,
                                            /*is_streaming_mode=*/false));

    std::vector<const char*> jsons = {R"({"age": 35, "city": "Chicago"})",
                                      R"({"age": 25, "other": "Hello"})", nullptr};
    auto variant_batch = BuildArray(jsons);
    arrow::StringBuilder sibling_builder;
    for (size_t i = 0; i < jsons.size(); ++i) {
        ASSERT_TRUE(sibling_builder.Append("t" + std::to_string(i)).ok());
    }
    std::shared_ptr<arrow::Array> sibling;
    ASSERT_TRUE(sibling_builder.Finish(&sibling).ok());
    auto struct_data = arrow::ArrayData::Make(
        struct_field->type(), static_cast<int64_t>(jsons.size()), {nullptr},
        {variant_batch->field(1)->data(), sibling->data()}, /*null_count=*/0);
    auto batch_data = arrow::ArrayData::Make(
        arrow::struct_({fields_[0], struct_field}), static_cast<int64_t>(jsons.size()), {nullptr},
        {variant_batch->field(0)->data(), struct_data}, /*null_count=*/0);
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
                         MakeBatch(std::make_shared<arrow::StructArray>(batch_data)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs,
                         helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
                                                /*expected_commit_messages=*/std::nullopt));
    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));

    auto access_field =
        BuildAccessField({{arrow::int64(), "$.age"}, {arrow::utf8(), "$.other"}}, "nv");
    auto read_schema = arrow::schema(
        {fields_[0],
         struct_field->WithType(arrow::struct_({access_field, struct_field->type()->field(1)}))});
    std::shared_ptr<arrow::StructArray> result_struct;
    ReadWithSchema(helper.get(), splits, read_schema, &result_struct);

    auto struct_type = std::static_pointer_cast<arrow::StructType>(result_struct->type());
    auto s_column = std::static_pointer_cast<arrow::StructArray>(
        result_struct->field(struct_type->GetFieldIndex("s")));
    const auto& nv = static_cast<const arrow::StructArray&>(*s_column->field(0));
    const auto& age = static_cast<const arrow::Int64Array&>(*nv.field(0));
    const auto& other = static_cast<const arrow::StringArray&>(*nv.field(1));
    const auto& kept_sibling = static_cast<const arrow::StringArray&>(*s_column->field(1));
    ASSERT_EQ(age.Value(0), 35);
    ASSERT_EQ(age.Value(1), 25);
    ASSERT_TRUE(nv.IsNull(2));
    ASSERT_TRUE(other.IsNull(0));
    ASSERT_EQ(other.GetString(1), "Hello");
    for (size_t i = 0; i < jsons.size(); ++i) {
        EXPECT_EQ(kept_sibling.GetString(static_cast<int64_t>(i)), "t" + std::to_string(i));
    }
}

TEST_F(VariantTableInteTest, TestArrayVariantAccessRead) {
    // Table: [id, arr: ARRAY<VARIANT>]
    auto list_field = arrow::field("arr", arrow::list(VariantTypeUtils::ToArrowField("element")));
    auto table_schema = arrow::schema({fields_[0], list_field});
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "-1"},
    };
    ASSERT_OK_AND_ASSIGN(auto helper,
                         TestHelper::Create(test_dir_, table_schema, /*partition_keys=*/{},
                                            /*primary_keys=*/{}, options,
                                            /*is_streaming_mode=*/false));

    // Row 0 holds two elements, row 1 is empty and row 2 holds one.
    std::vector<const char*> flat = {R"({"x": 1, "y": 2})", R"({"x": 3})", R"({"x": 5})"};
    std::vector<int32_t> offsets = {0, 2, 2, 3};
    arrow::Int32Builder offset_builder;
    ASSERT_TRUE(offset_builder.AppendValues(offsets).ok());
    std::shared_ptr<arrow::Array> offset_array;
    ASSERT_TRUE(offset_builder.Finish(&offset_array).ok());
    auto elements = BuildArray(flat);
    arrow::Int32Builder id_builder;
    ASSERT_TRUE(id_builder.AppendValues({0, 1, 2}).ok());
    std::shared_ptr<arrow::Array> ids;
    ASSERT_TRUE(id_builder.Finish(&ids).ok());
    auto list_data = arrow::ArrayData::Make(list_field->type(), /*length=*/3,
                                            {nullptr, offset_array->data()->buffers[1]},
                                            {elements->field(1)->data()}, /*null_count=*/0);
    auto batch_data = arrow::ArrayData::Make(arrow::struct_({fields_[0], list_field}), /*length=*/3,
                                             {nullptr}, {ids->data(), list_data}, /*null_count=*/0);
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
                         MakeBatch(std::make_shared<arrow::StructArray>(batch_data)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs,
                         helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
                                                /*expected_commit_messages=*/std::nullopt));
    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                         helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                                         /*is_streaming=*/false));

    auto access_field = BuildAccessField({{arrow::int64(), "$.x"}}, "element");
    auto read_schema = arrow::schema({fields_[0], list_field->WithType(arrow::list(access_field))});
    std::shared_ptr<arrow::StructArray> result_struct;
    ReadWithSchema(helper.get(), splits, read_schema, &result_struct);

    auto struct_type = std::static_pointer_cast<arrow::StructType>(result_struct->type());
    const auto& list = static_cast<const arrow::ListArray&>(
        *result_struct->field(struct_type->GetFieldIndex("arr")));
    ASSERT_EQ(list.length(), 3);
    ASSERT_EQ(list.value_length(0), 2);
    ASSERT_EQ(list.value_length(1), 0);
    ASSERT_EQ(list.value_length(2), 1);
    const auto& x = static_cast<const arrow::Int64Array&>(
        *static_cast<const arrow::StructArray&>(*list.values()).field(0));
    ASSERT_EQ(x.Value(list.value_offset(0)), 1);
    ASSERT_EQ(x.Value(list.value_offset(0) + 1), 3);
    ASSERT_EQ(x.Value(list.value_offset(2)), 5);
}

TEST_F(VariantTableInteTest, TestReadWithIOException) {
    // Injects an IO error at every position of the scan+read path and verifies each failure
    // surfaces as a clean error status.
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "-1"},
    };
    ASSERT_OK_AND_ASSIGN(
        auto helper, TestHelper::Create(test_dir_, schema_, /*partition_keys=*/{},
                                        /*primary_keys=*/{}, options, /*is_streaming_mode=*/false));
    std::vector<const char*> jsons = {R"({"age": 35, "city": "Hangzhou"})", nullptr, "[1, 2, 3]"};
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeBatch(BuildArray(jsons)));
    ASSERT_OK_AND_ASSIGN(auto commit_msgs,
                         helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
                                                /*expected_commit_messages=*/std::nullopt));

    bool run_complete = false;
    auto io_hook = IOHook::GetInstance();
    for (size_t i = 0; i < 500; i++) {
        ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
        io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
        Result<std::vector<std::shared_ptr<Split>>> splits =
            helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt,
                            /*is_streaming=*/false);
        CHECK_HOOK_STATUS(splits.status(), i);
        Result<std::shared_ptr<arrow::ChunkedArray>> read_result =
            helper->ReadResult(splits.value());
        CHECK_HOOK_STATUS(read_result.status(), i);
        run_complete = true;
        // All IO succeeded before the injected position was reached: check the data.
        io_hook->Clear();
        ReadAndCheck(helper.get(), splits.value(), {0, 1, 2}, jsons);
        break;
    }
    ASSERT_TRUE(run_complete);
}

TEST_F(VariantTableInteTest, TestWriteWithIOException) {
    // Injects an IO error at every position of the create+write+commit path (on a fresh table
    // directory per attempt) and verifies each failure surfaces as a clean error status.
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "parquet"},
        {Options::BUCKET, "-1"},
    };
    std::vector<const char*> jsons = {R"({"age": 35, "city": "Hangzhou"})", nullptr};
    bool run_complete = false;
    auto io_hook = IOHook::GetInstance();
    for (size_t i = 0; i < 500; i++) {
        std::string table_dir = test_dir_ + fmt::format("/io_exception_{}", i);
        ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
        io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
        Result<std::unique_ptr<TestHelper>> helper =
            TestHelper::Create(table_dir, schema_, /*partition_keys=*/{},
                               /*primary_keys=*/{}, options, /*is_streaming_mode=*/false);
        CHECK_HOOK_STATUS(helper.status(), i);
        Result<std::unique_ptr<RecordBatch>> batch = MakeBatch(BuildArray(jsons));
        CHECK_HOOK_STATUS(batch.status(), i);
        Result<std::vector<std::shared_ptr<CommitMessage>>> commit_msgs =
            helper.value()->WriteAndCommit(std::move(batch).value(), /*commit_identifier=*/0,
                                           /*expected_commit_messages=*/std::nullopt);
        CHECK_HOOK_STATUS(commit_msgs.status(), i);
        run_complete = true;
        // All IO succeeded before the injected position was reached: check the data.
        io_hook->Clear();
        ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
                             helper.value()->NewScan(StartupMode::LatestFull(),
                                                     /*snapshot_id=*/std::nullopt,
                                                     /*is_streaming=*/false));
        ReadAndCheck(helper.value().get(), splits, {0, 1}, jsons);
        break;
    }
    ASSERT_TRUE(run_complete);
}

TEST_F(VariantTableInteTest, TestOrcFormatRejected) {
    std::map<std::string, std::string> options = {
        {Options::MANIFEST_FORMAT, "avro"},
        {Options::FILE_FORMAT, "orc"},
        {Options::BUCKET, "-1"},
    };
    ASSERT_OK_AND_ASSIGN(
        auto helper, TestHelper::Create(test_dir_, schema_, /*partition_keys=*/{},
                                        /*primary_keys=*/{}, options, /*is_streaming_mode=*/false));
    ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeBatch(BuildArray({"{\"a\": 1}"})));
    auto result = helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
                                         /*expected_commit_messages=*/std::nullopt);
    ASSERT_FALSE(result.ok());
}

}  // namespace paimon::test
