Skip to content

Commit ad36d13

Browse files
Update sql literals to match spec (#2196)
1 parent 2fc0361 commit ad36d13

File tree

7 files changed

+457
-119
lines changed

7 files changed

+457
-119
lines changed

Cargo.lock

+16-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ axum = { version = "0.7", features = ["tracing"] }
127127
axum-extra = { version = "0.9", features = ["typed-header"] }
128128
backtrace = "0.3.66"
129129
base64 = "0.21.2"
130+
bigdecimal = "0.4.7"
130131
bitflags = "2.3.3"
131132
blake3 = "1.5.1"
132133
brotli = "3.5"
@@ -227,7 +228,6 @@ socket2 = "0.5"
227228
sqllogictest = "0.17"
228229
sqllogictest-engines = "0.17"
229230
sqlparser = "0.38.0"
230-
string-interner = "0.17.0"
231231
strum = { version = "0.25.0", features = ["derive"] }
232232
syn = { version = "2", features = ["full", "extra-traits"] }
233233
syntect = { version = "5.0.0", default-features = false, features = ["default-fancy"] }

crates/expr/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ license-file = "LICENSE"
77
description = "The logical expression representation for the SpacetimeDB query engine"
88

99
[dependencies]
10+
anyhow.workspace = true
11+
bigdecimal.workspace = true
1012
derive_more.workspace = true
11-
string-interner.workspace = true
13+
ethnum.workspace = true
1214
thiserror.workspace = true
1315
spacetimedb-lib.workspace = true
1416
spacetimedb-primitives.workspace = true

crates/expr/src/check.rs

+216-37
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,21 @@ mod tests {
239239
(
240240
"t",
241241
ProductType::from([
242-
("int", AlgebraicType::U32),
242+
("i8", AlgebraicType::I8),
243+
("u8", AlgebraicType::U8),
244+
("i16", AlgebraicType::I16),
245+
("u16", AlgebraicType::U16),
246+
("i32", AlgebraicType::I32),
243247
("u32", AlgebraicType::U32),
248+
("i64", AlgebraicType::I64),
249+
("u64", AlgebraicType::U64),
250+
("int", AlgebraicType::U32),
244251
("f32", AlgebraicType::F32),
252+
("f64", AlgebraicType::F64),
253+
("i128", AlgebraicType::I128),
254+
("u128", AlgebraicType::U128),
255+
("i256", AlgebraicType::I256),
256+
("u256", AlgebraicType::U256),
245257
("str", AlgebraicType::String),
246258
("arr", AlgebraicType::array(AlgebraicType::String)),
247259
]),
@@ -258,57 +270,224 @@ mod tests {
258270
])
259271
}
260272

273+
#[test]
274+
fn valid_literals() {
275+
let tx = SchemaViewer(module_def());
276+
277+
struct TestCase {
278+
sql: &'static str,
279+
msg: &'static str,
280+
}
281+
282+
for TestCase { sql, msg } in [
283+
TestCase {
284+
sql: "select * from t where i32 = -1",
285+
msg: "Leading `-`",
286+
},
287+
TestCase {
288+
sql: "select * from t where u32 = +1",
289+
msg: "Leading `+`",
290+
},
291+
TestCase {
292+
sql: "select * from t where u32 = 1e3",
293+
msg: "Scientific notation",
294+
},
295+
TestCase {
296+
sql: "select * from t where u32 = 1E3",
297+
msg: "Case insensitive scientific notation",
298+
},
299+
TestCase {
300+
sql: "select * from t where f32 = 1e3",
301+
msg: "Integers can parse as floats",
302+
},
303+
TestCase {
304+
sql: "select * from t where f32 = 1e-3",
305+
msg: "Negative exponent",
306+
},
307+
TestCase {
308+
sql: "select * from t where f32 = 0.1",
309+
msg: "Standard decimal notation",
310+
},
311+
TestCase {
312+
sql: "select * from t where f32 = .1",
313+
msg: "Leading `.`",
314+
},
315+
TestCase {
316+
sql: "select * from t where f32 = 1e40",
317+
msg: "Infinity",
318+
},
319+
TestCase {
320+
sql: "select * from t where u256 = 1e40",
321+
msg: "u256",
322+
},
323+
] {
324+
let result = parse_and_type_sub(sql, &tx);
325+
assert!(result.is_ok(), "{msg}");
326+
}
327+
}
328+
329+
#[test]
330+
fn valid_literals_for_type() {
331+
let tx = SchemaViewer(module_def());
332+
333+
for ty in [
334+
"i8", "u8", "i16", "u16", "i32", "u32", "i64", "u64", "f32", "f64", "i128", "u128", "i256", "u256",
335+
] {
336+
let sql = format!("select * from t where {ty} = 127");
337+
let result = parse_and_type_sub(&sql, &tx);
338+
assert!(result.is_ok(), "Faild to parse {ty}: {}", result.unwrap_err());
339+
}
340+
}
341+
342+
#[test]
343+
fn invalid_literals() {
344+
let tx = SchemaViewer(module_def());
345+
346+
struct TestCase {
347+
sql: &'static str,
348+
msg: &'static str,
349+
}
350+
351+
for TestCase { sql, msg } in [
352+
TestCase {
353+
sql: "select * from t where u8 = -1",
354+
msg: "Negative integer for unsigned column",
355+
},
356+
TestCase {
357+
sql: "select * from t where u8 = 1e3",
358+
msg: "Out of bounds",
359+
},
360+
TestCase {
361+
sql: "select * from t where u8 = 0.1",
362+
msg: "Float as integer",
363+
},
364+
TestCase {
365+
sql: "select * from t where u32 = 1e-3",
366+
msg: "Float as integer",
367+
},
368+
TestCase {
369+
sql: "select * from t where i32 = 1e-3",
370+
msg: "Float as integer",
371+
},
372+
] {
373+
let result = parse_and_type_sub(sql, &tx);
374+
assert!(result.is_err(), "{msg}");
375+
}
376+
}
377+
261378
#[test]
262379
fn valid() {
263380
let tx = SchemaViewer(module_def());
264381

265-
for sql in [
266-
"select * from t",
267-
"select * from t where true",
268-
"select * from t where t.u32 = 1",
269-
"select * from t where u32 = 1",
270-
"select * from t where t.u32 = 1 or t.str = ''",
271-
"select * from s where s.bytes = 0xABCD or bytes = X'ABCD'",
272-
"select * from s as r where r.bytes = 0xABCD or bytes = X'ABCD'",
273-
"select t.* from t join s",
274-
"select t.* from t join s join s as r where t.u32 = s.u32 and s.u32 = r.u32",
275-
"select t.* from t join s on t.u32 = s.u32 where t.f32 = 0.1",
382+
struct TestCase {
383+
sql: &'static str,
384+
msg: &'static str,
385+
}
386+
387+
for TestCase { sql, msg } in [
388+
TestCase {
389+
sql: "select * from t",
390+
msg: "Can select * on any table",
391+
},
392+
TestCase {
393+
sql: "select * from t where true",
394+
msg: "Boolean literals are valid in WHERE clause",
395+
},
396+
TestCase {
397+
sql: "select * from t where t.u32 = 1",
398+
msg: "Can qualify column references with table name",
399+
},
400+
TestCase {
401+
sql: "select * from t where u32 = 1",
402+
msg: "Can leave columns unqualified when unambiguous",
403+
},
404+
TestCase {
405+
sql: "select * from t where t.u32 = 1 or t.str = ''",
406+
msg: "Type OR with qualified column references",
407+
},
408+
TestCase {
409+
sql: "select * from s where s.bytes = 0xABCD or bytes = X'ABCD'",
410+
msg: "Type OR with mixed qualified and unqualified column references",
411+
},
412+
TestCase {
413+
sql: "select * from s as r where r.bytes = 0xABCD or bytes = X'ABCD'",
414+
msg: "Type OR with table alias",
415+
},
416+
TestCase {
417+
sql: "select t.* from t join s",
418+
msg: "Type cross join + projection",
419+
},
420+
TestCase {
421+
sql: "select t.* from t join s join s as r where t.u32 = s.u32 and s.u32 = r.u32",
422+
msg: "Type self join + projection",
423+
},
424+
TestCase {
425+
sql: "select t.* from t join s on t.u32 = s.u32 where t.f32 = 0.1",
426+
msg: "Type inner join + projection",
427+
},
276428
] {
277429
let result = parse_and_type_sub(sql, &tx);
278-
assert!(result.is_ok());
430+
assert!(result.is_ok(), "{msg}");
279431
}
280432
}
281433

282434
#[test]
283435
fn invalid() {
284436
let tx = SchemaViewer(module_def());
285437

286-
for sql in [
287-
// Table r does not exist
288-
"select * from r",
289-
// Field a does not exist on table t
290-
"select * from t where t.a = 1",
291-
// Field a does not exist on table t
292-
"select * from t as r where r.a = 1",
293-
// Field u32 is not a string
294-
"select * from t where u32 = 'str'",
295-
// Field u32 is not a float
296-
"select * from t where t.u32 = 1.3",
297-
// t is not in scope after alias
298-
"select * from t as r where t.u32 = 5",
299-
// Subscriptions must be typed to a single table
300-
"select u32 from t",
301-
// Subscriptions must be typed to a single table
302-
"select * from t join s",
303-
// Self join requires aliases
304-
"select t.* from t join t",
305-
// Product values are not comparable
306-
"select t.* from t join s on t.arr = s.arr",
307-
// Alias r is not in scope when it is referenced
308-
"select t.* from t join s on t.u32 = r.u32 join s as r",
438+
struct TestCase {
439+
sql: &'static str,
440+
msg: &'static str,
441+
}
442+
443+
for TestCase { sql, msg } in [
444+
TestCase {
445+
sql: "select * from r",
446+
msg: "Table r does not exist",
447+
},
448+
TestCase {
449+
sql: "select * from t where t.a = 1",
450+
msg: "Field a does not exist on table t",
451+
},
452+
TestCase {
453+
sql: "select * from t as r where r.a = 1",
454+
msg: "Field a does not exist on table t",
455+
},
456+
TestCase {
457+
sql: "select * from t where u32 = 'str'",
458+
msg: "Field u32 is not a string",
459+
},
460+
TestCase {
461+
sql: "select * from t where t.u32 = 1.3",
462+
msg: "Field u32 is not a float",
463+
},
464+
TestCase {
465+
sql: "select * from t as r where t.u32 = 5",
466+
msg: "t is not in scope after alias",
467+
},
468+
TestCase {
469+
sql: "select u32 from t",
470+
msg: "Subscriptions must be typed to a single table",
471+
},
472+
TestCase {
473+
sql: "select * from t join s",
474+
msg: "Subscriptions must be typed to a single table",
475+
},
476+
TestCase {
477+
sql: "select t.* from t join t",
478+
msg: "Self join requires aliases",
479+
},
480+
TestCase {
481+
sql: "select t.* from t join s on t.arr = s.arr",
482+
msg: "Product values are not comparable",
483+
},
484+
TestCase {
485+
sql: "select t.* from t join s on t.u32 = r.u32 join s as r",
486+
msg: "Alias r is not in scope when it is referenced",
487+
},
309488
] {
310489
let result = parse_and_type_sub(sql, &tx);
311-
assert!(result.is_err());
490+
assert!(result.is_err(), "{msg}");
312491
}
313492
}
314493
}

0 commit comments

Comments
 (0)