blob: 3df3ca447496a13a4a24ba4af1e22f436334dd5d [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.
"""
import time
import pyarrow as pa
import pytest
import fluss
async def test_subscribe_kv_table_changelog(connection, admin, wait_for_table_ready):
"""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.
"""
table_path = fluss.TablePath("fluss", "py_test_kv_changelog")
await admin.drop_table(table_path, ignore_if_not_exists=True)
schema = fluss.Schema(
pa.schema([pa.field("id", pa.int32()), pa.field("name", pa.string())]),
primary_keys=["id"],
)
table_descriptor = fluss.TableDescriptor(schema, bucket_count=1)
await admin.create_table(table_path, table_descriptor, ignore_if_exists=False)
await wait_for_table_ready(table_path)
table = await connection.get_table(table_path)
writer = table.new_upsert().create_writer()
# Await each write so the changelog offsets are produced in a fixed order.
await writer.upsert({"id": 1, "name": "alice"}).wait() # +I
await writer.upsert({"id": 2, "name": "bob"}).wait() # +I
await writer.upsert({"id": 1, "name": "alice2"}).wait() # -U / +U
await writer.delete({"id": 2}).wait() # -D
scanner = await table.new_scan().create_log_scanner()
scanner.subscribe(bucket_id=0, start_offset=fluss.EARLIEST_OFFSET)
records = await _poll_records(scanner, expected_count=5)
assert len(records) == 5
records.sort(key=lambda r: r.offset)
assert [r.offset for r in records] == [0, 1, 2, 3, 4]
assert [r.change_type for r in records] == [
fluss.ChangeType.Insert,
fluss.ChangeType.Insert,
fluss.ChangeType.UpdateBefore,
fluss.ChangeType.UpdateAfter,
fluss.ChangeType.Delete,
]
assert [(r.row["id"], r.row["name"]) for r in records] == [
(1, "alice"), # +I
(2, "bob"), # +I
(1, "alice"), # -U (old image)
(1, "alice2"), # +U (new image)
(2, "bob"), # -D (old image)
]
await admin.drop_table(table_path, ignore_if_not_exists=False)
async def test_record_batch_scanner_rejects_primary_key(connection, admin):
"""The Arrow batch scanner carries no per-record change types, so it rejects
primary-key tables (mirrors the core / Java restriction)."""
table_path = fluss.TablePath("fluss", "py_test_kv_changelog_batch_reject")
await admin.drop_table(table_path, ignore_if_not_exists=True)
schema = fluss.Schema(
pa.schema([pa.field("id", pa.int32()), pa.field("name", pa.string())]),
primary_keys=["id"],
)
await admin.create_table(
table_path, fluss.TableDescriptor(schema), ignore_if_exists=False
)
table = await connection.get_table(table_path)
with pytest.raises(fluss.FlussError):
await table.new_scan().create_record_batch_log_scanner()
await admin.drop_table(table_path, ignore_if_not_exists=False)
async def _poll_records(scanner, expected_count, timeout_s=10):
"""Poll a record-based scanner until expected_count records are collected."""
collected = []
deadline = time.monotonic() + timeout_s
while len(collected) < expected_count and time.monotonic() < deadline:
records = await scanner.poll(5000)
collected.extend(records)
return collected