Skip to content
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
28 changes: 16 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ axum = { version = "0.7", features = ["tracing"] }
axum-extra = { version = "0.9", features = ["typed-header"] }
backtrace = "0.3.66"
base64 = "0.21.2"
bigdecimal = "0.4.7"
bitflags = "2.3.3"
blake3 = "1.5.1"
brotli = "3.5"
Expand Down Expand Up @@ -227,7 +228,6 @@ socket2 = "0.5"
sqllogictest = "0.17"
sqllogictest-engines = "0.17"
sqlparser = "0.38.0"
string-interner = "0.17.0"
strum = { version = "0.25.0", features = ["derive"] }
syn = { version = "2", features = ["full", "extra-traits"] }
syntect = { version = "5.0.0", default-features = false, features = ["default-fancy"] }
Expand Down
4 changes: 3 additions & 1 deletion crates/expr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ license-file = "LICENSE"
description = "The logical expression representation for the SpacetimeDB query engine"

[dependencies]
anyhow.workspace = true
bigdecimal.workspace = true
derive_more.workspace = true
string-interner.workspace = true
ethnum.workspace = true
thiserror.workspace = true
spacetimedb-lib.workspace = true
spacetimedb-primitives.workspace = true
Expand Down
253 changes: 216 additions & 37 deletions crates/expr/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,21 @@ mod tests {
(
"t",
ProductType::from([
("int", AlgebraicType::U32),
("i8", AlgebraicType::I8),
("u8", AlgebraicType::U8),
("i16", AlgebraicType::I16),
("u16", AlgebraicType::U16),
("i32", AlgebraicType::I32),
("u32", AlgebraicType::U32),
("i64", AlgebraicType::I64),
("u64", AlgebraicType::U64),
("int", AlgebraicType::U32),
("f32", AlgebraicType::F32),
("f64", AlgebraicType::F64),
("i128", AlgebraicType::I128),
("u128", AlgebraicType::U128),
("i256", AlgebraicType::I256),
("u256", AlgebraicType::U256),
("str", AlgebraicType::String),
("arr", AlgebraicType::array(AlgebraicType::String)),
]),
Expand All @@ -258,57 +270,224 @@ mod tests {
])
}

#[test]
fn valid_literals() {
let tx = SchemaViewer(module_def());

struct TestCase {
sql: &'static str,
msg: &'static str,
}

for TestCase { sql, msg } in [
TestCase {
sql: "select * from t where i32 = -1",
msg: "Leading `-`",
},
TestCase {
sql: "select * from t where u32 = +1",
msg: "Leading `+`",
},
TestCase {
sql: "select * from t where u32 = 1e3",
msg: "Scientific notation",
},
TestCase {
sql: "select * from t where u32 = 1E3",
msg: "Case insensitive scientific notation",
},
TestCase {
sql: "select * from t where f32 = 1e3",
msg: "Integers can parse as floats",
},
TestCase {
sql: "select * from t where f32 = 1e-3",
msg: "Negative exponent",
},
TestCase {
sql: "select * from t where f32 = 0.1",
msg: "Standard decimal notation",
},
TestCase {
sql: "select * from t where f32 = .1",
msg: "Leading `.`",
},
TestCase {
sql: "select * from t where f32 = 1e40",
msg: "Infinity",
},
TestCase {
sql: "select * from t where u256 = 1e40",
msg: "u256",
},
] {
let result = parse_and_type_sub(sql, &tx);
assert!(result.is_ok(), "{msg}");
}
}

#[test]
fn valid_literals_for_type() {
let tx = SchemaViewer(module_def());

for ty in [
"i8", "u8", "i16", "u16", "i32", "u32", "i64", "u64", "f32", "f64", "i128", "u128", "i256", "u256",
] {
let sql = format!("select * from t where {ty} = 127");
let result = parse_and_type_sub(&sql, &tx);
assert!(result.is_ok(), "Faild to parse {ty}: {}", result.unwrap_err());
}
}

#[test]
fn invalid_literals() {
let tx = SchemaViewer(module_def());

struct TestCase {
sql: &'static str,
msg: &'static str,
}

for TestCase { sql, msg } in [
TestCase {
sql: "select * from t where u8 = -1",
msg: "Negative integer for unsigned column",
},
TestCase {
sql: "select * from t where u8 = 1e3",
msg: "Out of bounds",
},
TestCase {
sql: "select * from t where u8 = 0.1",
msg: "Float as integer",
},
TestCase {
sql: "select * from t where u32 = 1e-3",
msg: "Float as integer",
},
TestCase {
sql: "select * from t where i32 = 1e-3",
msg: "Float as integer",
},
] {
let result = parse_and_type_sub(sql, &tx);
assert!(result.is_err(), "{msg}");
}
}

