Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions paimon-python/pypaimon/catalog/table_query_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
62 changes: 27 additions & 35 deletions paimon-python/pypaimon/tests/table_query_auth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,39 +149,30 @@ def test_masking_has_restrictions(self):
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)
Expand Down Expand Up @@ -420,10 +411,11 @@ def test_empty_filter_returns_none(self):
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
Expand Down