This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 0ddd3a5  [api] Add authTableQuery method to RESTApi (#498)
0ddd3a5 is described below

commit 0ddd3a5af5b2143ac1e12e06607bd313ed467e53
Author: Jiajia Li <[email protected]>
AuthorDate: Mon Jul 13 13:10:45 2026 +0800

    [api] Add authTableQuery method to RESTApi (#498)
---
 crates/paimon/src/api/api_request.rs    | 27 ++++++++++++++++++++
 crates/paimon/src/api/api_response.rs   | 45 +++++++++++++++++++++++++++++++++
 crates/paimon/src/api/mod.rs            | 12 ++++-----
 crates/paimon/src/api/resource_paths.rs | 14 ++++++++++
 crates/paimon/src/api/rest_api.rs       | 25 ++++++++++++++----
 5 files changed, 112 insertions(+), 11 deletions(-)

diff --git a/crates/paimon/src/api/api_request.rs 
b/crates/paimon/src/api/api_request.rs
index 018673c..84ee7c5 100644
--- a/crates/paimon/src/api/api_request.rs
+++ b/crates/paimon/src/api/api_request.rs
@@ -167,6 +167,21 @@ impl AlterTableRequest {
     }
 }
 
+/// Request for auth table query: the projected columns of the query.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct AuthTableQueryRequest {
+    /// Projected column names; `None` means all columns.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub select: Option<Vec<String>>,
+}
+
+impl AuthTableQueryRequest {
+    /// Create a new AuthTableQueryRequest.
+    pub fn new(select: Option<Vec<String>>) -> Self {
+        Self { select }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -193,6 +208,18 @@ mod tests {
         assert!(json.contains("\"updates\""));
     }
 
+    #[test]
+    fn test_auth_table_query_request_serialization() {
+        let req = AuthTableQueryRequest::new(Some(vec!["a".to_string(), 
"b".to_string()]));
+        assert_eq!(
+            serde_json::to_string(&req).unwrap(),
+            r#"{"select":["a","b"]}"#
+        );
+        // `None` omits the key entirely (matches the server's optional field).
+        let req = AuthTableQueryRequest::new(None);
+        assert_eq!(serde_json::to_string(&req).unwrap(), "{}");
+    }
+
     #[test]
     fn test_rename_table_request_serialization() {
         let source = Identifier::new("db1".to_string(), "table1".to_string());
diff --git a/crates/paimon/src/api/api_response.rs 
b/crates/paimon/src/api/api_response.rs
index 9586c7f..0e31137 100644
--- a/crates/paimon/src/api/api_response.rs
+++ b/crates/paimon/src/api/api_response.rs
@@ -427,10 +427,55 @@ pub struct GetTableTokenResponse {
     pub expires_at_millis: Option<i64>,
 }
 
+/// Response for auth table query: the per-user row filter and column masking 
the
+/// client must enforce at read time for a `query-auth.enabled` table.
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AuthTableQueryResponse {
+    /// JSON-serialized row-filter predicates, ANDed together. Empty/None = no 
filter.
+    pub filter: Option<Vec<String>>,
+    /// column name -> JSON-serialized masking transform. Empty/None = no 
masking.
+    pub column_masking: Option<HashMap<String, String>>,
+}
+
+impl AuthTableQueryResponse {
+    /// True when the server imposes no row filter and no column masking.
+    pub fn is_unrestricted(&self) -> bool {
+        self.filter.as_ref().is_none_or(|f| f.is_empty())
+            && self.column_masking.as_ref().is_none_or(|m| m.is_empty())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
 
+    #[test]
+    fn test_auth_table_query_response_deserialization() {
+        // A restricted grant: pin the exact wire field names. A drift in 
either
+        // (`filter` / `columnMasking`) would deserialize to None and silently
+        // skip authorization, so this test fails closed against that.
+        let resp: AuthTableQueryResponse =
+            
serde_json::from_str(r#"{"filter":["p0","p1"],"columnMasking":{"ssn":"m0"}}"#).unwrap();
+        assert_eq!(resp.filter, Some(vec!["p0".to_string(), 
"p1".to_string()]));
+        assert_eq!(
+            resp.column_masking,
+            Some(HashMap::from([("ssn".to_string(), "m0".to_string())]))
+        );
+        assert!(!resp.is_unrestricted());
+
+        // The real server sends `{}` for an unrestricted grant (both fields 
are
+        // `@JsonInclude(NON_NULL)` in Java); it must parse to an empty grant.
+        let empty: AuthTableQueryResponse = 
serde_json::from_str("{}").unwrap();
+        assert_eq!(empty, AuthTableQueryResponse::default());
+        assert!(empty.is_unrestricted());
+
+        // Present-but-empty collections are also unrestricted.
+        let blank: AuthTableQueryResponse =
+            
serde_json::from_str(r#"{"filter":[],"columnMasking":{}}"#).unwrap();
+        assert!(blank.is_unrestricted());
+    }
+
     #[test]
     fn test_error_response_serialization() {
         let resp = ErrorResponse::new(
diff --git a/crates/paimon/src/api/mod.rs b/crates/paimon/src/api/mod.rs
index fb2da9f..809b902 100644
--- a/crates/paimon/src/api/mod.rs
+++ b/crates/paimon/src/api/mod.rs
@@ -31,16 +31,16 @@ mod api_response;
 
 // Re-export request types
 pub use api_request::{
-    AlterDatabaseRequest, AlterTableRequest, CreateDatabaseRequest, 
CreateFunctionRequest,
-    CreateTableRequest, CreateViewRequest, RenameTableRequest,
+    AlterDatabaseRequest, AlterTableRequest, AuthTableQueryRequest, 
CreateDatabaseRequest,
+    CreateFunctionRequest, CreateTableRequest, CreateViewRequest, 
RenameTableRequest,
 };
 
 // Re-export response types
 pub use api_response::{
-    AuditRESTResponse, ConfigResponse, ErrorResponse, GetDatabaseResponse, 
GetFunctionResponse,
-    GetTableResponse, GetTableTokenResponse, GetViewResponse, 
ListDatabasesResponse,
-    ListFunctionsResponse, ListPartitionsResponse, ListTablesResponse, 
ListViewsResponse,
-    PagedList,
+    AuditRESTResponse, AuthTableQueryResponse, ConfigResponse, ErrorResponse, 
GetDatabaseResponse,
+    GetFunctionResponse, GetTableResponse, GetTableTokenResponse, 
GetViewResponse,
+    ListDatabasesResponse, ListFunctionsResponse, ListPartitionsResponse, 
ListTablesResponse,
+    ListViewsResponse, PagedList,
 };
 
 // Re-export error types
diff --git a/crates/paimon/src/api/resource_paths.rs 
b/crates/paimon/src/api/resource_paths.rs
index 4983ae7..38c39c9 100644
--- a/crates/paimon/src/api/resource_paths.rs
+++ b/crates/paimon/src/api/resource_paths.rs
@@ -147,6 +147,11 @@ impl ResourcePaths {
         )
     }
 
+    /// Get the auth-table-query endpoint path (`.../tables/{table}/auth`).
+    pub fn auth_table(&self, database_name: &str, table_name: &str) -> String {
+        format!("{}/auth", self.table(database_name, table_name))
+    }
+
     /// Get the table details endpoint path.
     pub fn table_details(&self, database_name: &str) -> String {
         format!(
@@ -242,6 +247,15 @@ mod tests {
         assert!(table_path.contains("my-table"));
     }
 
+    #[test]
+    fn test_auth_table_path_encodes_names() {
+        let paths = ResourcePaths::new("catalog");
+        assert_eq!(
+            paths.auth_table("analytics db", "user events"),
+            "/v1/catalog/databases/analytics+db/tables/user+events/auth"
+        );
+    }
+
     #[test]
     fn test_config_path() {
         assert_eq!(ResourcePaths::config(), "/v1/config");
diff --git a/crates/paimon/src/api/rest_api.rs 
b/crates/paimon/src/api/rest_api.rs
index 5110133..cdde2b2 100644
--- a/crates/paimon/src/api/rest_api.rs
+++ b/crates/paimon/src/api/rest_api.rs
@@ -29,13 +29,13 @@ use crate::spec::{Partition, PartitionStatistics, Schema, 
SchemaChange, Snapshot
 use crate::Result;
 
 use super::api_request::{
-    AlterDatabaseRequest, AlterTableRequest, CreateDatabaseRequest, 
CreateFunctionRequest,
-    CreateTableRequest, CreateViewRequest, RenameTableRequest,
+    AlterDatabaseRequest, AlterTableRequest, AuthTableQueryRequest, 
CreateDatabaseRequest,
+    CreateFunctionRequest, CreateTableRequest, CreateViewRequest, 
RenameTableRequest,
 };
 use super::api_response::{
-    ConfigResponse, GetDatabaseResponse, GetFunctionResponse, 
GetTableResponse, GetViewResponse,
-    ListDatabasesResponse, ListFunctionsResponse, ListPartitionsResponse, 
ListTablesResponse,
-    ListViewsResponse, PagedList,
+    AuthTableQueryResponse, ConfigResponse, GetDatabaseResponse, 
GetFunctionResponse,
+    GetTableResponse, GetViewResponse, ListDatabasesResponse, 
ListFunctionsResponse,
+    ListPartitionsResponse, ListTablesResponse, ListViewsResponse, PagedList,
 };
 use super::auth::{AuthProviderFactory, RESTAuthFunction};
 use super::resource_paths::ResourcePaths;
@@ -628,6 +628,21 @@ impl RESTApi {
         self.client.get(&path, None::<&[(&str, &str)]>).await
     }
 
+    /// Fetch the per-user row filter and column masking for a 
`query-auth.enabled`
+    /// table. `select` is the query's projected columns (`None` = all 
columns).
+    pub async fn auth_table_query(
+        &self,
+        identifier: &Identifier,
+        select: Option<Vec<String>>,
+    ) -> Result<AuthTableQueryResponse> {
+        let database = identifier.database();
+        let table = identifier.object();
+        validate_non_empty_multi(&[(database, "database name"), (table, "table 
name")])?;
+        let path = self.resource_paths.auth_table(database, table);
+        let request = AuthTableQueryRequest::new(select);
+        self.client.post(&path, &request).await
+    }
+
     // ==================== Commit Operations ====================
 
     /// Commit a snapshot for a table.

Reply via email to