Family references discriminate through the referenced row's type
Intention: make local <prop>_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 = <alias>.<prop>_id) — the truth itself, instead of requiring a <prop>_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 <noreply@anthropic.com>
This commit is contained in:
@ -1408,8 +1408,13 @@
|
|||||||
" 'archived', entity_19.archived,",
|
" 'archived', entity_19.archived,",
|
||||||
" 'created_at', entity_19.created_at,",
|
" 'created_at', entity_19.created_at,",
|
||||||
" 'is_primary', contact_21.is_primary,",
|
" 'is_primary', contact_21.is_primary,",
|
||||||
" 'target', CASE",
|
" 'target', CASE (",
|
||||||
" WHEN relationship_20.target_type = 'phone_number' THEN ((",
|
" SELECT __fam.type",
|
||||||
|
" FROM agreego.entity __fam",
|
||||||
|
" WHERE",
|
||||||
|
" __fam.id = relationship_20.target_id",
|
||||||
|
" )",
|
||||||
|
" WHEN 'phone_number' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_22.id,",
|
" 'id', entity_22.id,",
|
||||||
" 'type', entity_22.type,",
|
" 'type', entity_22.type,",
|
||||||
@ -1423,7 +1428,7 @@
|
|||||||
" NOT entity_22.archived",
|
" NOT entity_22.archived",
|
||||||
" AND relationship_20.target_id = entity_22.id",
|
" AND relationship_20.target_id = entity_22.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN relationship_20.target_type = 'email_address' THEN ((",
|
" WHEN 'email_address' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_24.id,",
|
" 'id', entity_24.id,",
|
||||||
" 'type', entity_24.type,",
|
" 'type', entity_24.type,",
|
||||||
@ -1437,7 +1442,7 @@
|
|||||||
" NOT entity_24.archived",
|
" NOT entity_24.archived",
|
||||||
" AND relationship_20.target_id = entity_24.id",
|
" AND relationship_20.target_id = entity_24.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN relationship_20.target_type = 'address' THEN ((",
|
" WHEN 'address' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_26.id,",
|
" 'id', entity_26.id,",
|
||||||
" 'type', entity_26.type,",
|
" 'type', entity_26.type,",
|
||||||
@ -1659,8 +1664,13 @@
|
|||||||
" 'archived', entity_19.archived,",
|
" 'archived', entity_19.archived,",
|
||||||
" 'created_at', entity_19.created_at,",
|
" 'created_at', entity_19.created_at,",
|
||||||
" 'is_primary', contact_21.is_primary,",
|
" 'is_primary', contact_21.is_primary,",
|
||||||
" 'target', CASE",
|
" 'target', CASE (",
|
||||||
" WHEN relationship_20.target_type = 'phone_number' THEN ((",
|
" SELECT __fam.type",
|
||||||
|
" FROM agreego.entity __fam",
|
||||||
|
" WHERE",
|
||||||
|
" __fam.id = relationship_20.target_id",
|
||||||
|
" )",
|
||||||
|
" WHEN 'phone_number' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_22.id,",
|
" 'id', entity_22.id,",
|
||||||
" 'type', entity_22.type,",
|
" 'type', entity_22.type,",
|
||||||
@ -1674,7 +1684,7 @@
|
|||||||
" NOT entity_22.archived",
|
" NOT entity_22.archived",
|
||||||
" AND relationship_20.target_id = entity_22.id",
|
" AND relationship_20.target_id = entity_22.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN relationship_20.target_type = 'email_address' THEN ((",
|
" WHEN 'email_address' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_24.id,",
|
" 'id', entity_24.id,",
|
||||||
" 'type', entity_24.type,",
|
" 'type', entity_24.type,",
|
||||||
@ -1688,7 +1698,7 @@
|
|||||||
" NOT entity_24.archived",
|
" NOT entity_24.archived",
|
||||||
" AND relationship_20.target_id = entity_24.id",
|
" AND relationship_20.target_id = entity_24.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN relationship_20.target_type = 'address' THEN ((",
|
" WHEN 'address' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_26.id,",
|
" 'id', entity_26.id,",
|
||||||
" 'type', entity_26.type,",
|
" 'type', entity_26.type,",
|
||||||
@ -1889,8 +1899,8 @@
|
|||||||
"sql": [
|
"sql": [
|
||||||
[
|
[
|
||||||
"((SELECT jsonb_strip_nulls((",
|
"((SELECT jsonb_strip_nulls((",
|
||||||
" SELECT COALESCE(jsonb_agg(CASE",
|
" SELECT COALESCE(jsonb_agg(CASE organization_2.type",
|
||||||
" WHEN organization_2.type = 'bot' THEN ((",
|
" WHEN 'bot' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_3.id,",
|
" 'id', entity_3.id,",
|
||||||
" 'type', entity_3.type,",
|
" 'type', entity_3.type,",
|
||||||
@ -1907,7 +1917,7 @@
|
|||||||
" NOT entity_3.archived",
|
" NOT entity_3.archived",
|
||||||
" AND entity_3.id = entity_1.id",
|
" AND entity_3.id = entity_1.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN organization_2.type = 'organization' THEN ((",
|
" WHEN 'organization' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_6.id,",
|
" 'id', entity_6.id,",
|
||||||
" 'type', entity_6.type,",
|
" 'type', entity_6.type,",
|
||||||
@ -1921,7 +1931,7 @@
|
|||||||
" NOT entity_6.archived",
|
" NOT entity_6.archived",
|
||||||
" AND entity_6.id = entity_1.id",
|
" AND entity_6.id = entity_1.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN organization_2.type = 'person' THEN ((",
|
" WHEN 'person' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_8.id,",
|
" 'id', entity_8.id,",
|
||||||
" 'type', entity_8.type,",
|
" 'type', entity_8.type,",
|
||||||
@ -1959,8 +1969,8 @@
|
|||||||
"sql": [
|
"sql": [
|
||||||
[
|
[
|
||||||
"((SELECT jsonb_strip_nulls((",
|
"((SELECT jsonb_strip_nulls((",
|
||||||
" SELECT CASE",
|
" SELECT CASE organization_2.type",
|
||||||
" WHEN organization_2.type = 'bot' THEN ((",
|
" WHEN 'bot' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_3.id,",
|
" 'id', entity_3.id,",
|
||||||
" 'type', entity_3.type,",
|
" 'type', entity_3.type,",
|
||||||
@ -1976,7 +1986,7 @@
|
|||||||
" NOT entity_3.archived",
|
" NOT entity_3.archived",
|
||||||
" AND entity_3.id = entity_1.id",
|
" AND entity_3.id = entity_1.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN organization_2.type = 'person' THEN ((",
|
" WHEN 'person' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_6.id,",
|
" 'id', entity_6.id,",
|
||||||
" 'type', entity_6.type,",
|
" 'type', entity_6.type,",
|
||||||
@ -2013,8 +2023,8 @@
|
|||||||
"sql": [
|
"sql": [
|
||||||
[
|
[
|
||||||
"((SELECT jsonb_strip_nulls((",
|
"((SELECT jsonb_strip_nulls((",
|
||||||
" SELECT CASE",
|
" SELECT CASE organization_2.type",
|
||||||
" WHEN organization_2.type = 'person' THEN ((",
|
" WHEN 'person' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_3.id,",
|
" 'id', entity_3.id,",
|
||||||
" 'type', entity_3.type,",
|
" 'type', entity_3.type,",
|
||||||
@ -2121,8 +2131,13 @@
|
|||||||
" 'archived', entity_21.archived,",
|
" 'archived', entity_21.archived,",
|
||||||
" 'created_at', entity_21.created_at,",
|
" 'created_at', entity_21.created_at,",
|
||||||
" 'is_primary', contact_23.is_primary,",
|
" 'is_primary', contact_23.is_primary,",
|
||||||
" 'target', CASE",
|
" 'target', CASE (",
|
||||||
" WHEN relationship_22.target_type = 'phone_number' THEN ((",
|
" SELECT __fam.type",
|
||||||
|
" FROM agreego.entity __fam",
|
||||||
|
" WHERE",
|
||||||
|
" __fam.id = relationship_22.target_id",
|
||||||
|
" )",
|
||||||
|
" WHEN 'phone_number' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_24.id,",
|
" 'id', entity_24.id,",
|
||||||
" 'type', entity_24.type,",
|
" 'type', entity_24.type,",
|
||||||
@ -2136,7 +2151,7 @@
|
|||||||
" NOT entity_24.archived",
|
" NOT entity_24.archived",
|
||||||
" AND relationship_22.target_id = entity_24.id",
|
" AND relationship_22.target_id = entity_24.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN relationship_22.target_type = 'email_address' THEN ((",
|
" WHEN 'email_address' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_26.id,",
|
" 'id', entity_26.id,",
|
||||||
" 'type', entity_26.type,",
|
" 'type', entity_26.type,",
|
||||||
@ -2150,7 +2165,7 @@
|
|||||||
" NOT entity_26.archived",
|
" NOT entity_26.archived",
|
||||||
" AND relationship_22.target_id = entity_26.id",
|
" AND relationship_22.target_id = entity_26.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN relationship_22.target_type = 'address' THEN ((",
|
" WHEN 'address' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_28.id,",
|
" 'id', entity_28.id,",
|
||||||
" 'type', entity_28.type,",
|
" 'type', entity_28.type,",
|
||||||
@ -2246,15 +2261,15 @@
|
|||||||
"sql": [
|
"sql": [
|
||||||
[
|
[
|
||||||
"((SELECT jsonb_strip_nulls((",
|
"((SELECT jsonb_strip_nulls((",
|
||||||
" SELECT COALESCE(jsonb_agg(CASE",
|
" SELECT COALESCE(jsonb_agg(CASE widget_2.kind",
|
||||||
" WHEN widget_2.kind = 'stock' THEN (jsonb_build_object(",
|
" WHEN 'stock' THEN (jsonb_build_object(",
|
||||||
" 'id', entity_1.id,",
|
" 'id', entity_1.id,",
|
||||||
" 'type', entity_1.type,",
|
" 'type', entity_1.type,",
|
||||||
" 'archived', entity_1.archived,",
|
" 'archived', entity_1.archived,",
|
||||||
" 'created_at', entity_1.created_at,",
|
" 'created_at', entity_1.created_at,",
|
||||||
" 'kind', widget_2.kind",
|
" 'kind', widget_2.kind",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN widget_2.kind = 'tasks' THEN (jsonb_build_object(",
|
" WHEN 'tasks' THEN (jsonb_build_object(",
|
||||||
" 'id', entity_1.id,",
|
" 'id', entity_1.id,",
|
||||||
" 'type', entity_1.type,",
|
" 'type', entity_1.type,",
|
||||||
" 'archived', entity_1.archived,",
|
" 'archived', entity_1.archived,",
|
||||||
@ -2360,8 +2375,8 @@
|
|||||||
" 'total', order_2.total,",
|
" 'total', order_2.total,",
|
||||||
" 'customer_id', order_2.customer_id,",
|
" 'customer_id', order_2.customer_id,",
|
||||||
" 'counterparty', (",
|
" 'counterparty', (",
|
||||||
" SELECT CASE",
|
" SELECT CASE organization_4.type",
|
||||||
" WHEN organization_4.type = 'bot' THEN ((",
|
" WHEN 'bot' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_5.id,",
|
" 'id', entity_5.id,",
|
||||||
" 'type', entity_5.type,",
|
" 'type', entity_5.type,",
|
||||||
@ -2378,7 +2393,7 @@
|
|||||||
" NOT entity_5.archived",
|
" NOT entity_5.archived",
|
||||||
" AND entity_5.id = entity_3.id",
|
" AND entity_5.id = entity_3.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN organization_4.type = 'organization' THEN ((",
|
" WHEN 'organization' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_8.id,",
|
" 'id', entity_8.id,",
|
||||||
" 'type', entity_8.type,",
|
" 'type', entity_8.type,",
|
||||||
@ -2392,7 +2407,7 @@
|
|||||||
" NOT entity_8.archived",
|
" NOT entity_8.archived",
|
||||||
" AND entity_8.id = entity_3.id",
|
" AND entity_8.id = entity_3.id",
|
||||||
" ))",
|
" ))",
|
||||||
" WHEN organization_4.type = 'person' THEN ((",
|
" WHEN 'person' THEN ((",
|
||||||
" SELECT jsonb_build_object(",
|
" SELECT jsonb_build_object(",
|
||||||
" 'id', entity_10.id,",
|
" 'id', entity_10.id,",
|
||||||
" 'type', entity_10.type,",
|
" 'type', entity_10.type,",
|
||||||
|
|||||||
@ -118,7 +118,7 @@ impl DatabaseExecutor for SpiExecutor {
|
|||||||
Spi::connect(|client| {
|
Spi::connect(|client| {
|
||||||
let mut tup_table = client
|
let mut tup_table = client
|
||||||
.select(
|
.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,
|
None,
|
||||||
&[],
|
&[],
|
||||||
)
|
)
|
||||||
@ -156,7 +156,7 @@ impl DatabaseExecutor for SpiExecutor {
|
|||||||
Spi::connect(|client| {
|
Spi::connect(|client| {
|
||||||
let mut tup_table = client
|
let mut tup_table = client
|
||||||
.select(
|
.select(
|
||||||
"SELECT COALESCE(current_setting('punc.external', true), 'false')::boolean",
|
"SELECT COALESCE(NULLIF(current_setting('punc.external', true), ''), 'false')::boolean",
|
||||||
None,
|
None,
|
||||||
&[],
|
&[],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -337,10 +337,25 @@ impl<'a> Compiler<'a> {
|
|||||||
.get()
|
.get()
|
||||||
.ok_or("Missing compiled discriminator for polymorphism")?;
|
.ok_or("Missing compiled discriminator for polymorphism")?;
|
||||||
|
|
||||||
let type_col = if let Some(prop) = &node.property_name {
|
// The CASE operand that selects the polymorphic branch. For a `type`-strategy
|
||||||
format!("{}_{}", prop, disc)
|
// FAMILY REFERENCE (a property like `payer` or `source` pointing at another
|
||||||
|
// entity by `<prop>_id`), discriminate through the referenced row's own type
|
||||||
|
// in agreego.entity — the truth — instead of requiring a local `<prop>_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 {
|
} else {
|
||||||
disc.to_string()
|
format!("{}.{}", node.parent_alias, disc)
|
||||||
};
|
};
|
||||||
|
|
||||||
for (disc_val, (idx_opt, target_id_opt)) in options {
|
for (disc_val, (idx_opt, target_id_opt)) in options {
|
||||||
@ -369,10 +384,7 @@ impl<'a> Compiler<'a> {
|
|||||||
sql
|
sql
|
||||||
};
|
};
|
||||||
|
|
||||||
case_statements.push(format!(
|
case_statements.push(format!("WHEN '{}' THEN ({})", disc_val, val_sql));
|
||||||
"WHEN {}.{} = '{}' THEN ({})",
|
|
||||||
node.parent_alias, type_col, disc_val, val_sql
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else if let Some(idx) = idx_opt {
|
} else if let Some(idx) = idx_opt {
|
||||||
if let Some(target_schema) = node
|
if let Some(target_schema) = node
|
||||||
@ -404,10 +416,7 @@ impl<'a> Compiler<'a> {
|
|||||||
sql
|
sql
|
||||||
};
|
};
|
||||||
|
|
||||||
case_statements.push(format!(
|
case_statements.push(format!("WHEN '{}' THEN ({})", disc_val, val_sql));
|
||||||
"WHEN {}.{} = '{}' THEN ({})",
|
|
||||||
node.parent_alias, type_col, disc_val, val_sql
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -415,7 +424,12 @@ impl<'a> Compiler<'a> {
|
|||||||
return Ok(("NULL".to_string(), "string".to_string()));
|
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()))
|
Ok((sql, "object".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user