Skip to content

Commit 95bc169

Browse files
authored
fix(core): normalize the name of session property (#1331)
1 parent 718cf49 commit 95bc169

File tree

7 files changed

+91
-27
lines changed

7 files changed

+91
-27
lines changed

ibis-server/tests/routers/v3/connector/postgres/test_query.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@
7878
"name": "c_name_access",
7979
"requiredProperties": [
8080
{
81-
"name": "session_level",
81+
# To test the name is case insensitive
82+
"name": "Session_level",
8283
"required": False,
8384
}
8485
],
@@ -672,14 +673,30 @@ async def test_clac_query(client, manifest_str, connection_info):
672673
"sql": "SELECT * FROM customer limit 1",
673674
},
674675
headers={
675-
X_WREN_VARIABLE_PREFIX + "session_level": "2",
676+
X_WREN_VARIABLE_PREFIX + "session_level": "1",
676677
},
677678
)
678679
assert response.status_code == 200
679680
result = response.json()
680681
assert len(result["data"]) == 1
681682
assert len(result["data"][0]) == 2
682683

684+
response = await client.post(
685+
url=f"{base_url}/query",
686+
json={
687+
"connectionInfo": connection_info,
688+
"manifestStr": base64_manifest_with_required_properties,
689+
"sql": "SELECT * FROM customer limit 1",
690+
},
691+
headers={
692+
X_WREN_VARIABLE_PREFIX + "session_level": "2",
693+
},
694+
)
695+
assert response.status_code == 200
696+
result = response.json()
697+
assert len(result["data"]) == 1
698+
assert len(result["data"][0]) == 1
699+
683700

684701
async def test_connection_timeout(
685702
client, manifest_str, connection_info, connection_url

wren-core-base/manifest-macro/src/lib.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,51 @@ pub fn session_property(python_binding: proc_macro::TokenStream) -> proc_macro::
404404
};
405405
let expanded = quote! {
406406
#python_binding
407-
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
407+
#[derive(Serialize, Debug, PartialEq, Eq, Hash, Clone)]
408408
#[serde(rename_all = "camelCase")]
409409
pub struct SessionProperty {
410410
pub name: String,
411411
pub required: bool,
412412
pub default_expr: Option<String>,
413+
// To avoid duplicate clone for normalized name(to_lowercase), we store it here
414+
#[serde(skip_serializing, default = "String::new")]
415+
pub normalized_name: String,
416+
}
417+
418+
impl SessionProperty {
419+
#[cfg(not(feature = "python-binding"))]
420+
pub fn new(name: String, required: bool, default_expr: Option<String>) -> Self {
421+
let normalized_name = name.to_lowercase();
422+
Self {
423+
name,
424+
required,
425+
default_expr,
426+
normalized_name,
427+
}
428+
}
429+
}
430+
431+
impl<'de> serde::Deserialize<'de> for SessionProperty {
432+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
433+
where
434+
D: serde::Deserializer<'de>,
435+
{
436+
#[derive(Deserialize)]
437+
#[serde(rename_all = "camelCase")]
438+
struct SessionPropertyHelper {
439+
name: String,
440+
required: bool,
441+
default_expr: Option<String>,
442+
}
443+
444+
let helper = SessionPropertyHelper::deserialize(deserializer)?;
445+
Ok(SessionProperty {
446+
normalized_name: helper.name.to_lowercase(),
447+
name: helper.name,
448+
required: helper.required,
449+
default_expr: helper.default_expr,
450+
})
451+
}
413452
}
414453
};
415454
proc_macro::TokenStream::from(expanded)

