blob: 8be2aca885c708d7cb6776e5d4c712b7d21ea359 [file]
/*
* 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.
*/
// Integration tests for primary-key (KV) table changelog (CDC) scanning.
// Mirrors crates/fluss/tests/integration/kv_changelog.rs.
#include <gtest/gtest.h>
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "test_utils.h"
class KvChangelogTest : public ::testing::Test {
protected:
fluss::Admin& admin() { return fluss_test::FlussTestEnvironment::Instance()->GetAdmin(); }
fluss::Connection& connection() {
return fluss_test::FlussTestEnvironment::Instance()->GetConnection();
}
};
// A record-mode scanner over a PK table yields its CDC changelog. With the
// default FULL changelog image: inserting a new key emits +I, overwriting an
// existing key emits -U (old image) then +U (new image), and a delete emits -D
// (old image). A single bucket keeps the offsets contiguous.
TEST_F(KvChangelogTest, SubscribeKvTableChangelog) {
auto& adm = admin();
auto& conn = connection();
fluss::TablePath table_path("fluss", "test_kv_changelog_cpp");
auto schema = fluss::Schema::NewBuilder()
.AddColumn("id", fluss::DataType::Int())
.AddColumn("name", fluss::DataType::String())
.SetPrimaryKeys({"id"})
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
.SetBucketCount(1)
.SetProperty("table.replication.factor", "1")
.Build();
fluss_test::CreateTable(adm, table_path, table_descriptor);
fluss::Table table;
ASSERT_OK(conn.GetTable(table_path, table));
auto table_upsert = table.NewUpsert();
fluss::UpsertWriter upsert_writer;
ASSERT_OK(table_upsert.CreateWriter(upsert_writer));
// Await each write so the changelog offsets are produced in a fixed order.
{ // +I (1, alice)
fluss::GenericRow row(2);
row.SetInt32(0, 1);
row.SetString(1, "alice");
fluss::WriteResult wr;
ASSERT_OK(upsert_writer.Upsert(row, wr));
ASSERT_OK(wr.Wait());
}
{ // +I (2, bob)
fluss::GenericRow row(2);
row.SetInt32(0, 2);
row.SetString(1, "bob");
fluss::WriteResult wr;
ASSERT_OK(upsert_writer.Upsert(row, wr));
ASSERT_OK(wr.Wait());
}
{ // overwrite id=1 -> -U (1, alice) then +U (1, alice2)
fluss::GenericRow row(2);
row.SetInt32(0, 1);
row.SetString(1, "alice2");
fluss::WriteResult wr;
ASSERT_OK(upsert_writer.Upsert(row, wr));
ASSERT_OK(wr.Wait());
}
{ // -D (2, bob)
fluss::GenericRow del(2);
del.SetInt32(0, 2);
fluss::WriteResult wr;
ASSERT_OK(upsert_writer.Delete(del, wr));
ASSERT_OK(wr.Wait());
}
auto table_scan = table.NewScan();
fluss::LogScanner log_scanner;
ASSERT_OK(table_scan.CreateLogScanner(log_scanner));
ASSERT_OK(log_scanner.Subscribe(0, fluss::EARLIEST_OFFSET));
struct Decoded {
int64_t offset;
fluss::ChangeType change_type;
int32_t id;
std::string name;
};
std::vector<Decoded> records;
fluss_test::PollRecords(
log_scanner, 5,
[](const fluss::ScanRecord& rec) {
return Decoded{rec.offset, rec.change_type, rec.row.GetInt32(0),
std::string(rec.row.GetString(1))};
},
records);
ASSERT_EQ(records.size(), 5u);
std::sort(records.begin(), records.end(),
[](const Decoded& a, const Decoded& b) { return a.offset < b.offset; });
const std::vector<fluss::ChangeType> expected_types = {
fluss::ChangeType::Insert, fluss::ChangeType::Insert, fluss::ChangeType::UpdateBefore,
fluss::ChangeType::UpdateAfter, fluss::ChangeType::Delete};
const std::vector<std::pair<int32_t, std::string>> expected_rows = {
{1, "alice"}, // +I
{2, "bob"}, // +I
{1, "alice"}, // -U (old image)
{1, "alice2"}, // +U (new image)
{2, "bob"}, // -D (old image)
};
for (size_t i = 0; i < records.size(); ++i) {
EXPECT_EQ(records[i].offset, static_cast<int64_t>(i));
EXPECT_EQ(static_cast<int>(records[i].change_type), static_cast<int>(expected_types[i]))
<< "change_type mismatch at " << i;
EXPECT_EQ(records[i].id, expected_rows[i].first) << "id mismatch at " << i;
EXPECT_EQ(records[i].name, expected_rows[i].second) << "name mismatch at " << i;
}
ASSERT_OK(adm.DropTable(table_path, false));
}
// The Arrow batch scanner carries no per-record change types, so it rejects
// primary-key tables (mirrors the core / Java restriction).
TEST_F(KvChangelogTest, RecordBatchScannerRejectsPrimaryKey) {
auto& adm = admin();
auto& conn = connection();
fluss::TablePath table_path("fluss", "test_kv_changelog_batch_reject_cpp");
auto schema = fluss::Schema::NewBuilder()
.AddColumn("id", fluss::DataType::Int())
.AddColumn("name", fluss::DataType::String())
.SetPrimaryKeys({"id"})
.Build();
auto table_descriptor = fluss::TableDescriptor::NewBuilder()
.SetSchema(schema)
.SetProperty("table.replication.factor", "1")
.Build();
fluss_test::CreateTable(adm, table_path, table_descriptor);
fluss::Table table;
ASSERT_OK(conn.GetTable(table_path, table));
auto table_scan = table.NewScan();
fluss::LogScanner batch_scanner;
auto result = table_scan.CreateRecordBatchLogScanner(batch_scanner);
EXPECT_FALSE(result.Ok()) << "batch scanner should reject a primary-key table";
ASSERT_OK(adm.DropTable(table_path, false));
}