Compare commits

..

6 Commits

Author SHA1 Message Date
8ca9017cc4 version: 1.0.88 2026-03-23 14:37:29 -04:00
10c57e59ec fixed nested filtering syntax 2026-03-23 14:37:22 -04:00
ef4571767c version: 1.0.87 2026-03-23 12:49:36 -04:00
29bd25eaff fixed filter override for archived 2026-03-23 12:49:30 -04:00
4d9b510819 version: 1.0.86 2026-03-23 12:26:03 -04:00
3c4b1066df fixed merger with anchor test issue 2026-03-23 12:25:55 -04:00
7 changed files with 183 additions and 78 deletions

View File

@ -2403,7 +2403,7 @@
}
},
{
"description": "Anchor order and insert new line",
"description": "Anchor order and insert new line (no line id)",
"action": "merge",
"data": {
"id": "abc",
@ -2502,6 +2502,114 @@
]
]
}
},
{
"description": "Anchor order and insert new line (with line id)",
"action": "merge",
"data": {
"id": "abc",
"type": "order",
"lines": [
{
"id": "11111111-2222-3333-4444-555555555555",
"type": "order_line",
"product": "Widget",
"price": 99.0
}
]
},
"schema_id": "order",
"expect": {
"success": true,
"sql": [
[
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"order_line\" t1",
"LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id",
"WHERE t1.id = '11111111-2222-3333-4444-555555555555'"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '11111111-2222-3333-4444-555555555555',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.\"order_line\" (",
" \"id\",",
" \"order_id\",",
" \"price\",",
" \"product\",",
" \"type\"",
")",
"VALUES (",
" '11111111-2222-3333-4444-555555555555',",
" 'abc',",
" 99,",
" 'Widget',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"order_id\":\"abc\",",
" \"price\":99.0,",
" \"product\":\"Widget\",",
" \"type\":\"order_line\"",
" }',",
" '11111111-2222-3333-4444-555555555555',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"11111111-2222-3333-4444-555555555555\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"abc\",",
" \"price\":99.0,",
" \"product\":\"Widget\",",
" \"type\":\"order_line\"",
" },",
" \"new\":{",
" \"order_id\":\"abc\",",
" \"price\":99.0,",
" \"product\":\"Widget\",",
" \"type\":\"order_line\"",
" }",
" }')"
]
]
}
}
]
}

View File

@ -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"
}
},

View File

@ -1,38 +0,0 @@
{
"description": "Person ad-hoc email addresses select",
"action": "query",
"schema_id": "full.person/email_addresses",
"expect": {
"success": true,
"sql": [
[
"(SELECT jsonb_build_object(",
" 'archived', entity_3.archived,",
" 'created_at', entity_3.created_at,",
" 'id', entity_3.id,",
" 'is_primary', contact_1.is_primary,",
" 'name', entity_3.name,",
" 'target',",
" (SELECT jsonb_build_object(",
" 'address', email_address_4.address,",
" 'archived', entity_5.archived,",
" 'created_at', entity_5.created_at,",
" 'id', entity_5.id,",
" 'name', entity_5.name,",
" 'type', entity_5.type",
" )",
" FROM agreego.email_address email_address_4",
" JOIN agreego.entity entity_5 ON entity_5.id = email_address_4.id",
" WHERE",
" NOT entity_5.archived",
" AND relationship_2.target_id = entity_5.id),",
" 'type', entity_3.type",
")",
"FROM agreego.contact contact_1",
"JOIN agreego.relationship relationship_2 ON relationship_2.id = contact_1.id",
"JOIN agreego.entity entity_3 ON entity_3.id = relationship_2.id",
"WHERE NOT entity_3.archived)"
]
]
}
}

View File

@ -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)?;
@ -491,7 +486,14 @@ impl<'a> Compiler<'a> {
.unwrap_or_else(|| base_alias.clone());
let mut where_clauses = Vec::new();
where_clauses.push(format!("NOT {}.archived", entity_alias));
// Dynamically apply the 'active-only' default ONLY if the client
// didn't explicitly request to filter on 'archived' themselves!
let has_archived_override = self.filter_keys.iter().any(|k| k == "archived");
if !has_archived_override {
where_clauses.push(format!("NOT {}.archived", entity_alias));
}
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
self.compile_relation_conditions(
@ -580,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

View File

@ -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<String, serde_json::Value>>,
@ -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));

View File

@ -8554,3 +8554,9 @@ fn test_merger_0_9() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 9).unwrap();
}
#[test]
fn test_merger_0_10() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 10).unwrap();
}

View File

@ -1 +1 @@
1.0.85
1.0.88