From 1619ca440038560ef910bb9b71900e95f93d00c7 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Mon, 3 Aug 2026 16:40:26 -0400 Subject: [PATCH] fixed hierarchy null jsonb merging issue with merger when a part of the more specific hierarchy does not exist yet --- fixtures/merger.json | 30 +++++++++++++++--------------- fixtures/queryer.json | 8 ++++---- src/merger/mod.rs | 4 ++-- src/tests/formatter.rs | 4 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/fixtures/merger.json b/fixtures/merger.json index 9154a16..af8e2c5 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -1316,7 +1316,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -1469,14 +1469,14 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", " t1.id = '{{uuid:data.id}}'", - "UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "UNION SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -1631,14 +1631,14 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", " t1.id = '{{uuid:data.id}}'", - "UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "UNION SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -1648,7 +1648,7 @@ " AND \"last_name\" = 'LookupLast'", " AND \"date_of_birth\" = '{{timestamp}}'", " AND \"pronouns\" = 'they/them')", - "UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "UNION SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -1802,14 +1802,14 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", " t1.id = '{{uuid:data.id}}'", - "UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "UNION SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -1951,7 +1951,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -2074,7 +2074,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -2413,7 +2413,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"order\" t2 ON t2.id = t1.id", "WHERE", @@ -3215,7 +3215,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", @@ -3568,7 +3568,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"order_line\" t2 ON t2.id = t1.id", "WHERE", @@ -3692,7 +3692,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"invoice\" t2 ON t2.id = t1.id", "WHERE", @@ -3808,7 +3808,7 @@ "success": true, "sql": [ [ - "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", + "(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"account\" t2 ON t2.id = t1.id", "WHERE", diff --git a/fixtures/queryer.json b/fixtures/queryer.json index ed22ca9..722f3e4 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -1739,10 +1739,10 @@ " AND entity_1.created_at <= ($16 #>> '{}')::TIMESTAMPTZ", " AND entity_1.created_at <> ($17 #>> '{}')::TIMESTAMPTZ", " AND person_3.first_name ILIKE $18 #>> '{}'", - " AND person_3.first_name > ($19 #>> '{)", - " AND person_3.first_name >= ($20 #>> '{)", - " AND person_3.first_name < ($21 #>> '{)", - " AND person_3.first_name <= ($22 #>> '{)", + " AND person_3.first_name > ($19 #>> '{}')", + " AND person_3.first_name >= ($20 #>> '{}')", + " AND person_3.first_name < ($21 #>> '{}')", + " AND person_3.first_name <= ($22 #>> '{}')", " AND person_3.first_name NOT ILIKE $23 #>> '{}'", " AND person_3.first_name NOT IN (SELECT value FROM jsonb_array_elements_text(($24 #>> '{}')::jsonb))", " AND person_3.first_name IN (SELECT value FROM jsonb_array_elements_text(($25 #>> '{}')::jsonb))", diff --git a/src/merger/mod.rs b/src/merger/mod.rs index d1bb73e..f6d0d74 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -787,7 +787,7 @@ impl Merger { let fetch_sql_template = if let Some(cached) = self.cache.get(entity_type_name) { cached } else { - let mut select_list = String::from("to_jsonb(t1.*)"); + let mut select_list = String::from("COALESCE(to_jsonb(t1.*), '{}')"); let mut join_clauses = format!("FROM agreego.\"{}\" t1", entity_type.hierarchy[0]); for (i, table_name) in entity_type.hierarchy.iter().enumerate().skip(1) { @@ -796,7 +796,7 @@ impl Merger { " LEFT JOIN agreego.\"{}\" {} ON {}.id = t1.id", table_name, t_alias, t_alias )); - select_list.push_str(&format!(" || to_jsonb({}.*)", t_alias)); + select_list.push_str(&format!(" || COALESCE(to_jsonb({}.*), '{{}}')", t_alias)); } let template = format!("SELECT {} {}", select_list, join_clauses); diff --git a/src/tests/formatter.rs b/src/tests/formatter.rs index 80b6f1a..bceea4e 100644 --- a/src/tests/formatter.rs +++ b/src/tests/formatter.rs @@ -119,7 +119,7 @@ impl SqlFormatter { for (i, val) in val_tokens.iter().enumerate() { let comma = if i < val_tokens.len() - 1 { "," } else { "" }; - if val.starts_with("'{") && val.ends_with("}'") { + if val.starts_with("'{") && val.ends_with("}'") && val.len() > 4 { let inner = &val[1..val.len() - 1]; // Unescape single quotes from SQL strings let unescaped = inner.replace("''", "'"); @@ -341,7 +341,7 @@ impl SqlFormatter { } Expr::Value(sqlparser::ast::ValueWithSpan { value: Value::SingleQuotedString(s), .. }) | Expr::Value(sqlparser::ast::ValueWithSpan { value: Value::EscapedStringLiteral(s), .. }) => { - if s.starts_with('{') && s.ends_with('}') { + if s.starts_with('{') && s.ends_with('}') && s.len() > 2 { if let Ok(json) = serde_json::from_str::(s) { if let Ok(pretty) = serde_json::to_string_pretty(&json) { let lines: Vec<&str> = pretty.split('\n').collect();