|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | + * or more contributor license agreements. See the NOTICE file |
| 4 | + * distributed with this work for additional information |
| 5 | + * regarding copyright ownership. The ASF licenses this file |
| 6 | + * to you under the Apache License, Version 2.0 (the |
| 7 | + * "License"); you may not use this file except in compliance |
| 8 | + * with the License. You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | + |
| 20 | +// Integration tests for primary-key (KV) table changelog (CDC) scanning. |
| 21 | +// Mirrors crates/fluss/tests/integration/kv_changelog.rs. |
| 22 | + |
| 23 | +#include <gtest/gtest.h> |
| 24 | + |
| 25 | +#include <algorithm> |
| 26 | +#include <string> |
| 27 | +#include <utility> |
| 28 | +#include <vector> |
| 29 | + |
| 30 | +#include "test_utils.h" |
| 31 | + |
| 32 | +class KvChangelogTest : public ::testing::Test { |
| 33 | + protected: |
| 34 | + fluss::Admin& admin() { return fluss_test::FlussTestEnvironment::Instance()->GetAdmin(); } |
| 35 | + |
| 36 | + fluss::Connection& connection() { |
| 37 | + return fluss_test::FlussTestEnvironment::Instance()->GetConnection(); |
| 38 | + } |
| 39 | +}; |
| 40 | + |
| 41 | +// A record-mode scanner over a PK table yields its CDC changelog. With the |
| 42 | +// default FULL changelog image: inserting a new key emits +I, overwriting an |
| 43 | +// existing key emits -U (old image) then +U (new image), and a delete emits -D |
| 44 | +// (old image). A single bucket keeps the offsets contiguous. |
| 45 | +TEST_F(KvChangelogTest, SubscribeKvTableChangelog) { |
| 46 | + auto& adm = admin(); |
| 47 | + auto& conn = connection(); |
| 48 | + |
| 49 | + fluss::TablePath table_path("fluss", "test_kv_changelog_cpp"); |
| 50 | + |
| 51 | + auto schema = fluss::Schema::NewBuilder() |
| 52 | + .AddColumn("id", fluss::DataType::Int()) |
| 53 | + .AddColumn("name", fluss::DataType::String()) |
| 54 | + .SetPrimaryKeys({"id"}) |
| 55 | + .Build(); |
| 56 | + |
| 57 | + auto table_descriptor = fluss::TableDescriptor::NewBuilder() |
| 58 | + .SetSchema(schema) |
| 59 | + .SetBucketCount(1) |
| 60 | + .SetProperty("table.replication.factor", "1") |
| 61 | + .Build(); |
| 62 | + |
| 63 | + fluss_test::CreateTable(adm, table_path, table_descriptor); |
| 64 | + |
| 65 | + fluss::Table table; |
| 66 | + ASSERT_OK(conn.GetTable(table_path, table)); |
| 67 | + |
| 68 | + auto table_upsert = table.NewUpsert(); |
| 69 | + fluss::UpsertWriter upsert_writer; |
| 70 | + ASSERT_OK(table_upsert.CreateWriter(upsert_writer)); |
| 71 | + |
| 72 | + // Await each write so the changelog offsets are produced in a fixed order. |
| 73 | + { // +I (1, alice) |
| 74 | + fluss::GenericRow row(2); |
| 75 | + row.SetInt32(0, 1); |
| 76 | + row.SetString(1, "alice"); |
| 77 | + fluss::WriteResult wr; |
| 78 | + ASSERT_OK(upsert_writer.Upsert(row, wr)); |
| 79 | + ASSERT_OK(wr.Wait()); |
| 80 | + } |
| 81 | + { // +I (2, bob) |
| 82 | + fluss::GenericRow row(2); |
| 83 | + row.SetInt32(0, 2); |
| 84 | + row.SetString(1, "bob"); |
| 85 | + fluss::WriteResult wr; |
| 86 | + ASSERT_OK(upsert_writer.Upsert(row, wr)); |
| 87 | + ASSERT_OK(wr.Wait()); |
| 88 | + } |
| 89 | + { // overwrite id=1 -> -U (1, alice) then +U (1, alice2) |
| 90 | + fluss::GenericRow row(2); |
| 91 | + row.SetInt32(0, 1); |
| 92 | + row.SetString(1, "alice2"); |
| 93 | + fluss::WriteResult wr; |
| 94 | + ASSERT_OK(upsert_writer.Upsert(row, wr)); |
| 95 | + ASSERT_OK(wr.Wait()); |
| 96 | + } |
| 97 | + { // -D (2, bob) |
| 98 | + fluss::GenericRow del(2); |
| 99 | + del.SetInt32(0, 2); |
| 100 | + fluss::WriteResult wr; |
| 101 | + ASSERT_OK(upsert_writer.Delete(del, wr)); |
| 102 | + ASSERT_OK(wr.Wait()); |
| 103 | + } |
| 104 | + |
| 105 | + auto table_scan = table.NewScan(); |
| 106 | + fluss::LogScanner log_scanner; |
| 107 | + ASSERT_OK(table_scan.CreateLogScanner(log_scanner)); |
| 108 | + ASSERT_OK(log_scanner.Subscribe(0, fluss::EARLIEST_OFFSET)); |
| 109 | + |
| 110 | + struct Decoded { |
| 111 | + int64_t offset; |
| 112 | + fluss::ChangeType change_type; |
| 113 | + int32_t id; |
| 114 | + std::string name; |
| 115 | + }; |
| 116 | + |
| 117 | + std::vector<Decoded> records; |
| 118 | + fluss_test::PollRecords( |
| 119 | + log_scanner, 5, |
| 120 | + [](const fluss::ScanRecord& rec) { |
| 121 | + return Decoded{rec.offset, rec.change_type, rec.row.GetInt32(0), |
| 122 | + std::string(rec.row.GetString(1))}; |
| 123 | + }, |
| 124 | + records); |
| 125 | + |
| 126 | + ASSERT_EQ(records.size(), 5u); |
| 127 | + std::sort(records.begin(), records.end(), |
| 128 | + [](const Decoded& a, const Decoded& b) { return a.offset < b.offset; }); |
| 129 | + |
| 130 | + const std::vector<fluss::ChangeType> expected_types = { |
| 131 | + fluss::ChangeType::Insert, fluss::ChangeType::Insert, fluss::ChangeType::UpdateBefore, |
| 132 | + fluss::ChangeType::UpdateAfter, fluss::ChangeType::Delete}; |
| 133 | + const std::vector<std::pair<int32_t, std::string>> expected_rows = { |
| 134 | + {1, "alice"}, // +I |
| 135 | + {2, "bob"}, // +I |
| 136 | + {1, "alice"}, // -U (old image) |
| 137 | + {1, "alice2"}, // +U (new image) |
| 138 | + {2, "bob"}, // -D (old image) |
| 139 | + }; |
| 140 | + |
| 141 | + for (size_t i = 0; i < records.size(); ++i) { |
| 142 | + EXPECT_EQ(records[i].offset, static_cast<int64_t>(i)); |
| 143 | + EXPECT_EQ(static_cast<int>(records[i].change_type), static_cast<int>(expected_types[i])) |
| 144 | + << "change_type mismatch at " << i; |
| 145 | + EXPECT_EQ(records[i].id, expected_rows[i].first) << "id mismatch at " << i; |
| 146 | + EXPECT_EQ(records[i].name, expected_rows[i].second) << "name mismatch at " << i; |
| 147 | + } |
| 148 | + |
| 149 | + ASSERT_OK(adm.DropTable(table_path, false)); |
| 150 | +} |
| 151 | + |
| 152 | +// The Arrow batch scanner carries no per-record change types, so it rejects |
| 153 | +// primary-key tables (mirrors the core / Java restriction). |
| 154 | +TEST_F(KvChangelogTest, RecordBatchScannerRejectsPrimaryKey) { |
| 155 | + auto& adm = admin(); |
| 156 | + auto& conn = connection(); |
| 157 | + |
| 158 | + fluss::TablePath table_path("fluss", "test_kv_changelog_batch_reject_cpp"); |
| 159 | + |
| 160 | + auto schema = fluss::Schema::NewBuilder() |
| 161 | + .AddColumn("id", fluss::DataType::Int()) |
| 162 | + .AddColumn("name", fluss::DataType::String()) |
| 163 | + .SetPrimaryKeys({"id"}) |
| 164 | + .Build(); |
| 165 | + |
| 166 | + auto table_descriptor = fluss::TableDescriptor::NewBuilder() |
| 167 | + .SetSchema(schema) |
| 168 | + .SetProperty("table.replication.factor", "1") |
| 169 | + .Build(); |
| 170 | + |
| 171 | + fluss_test::CreateTable(adm, table_path, table_descriptor); |
| 172 | + |
| 173 | + fluss::Table table; |
| 174 | + ASSERT_OK(conn.GetTable(table_path, table)); |
| 175 | + |
| 176 | + auto table_scan = table.NewScan(); |
| 177 | + fluss::LogScanner batch_scanner; |
| 178 | + auto result = table_scan.CreateRecordBatchLogScanner(batch_scanner); |
| 179 | + EXPECT_FALSE(result.Ok()) << "batch scanner should reject a primary-key table"; |
| 180 | + |
| 181 | + ASSERT_OK(adm.DropTable(table_path, false)); |
| 182 | +} |
0 commit comments