#[test]
fn valid() {
let tx = SchemaViewer(module_def());

for sql in [
"select * from t",
"select * from t where true",
"select * from t where t.u32 = 1",
"select * from t where u32 = 1",
"select * from t where t.u32 = 1 or t.str = ''",
"select * from s where s.bytes = 0xABCD or bytes = X'ABCD'",
"select * from s as r where r.bytes = 0xABCD or bytes = X'ABCD'",
"select t.* from t join s",
"select t.* from t join s join s as r where t.u32 = s.u32 and s.u32 = r.u32",
"select t.* from t join s on t.u32 = s.u32 where t.f32 = 0.1",
struct TestCase {
sql: &'static str,
msg: &'static str,
}

for TestCase { sql, msg } in [
TestCase {
sql: "select * from t",
msg: "Can select * on any table",
},
TestCase {
sql: "select * from t where true",
msg: "Boolean literals are valid in WHERE clause",
},
TestCase {
sql: "select * from t where t.u32 = 1",
msg: "Can qualify column references with table name",
},
TestCase {
sql: "select * from t where u32 = 1",
msg: "Can leave columns unqualified when unambiguous",
},
TestCase {
sql: "select * from t where t.u32 = 1 or t.str = ''",
msg: "Type OR with qualified column references",
},
TestCase {
sql: "select * from s where s.bytes = 0xABCD or bytes = X'ABCD'",
msg: "Type OR with mixed qualified and unqualified column references",
},
TestCase {
sql: "select * from s as r where r.bytes = 0xABCD or bytes = X'ABCD'",
msg: "Type OR with table alias",
},
TestCase {
sql: "select t.* from t join s",
msg: "Type cross join + projection",
},
TestCase {
sql: "select t.* from t join s join s as r where t.u32 = s.u32 and s.u32 = r.u32",
msg: "Type self join + projection",
},
TestCase {
sql: "select t.* from t join s on t.u32 = s.u32 where t.f32 = 0.1",
msg: "Type inner join + projection",
},
] {
let result = parse_and_type_sub(sql, &tx);
assert!(result.is_ok());
assert!(result.is_ok(), "{msg}");
}
}

#[test]
fn invalid() {
let tx = SchemaViewer(module_def());

for sql in [
// Table r does not exist
"select * from r",
// Field a does not exist on table t
"select * from t where t.a = 1",
// Field a does not exist on table t
"select * from t as r where r.a = 1",
// Field u32 is not a string
"select * from t where u32 = 'str'",
// Field u32 is not a float
"select * from t where t.u32 = 1.3",
// t is not in scope after alias
"select * from t as r where t.u32 = 5",
// Subscriptions must be typed to a single table
"select u32 from t",
// Subscriptions must be typed to a single table
"select * from t join s",
// Self join requires aliases
"select t.* from t join t",
// Product values are not comparable
"select t.* from t join s on t.arr = s.arr",
// Alias r is not in scope when it is referenced
"select t.* from t join s on t.u32 = r.u32 join s as r",
struct TestCase {
sql: &'static str,
msg: &'static str,
}

for TestCase { sql, msg } in [
TestCase {
sql: "select * from r",
msg: "Table r does not exist",
},
TestCase {
sql: "select * from t where t.a = 1",
msg: "Field a does not exist on table t",
},
TestCase {
sql: "select * from t as r where r.a = 1",
msg: "Field a does not exist on table t",
},
TestCase {
sql: "select * from t where u32 = 'str'",
msg: "Field u32 is not a string",
},
TestCase {
sql: "select * from t where t.u32 = 1.3",
msg: "Field u32 is not a float",
},
TestCase {
sql: "select * from t as r where t.u32 = 5",
msg: "t is not in scope after alias",
},
TestCase {
sql: "select u32 from t",
msg: "Subscriptions must be typed to a single table",
},
TestCase {
sql: "select * from t join s",
msg: "Subscriptions must be typed to a single table",
},
TestCase {
sql: "select t.* from t join t",
msg: "Self join requires aliases",
},
TestCase {
sql: "select t.* from t join s on t.arr = s.arr",
msg: "Product values are not comparable",
},
TestCase {
sql: "select t.* from t join s on t.u32 = r.u32 join s as r",
msg: "Alias r is not in scope when it is referenced",
},
] {
let result = parse_and_type_sub(sql, &tx);
assert!(result.is_err());
assert!(result.is_err(), "{msg}");
}
}
}
Loading