Skip to content

Commit 86b3ac1

Browse files
Fix SQL parser to support negative numbers in INSERT statements (#4660)
This fix is intended to resolve the community reported issue #4659 # Description of Changes Add handling for `UnaryOperator::Minus` and `UnaryOperator::Plus` in a new `parse_insert_value()` used during `INSERT` operations. This works by convert negative unary expressions to signed numeric strings in INSERT VALUES clauses. # API and ABI breaking changes None. # Expected complexity level and risk 1 (Low). Localized parser fix. # Testing - [X] Ran local tests to verify negative numbers work in Rust, TypeScript, and C# modules - [X] Confirmed positive numbers and invalid expressions still work correctly * Testing involved running `spacetime sql module "INSERT INTO table (col) VALUES (-100.0);"` from a CLI. --------- Co-authored-by: joshua-spacetime <josh@clockworklabs.io>
1 parent 033c625 commit 86b3ac1

2 files changed

Lines changed: 69 additions & 28 deletions

File tree

crates/sql-parser/src/parser/mod.rs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,6 @@ const _: () = assert!(size_of::<SqlParseResult<SqlExpr>>() == 40);
217217

218218
/// Parse a scalar expression
219219
fn parse_expr(expr: Expr, depth: usize) -> SqlParseResult<SqlExpr> {
220-
fn signed_num(sign: impl Into<String>, expr: Expr) -> Result<SqlExpr, Box<SqlUnsupported>> {
221-
match expr {
222-
Expr::Value(Value::Number(n, _)) => Ok(SqlExpr::Lit(SqlLiteral::Num((sign.into() + &n).into_boxed_str()))),
223-
expr => Err(SqlUnsupported::Expr(expr).into()),
224-
}
225-
}
226220
recursion::guard(depth, recursion::MAX_RECURSION_EXPR, "sql-parser::parse_expr")?;
227221
match expr {
228222
Expr::Nested(expr) => parse_expr(*expr, depth + 1),
@@ -231,15 +225,19 @@ fn parse_expr(expr: Expr, depth: usize) -> SqlParseResult<SqlExpr> {
231225
Expr::UnaryOp {
232226
op: UnaryOperator::Plus,
233227
expr,
234-
} if matches!(&*expr, Expr::Value(Value::Number(..))) => {
235-
signed_num("+", *expr).map_err(SqlParseError::SqlUnsupported)
236-
}
228+
} => Ok(SqlExpr::Lit(parse_signed_literal_expr(
229+
UnaryOperator::Plus,
230+
*expr,
231+
SqlUnsupported::Expr,
232+
)?)),
237233
Expr::UnaryOp {
238234
op: UnaryOperator::Minus,
239235
expr,
240-
} if matches!(&*expr, Expr::Value(Value::Number(..))) => {
241-
signed_num("-", *expr).map_err(SqlParseError::SqlUnsupported)
242-
}
236+
} => Ok(SqlExpr::Lit(parse_signed_literal_expr(
237+
UnaryOperator::Minus,
238+
*expr,
239+
SqlUnsupported::Expr,
240+
)?)),
243241
Expr::Identifier(ident) => Ok(SqlExpr::Var(ident.into())),
244242
Expr::CompoundIdentifier(mut idents) if idents.len() == 2 => {
245243
let table = idents.swap_remove(0).into();
@@ -273,6 +271,44 @@ fn parse_expr(expr: Expr, depth: usize) -> SqlParseResult<SqlExpr> {
273271
}
274272
}
275273

274+
fn parse_signed_literal_expr(
275+
op: UnaryOperator,
276+
expr: Expr,
277+
unsupported: fn(Expr) -> SqlUnsupported,
278+
) -> SqlParseResult<SqlLiteral> {
279+
match expr {
280+
Expr::Value(Value::Number(n, _)) => {
281+
let sign = match op {
282+
UnaryOperator::Plus => "+",
283+
UnaryOperator::Minus => "-",
284+
_ => unreachable!("caller only passes unary plus/minus"),
285+
};
286+
Ok(SqlLiteral::Num(format!("{sign}{n}").into_boxed_str()))
287+
}
288+
expr => Err(unsupported(Expr::UnaryOp {
289+
op,
290+
expr: Box::new(expr),
291+
})
292+
.into()),
293+
}
294+
}
295+
296+
/// Parse a literal expression.
297+
pub(crate) fn parse_literal_expr(expr: Expr, unsupported: fn(Expr) -> SqlUnsupported) -> SqlParseResult<SqlLiteral> {
298+
match expr {
299+
Expr::Value(value) => parse_literal(value),
300+
Expr::UnaryOp {
301+
op: UnaryOperator::Plus,
302+
expr,
303+
} => parse_signed_literal_expr(UnaryOperator::Plus, *expr, unsupported),
304+
Expr::UnaryOp {
305+
op: UnaryOperator::Minus,
306+
expr,
307+
} => parse_signed_literal_expr(UnaryOperator::Minus, *expr, unsupported),
308+
expr => Err(unsupported(expr).into()),
309+
}
310+
}
311+
276312
/// Parse an optional scalar expression
277313
pub(crate) fn parse_expr_opt(opt: Option<Expr>) -> SqlParseResult<Option<SqlExpr>> {
278314
opt.map(|expr| parse_expr(expr, 0)).transpose()

crates/sql-parser/src/parser/sql.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ use crate::ast::{
142142
};
143143

144144
use super::{
145-
errors::SqlUnsupported, parse_expr_opt, parse_ident, parse_literal, parse_parts, parse_projection, RelParser,
145+
errors::SqlUnsupported, parse_expr_opt, parse_ident, parse_literal_expr, parse_parts, parse_projection, RelParser,
146146
SqlParseResult,
147147
};
148148

@@ -242,11 +242,7 @@ fn parse_values(values: Query) -> SqlParseResult<SqlValues> {
242242
for row in rows {
243243
let mut literals = Vec::new();
244244
for expr in row {
245-
if let Expr::Value(value) = expr {
246-
literals.push(parse_literal(value)?);
247-
} else {
248-
return Err(SqlUnsupported::InsertValue(expr).into());
249-
}
245+
literals.push(parse_literal_expr(expr, SqlUnsupported::InsertValue)?);
250246
}
251247
row_literals.push(literals);
252248
}
@@ -274,10 +270,10 @@ fn parse_assignments(assignments: Vec<Assignment>) -> SqlParseResult<Vec<SqlSet>
274270

275271
/// Parse a column/variable assignment in an UPDATE or SET statement
276272
fn parse_assignment(Assignment { id, value }: Assignment) -> SqlParseResult<SqlSet> {
277-
match value {
278-
Expr::Value(value) => Ok(SqlSet(parse_parts(id)?, parse_literal(value)?)),
279-
_ => Err(SqlUnsupported::Assignment(value).into()),
280-
}
273+
Ok(SqlSet(
274+
parse_parts(id)?,
275+
parse_literal_expr(value, SqlUnsupported::Assignment)?,
276+
))
281277
}
282278

283279
/// Parse a DELETE statement
@@ -311,12 +307,7 @@ fn parse_set_var(variable: ObjectName, mut value: Vec<Expr>) -> SqlParseResult<S
311307
if value.len() == 1 {
312308
Ok(SqlSet(
313309
parse_ident(variable)?,
314-
match value.swap_remove(0) {
315-
Expr::Value(value) => parse_literal(value)?,
316-
expr => {
317-
return Err(SqlUnsupported::Assignment(expr).into());
318-
}
319-
},
310+
parse_literal_expr(value.swap_remove(0), SqlUnsupported::Assignment)?,
320311
))
321312
} else {
322313
Err(SqlUnsupported::feature(Statement::SetVariable {
@@ -454,6 +445,20 @@ mod tests {
454445
}
455446
}
456447

448+
#[test]
449+
fn signed_numeric_literals_are_supported_across_sql_api() {
450+
for sql in [
451+
"select a from t where b = -1",
452+
"delete from t where a = +1",
453+
"insert into t values (-1, +2.5)",
454+
"update t set a = -1, b = +2 where c = -3",
455+
"set x = -1",
456+
"set y to +2.5",
457+
] {
458+
assert!(parse_sql(sql).is_ok());
459+
}
460+
}
461+
457462
#[test]
458463
fn invalid() {
459464
for sql in [

0 commit comments

Comments
 (0)