This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new b17bbde9b6 [python] Align row tracking and data evolution schema
validation with… (#10527)
b17bbde9b6 is described below
commit b17bbde9b6351b97b95d339844279d5f1e694b0f
Author: zhuanshenbsj1 <[email protected]>
AuthorDate: Wed Oct 14 09:58:13 2026 +0800
[python] Align row tracking and data evolution schema validation with…
(#10527)
---
paimon-python/pypaimon/schema/schema_manager.py | 20 +++++
.../pypaimon/tests/daft/daft_sink_test.py | 3 -
.../pypaimon/tests/global_index_build_test.py | 11 +--
.../tests/manifest/manifest_sidecar_test.py | 1 +
.../pypaimon/tests/multimodal_table_test.py | 17 ++--
.../tests/ray_data_evolution_merge_into_test.py | 14 ++--
.../pypaimon/tests/schema_manager_test.py | 91 ++++++++++++++++++++++
7 files changed, 131 insertions(+), 26 deletions(-)
diff --git a/paimon-python/pypaimon/schema/schema_manager.py
b/paimon-python/pypaimon/schema/schema_manager.py
index 950a2e308b..17953fd33a 100644
--- a/paimon-python/pypaimon/schema/schema_manager.py
+++ b/paimon-python/pypaimon/schema/schema_manager.py
@@ -483,6 +483,25 @@ def _validate_options(options: dict):
)
+def _validate_row_tracking(options: dict, primary_keys: List[str]):
+ """Mirror the row tracking checks of Java
SchemaValidation#validateRowTracking."""
+ core_options = CoreOptions(Options(options or {}))
+ row_tracking_enabled = core_options.row_tracking_enabled()
+ if row_tracking_enabled:
+ if primary_keys:
+ raise ValueError("Cannot define primary-key for row tracking
table.")
+ if core_options.bucket() != -1:
+ raise ValueError(
+ f"Cannot define {CoreOptions.BUCKET.key()} for row tracking
table, "
+ f"it only support bucket = -1"
+ )
+ if core_options.data_evolution_enabled() and not row_tracking_enabled:
+ raise ValueError(
+ f"Data evolution config must enabled with "
+ f"{CoreOptions.ROW_TRACKING_ENABLED.key()}"
+ )
+
+
def _contains_blob_type(data_type: DataType) -> bool:
if is_blob_type(data_type):
return True
@@ -674,6 +693,7 @@ class SchemaManager:
new_schema.primary_keys,
new_schema.partition_keys,
)
+ _validate_row_tracking(new_schema.options, new_schema.primary_keys)
schema_path = self._to_schema_path(new_schema.id)
try:
result = self.file_io.try_to_write_atomic(schema_path,
JSON.to_json(new_schema, indent=2))
diff --git a/paimon-python/pypaimon/tests/daft/daft_sink_test.py
b/paimon-python/pypaimon/tests/daft/daft_sink_test.py
index bf14da8a28..326264e7cf 100644
--- a/paimon-python/pypaimon/tests/daft/daft_sink_test.py
+++ b/paimon-python/pypaimon/tests/daft/daft_sink_test.py
@@ -548,7 +548,6 @@ class TestBlobType:
paimon_schema = pypaimon.Schema.from_pyarrow_schema(
pa_schema,
options={
- "bucket": "1",
"file.format": "parquet",
"row-tracking.enabled": "true",
"data-evolution.enabled": "true",
@@ -588,7 +587,6 @@ class TestBlobType:
paimon_schema = pypaimon.Schema.from_pyarrow_schema(
pa_schema,
options={
- "bucket": "1",
"file.format": "parquet",
"row-tracking.enabled": "true",
"data-evolution.enabled": "true",
@@ -641,7 +639,6 @@ class TestBlobType:
paimon_schema = pypaimon.Schema.from_pyarrow_schema(
pa_schema,
options={
- "bucket": "1",
"file.format": "parquet",
"row-tracking.enabled": "true",
"data-evolution.enabled": "true",
diff --git a/paimon-python/pypaimon/tests/global_index_build_test.py
b/paimon-python/pypaimon/tests/global_index_build_test.py
index cd6c21bb55..687f45fcd9 100644
--- a/paimon-python/pypaimon/tests/global_index_build_test.py
+++ b/paimon-python/pypaimon/tests/global_index_build_test.py
@@ -1075,15 +1075,12 @@ class GlobalIndexBuildTest(
('id', pa.int32()),
('embedding', pa.list_(pa.float32())),
])
+ # Row tracking tables with a fixed bucket are rejected at schema
validation,
+ # so they can no longer reach the global index builder.
bucket_options = dict(self.table_options)
bucket_options['bucket'] = '1'
- bucket_table = self._create_table(pa_schema=schema,
options=bucket_options)
- with self.assertRaisesRegex(ValueError, 'unaware-bucket'):
- bucket_table.create_global_index(
- 'embedding',
- index_type='ivf-flat',
- options={'ivf-flat.dimension': '2'},
- )
+ with self.assertRaisesRegex(ValueError, 'it only support bucket = -1'):
+ self._create_table(pa_schema=schema, options=bucket_options)
dv_options = dict(self.table_options)
dv_options['deletion-vectors.enabled'] = 'true'
diff --git a/paimon-python/pypaimon/tests/manifest/manifest_sidecar_test.py
b/paimon-python/pypaimon/tests/manifest/manifest_sidecar_test.py
index 868b1518cd..cc8da04d42 100644
--- a/paimon-python/pypaimon/tests/manifest/manifest_sidecar_test.py
+++ b/paimon-python/pypaimon/tests/manifest/manifest_sidecar_test.py
@@ -568,6 +568,7 @@ class
ManifestSidecarScanTest(existing.ManifestEntryIdentifierTest):
pa.schema([('p', pa.int32()), ('q', pa.string()), ('value',
pa.string())]),
partition_keys=['p', 'q'],
options={'manifest.sidecar.enabled': 'true',
+ 'row-tracking.enabled': 'true',
'data-evolution.enabled': 'true'})
self.catalog.create_table('default.explain_sidecar', schema, False)
self.table = self.catalog.get_table('default.explain_sidecar')
diff --git a/paimon-python/pypaimon/tests/multimodal_table_test.py
b/paimon-python/pypaimon/tests/multimodal_table_test.py
index 13d306269c..629fffbca0 100644
--- a/paimon-python/pypaimon/tests/multimodal_table_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_table_test.py
@@ -29,6 +29,7 @@ import pyarrow as pa
import pytest
import pypaimon.multimodal as pmm
from pypaimon.multimodal import source_col
+from pypaimon.common.json_util import JSON
from pypaimon.common.predicate_builder import PredicateBuilder
from pypaimon import Schema as PaimonSchema
from pypaimon.globalindex.global_index_result import GlobalIndexResult
@@ -1189,13 +1190,15 @@ class MultimodalTableTest(unittest.TestCase):
def test_get_table_rejects_primary_key_table(self):
self.conn.catalog.create_database("default", ignore_if_exists=True)
self.conn.catalog.create_table(
- "default.pk",
- _raw_schema(
- options=dict(_PARQUET_OPTIONS, **{"bucket": "1"}),
- primary_keys=["id"],
- ),
- False,
- )
+ "default.pk", _raw_schema(options=_PARQUET_OPTIONS), False)
+ # Row tracking tables cannot define primary keys, so such a table can
+ # only come from writers that skipped schema validation. Simulate it
+ # by rewriting the committed schema file.
+ table = self.conn.catalog.get_table("default.pk")
+ legacy_schema = replace(table.table_schema, primary_keys=["id"])
+ table.file_io.overwrite_file_utf8(
+ table.schema_manager._to_schema_path(legacy_schema.id),
+ JSON.to_json(legacy_schema, indent=2))
with self.assertRaisesRegex(ValueError, "primary keys"):
self.conn.get_table("pk")
diff --git a/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
b/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
index 84fdbd0d89..461408495b 100644
--- a/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
+++ b/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
@@ -441,16 +441,12 @@ class RayDataEvolutionMergeIntoTest(unittest.TestCase):
self.assertIn('data-evolution.enabled', str(ctx.exception))
def test_no_row_tracking_rejected(self):
- target = self._create_table(options={'data-evolution.enabled': 'true'})
+ # Data evolution requires row tracking. Schema validation now rejects
+ # this combination at table creation (aligned with Java
+ # SchemaValidation#validateRowTracking), so the table can no longer be
+ # created and merge_into never gets a chance to run.
with self.assertRaises(ValueError) as ctx:
- merge_into(
- target=target,
- source=self._source(),
- catalog_options=self.catalog_options,
- on=['id'],
- when_matched=[WhenMatched.update('*')],
- num_partitions=_TEST_NUM_PARTITIONS,
- )
+ self._create_table(options={'data-evolution.enabled': 'true'})
self.assertIn('row-tracking.enabled', str(ctx.exception))
def test_source_missing_on_col_raises(self):
diff --git a/paimon-python/pypaimon/tests/schema_manager_test.py
b/paimon-python/pypaimon/tests/schema_manager_test.py
index f58d8876c5..dd73d9142d 100644
--- a/paimon-python/pypaimon/tests/schema_manager_test.py
+++ b/paimon-python/pypaimon/tests/schema_manager_test.py
@@ -295,5 +295,96 @@ class TestOptionValidation(unittest.TestCase):
self.assertNotIn(key, manager.latest().options)
+class TestRowTrackingValidation(unittest.TestCase):
+ """Row tracking tables must follow Java
SchemaValidation#validateRowTracking."""
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp()
+ self.file_io = FileIO.get(self.temp_dir, Options({}))
+ self.table_path = f"{self.temp_dir}/test_db.db/test_table"
+
+ def tearDown(self):
+ import shutil
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+ @staticmethod
+ def _schema(options=None, primary_keys=None):
+ return Schema(
+ fields=[
+ DataField(0, "id", AtomicType("INT", False)),
+ DataField(1, "name", AtomicType("STRING")),
+ ],
+ primary_keys=primary_keys or [],
+ options=options or {},
+ )
+
+ def test_create_rejects_row_tracking_with_fixed_or_postpone_bucket(self):
+ manager = SchemaManager(self.file_io, self.table_path)
+ for bucket in ["1", "16", "-2"]:
+ with self.subTest(bucket=bucket):
+ with self.assertRaisesRegex(
+ ValueError,
+ "Cannot define bucket for row tracking table, "
+ "it only support bucket = -1",
+ ):
+ manager.create_table(self._schema({
+ "row-tracking.enabled": "true",
+ "bucket": bucket,
+ }))
+ self.assertIsNone(manager.latest())
+
+ def test_create_rejects_row_tracking_with_primary_key(self):
+ manager = SchemaManager(self.file_io, self.table_path)
+ with self.assertRaisesRegex(
+ ValueError, "Cannot define primary-key for row tracking table"
+ ):
+ manager.create_table(self._schema(
+ {"row-tracking.enabled": "true"}, primary_keys=["id"]))
+ self.assertIsNone(manager.latest())
+
+ def test_create_allows_row_tracking_with_unaware_bucket(self):
+ for options in [
+ {"row-tracking.enabled": "true"},
+ {"row-tracking.enabled": "true", "bucket": "-1"},
+ {"row-tracking.enabled": "true", "data-evolution.enabled": "true"},
+ ]:
+ with self.subTest(options=options):
+ temp_dir = tempfile.mkdtemp(dir=self.temp_dir)
+ manager = SchemaManager(self.file_io, f"{temp_dir}/t")
+ manager.create_table(self._schema(options))
+ self.assertEqual(manager.latest().id, 0)
+
+ def test_create_rejects_data_evolution_without_row_tracking(self):
+ manager = SchemaManager(self.file_io, self.table_path)
+ for options in [
+ {"data-evolution.enabled": "true"},
+ {"data-evolution.enabled": "true", "row-tracking.enabled":
"false"},
+ ]:
+ with self.subTest(options=options):
+ with self.assertRaisesRegex(
+ ValueError,
+ "Data evolution config must enabled with
row-tracking.enabled",
+ ):
+ manager.create_table(self._schema(options))
+ self.assertIsNone(manager.latest())
+
+ def test_create_allows_fixed_bucket_without_row_tracking(self):
+ manager = SchemaManager(self.file_io, self.table_path)
+ manager.create_table(self._schema({"bucket": "4"},
primary_keys=["id"]))
+ self.assertEqual(manager.latest().id, 0)
+
+ def test_alter_rejects_enabling_row_tracking_on_fixed_bucket_table(self):
+ manager = SchemaManager(self.file_io, self.table_path)
+ manager.create_table(self._schema({"bucket": "4"},
primary_keys=["id"]))
+
+ with self.assertRaisesRegex(
+ RuntimeError, "Cannot define primary-key for row tracking table"
+ ):
+ manager.commit_changes([SetOption("row-tracking.enabled", "true")])
+
+ self.assertEqual(manager.latest().id, 0)
+ self.assertNotIn("row-tracking.enabled", manager.latest().options)
+
+
if __name__ == '__main__':
unittest.main()