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 156b5511f5 [python] Refuse a blank query auth rule instead of skipping
it (#10189)
156b5511f5 is described below
commit 156b5511f5e9f344847b40cee75393f430bbebb6
Author: Jiajia Li <[email protected]>
AuthorDate: Sat Oct 3 17:11:07 2026 +0800
[python] Refuse a blank query auth rule instead of skipping it (#10189)
---
paimon-python/pypaimon/catalog/table_query_auth.py | 29 ++++++++--
.../pypaimon/tests/table_query_auth_test.py | 62 ++++++++++------------
2 files changed, 51 insertions(+), 40 deletions(-)
diff --git a/paimon-python/pypaimon/catalog/table_query_auth.py
b/paimon-python/pypaimon/catalog/table_query_auth.py
index fc82a07ffc..a1add858d0 100644
--- a/paimon-python/pypaimon/catalog/table_query_auth.py
+++ b/paimon-python/pypaimon/catalog/table_query_auth.py
@@ -100,11 +100,30 @@ def reject_search_under_query_auth(table) -> None:
class TableQueryAuthResult:
def __init__(self, filter: Optional[List[str]], column_masking:
Optional[Dict[str, str]]):
- self.filter = [f for f in filter if f] if filter else filter
- self.column_masking = (
- {k: v for k, v in column_masking.items() if k and v}
- if column_masking else column_masking
- )
+ # A blank rule is a malformed response, not the absence of one:
skipping it would read
+ # every row or return the column unmasked, so refuse the read as Java
does.
+ if filter is not None and not isinstance(filter, list):
+ raise ValueError(
+ "Row filter must be a list, not {}; refusing to read.".format(
+ type(filter).__name__))
+ if column_masking is not None and not isinstance(column_masking, dict):
+ raise ValueError(
+ "Column masking must be a map, not {}; refusing to
read.".format(
+ type(column_masking).__name__))
+ for rule in filter or []:
+ if not rule:
+ raise ValueError(
+ "Row filter cannot be empty; refusing to read rather than
returning "
+ "every row.")
+ for column, transform in (column_masking or {}).items():
+ if not column:
+ raise ValueError("Column masking target cannot be empty;
refusing to read.")
+ if not transform:
+ raise ValueError(
+ "Column masking on '{}' cannot be empty; refusing to read
rather than "
+ "returning the column unmasked.".format(column))
+ self.filter = filter
+ self.column_masking = column_masking
@property
def has_restrictions(self):
diff --git a/paimon-python/pypaimon/tests/table_query_auth_test.py
b/paimon-python/pypaimon/tests/table_query_auth_test.py
index 05deff7101..2db77f6b97 100644
--- a/paimon-python/pypaimon/tests/table_query_auth_test.py
+++ b/paimon-python/pypaimon/tests/table_query_auth_test.py
@@ -149,39 +149,30 @@ class
TestTableQueryAuthResultConvertPlan(unittest.TestCase):
result = TableQueryAuthResult(None, {"col": '{"name":"NULL"}'})
self.assertTrue(result.has_restrictions)
- def test_blank_filter_entries_are_skipped(self):
- result = TableQueryAuthResult(["", None], None)
- self.assertFalse(result.filter)
-
- def test_mixed_blank_and_valid_filter_entries(self):
- valid = _simple_filter_json()
- result = TableQueryAuthResult(["", valid], None)
- self.assertEqual(len(result.filter), 1)
- self.assertEqual(result.filter[0], valid)
-
- def test_blank_filter_no_extra_fields(self):
- result = TableQueryAuthResult([""], None)
- extra = result.get_extra_fields(
- [_FakeField("a")], [_FakeField("a"), _FakeField("b")])
- self.assertEqual(extra, [])
-
- def test_blank_filter_no_row_filter(self):
- result = TableQueryAuthResult(["", None], None)
- self.assertIsNone(result.extract_row_filter())
-
- def test_blank_column_masking_values_stripped(self):
- result = TableQueryAuthResult(None, {"col": "", "col3":
'{"name":"NULL"}'})
- self.assertEqual(list(result.column_masking.keys()), ["col3"])
-
- def test_blank_column_masking_keys_stripped(self):
- result = TableQueryAuthResult(None, {"": '{"name":"NULL"}'})
- self.assertEqual(result.column_masking, {})
-
- def test_blank_masking_returns_original_plan(self):
- result = TableQueryAuthResult(None, {"col": ""})
- plan = _FakePlan([_FakeSplit()])
- converted = result.convert_plan(plan)
- self.assertIs(converted, plan)
+ def test_blank_filter_entry_is_rejected(self):
+ # Skipping it would read every row.
+ for rules in (["", _simple_filter_json()], [None]):
+ with self.assertRaisesRegex(ValueError, "Row filter cannot be
empty"):
+ TableQueryAuthResult(rules, None)
+
+ def test_blank_column_masking_transform_is_rejected(self):
+ # Skipping it would return the column unmasked.
+ with self.assertRaisesRegex(ValueError, "Column masking on 'col'
cannot be empty"):
+ TableQueryAuthResult(None, {"col": "", "col3": '{"name":"NULL"}'})
+
+ def test_blank_column_masking_target_is_rejected(self):
+ with self.assertRaisesRegex(ValueError, "Column masking target cannot
be empty"):
+ TableQueryAuthResult(None, {"": '{"name":"NULL"}'})
+
+ def test_malformed_filter_is_rejected(self):
+ for rules in ({}, "", 0, False, {"a": "b"}):
+ with self.assertRaisesRegex(ValueError, "Row filter must be a
list"):
+ TableQueryAuthResult(rules, None)
+
+ def test_malformed_column_masking_is_rejected(self):
+ for masking in ([], "", 0, False, ['{"name":"NULL"}']):
+ with self.assertRaisesRegex(ValueError, "Column masking must be a
map"):
+ TableQueryAuthResult(None, masking)
def test_wraps_splits_with_filter(self):
result = TableQueryAuthResult([_simple_filter_json()], None)
@@ -420,10 +411,11 @@ class TestResolveAuthResult(unittest.TestCase):
fn = lambda select: TableQueryAuthResult([], {})
self.assertIsNone(resolve_auth_result(fn, None))
- def test_blank_filter_stripped_returns_none(self):
+ def test_blank_filter_fails_resolution(self):
from pypaimon.read.query_auth_split import resolve_auth_result
fn = lambda select: TableQueryAuthResult(["", None], None)
- self.assertIsNone(resolve_auth_result(fn, None))
+ with self.assertRaisesRegex(ValueError, "Row filter cannot be empty"):
+ resolve_auth_result(fn, None)
def test_with_filter_returns_result(self):
from pypaimon.read.query_auth_split import resolve_auth_result