From f60158564da6eaa8411bcf6cefdcd47052ba2002 Mon Sep 17 00:00:00 2001 From: Satya Date: Tue, 25 Aug 2026 18:21:44 -0400 Subject: [PATCH] Family references discriminate through the referenced row's type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intention: make local _type columns optional denormalization instead of an engine requirement — writers should only ever have to say WHO. Outcomes: - compile_one_of: a type-strategy family REFERENCE (payer, source, target) now selects its polymorphic branch via the referenced row's own type — CASE (SELECT type FROM agreego.entity WHERE id = ._id) — the truth itself, instead of requiring a _type column beside the id. kind-strategy STI and a row's self-discrimination still read locally (there is no referenced row to consult). All CASEs are now simple-form, so the operand is evaluated once even as a subquery. - GUC readers guard against the empty string: a rolled-back transaction that FIRST-sets a custom GUC leaves it '' session-wide (not unset), and COALESCE alone never fires — auth.user_id and punc.external now NULLIF first, matching agreego.get_cue's own convention. - Golden fixtures regenerated via UPDATE_EXPECT; 1286 tests green. Co-Authored-By: Claude Fable 5 --- fixtures/queryer.json | 71 ++++++++++++++++++++-------------- src/database/executors/pgrx.rs | 4 +- src/queryer/compiler.rs | 38 ++++++++++++------ 3 files changed, 71 insertions(+), 42 deletions(-) diff --git a/fixtures/queryer.json b/fixtures/queryer.json index 9a8b4d9..cd88a23 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -1408,8 +1408,13 @@ " 'archived', entity_19.archived,", " 'created_at', entity_19.created_at,", " 'is_primary', contact_21.is_primary,", - " 'target', CASE", - " WHEN relationship_20.target_type = 'phone_number' THEN ((", + " 'target', CASE (", + " SELECT __fam.type", + " FROM agreego.entity __fam", + " WHERE", + " __fam.id = relationship_20.target_id", + " )", + " WHEN 'phone_number' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_22.id,", " 'type', entity_22.type,", @@ -1423,7 +1428,7 @@ " NOT entity_22.archived", " AND relationship_20.target_id = entity_22.id", " ))", - " WHEN relationship_20.target_type = 'email_address' THEN ((", + " WHEN 'email_address' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_24.id,", " 'type', entity_24.type,", @@ -1437,7 +1442,7 @@ " NOT entity_24.archived", " AND relationship_20.target_id = entity_24.id", " ))", - " WHEN relationship_20.target_type = 'address' THEN ((", + " WHEN 'address' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_26.id,", " 'type', entity_26.type,", @@ -1659,8 +1664,13 @@ " 'archived', entity_19.archived,", " 'created_at', entity_19.created_at,", " 'is_primary', contact_21.is_primary,", - " 'target', CASE", - " WHEN relationship_20.target_type = 'phone_number' THEN ((", + " 'target', CASE (", + " SELECT __fam.type", + " FROM agreego.entity __fam", + " WHERE", + " __fam.id = relationship_20.target_id", + " )", + " WHEN 'phone_number' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_22.id,", " 'type', entity_22.type,", @@ -1674,7 +1684,7 @@ " NOT entity_22.archived", " AND relationship_20.target_id = entity_22.id", " ))", - " WHEN relationship_20.target_type = 'email_address' THEN ((", + " WHEN 'email_address' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_24.id,", " 'type', entity_24.type,", @@ -1688,7 +1698,7 @@ " NOT entity_24.archived", " AND relationship_20.target_id = entity_24.id", " ))", - " WHEN relationship_20.target_type = 'address' THEN ((", + " WHEN 'address' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_26.id,", " 'type', entity_26.type,", @@ -1889,8 +1899,8 @@ "sql": [ [ "((SELECT jsonb_strip_nulls((", - " SELECT COALESCE(jsonb_agg(CASE", - " WHEN organization_2.type = 'bot' THEN ((", + " SELECT COALESCE(jsonb_agg(CASE organization_2.type", + " WHEN 'bot' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_3.id,", " 'type', entity_3.type,", @@ -1907,7 +1917,7 @@ " NOT entity_3.archived", " AND entity_3.id = entity_1.id", " ))", - " WHEN organization_2.type = 'organization' THEN ((", + " WHEN 'organization' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_6.id,", " 'type', entity_6.type,", @@ -1921,7 +1931,7 @@ " NOT entity_6.archived", " AND entity_6.id = entity_1.id", " ))", - " WHEN organization_2.type = 'person' THEN ((", + " WHEN 'person' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_8.id,", " 'type', entity_8.type,", @@ -1959,8 +1969,8 @@ "sql": [ [ "((SELECT jsonb_strip_nulls((", - " SELECT CASE", - " WHEN organization_2.type = 'bot' THEN ((", + " SELECT CASE organization_2.type", + " WHEN 'bot' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_3.id,", " 'type', entity_3.type,", @@ -1976,7 +1986,7 @@ " NOT entity_3.archived", " AND entity_3.id = entity_1.id", " ))", - " WHEN organization_2.type = 'person' THEN ((", + " WHEN 'person' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_6.id,", " 'type', entity_6.type,", @@ -2013,8 +2023,8 @@ "sql": [ [ "((SELECT jsonb_strip_nulls((", - " SELECT CASE", - " WHEN organization_2.type = 'person' THEN ((", + " SELECT CASE organization_2.type", + " WHEN 'person' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_3.id,", " 'type', entity_3.type,", @@ -2121,8 +2131,13 @@ " 'archived', entity_21.archived,", " 'created_at', entity_21.created_at,", " 'is_primary', contact_23.is_primary,", - " 'target', CASE", - " WHEN relationship_22.target_type = 'phone_number' THEN ((", + " 'target', CASE (", + " SELECT __fam.type", + " FROM agreego.entity __fam", + " WHERE", + " __fam.id = relationship_22.target_id", + " )", + " WHEN 'phone_number' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_24.id,", " 'type', entity_24.type,", @@ -2136,7 +2151,7 @@ " NOT entity_24.archived", " AND relationship_22.target_id = entity_24.id", " ))", - " WHEN relationship_22.target_type = 'email_address' THEN ((", + " WHEN 'email_address' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_26.id,", " 'type', entity_26.type,", @@ -2150,7 +2165,7 @@ " NOT entity_26.archived", " AND relationship_22.target_id = entity_26.id", " ))", - " WHEN relationship_22.target_type = 'address' THEN ((", + " WHEN 'address' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_28.id,", " 'type', entity_28.type,", @@ -2246,15 +2261,15 @@ "sql": [ [ "((SELECT jsonb_strip_nulls((", - " SELECT COALESCE(jsonb_agg(CASE", - " WHEN widget_2.kind = 'stock' THEN (jsonb_build_object(", + " SELECT COALESCE(jsonb_agg(CASE widget_2.kind", + " WHEN 'stock' THEN (jsonb_build_object(", " 'id', entity_1.id,", " 'type', entity_1.type,", " 'archived', entity_1.archived,", " 'created_at', entity_1.created_at,", " 'kind', widget_2.kind", " ))", - " WHEN widget_2.kind = 'tasks' THEN (jsonb_build_object(", + " WHEN 'tasks' THEN (jsonb_build_object(", " 'id', entity_1.id,", " 'type', entity_1.type,", " 'archived', entity_1.archived,", @@ -2360,8 +2375,8 @@ " 'total', order_2.total,", " 'customer_id', order_2.customer_id,", " 'counterparty', (", - " SELECT CASE", - " WHEN organization_4.type = 'bot' THEN ((", + " SELECT CASE organization_4.type", + " WHEN 'bot' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_5.id,", " 'type', entity_5.type,", @@ -2378,7 +2393,7 @@ " NOT entity_5.archived", " AND entity_5.id = entity_3.id", " ))", - " WHEN organization_4.type = 'organization' THEN ((", + " WHEN 'organization' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_8.id,", " 'type', entity_8.type,", @@ -2392,7 +2407,7 @@ " NOT entity_8.archived", " AND entity_8.id = entity_3.id", " ))", - " WHEN organization_4.type = 'person' THEN ((", + " WHEN 'person' THEN ((", " SELECT jsonb_build_object(", " 'id', entity_10.id,", " 'type', entity_10.type,", diff --git a/src/database/executors/pgrx.rs b/src/database/executors/pgrx.rs index 9041de7..96aa915 100644 --- a/src/database/executors/pgrx.rs +++ b/src/database/executors/pgrx.rs @@ -118,7 +118,7 @@ impl DatabaseExecutor for SpiExecutor { Spi::connect(|client| { let mut tup_table = client .select( - "SELECT COALESCE(current_setting('auth.user_id', true), 'ffffffff-ffff-ffff-ffff-ffffffffffff')", + "SELECT COALESCE(NULLIF(current_setting('auth.user_id', true), ''), 'ffffffff-ffff-ffff-ffff-ffffffffffff')", None, &[], ) @@ -156,7 +156,7 @@ impl DatabaseExecutor for SpiExecutor { Spi::connect(|client| { let mut tup_table = client .select( - "SELECT COALESCE(current_setting('punc.external', true), 'false')::boolean", + "SELECT COALESCE(NULLIF(current_setting('punc.external', true), ''), 'false')::boolean", None, &[], ) diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index 6c26986..4d78330 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -337,10 +337,25 @@ impl<'a> Compiler<'a> { .get() .ok_or("Missing compiled discriminator for polymorphism")?; - let type_col = if let Some(prop) = &node.property_name { - format!("{}_{}", prop, disc) + // The CASE operand that selects the polymorphic branch. For a `type`-strategy + // FAMILY REFERENCE (a property like `payer` or `source` pointing at another + // entity by `_id`), discriminate through the referenced row's own type + // in agreego.entity — the truth — instead of requiring a local `_type` + // column on the parent. Local `_type` columns thereby become optional + // denormalization rather than a requirement of the engine. `kind`-strategy + // (single-table STI) and self-discrimination (a row's own `type`) still read + // locally: there is no referenced row to consult. + let disc_operand = if let Some(prop) = &node.property_name { + if disc == "type" { + format!( + "(SELECT __fam.type FROM agreego.entity __fam WHERE __fam.id = {}.{}_id)", + node.parent_alias, prop + ) + } else { + format!("{}.{}_{}", node.parent_alias, prop, disc) + } } else { - disc.to_string() + format!("{}.{}", node.parent_alias, disc) }; for (disc_val, (idx_opt, target_id_opt)) in options { @@ -369,10 +384,7 @@ impl<'a> Compiler<'a> { sql }; - case_statements.push(format!( - "WHEN {}.{} = '{}' THEN ({})", - node.parent_alias, type_col, disc_val, val_sql - )); + case_statements.push(format!("WHEN '{}' THEN ({})", disc_val, val_sql)); } } else if let Some(idx) = idx_opt { if let Some(target_schema) = node @@ -404,10 +416,7 @@ impl<'a> Compiler<'a> { sql }; - case_statements.push(format!( - "WHEN {}.{} = '{}' THEN ({})", - node.parent_alias, type_col, disc_val, val_sql - )); + case_statements.push(format!("WHEN '{}' THEN ({})", disc_val, val_sql)); } } } @@ -415,7 +424,12 @@ impl<'a> Compiler<'a> { return Ok(("NULL".to_string(), "string".to_string())); } - let sql = format!("CASE {} ELSE NULL END", case_statements.join(" ")); + // Simple CASE: the operand is evaluated once even when it is a subquery. + let sql = format!( + "CASE {} {} ELSE NULL END", + disc_operand, + case_statements.join(" ") + ); Ok((sql, "object".to_string())) }