From 10c57e59ec4152d246366026703b9c16251b237e Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Mon, 23 Mar 2026 14:37:22 -0400 Subject: [PATCH] fixed nested filtering syntax --- fixtures/queryer.json | 4 ++-- src/queryer/compiler.rs | 39 +++++++++++++----------------- src/queryer/mod.rs | 53 +++++++++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 37 deletions(-) diff --git a/fixtures/queryer.json b/fixtures/queryer.json index 143690f..69acf3a 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -1163,7 +1163,7 @@ "$eq": true, "$ne": false }, - "contacts.#.is_primary": { + "contacts/is_primary": { "$eq": true }, "created_at": { @@ -1203,7 +1203,7 @@ "$eq": "%Doe%", "$ne": "%Smith%" }, - "phone_numbers.#.target.number": { + "phone_numbers/target/number": { "$eq": "555-1234" } }, diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index d559256..1dc919e 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -64,11 +64,7 @@ impl<'a> Compiler<'a> { fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> { if let Some(items) = &node.schema.obj.items { - let next_path = if node.ast_path.is_empty() { - String::from("#") - } else { - format!("{}.#", node.ast_path) - }; + let next_path = node.ast_path.clone(); if let Some(ref_id) = &items.obj.r#ref { if let Some(type_def) = self.db.types.get(ref_id) { @@ -448,22 +444,21 @@ impl<'a> Compiler<'a> { } } - let mut child_node = node.clone(); - child_node.parent_alias = owner_alias.clone(); - let arc_aliases = std::sync::Arc::new(table_aliases.clone()); - child_node.parent_type_aliases = Some(arc_aliases); - child_node.parent_type = Some(r#type); - child_node.parent_schema = Some(std::sync::Arc::clone(&node.schema)); - child_node.property_name = Some(prop_key.clone()); - child_node.depth += 1; - let next_path = if node.ast_path.is_empty() { - prop_key.clone() - } else { - format!("{}.{}", node.ast_path, prop_key) + let child_node = Node { + schema: std::sync::Arc::clone(prop_schema), + parent_alias: owner_alias.clone(), + parent_type_aliases: Some(std::sync::Arc::new(table_aliases.clone())), + parent_type: Some(r#type), + parent_schema: Some(std::sync::Arc::clone(&node.schema)), + property_name: Some(prop_key.clone()), + depth: node.depth + 1, + ast_path: if node.ast_path.is_empty() { + prop_key.clone() + } else { + format!("{}/{}", node.ast_path, prop_key) + }, }; - child_node.ast_path = next_path; - child_node.schema = std::sync::Arc::clone(prop_schema); let (val_sql, val_type) = self.compile_node(child_node)?; @@ -587,15 +582,15 @@ impl<'a> Compiler<'a> { let op = parts.next().unwrap_or("$eq"); let field_name = if node.ast_path.is_empty() { - if full_field_path.contains('.') || full_field_path.contains('#') { + if full_field_path.contains('/') { continue; } full_field_path } else { - let prefix = format!("{}.", node.ast_path); + let prefix = format!("{}/", node.ast_path); if full_field_path.starts_with(&prefix) { let remainder = &full_field_path[prefix.len()..]; - if remainder.contains('.') || remainder.contains('#') { + if remainder.contains('/') { continue; } remainder diff --git a/src/queryer/mod.rs b/src/queryer/mod.rs index 9a5b296..5bcc077 100644 --- a/src/queryer/mod.rs +++ b/src/queryer/mod.rs @@ -54,6 +54,45 @@ impl Queryer { self.execute_sql(schema_id, &sql, &args) } + fn extract_filters( + prefix: String, + val: &serde_json::Value, + entries: &mut Vec<(String, serde_json::Value)>, + ) -> Result<(), String> { + if let Some(obj) = val.as_object() { + let mut is_op_obj = false; + if let Some(first_key) = obj.keys().next() { + if first_key.starts_with('$') { + is_op_obj = true; + } + } + + if is_op_obj { + for (op, op_val) in obj { + if !op.starts_with('$') { + return Err(format!("Filter operator must start with '$', got: {}", op)); + } + entries.push((format!("{}:{}", prefix, op), op_val.clone())); + } + } else { + for (k, v) in obj { + let next_prefix = if prefix.is_empty() { + k.clone() + } else { + format!("{}/{}", prefix, k) + }; + Self::extract_filters(next_prefix, v, entries)?; + } + } + } else { + return Err(format!( + "Filter for path '{}' must be an operator object like {{$eq: ...}} or a nested map.", + prefix + )); + } + Ok(()) + } + fn parse_filter_entries( &self, filters_map: Option<&serde_json::Map>, @@ -61,19 +100,7 @@ impl Queryer { let mut filter_entries: Vec<(String, serde_json::Value)> = Vec::new(); if let Some(fm) = filters_map { for (key, val) in fm { - if let Some(obj) = val.as_object() { - for (op, op_val) in obj { - if !op.starts_with('$') { - return Err(format!("Filter operator must start with '$', got: {}", op)); - } - filter_entries.push((format!("{}:{}", key, op), op_val.clone())); - } - } else { - return Err(format!( - "Filter for field '{}' must be an object with operators like $eq, $in, etc.", - key - )); - } + Self::extract_filters(key.clone(), val, &mut filter_entries)?; } } filter_entries.sort_by(|a, b| a.0.cmp(&b.0));