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 ac770292d6 [python] Reject vector and full-text search on a query-auth
table (#10027)
ac770292d6 is described below
commit ac770292d6a940f9d3984045b494cd7aa7606afd
Author: Jiajia Li <[email protected]>
AuthorDate: Mon Sep 21 10:39:07 2026 +0800
[python] Reject vector and full-text search on a query-auth table (#10027)
---
paimon-python/pypaimon/catalog/table_query_auth.py | 11 +++
.../table/source/batch_vector_search_builder.py | 2 +
.../table/source/full_text_search_builder.py | 3 +
.../pypaimon/table/source/hybrid_search_builder.py | 2 +
.../pypaimon/table/source/vector_search_builder.py | 3 +
.../tests/search_query_auth_reject_test.py | 96 ++++++++++++++++++++++
6 files changed, 117 insertions(+)
diff --git a/paimon-python/pypaimon/catalog/table_query_auth.py
b/paimon-python/pypaimon/catalog/table_query_auth.py
index 5a546d2848..523c42742d 100644
--- a/paimon-python/pypaimon/catalog/table_query_auth.py
+++ b/paimon-python/pypaimon/catalog/table_query_auth.py
@@ -30,6 +30,17 @@ from pypaimon.read.query_auth_split import QueryAuthSplit
from pypaimon.schema.data_types import DataField
+def reject_search_under_query_auth(table) -> None:
+ """Refuses a search on a query-auth table. Called from the methods that
build a scan or a
+ read rather than from the builder constructors, which a deserialized
builder skips."""
+ from pypaimon.table.file_store_table import FileStoreTable
+
+ if isinstance(table, FileStoreTable) and table.options.query_auth_enabled:
+ raise ValueError(
+ "Search is not supported on a query-auth table: the index ranks
raw values, "
+ "which a column mask invalidates.")
+
+
class TableQueryAuthResult:
def __init__(self, filter: Optional[List[str]], column_masking:
Optional[Dict[str, str]]):
diff --git a/paimon-python/pypaimon/table/source/batch_vector_search_builder.py
b/paimon-python/pypaimon/table/source/batch_vector_search_builder.py
index ce2f3ed9d4..b07cd1e835 100644
--- a/paimon-python/pypaimon/table/source/batch_vector_search_builder.py
+++ b/paimon-python/pypaimon/table/source/batch_vector_search_builder.py
@@ -19,6 +19,7 @@
from abc import ABC, abstractmethod
+from pypaimon.catalog.table_query_auth import reject_search_under_query_auth
from pypaimon.table.source.vector_search_builder import (
AbstractVectorSearchBuilderImpl,
)
@@ -110,6 +111,7 @@ class
BatchVectorSearchBuilderImpl(AbstractVectorSearchBuilderImpl,
def new_batch_vector_search_read(self):
# type: () -> BatchVectorSearchRead
+ reject_search_under_query_auth(self._table)
if self._limit <= 0:
raise ValueError("Limit must be positive, set via with_limit()")
if self._vector_column is None:
diff --git a/paimon-python/pypaimon/table/source/full_text_search_builder.py
b/paimon-python/pypaimon/table/source/full_text_search_builder.py
index 1e094e543f..420f918ac5 100644
--- a/paimon-python/pypaimon/table/source/full_text_search_builder.py
+++ b/paimon-python/pypaimon/table/source/full_text_search_builder.py
@@ -20,6 +20,7 @@
from abc import ABC, abstractmethod
from typing import Optional
+from pypaimon.catalog.table_query_auth import reject_search_under_query_auth
from pypaimon.common.predicate_builder import PredicateBuilder
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.table.source.full_text_read import FullTextRead,
DataEvolutionFullTextRead
@@ -118,6 +119,7 @@ class FullTextSearchBuilderImpl(FullTextSearchBuilder):
return predicate.new_index(name_to_idx[predicate.field])
def new_full_text_scan(self) -> FullTextScan:
+ reject_search_under_query_auth(self._table)
definition = self._primary_key_full_text_definition()
if definition is not None:
from pypaimon.table.source.primary_key_full_text_scan import
PrimaryKeyFullTextScan
@@ -130,6 +132,7 @@ class FullTextSearchBuilderImpl(FullTextSearchBuilder):
)
def new_full_text_read(self) -> FullTextRead:
+ reject_search_under_query_auth(self._table)
if self._limit <= 0:
raise ValueError("Limit must be positive, set via with_limit()")
definition = self._primary_key_full_text_definition()
diff --git a/paimon-python/pypaimon/table/source/hybrid_search_builder.py
b/paimon-python/pypaimon/table/source/hybrid_search_builder.py
index a3e43c3fb6..01736816db 100644
--- a/paimon-python/pypaimon/table/source/hybrid_search_builder.py
+++ b/paimon-python/pypaimon/table/source/hybrid_search_builder.py
@@ -25,6 +25,7 @@ from concurrent.futures import FIRST_EXCEPTION,
ThreadPoolExecutor, wait
from dataclasses import dataclass, field
from typing import Dict, List, Optional
+from pypaimon.catalog.table_query_auth import reject_search_under_query_auth
from pypaimon.common.predicate_builder import PredicateBuilder
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.globalindex.vector_search_result import (
@@ -312,6 +313,7 @@ class HybridSearchBuilderImpl(HybridSearchBuilder):
return self
def route_builders(self) -> List[HybridSearchRouteBuilder]:
+ reject_search_under_query_auth(self._table)
self._validate_search()
from pypaimon.snapshot.time_travel_util import TimeTravelUtil
execution = copy(self)
diff --git a/paimon-python/pypaimon/table/source/vector_search_builder.py
b/paimon-python/pypaimon/table/source/vector_search_builder.py
index af823e58c2..2c7e309a3f 100644
--- a/paimon-python/pypaimon/table/source/vector_search_builder.py
+++ b/paimon-python/pypaimon/table/source/vector_search_builder.py
@@ -19,6 +19,7 @@
from abc import ABC, abstractmethod
+from pypaimon.catalog.table_query_auth import reject_search_under_query_auth
from pypaimon.common.predicate_builder import PredicateBuilder
from pypaimon.table.source.vector_search_read import DataEvolutionVectorRead
from pypaimon.table.source.vector_search_scan import DataEvolutionVectorScan
@@ -215,6 +216,7 @@ class AbstractVectorSearchBuilderImpl:
def new_vector_search_scan(self):
# type: () -> VectorSearchScan
+ reject_search_under_query_auth(self._table)
if self._vector_column is None:
raise ValueError("Vector column must be set via
with_vector_column()")
scan_class = DataEvolutionVectorScan
@@ -250,6 +252,7 @@ class
VectorSearchBuilderImpl(AbstractVectorSearchBuilderImpl, VectorSearchBuild
def new_vector_search_read(self):
# type: () -> VectorSearchRead
+ reject_search_under_query_auth(self._table)
if self._limit <= 0:
raise ValueError("Limit must be positive, set via with_limit()")
if self._vector_column is None:
diff --git a/paimon-python/pypaimon/tests/search_query_auth_reject_test.py
b/paimon-python/pypaimon/tests/search_query_auth_reject_test.py
new file mode 100644
index 0000000000..ddc3cea515
--- /dev/null
+++ b/paimon-python/pypaimon/tests/search_query_auth_reject_test.py
@@ -0,0 +1,96 @@
+################################################################################
+# 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.
+################################################################################
+
+import os
+import shutil
+import tempfile
+import unittest
+
+import pyarrow as pa
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.table.source.batch_vector_search_builder import (
+ BatchVectorSearchBuilderImpl,
+)
+from pypaimon.table.source.full_text_search_builder import
FullTextSearchBuilderImpl
+from pypaimon.table.source.hybrid_search_builder import HybridSearchBuilderImpl
+from pypaimon.table.source.vector_search_builder import VectorSearchBuilderImpl
+
+REJECTED = "Search is not supported on a query-auth table"
+
+
+def _entry_points(table):
+ return {
+ "vector scan": VectorSearchBuilderImpl(table).new_vector_search_scan,
+ "vector read": VectorSearchBuilderImpl(table).new_vector_search_read,
+ "batch vector scan": BatchVectorSearchBuilderImpl(
+ table).new_vector_search_scan,
+ "batch vector read": BatchVectorSearchBuilderImpl(
+ table).new_batch_vector_search_read,
+ "full-text scan": FullTextSearchBuilderImpl(table).new_full_text_scan,
+ "full-text read": FullTextSearchBuilderImpl(table).new_full_text_read,
+ "hybrid routes": HybridSearchBuilderImpl(table).route_builders,
+ }
+
+
+class TestSearchRejectedUnderQueryAuth(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.tempdir = tempfile.mkdtemp()
+ cls.catalog = CatalogFactory.create(
+ {'warehouse': os.path.join(cls.tempdir, 'warehouse')})
+ cls.catalog.create_database('db', False)
+ pa_schema = pa.schema([
+ ('id', pa.int32()),
+ ('embedding', pa.list_(pa.float32())),
+ ('text', pa.string()),
+ ])
+ cls.catalog.create_table(
+ 'db.plain', Schema.from_pyarrow_schema(pa_schema), False)
+ cls.catalog.create_table(
+ 'db.authed',
+ Schema.from_pyarrow_schema(
+ pa_schema, options={'query-auth.enabled': 'true'}),
+ False)
+ cls.plain = cls.catalog.get_table('db.plain')
+ cls.authed = cls.catalog.get_table('db.authed')
+
+ @classmethod
+ def tearDownClass(cls):
+ shutil.rmtree(cls.tempdir, ignore_errors=True)
+
+ def test_every_scan_and_read_entry_point_is_rejected(self):
+ self.assertTrue(self.authed.options.query_auth_enabled)
+ for name, entry in _entry_points(self.authed).items():
+ with self.subTest(entry=name):
+ with self.assertRaises(ValueError) as ctx:
+ entry()
+ self.assertIn(REJECTED, str(ctx.exception))
+
+ def test_without_query_auth_the_builders_reach_their_own_validation(self):
+ self.assertFalse(self.plain.options.query_auth_enabled)
+ for name, entry in _entry_points(self.plain).items():
+ with self.subTest(entry=name):
+ with self.assertRaises(ValueError) as ctx:
+ entry()
+ self.assertNotIn(REJECTED, str(ctx.exception))
+
+
+if __name__ == '__main__':
+ unittest.main()