diff --git a/store/postgres/src/relational/query_tests.rs b/store/postgres/src/relational/query_tests.rs index 3b4582c5b5b..dc557d78ebe 100644 --- a/store/postgres/src/relational/query_tests.rs +++ b/store/postgres/src/relational/query_tests.rs @@ -13,7 +13,9 @@ use crate::{ block_range::BoundSide, layout_for_tests::{Namespace, make_dummy_site}, relational::{Catalog, ColumnType, Layout}, - relational_queries::{FindRangeQuery, FromColumnValue, InsertQuery}, + relational_queries::{ + FindPossibleDeletionsQuery, FindRangeQuery, FromColumnValue, InsertQuery, + }, }; use crate::relational_queries::Filter; @@ -200,6 +202,112 @@ fn test_id_type_casting(table: &crate::relational::Table, expected_cast: &str, t ); } +#[test] +fn find_possible_deletions_query_id_type_casting() { + let string_schema = " + type StringEntity @entity { + id: String!, + name: String + }"; + + let bytes_schema = " + type BytesEntity @entity { + id: Bytes!, + address: Bytes + }"; + + let int8_schema = " + type Int8Entity @entity { + id: Int8!, + value: Int8 + }"; + + let string_layout = test_layout(string_schema); + let bytes_layout = test_layout(bytes_schema); + let int8_layout = test_layout(int8_schema); + + let string_table = string_layout + .table_for_entity( + &string_layout + .input_schema + .entity_type("StringEntity") + .unwrap(), + ) + .unwrap(); + let bytes_table = bytes_layout + .table_for_entity( + &bytes_layout + .input_schema + .entity_type("BytesEntity") + .unwrap(), + ) + .unwrap(); + let int8_table = int8_layout + .table_for_entity(&int8_layout.input_schema.entity_type("Int8Entity").unwrap()) + .unwrap(); + + test_possible_deletions_id_type_casting( + string_table.as_ref(), + "e.id::bytea", + "String ID should be cast to bytea", + ); + test_possible_deletions_id_type_casting( + bytes_table.as_ref(), + "e.id", + "Bytes ID should remain as e.id", + ); + test_possible_deletions_id_type_casting( + int8_table.as_ref(), + "e.id::text::bytea", + "Int8 ID should be cast to text then bytea", + ); + + let tables = vec![ + string_table.as_ref(), + bytes_table.as_ref(), + int8_table.as_ref(), + ]; + let query = FindPossibleDeletionsQuery::new(&tables, 100); + let sql = debug_query::(&query).to_string(); + + assert!( + sql.contains("id::bytea"), + "String entity ID casting should be present in possible-deletions UNION query, got: {}", + sql + ); + assert!( + sql.contains("e.id"), + "Bytes entity ID should be present in possible-deletions UNION query" + ); + assert!( + sql.contains("id::text::bytea"), + "Int8 entity ID casting should be present in possible-deletions UNION query, got: {}", + sql + ); + assert!( + sql.contains("union all"), + "Multiple tables should generate UNION ALL queries" + ); +} + +fn test_possible_deletions_id_type_casting( + table: &crate::relational::Table, + expected_cast: &str, + test_name: &str, +) { + let tables = vec![table]; + let query = FindPossibleDeletionsQuery::new(&tables, 100); + let sql = debug_query::(&query).to_string(); + + assert!( + sql.contains(expected_cast), + "{}: Expected '{}' in SQL, got: {}", + test_name, + expected_cast, + sql + ); +} + fn insert_sql_for_schema(gql: &str, entity_type_name: &str) -> String { use graph::components::store::write::EntityModification; diff --git a/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index d87b950adc1..582762a2c1b 100644 --- a/store/postgres/src/relational_queries.rs +++ b/store/postgres/src/relational_queries.rs @@ -2090,7 +2090,18 @@ impl<'a> QueryFragment for FindPossibleDeletionsQuery<'a> { } else { out.push_sql("0 as causality_region, "); } - out.push_sql("e.id\n"); + let pk_column = table.primary_key(); + + // Cast id to bytea so that every branch of the UNION has the same + // column type; the id can be text, bytea, or numeric depending on the + // entity type, and `UNION ALL` requires a common type per column. + match pk_column.column_type { + ColumnType::String => out.push_sql("e.id::bytea"), + ColumnType::Bytes => out.push_sql("e.id"), + ColumnType::Int8 => out.push_sql("e.id::text::bytea"), + _ => out.push_sql("e.id::bytea"), + } + out.push_sql("\n"); out.push_sql(" from "); out.push_sql(table.qualified_name.as_str()); out.push_sql(" e\n where ");