Repository: tursodatabase/limbo (rust)

Issue:
Incorrect text to numeric conversion in `SELECT -'e'`
`SELECT -'e'` incorrectly returns `-0.0`. 

- [`parse_numeric_str`](https://github.com/tursodatabase/limbo/blob/669317c11e24b56ab256d88c81161c9e69252ee8/core/util.rs#L715) doesn't handle the case that a numeric value shouldn't start with a `e` or `E`.
- The case where the last byte is checked to be `e` or `E` is hit and it shrinks the slice to up until that point, returning `Ok((OwnedValueType::Float, ""))`. 
- [`checked_cast_text_to_numeric`](https://github.com/tursodatabase/limbo/blob/669317c11e24b56ab256d88c81161c9e69252ee8/core/util.rs#L683C8-L683C36) tries to parse a `f64` from a empty string, fails, and returns `Ok(OwnedValue::Float(0.0))`. 

https://github.com/tursodatabase/limbo/blob/669317c11e24b56ab256d88c81161c9e69252ee8/core/util.rs#L715-L763

The correct behavior would be to detect that the text is not convertible and return `OwnedValue::Integer(0)` from [`cast_text_to_numeric`](https://github.com/tursodatabase/limbo/blob/669317c11e24b56ab256d88c81161c9e69252ee8/core/util.rs#L765C8-L765C28) with [`parse_numeric_str`](https://github.com/tursodatabase/limbo/blob/669317c11e24b56ab256d88c81161c9e69252ee8/core/util.rs#L715) returning `Err`.

Found while fuzzing in https://github.com/tursodatabase/limbo/pull/1116.

```
thread '<unnamed>' panicked at fuzz_targets/expression.rs:209:5:
assertion `left == right` failed: with expression Output { query: "- (?)", parameters: [Text("E")], depth: 1 }
  left: Integer(0)
 right: Float(-0.0)

[...]

Output of `std::fmt::Debug`:

        Unary(
            Negative,
            Value(
                Text(
                    "E",
                ),
            ),
        )
```

Pull request:
Fix numeric conversion in `SELECT -'e'`
closes #1157 

Patch:
diff --git a/core/util.rs b/core/util.rs
index 3e141f9a..13b53bcb 100644
--- a/core/util.rs
+++ b/core/util.rs
@@ -713,10 +713,17 @@ pub fn checked_cast_text_to_numeric(text: &str) -> std::result::Result<OwnedValu
 }
 
 fn parse_numeric_str(text: &str) -> Result<(OwnedValueType, &str), ()> {
-    let bytes = text.trim_start().as_bytes();
-    if bytes.is_empty() {
+    let text = text.trim();
+    let bytes = text.as_bytes();
+
+    if bytes.is_empty()
+        || bytes[0] == b'e'
+        || bytes[0] == b'E'
+        || (bytes[0] == b'.' && (bytes[1] == b'e' || bytes[1] == b'E'))
+    {
         return Err(());
     }
+
     let mut end = 0;
     let mut has_decimal = false;
     let mut has_exponent = false;
@@ -1512,5 +1519,117 @@ pub mod tests {
         assert_eq!(cast_text_to_numeric("-0.0"), OwnedValue::Float(0.0));
         assert_eq!(cast_text_to_numeric("0.0"), OwnedValue::Float(0.0));
         assert_eq!(cast_text_to_numeric("-"), OwnedValue::Integer(0));
+        assert_eq!(cast_text_to_numeric("-e"), OwnedValue::Integer(0));
+        assert_eq!(cast_text_to_numeric("-E"), OwnedValue::Integer(0));
+    }
+
+    #[test]
+    fn test_parse_numeric_str_valid_integer() {
+        assert_eq!(
+            parse_numeric_str("123"),
+            Ok((OwnedValueType::Integer, "123"))
+        );
+        assert_eq!(
+            parse_numeric_str("-456"),
+            Ok((OwnedValueType::Integer, "-456"))
+        );
+        assert_eq!(
+            parse_numeric_str("000789"),
+            Ok((OwnedValueType::Integer, "000789"))
+        );
+    }
+
+    #[test]
+    fn test_parse_numeric_str_valid_float() {
+        assert_eq!(
+            parse_numeric_str("123.456"),
+            Ok((OwnedValueType::Float, "123.456"))
+        );
+        assert_eq!(
+            parse_numeric_str("-0.789"),
+            Ok((OwnedValueType::Float, "-0.789"))
+        );
+        assert_eq!(
+            parse_numeric_str("1e10"),
+            Ok((OwnedValueType::Float, "1e10"))
+        );
+        assert_eq!(
+            parse_numeric_str("-1.23e-4"),
+            Ok((OwnedValueType::Float, "-1.23e-4"))
+        );
+        assert_eq!(
+            parse_numeric_str("1.23E+4"),
+            Ok((OwnedValueType::Float, "1.23E+4"))
+        );
+        assert_eq!(
+            parse_numeric_str("1.2.3"),
+            Ok((OwnedValueType::Float, "1.2"))
+        )
+    }
+
+    #[test]
+    fn test_parse_numeric_str_edge_cases() {
+        assert_eq!(parse_numeric_str("1e"), Ok((OwnedValueType::Float, "1")));
+        assert_eq!(parse_numeric_str("1e-"), Ok((OwnedValueType::Float, "1")));
+        assert_eq!(parse_numeric_str("1e+"), Ok((OwnedValueType::Float, "1")));
+        assert_eq!(parse_numeric_str("-1e"), Ok((OwnedValueType::Float, "-1")));
+        assert_eq!(parse_numeric_str("-1e-"), Ok((OwnedValueType::Float, "-1")));
+    }
+
+    #[test]
+    fn test_parse_numeric_str_invalid() {
+        assert_eq!(parse_numeric_str(""), Err(()));
+        assert_eq!(parse_numeric_str("abc"), Err(()));
+        assert_eq!(parse_numeric_str("-"), Err(()));
+        assert_eq!(parse_numeric_str("e10"), Err(()));
+        assert_eq!(parse_numeric_str(".e10"), Err(()));
+    }
+
+    #[test]
+    fn test_parse_numeric_str_with_whitespace() {
+        assert_eq!(
+            parse_numeric_str("   123"),
+            Ok((OwnedValueType::Integer, "123"))
+        );
+        assert_eq!(
+            parse_numeric_str("  -456.78  "),
+            Ok((OwnedValueType::Float, "-456.78"))
+        );
+        assert_eq!(
+            parse_numeric_str("  1.23e4  "),
+            Ok((OwnedValueType::Float, "1.23e4"))
+        );
+    }
+
+    #[test]
+    fn test_parse_numeric_str_leading_zeros() {
+        assert_eq!(
+            parse_numeric_str("000123"),
+            Ok((OwnedValueType::Integer, "000123"))
+        );
+        assert_eq!(
+            parse_numeric_str("000.456"),
+            Ok((OwnedValueType::Float, "000.456"))
+        );
+        assert_eq!(
+            parse_numeric_str("0001e3"),
+            Ok((OwnedValueType::Float, "0001e3"))
+        );
+    }
+
+    #[test]
+    fn test_parse_numeric_str_trailing_characters() {
+        assert_eq!(
+            parse_numeric_str("123abc"),
+            Ok((OwnedValueType::Integer, "123"))
+        );
+        assert_eq!(
+            parse_numeric_str("456.78xyz"),
+            Ok((OwnedValueType::Float, "456.78"))
+        );
+        assert_eq!(
+            parse_numeric_str("1.23e4extra"),
+            Ok((OwnedValueType::Float, "1.23e4"))
+        );
     }
 }


Test patch:
diff --git a/testing/select.test b/testing/select.test
index f10f33b2..03b21c00 100755
--- a/testing/select.test
+++ b/testing/select.test
@@ -165,3 +165,12 @@ do_execsql_test select-not-like-expression {
 do_execsql_test select-like-expression {
     select 2 % 0.5
 } {}
+
+# regression test for https://github.com/tursodatabase/limbo/issues/1157
+do_execsql_test select-invalid-numeric-text {
+  select -'e';
+} {0}
+
+do_execsql_test select-invalid-numeric-text {
+  select -'E';
+} {0}
\ No newline at end of file