wren-core-base/src/mdl/builder.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,18 +175,10 @@ impl ModelBuilder {
175175

176176
impl SessionProperty {
177177
pub fn new_required(name: &str) -> Self {
178-
SessionProperty {
179-
name: name.to_string(),
180-
required: true,
181-
default_expr: None,
182-
}
178+
SessionProperty::new(name.to_string(), true, None)
183179
}
184180
pub fn new_optional(name: &str, default_expr: Option<String>) -> Self {
185-
SessionProperty {
186-
name: name.to_string(),
187-
required: false,
188-
default_expr,
189-
}
181+
SessionProperty::new(name.to_string(), false, default_expr)
190182
}
191183
}
192184
pub struct ColumnBuilder {
@@ -845,4 +837,15 @@ mod test {
845837
.data_source(MySQL);
846838
assert_eq!(mdl, expected.build());
847839
}
840+
841+
#[test]
842+
fn test_session_property_roundtrip() {
843+
let expected = SessionProperty::new_optional("session_id", Some("1".to_string()));
844+
845+
let json_str = serde_json::to_string(&expected).unwrap();
846+
assert!(!json_str.contains(r#"normalizedName"#));
847+
let actual: SessionProperty = serde_json::from_str(&json_str).unwrap();
848+
assert_eq!(actual.normalized_name(), actual.name.to_lowercase());
849+
assert_eq!(actual, expected)
850+
}
848851
}

wren-core-base/src/mdl/manifest.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,12 @@ impl View {
347347
}
348348
}
349349

350+
impl SessionProperty {
351+
pub fn normalized_name(&self) -> &str {
352+
&self.normalized_name
353+
}
354+
}
355+
350356
#[cfg(test)]
351357
mod tests {
352358
use crate::mdl::manifest::table_reference;

wren-core-base/src/mdl/py_method.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ mod manifest_python_impl {
7373
impl SessionProperty {
7474
#[new]
7575
#[pyo3(signature = (name, required = false, default_expr = None))]
76-
fn new(name: String, required: bool, default_expr: Option<String>) -> Self {
76+
pub fn new(name: String, required: bool, default_expr: Option<String>) -> Self {
7777
Self {
78+
normalized_name: name.to_lowercase(),
7879
name,
7980
required,
8081
default_expr,

wren-core/core/src/logical_plan/analyze/access_control.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,12 @@ pub fn validate_rlac_rule(rule: &RowLevelAccessControl, model: &Model) -> Result
9696

9797
let required_properties: Vec<_> = required_properties
9898
.iter()
99-
.map(|property| property.name.to_lowercase())
99+
.map(|property| property.normalized_name())
100100
.collect();
101101

102102
let missed_properties: Vec<_> = session_properties
103103
.iter()
104-
.filter(|property| !required_properties.contains(property))
104+
.filter(|property| !required_properties.contains(&property.as_str()))
105105
.collect();
106106
if !missed_properties.is_empty() {
107107
return plan_err!(
@@ -144,9 +144,7 @@ pub fn build_filter_expression(
144144
let Some(property_value) = properties.get(&property_name).or_else(|| {
145145
required_properties
146146
.iter()
147-
.filter(|r| {
148-
!r.required && r.name.eq_ignore_ascii_case(&property_name)
149-
})
147+
.filter(|r| !r.required && r.normalized_name().eq(&property_name))
150148
.map(|r| &r.default_expr)
151149
.next()
152150
}) else {
@@ -262,7 +260,7 @@ pub fn validate_rule(
262260
.iter()
263261
.map(|property| {
264262
if property.required {
265-
if !is_property_present(headers, &property.name) {
263+
if !is_property_present(headers, property) {
266264
return plan_err!(
267265
"session property {} is required for `{}` rule but not found in headers",
268266
property.name,
@@ -271,7 +269,7 @@ pub fn validate_rule(
271269
}
272270
Ok(true)
273271
} else {
274-
let exist = is_property_present(headers, &property.name);
272+
let exist = is_property_present(headers, property);
275273
if exist
276274
|| property
277275
.default_expr
@@ -308,7 +306,7 @@ pub(crate) fn validate_clac_rule(
308306
}
309307

310308
let property = &clac.required_properties[0];
311-
let value_opt = properties.get(&property.name);
309+
let value_opt = properties.get(property.normalized_name());
312310

313311
match value_opt {
314312
Some(Some(value)) => (clac.eval(value), Some(clac.name.clone())),
@@ -368,10 +366,10 @@ pub(crate) fn validate_clac_rule(
368366
/// If the property is present and not empty, return true.
369367
fn is_property_present(
370368
headers: &HashMap<String, Option<String>>,
371-
property_name: &str,
369+
property: &SessionProperty,
372370
) -> bool {
373371
headers
374-
.get(&property_name.to_lowercase())
372+
.get(property.normalized_name())
375373
.map(|v| v.as_ref().is_some_and(|value| !value.is_empty()))
376374
.unwrap_or(false)
377375
}

wren-core/core/src/mdl/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2707,7 +2707,7 @@ mod test {
27072707
ColumnBuilder::new("c_name", "string")
27082708
.column_level_access_control(
27092709
"cls rule",
2710-
vec![SessionProperty::new_required("session_level")],
2710+
vec![SessionProperty::new_required("Session_level")],
27112711
ColumnLevelOperator::Equals,
27122712
"1",
27132713
)
@@ -2751,7 +2751,7 @@ mod test {
27512751
Err(e) => {
27522752
assert_snapshot!(
27532753
e.to_string(),
2754-
@"Error during planning: session property session_level is required for `cls rule` rule but not found in headers"
2754+
@"Error during planning: session property Session_level is required for `cls rule` rule but not found in headers"
27552755
)
27562756
}
27572757
_ => panic!("Expected error"),
@@ -3710,7 +3710,7 @@ mod test {
37103710
) -> HashMap<String, Option<String>> {
37113711
let mut headers = HashMap::new();
37123712
for (key, value) in field {
3713-
headers.insert(key.clone(), value.clone());
3713+
headers.insert(key.to_lowercase(), value.clone());
37143714
}
37153715
headers
37163716
}

0 commit comments

Comments
 (0)