Skip to content

store: ensure consistent id type in FindRangeQuery SQL output #6080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion store/postgres/src/relational/query_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ use std::{collections::BTreeSet, sync::Arc};

use diesel::{debug_query, pg::Pg};
use graph::{
data_source::CausalityRegion,
prelude::{r, serde_json as json, DeploymentHash, EntityFilter},
schema::InputSchema,
};

use crate::{
block_range::BoundSide,
layout_for_tests::{make_dummy_site, Namespace},
relational::{Catalog, ColumnType, Layout},
relational_queries::FromColumnValue,
relational_queries::{FindRangeQuery, FromColumnValue},
};

use crate::relational_queries::Filter;
Expand Down Expand Up @@ -86,3 +88,112 @@ fn prefix() {
let filter = EntityFilter::In("address".to_string(), vec!["0xbeef".into()]);
filter_contains(filter, r#"substring(c."address", 1, 64) in ($1)"#);
}

#[test]
fn find_range_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();

let causality_region = CausalityRegion::ONCHAIN;
let bound_side = BoundSide::Lower;
let block_range = 100..200;

test_id_type_casting(
string_table.as_ref(),
"id::bytea",
"String ID should be cast to bytea",
);
test_id_type_casting(bytes_table.as_ref(), "id", "Bytes ID should remain as id");
test_id_type_casting(
int8_table.as_ref(),
"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 = FindRangeQuery::new(&tables, causality_region, bound_side, block_range);
let sql = debug_query::<Pg, _>(&query).to_string();

assert!(
sql.contains("id::bytea"),
"String entity ID casting should be present in UNION query"
);
assert!(
sql.contains("id as id"),
"Bytes entity ID should be present in UNION query"
);
assert!(
sql.contains("id::text::bytea"),
"Int8 entity ID casting should be present in UNION query"
);

assert!(
sql.contains("union all"),
"Multiple tables should generate UNION ALL queries"
);
assert!(
sql.contains("order by block_number, entity, id"),
"Query should end with proper ordering"
);
}

fn test_id_type_casting(table: &crate::relational::Table, expected_cast: &str, test_name: &str) {
let causality_region = CausalityRegion::ONCHAIN;
let bound_side = BoundSide::Lower;
let block_range = 100..200;

let tables = vec![table];
let query = FindRangeQuery::new(&tables, causality_region, bound_side, block_range);
let sql = debug_query::<Pg, _>(&query).to_string();

assert!(
sql.contains(expected_cast),
"{}: Expected '{}' in SQL, got: {}",
test_name,
expected_cast,
sql
);
}
16 changes: 14 additions & 2 deletions store/postgres/src/relational_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1884,7 +1884,19 @@ impl<'a> QueryFragment<Pg> for FindRangeQuery<'a> {
} else {
self.mut_range.compare_column(&mut out)
}
out.push_sql("as block_number, id, vid\n");
// Cast id to bytea to ensure consistent types across UNION
// The actual id type can be text, bytea, or numeric depending on the entity
out.push_sql("as block_number, ");
let pk_column = table.primary_key();

// We only support entity id types of string, bytes, and int8.
match pk_column.column_type {
ColumnType::String => out.push_sql("id::bytea"),
ColumnType::Bytes => out.push_sql("id"),
ColumnType::Int8 => out.push_sql("id::text::bytea"),
_ => out.push_sql("id::bytea"),
}
out.push_sql(" as id, vid\n");
out.push_sql(" from ");
out.push_sql(table.qualified_name.as_str());
out.push_sql(" e\n where");
Expand All @@ -1906,7 +1918,7 @@ impl<'a> QueryFragment<Pg> for FindRangeQuery<'a> {
// In case we have only immutable entities, the upper range will not create any
// select statement. So here we have to generate an SQL statement thet returns
// empty result.
out.push_sql("select 'dummy_entity' as entity, to_jsonb(1) as data, 1 as block_number, 1 as id, 1 as vid where false");
out.push_sql("select 'dummy_entity' as entity, to_jsonb(1) as data, 1 as block_number, '\\x'::bytea as id, 1 as vid where false");
} else {
out.push_sql("\norder by block_number, entity, id");
}
Expand Down
6 changes: 6 additions & 0 deletions tests/integration-tests/source-subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ type Block2 @entity(immutable: true) {
hash: Bytes!
testMessage: String
}

type Block3 @entity(immutable: true) {
id: Bytes!
number: BigInt!
testMessage: String
}
8 changes: 7 additions & 1 deletion tests/integration-tests/source-subgraph/src/mapping.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ethereum, log, store } from '@graphprotocol/graph-ts';
import { Block, Block2 } from '../generated/schema';
import { Block, Block2, Block3 } from '../generated/schema';

export function handleBlock(block: ethereum.Block): void {
log.info('handleBlock {}', [block.number.toString()]);
Expand All @@ -22,4 +22,10 @@ export function handleBlock(block: ethereum.Block): void {
blockEntity3.hash = block.hash;
blockEntity3.testMessage = block.number.toString().concat('-message');
blockEntity3.save();

let id4 = block.hash;
let blockEntity4 = new Block3(id4);
blockEntity4.number = block.number;
blockEntity4.testMessage = block.number.toString().concat('-message');
blockEntity4.save();
}
6 changes: 6 additions & 0 deletions tests/integration-tests/subgraph-data-sources/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ type MirrorBlock @entity {
hash: Bytes!
testMessage: String
}

type MirrorBlockBytes @entity {
id: Bytes!
number: BigInt!
testMessage: String
}
14 changes: 12 additions & 2 deletions tests/integration-tests/subgraph-data-sources/src/mapping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { log, store } from '@graphprotocol/graph-ts';
import { Block, Block2 } from '../generated/subgraph-QmWi3H11QFE2PiWx6WcQkZYZdA5UasaBptUJqGn54MFux5';
import { MirrorBlock } from '../generated/schema';
import { Block, Block2, Block3 } from '../generated/subgraph-QmRWTEejPDDwALaquFGm6X2GBbbh5osYDXwCRRkoZ6KQhb';
import { MirrorBlock, MirrorBlockBytes } from '../generated/schema';

export function handleEntity(block: Block): void {
let id = block.id;
Expand All @@ -23,6 +23,16 @@ export function handleEntity2(block: Block2): void {
blockEntity.save();
}

export function handleEntity3(block: Block3): void {
let id = block.id;

let blockEntity = new MirrorBlockBytes(id);
blockEntity.number = block.number;
blockEntity.testMessage = block.testMessage;

blockEntity.save();
}

export function loadOrCreateMirrorBlock(id: string): MirrorBlock {
let block = MirrorBlock.load(id);
if (!block) {
Expand Down
4 changes: 3 additions & 1 deletion tests/integration-tests/subgraph-data-sources/subgraph.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dataSources:
name: Contract
network: test
source:
address: 'QmWi3H11QFE2PiWx6WcQkZYZdA5UasaBptUJqGn54MFux5'
address: 'QmRWTEejPDDwALaquFGm6X2GBbbh5osYDXwCRRkoZ6KQhb'
startBlock: 0
mapping:
apiVersion: 0.0.7
Expand All @@ -18,4 +18,6 @@ dataSources:
entity: Block
- handler: handleEntity2
entity: Block2
- handler: handleEntity3
entity: Block3
file: ./src/mapping.ts