Skip to content

Commit 010f194

Browse files
committed
store: Add SQL query tests
1 parent 9469b83 commit 010f194

File tree

3 files changed

+286
-3
lines changed

3 files changed

+286
-3
lines changed

store/test-store/tests/graphql.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod graphql {
22
pub mod introspection;
33
pub mod query;
4+
pub mod sql;
45
}

store/test-store/tests/graphql/query.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl std::fmt::Display for IdVal {
9797
}
9898

9999
#[derive(Clone, Copy, Debug)]
100-
enum IdType {
100+
pub enum IdType {
101101
String,
102102
Bytes,
103103
Int8,
@@ -157,7 +157,7 @@ impl IdType {
157157
}
158158
}
159159

160-
fn deployment_id(&self) -> &str {
160+
pub fn deployment_id(&self) -> &str {
161161
match self {
162162
IdType::String => "graphqlTestsQuery",
163163
IdType::Bytes => "graphqlTestsQueryBytes",
@@ -176,7 +176,7 @@ async fn setup_readonly(store: &Store) -> DeploymentLocator {
176176
/// data. If the `id` is the same as `id_type.deployment_id()`, the test
177177
/// must not modify the deployment in any way as these are reused for other
178178
/// tests that expect pristine data
179-
async fn setup(
179+
pub async fn setup(
180180
store: &Store,
181181
id: &str,
182182
features: BTreeSet<SubgraphFeature>,
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// SQL Query Tests for Graph Node
2+
// These tests parallel the GraphQL tests in query.rs but use SQL queries
3+
4+
use graph::components::store::QueryStoreManager;
5+
use graph::data::query::QueryTarget;
6+
use graph::data::store::SqlQueryObject;
7+
use graph::prelude::{r, QueryExecutionError};
8+
use std::collections::BTreeSet;
9+
use test_store::{run_test_sequentially, STORE};
10+
11+
// Import test setup from query.rs module
12+
use super::query::{setup, IdType};
13+
14+
/// Synchronous wrapper for SQL query execution
15+
fn run_sql_query<F>(sql: &str, test: F)
16+
where
17+
F: Fn(Result<Vec<SqlQueryObject>, QueryExecutionError>, IdType) + Send + 'static,
18+
{
19+
let sql = sql.to_string(); // Convert to owned String
20+
run_test_sequentially(move |store| async move {
21+
for id_type in [IdType::String, IdType::Bytes, IdType::Int8] {
22+
let name = id_type.deployment_id();
23+
let deployment = setup(store.as_ref(), name, BTreeSet::new(), id_type).await;
24+
25+
let query_store = STORE
26+
.query_store(QueryTarget::Deployment(
27+
deployment.hash.clone(),
28+
Default::default(),
29+
))
30+
.await
31+
.unwrap();
32+
33+
let result = query_store.execute_sql(&sql);
34+
test(result, id_type);
35+
}
36+
})
37+
}
38+
39+
#[test]
40+
fn sql_can_query_simple_select() {
41+
const SQL: &str = "SELECT id, name FROM musician ORDER BY id";
42+
43+
run_sql_query(SQL, |result, _| {
44+
let results = result.expect("SQL query should succeed");
45+
assert_eq!(results.len(), 5, "Should return 5 musicians");
46+
47+
// Check first musician
48+
if let Some(first) = results.first() {
49+
if let r::Value::Object(ref obj) = first.0 {
50+
if let Some(r::Value::String(name)) = obj.get("name") {
51+
assert_eq!(name, "John", "First musician should be John");
52+
}
53+
}
54+
}
55+
});
56+
}
57+
58+
#[test]
59+
fn sql_can_query_with_where_clause() {
60+
const SQL: &str = "SELECT id, name FROM musician WHERE name = 'John'";
61+
62+
run_sql_query(SQL, |result, _| {
63+
let results = result.expect("SQL query should succeed");
64+
assert_eq!(results.len(), 1, "Should return 1 musician named John");
65+
66+
if let Some(first) = results.first() {
67+
if let r::Value::Object(ref obj) = first.0 {
68+
if let Some(r::Value::String(name)) = obj.get("name") {
69+
assert_eq!(name, "John", "Should return John");
70+
}
71+
}
72+
}
73+
});
74+
}
75+
76+
#[test]
77+
fn sql_can_query_with_aggregation() {
78+
const SQL: &str = "SELECT COUNT(*) as total FROM musician";
79+
80+
run_sql_query(SQL, |result, _| {
81+
let results = result.expect("SQL query should succeed");
82+
assert_eq!(results.len(), 1, "Should return 1 row with count");
83+
84+
if let Some(first) = results.first() {
85+
if let r::Value::Object(ref obj) = first.0 {
86+
if let Some(total) = obj.get("total") {
87+
// The count should be a number (could be various forms)
88+
match total {
89+
r::Value::Int(n) => assert_eq!(*n, 5),
90+
r::Value::String(s) => assert_eq!(s, "5"),
91+
_ => panic!("Total should be a number: {:?}", total),
92+
}
93+
}
94+
}
95+
}
96+
});
97+
}
98+
99+
#[test]
100+
fn sql_can_query_with_limit_offset() {
101+
const SQL: &str = "SELECT id, name FROM musician ORDER BY id LIMIT 2 OFFSET 1";
102+
103+
run_sql_query(SQL, |result, _| {
104+
let results = result.expect("SQL query should succeed");
105+
assert_eq!(results.len(), 2, "Should return 2 musicians with offset");
106+
107+
// Should skip first musician (order may vary by id type)
108+
if let Some(first) = results.first() {
109+
if let r::Value::Object(ref obj) = first.0 {
110+
if let Some(r::Value::String(name)) = obj.get("name") {
111+
// Just check we got a valid musician name
112+
assert!(["John", "Lisa", "Tom", "Valerie", "Paul"].contains(&name.as_str()));
113+
}
114+
}
115+
}
116+
});
117+
}
118+
119+
#[test]
120+
fn sql_can_query_with_group_by() {
121+
const SQL: &str = "
122+
SELECT COUNT(*) as musician_count
123+
FROM musician
124+
GROUP BY name
125+
ORDER BY musician_count DESC
126+
";
127+
128+
run_sql_query(SQL, |result, _| {
129+
let results = result.expect("SQL query should succeed");
130+
assert!(!results.is_empty(), "Should return grouped musician counts");
131+
});
132+
}
133+
134+
// Validation Tests
135+
136+
#[test]
137+
fn sql_validates_table_names() {
138+
const SQL: &str = "SELECT * FROM invalid_table";
139+
140+
run_sql_query(SQL, |result, _| {
141+
assert!(result.is_err(), "Query with invalid table should fail");
142+
if let Err(e) = result {
143+
let error_msg = e.to_string();
144+
assert!(
145+
error_msg.contains("Unknown table") || error_msg.contains("invalid_table"),
146+
"Error should mention unknown table: {}",
147+
error_msg
148+
);
149+
}
150+
});
151+
}
152+
153+
#[test]
154+
fn sql_validates_functions() {
155+
// Try to use a potentially dangerous function
156+
const SQL: &str = "SELECT pg_sleep(1)";
157+
158+
run_sql_query(SQL, |result, _| {
159+
assert!(result.is_err(), "Query with blocked function should fail");
160+
if let Err(e) = result {
161+
let error_msg = e.to_string();
162+
assert!(
163+
error_msg.contains("Unknown or unsupported function")
164+
|| error_msg.contains("pg_sleep"),
165+
"Error should mention unsupported function: {}",
166+
error_msg
167+
);
168+
}
169+
});
170+
}
171+
172+
#[test]
173+
fn sql_blocks_ddl_statements() {
174+
const SQL: &str = "DROP TABLE musician";
175+
176+
run_sql_query(SQL, |result, _| {
177+
assert!(result.is_err(), "DDL statements should be blocked");
178+
if let Err(e) = result {
179+
let error_msg = e.to_string();
180+
assert!(
181+
error_msg.contains("Only SELECT query is supported") || error_msg.contains("DROP"),
182+
"Error should mention unsupported statement type: {}",
183+
error_msg
184+
);
185+
}
186+
});
187+
}
188+
189+
#[test]
190+
fn sql_blocks_dml_statements() {
191+
const SQL: &str = "DELETE FROM musician WHERE id = 'm1'";
192+
193+
run_sql_query(SQL, |result, _| {
194+
assert!(result.is_err(), "DML statements should be blocked");
195+
if let Err(e) = result {
196+
let error_msg = e.to_string();
197+
assert!(
198+
error_msg.contains("Only SELECT query is supported")
199+
|| error_msg.contains("DELETE"),
200+
"Error should mention unsupported statement type: {}",
201+
error_msg
202+
);
203+
}
204+
});
205+
}
206+
207+
#[test]
208+
fn sql_blocks_multi_statement() {
209+
const SQL: &str = "SELECT * FROM musician; SELECT * FROM band";
210+
211+
run_sql_query(SQL, |result, _| {
212+
assert!(result.is_err(), "Multi-statement queries should be blocked");
213+
if let Err(e) = result {
214+
let error_msg = e.to_string();
215+
assert!(
216+
error_msg.contains("Multi statement is not supported")
217+
|| error_msg.contains("multiple statements"),
218+
"Error should mention multi-statement restriction: {}",
219+
error_msg
220+
);
221+
}
222+
});
223+
}
224+
225+
#[test]
226+
fn sql_can_query_with_case_expression() {
227+
const SQL: &str = "
228+
SELECT
229+
id,
230+
name,
231+
CASE
232+
WHEN favorite_count > 10 THEN 'popular'
233+
WHEN favorite_count > 5 THEN 'liked'
234+
ELSE 'normal'
235+
END as popularity
236+
FROM musician
237+
ORDER BY id
238+
LIMIT 5
239+
";
240+
241+
run_sql_query(SQL, |result, _| {
242+
let results = result.expect("SQL query with CASE should succeed");
243+
assert!(
244+
results.len() <= 5,
245+
"Should return limited musicians with popularity"
246+
);
247+
248+
// Check that popularity field exists in first result
249+
if let Some(first) = results.first() {
250+
if let r::Value::Object(ref obj) = first.0 {
251+
assert!(
252+
obj.get("popularity").is_some(),
253+
"Should have popularity field"
254+
);
255+
}
256+
}
257+
});
258+
}
259+
260+
#[test]
261+
fn sql_can_query_with_subquery() {
262+
const SQL: &str = "
263+
WITH active_musicians AS (
264+
SELECT id, name
265+
FROM musician
266+
WHERE name IS NOT NULL
267+
)
268+
SELECT COUNT(*) as active_count FROM active_musicians
269+
";
270+
271+
run_sql_query(SQL, |result, _| {
272+
let results = result.expect("SQL query with CTE should succeed");
273+
assert_eq!(results.len(), 1, "Should return one count result");
274+
275+
if let Some(first) = results.first() {
276+
if let r::Value::Object(ref obj) = first.0 {
277+
let count = obj.get("active_count");
278+
assert!(count.is_some(), "Should have active_count field");
279+
}
280+
}
281+
});
282+
}

0 commit comments

Comments
 (0)