Repository: edgedb/edgedb

Issue:
Bad error reporting after constant extraction
```
edgedb> SELECT ''' 1 ''';
error: Unexpected '('
  ┌─ query:1:10
  │
1 │ SELECT ''' 1 ''';
  │          ^^^^^^ error
```
There is no paren in original text so error is misleading.


Discussion:
As discussed offline we can mitigate this by adding a check to the lexer: error with SyntaxError if a literal token immediately follows another literal token. IIRC we don't have any legal syntax like that in EdgeQL.
The error from #1676 is different:
```
select str{'1'};
error: Unexpected '('
  ┌─ query:1:12
  │
1 │ select str{'1'};
  │            ^^^^ error
```
While bug is basically the same. We can't apply what @1st1 suggests to this error. So should we fallback to re-parsing original text on any syntax error instead?
Why do we inject `(`?
> Why do we inject `(`?

Because some operators have greater precedence than `<type cast>`.
I only see the `[index]` operator having greater precedence than cast (`DETACHED` is irrelevant for constant extraction). Perhaps we can do something about that? E.g. see if the constant is followed by `[` and only then inject `(`s?
We discussed that (it's possible but not future proof). But then it woulld be `unexpected '<'` which isn't any better.
> But then it woulld be unexpected '<' which isn't any better.

Ah, that's right. :/

> So should we fallback to re-parsing original text on any syntax error instead?

I guess there's no way around that. @elprans thoughts?
> So should we fallback to re-parsing original text on any syntax error instead?

Yes, and I don't see a problem with that.

Resolving patch:
diff --git a/edb/edgeql/tokenizer.py b/edb/edgeql/tokenizer.py
--- a/edb/edgeql/tokenizer.py
+++ b/edb/edgeql/tokenizer.py
@@ -20,6 +20,7 @@
 from typing import *
 
 import re
+import hashlib
 
 from edb._edgeql_rust import tokenize as _tokenize, TokenizerError, Token
 from edb._edgeql_rust import normalize as _normalize, Entry
@@ -33,12 +34,16 @@
 class Source:
 
     def __init__(self, text: str, tokens: List[Token]) -> None:
+        self._cache_key = hashlib.blake2b(text.encode('utf-8')).digest()
         self._text = text
         self._tokens = tokens
 
     def text(self) -> str:
         return self._text
 
+    def cache_key(self) -> bytes:
+        return self._cache_key
+
     def variables(self) -> Dict[str, Any]:
         return {}
 
@@ -61,8 +66,9 @@ def from_string(cls, text: str) -> Source:
 
 class NormalizedSource(Source):
 
-    def __init__(self, normalized: Entry) -> None:
-        self._text = normalized.key()
+    def __init__(self, normalized: Entry, text: str) -> None:
+        self._text = text
+        self._cache_key = normalized.key()
         self._tokens = normalized.tokens()
         self._variables = normalized.variables()
         self._first_extra = normalized.first_extra()
@@ -72,6 +78,9 @@ def __init__(self, normalized: Entry) -> None:
     def text(self) -> str:
         return self._text
 
+    def cache_key(self) -> bytes:
+        return self._cache_key
+
     def variables(self) -> Dict[str, Any]:
         return self._variables
 
@@ -89,7 +98,7 @@ def extra_blob(self) -> bytes:
 
     @classmethod
     def from_string(cls, text: str) -> NormalizedSource:
-        return cls(normalize(text))
+        return cls(normalize(text), text)
 
 
 def tokenize(eql: str) -> List[Token]:
diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py
--- a/edb/server/compiler/compiler.py
+++ b/edb/server/compiler/compiler.py
@@ -1415,6 +1415,32 @@ def _compile(
         ctx: CompileContext,
         source: edgeql.Source,
     ) -> List[dbstate.QueryUnit]:
+        try:
+            return self._try_compile(ctx=ctx, source=source)
+        except errors.EdgeQLSyntaxError as original_err:
+            if isinstance(source, edgeql.NormalizedSource):
+                # try non-normalized source
+                try:
+                    original = edgeql.Source.from_string(source.text())
+                    ctx = dataclasses.replace(ctx, source=original)
+                    self._try_compile(ctx=ctx, source=original)
+                except errors.EdgeQLSyntaxError as denormalized_err:
+                    raise denormalized_err
+                except Exception:
+                    raise AssertionError(
+                        "Normalized and non-normalized query errors differ")
+                else:
+                    raise AssertionError(
+                        "Normalized query is broken while original is valid")
+            else:
+                raise original_err
+
+    def _try_compile(
+        self,
+        *,
+        ctx: CompileContext,
+        source: edgeql.Source,
+    ) -> List[dbstate.QueryUnit]:
 
         # When True it means that we're compiling for "connection.query()".
         # That means that the returned QueryUnit has to have the in/out codec


Test patch:
diff --git a/edb/edgeql-rust/tests/normalize.rs b/edb/edgeql-rust/tests/normalize.rs
--- a/edb/edgeql-rust/tests/normalize.rs
+++ b/edb/edgeql-rust/tests/normalize.rs
@@ -6,7 +6,7 @@ fn test_verbatim() {
     let entry = normalize(r###"
         SELECT $1 + $2
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT$1+$2");
+    assert_eq!(entry.processed_source, "SELECT$1+$2");
     assert_eq!(entry.variables, vec![]);
 }
 
@@ -15,7 +15,7 @@ fn test_configure() {
     let entry = normalize(r###"
         CONFIGURE SYSTEM SET some_setting := 7
     "###).unwrap();
-    assert_eq!(entry.key, "CONFIGURE SYSTEM SET some_setting:=7");
+    assert_eq!(entry.processed_source, "CONFIGURE SYSTEM SET some_setting:=7");
     assert_eq!(entry.variables, vec![]);
 }
 
@@ -24,7 +24,8 @@ fn test_int() {
     let entry = normalize(r###"
         SELECT 1 + 2
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT(<__std__::int64>$0)+(<__std__::int64>$1)");
+    assert_eq!(entry.processed_source,
+        "SELECT(<__std__::int64>$0)+(<__std__::int64>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
             value: Value::Int(1),
@@ -40,7 +41,8 @@ fn test_str() {
     let entry = normalize(r###"
         SELECT "x" + "yy"
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT(<__std__::str>$0)+(<__std__::str>$1)");
+    assert_eq!(entry.processed_source,
+        "SELECT(<__std__::str>$0)+(<__std__::str>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
             value: Value::Str("x".into()),
@@ -56,7 +58,7 @@ fn test_float() {
     let entry = normalize(r###"
         SELECT 1.5 + 23.25
     "###).unwrap();
-    assert_eq!(entry.key,
+    assert_eq!(entry.processed_source,
         "SELECT(<__std__::float64>$0)+(<__std__::float64>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
@@ -73,7 +75,8 @@ fn test_bigint() {
     let entry = normalize(r###"
         SELECT 1n + 23n
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT(<__std__::bigint>$0)+(<__std__::bigint>$1)");
+    assert_eq!(entry.processed_source,
+        "SELECT(<__std__::bigint>$0)+(<__std__::bigint>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
             value: Value::BigInt(1.into()),
@@ -89,7 +92,8 @@ fn test_bigint_exponent() {
     let entry = normalize(r###"
         SELECT 1e10n + 23e13n
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT(<__std__::bigint>$0)+(<__std__::bigint>$1)");
+    assert_eq!(entry.processed_source,
+        "SELECT(<__std__::bigint>$0)+(<__std__::bigint>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
             value: Value::BigInt(10000000000u64.into()),
@@ -105,7 +109,7 @@ fn test_decimal() {
     let entry = normalize(r###"
         SELECT 1.33n + 23.77n
     "###).unwrap();
-    assert_eq!(entry.key,
+    assert_eq!(entry.processed_source,
         "SELECT(<__std__::decimal>$0)+(<__std__::decimal>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
@@ -122,7 +126,7 @@ fn test_positional() {
     let entry = normalize(r###"
         SELECT <int64>$0 + 2
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT<int64>$0+(<__std__::int64>$1)");
+    assert_eq!(entry.processed_source, "SELECT<int64>$0+(<__std__::int64>$1)");
     assert_eq!(entry.variables, vec![
         Variable {
             value: Value::Int(2),
@@ -135,7 +139,7 @@ fn test_named() {
     let entry = normalize(r###"
         SELECT <int64>$test_var + 2
     "###).unwrap();
-    assert_eq!(entry.key,
+    assert_eq!(entry.processed_source,
         "SELECT<int64>$test_var+(<__std__::int64>$__edb_arg_1)");
     assert_eq!(entry.variables, vec![
         Variable {
@@ -149,7 +153,8 @@ fn test_limit_1() {
     let entry = normalize(r###"
         SELECT User { one := 1 } LIMIT 1
     "###).unwrap();
-    assert_eq!(entry.key, "SELECT User{one:=(<__std__::int64>$0)}LIMIT 1");
+    assert_eq!(entry.processed_source,
+        "SELECT User{one:=(<__std__::int64>$0)}LIMIT 1");
     assert_eq!(entry.variables, vec![
         Variable {
             value: Value::Int(1),
@@ -162,7 +167,7 @@ fn test_tuple_access() {
     let entry = normalize(r###"
         SELECT User { one := 2, two := .field.2, three := .field  . 3 }
     "###).unwrap();
-    assert_eq!(entry.key,
+    assert_eq!(entry.processed_source,
         "SELECT User{one:=(<__std__::int64>$0),\
                      two:=.field.2,three:=.field.3}");
     assert_eq!(entry.variables, vec![
diff --git a/tests/test_edgeql_expressions.py b/tests/test_edgeql_expressions.py
--- a/tests/test_edgeql_expressions.py
+++ b/tests/test_edgeql_expressions.py
@@ -4620,3 +4620,12 @@ async def test_edgeql_expr_index_01(self):
             await self.con.execute("""
                 SELECT 1[1];
             """)
+
+    async def test_edgeql_expr_error_after_extraction_01(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                "Unexpected \"'1'\""):
+
+            await self.con.query("""
+                SELECT '''1''';
+            """)