Repository: edgedb/edgedb

Issue:
Dump fails on schemas with quoted properties
- EdgeDB Version: 3.0-dev.6872+local

Steps to Reproduce:

1. Create type with quoted property:
```
dumpbug> create type Test {
........   create property `123` -> str;
........ };
OK: CREATE TYPE
```
2. Try to dump database:
```
$ edb cli -d dumpbug dump dumpbug
edgedb error: EdgeQLSyntaxError: error receiving dump header: Unexpected '123'
  Server traceback:
      Traceback (most recent call last):
        File "/home/james/dev/edgedb/edb/common/parsing.py", line 420, in parse
          self.parser.token(token)
        File "/home/james/dev/edgedb-dev/lib/python3.10/site-packages/parsing/lrparser.py", line 57, in token
          self._act(token, tokenSpec)  # type: ignore
        File "/home/james/dev/edgedb-dev/lib/python3.10/site-packages/parsing/lrparser.py", line 81, in _act
          raise UnexpectedToken("Unexpected token: %r" % sym)
      parsing.errors.UnexpectedToken: Unexpected token: <Token ICONST "123">

      The above exception was the direct cause of the following exception:

      Traceback (most recent call last):
        File "/home/james/dev/edgedb/edb/server/compiler_pool/worker_proc.py", line 55, in worker
          res = meth(*args)
        File "/home/james/dev/edgedb/edb/server/compiler/compiler.py", line 2280, in describe_database_dump
          schema_ddl = s_ddl.ddl_text_from_schema(
        File "/home/james/dev/edgedb/edb/schema/ddl.py", line 899, in ddl_text_from_schema
          return ddl_text_from_delta(None, schema, diff)
        File "/home/james/dev/edgedb/edb/schema/ddl.py", line 817, in ddl_text_from_delta
          return text_from_delta(schema_a, schema_b, delta, sdlmode=False)
        File "/home/james/dev/edgedb/edb/schema/ddl.py", line 787, in text_from_delta
          stmts = statements_from_delta(
        File "/home/james/dev/edgedb/edb/schema/ddl.py", line 714, in statements_from_delta
          stmts = ddlast_from_delta(
        File "/home/james/dev/edgedb/edb/schema/ddl.py", line 695, in ddlast_from_delta
          ql_ast = command.get_ast(schema, context)
        File "/home/james/dev/edgedb/edb/schema/delta.py", line 2628, in get_ast
          return self._get_ast(schema, context, parent_node=parent_node)
        File "/home/james/dev/edgedb/edb/schema/migrations.py", line 196, in _get_ast
          node = super()._get_ast(schema, context, parent_node=parent_node)
        File "/home/james/dev/edgedb/edb/schema/delta.py", line 2945, in _get_ast
          node = super()._get_ast(schema, context, parent_node=parent_node)
        File "/home/james/dev/edgedb/edb/schema/delta.py", line 2188, in _get_ast
          self._apply_fields_ast(schema, context, op)
        File "/home/james/dev/edgedb/edb/schema/delta.py", line 2223, in _apply_fields_ast
          self._apply_field_ast(schema, context, node, fop)
        File "/home/james/dev/edgedb/edb/schema/migrations.py", line 213, in _apply_field_ast
          qlparser.parse_block(op.new_value),
        File "/home/james/dev/edgedb/edb/edgeql/parser/__init__.py", line 77, in parse_block
          return parser.parse(source)
        File "/home/james/dev/edgedb/edb/common/parsing.py", line 433, in parse
          raise self.get_exception(
      edb.errors.EdgeQLSyntaxError: Unexpected '123'
```



Discussion:

Resolving patch:
diff --git a/edb/edgeql/codegen.py b/edb/edgeql/codegen.py
--- a/edb/edgeql/codegen.py
+++ b/edb/edgeql/codegen.py
@@ -64,13 +64,13 @@ def any_ident_to_str(ident: str) -> str:
         return ident_to_str(ident)
 
 
-def ident_to_str(ident: str) -> str:
-    return edgeql_quote.quote_ident(ident)
+def ident_to_str(ident: str, allow_num: bool=False) -> str:
+    return edgeql_quote.quote_ident(ident, allow_num=allow_num)
 
 
 def param_to_str(ident: str) -> str:
     return '$' + edgeql_quote.quote_ident(
-        ident, allow_reserved=True)
+        ident, allow_reserved=True, allow_num=True)
 
 
 def module_to_str(module: str) -> str:
@@ -553,7 +553,7 @@ def visit_Ptr(self, node: qlast.Ptr, *, quote: bool = True) -> None:
         elif node.direction and node.direction != '>':
             self.write(node.direction)
 
-        self.visit(node.ptr)
+        self.write(ident_to_str(node.ptr.name, allow_num=True))
 
     def visit_TypeIntersection(self, node: qlast.TypeIntersection) -> None:
         self._write_keywords('[IS ')
diff --git a/edb/edgeql/quote.py b/edb/edgeql/quote.py
--- a/edb/edgeql/quote.py
+++ b/edb/edgeql/quote.py
@@ -26,6 +26,10 @@
 
 _re_ident = re.compile(r'''(?x)
     [^\W\d]\w*  # alphanumeric identifier
+''')
+
+_re_ident_or_num = re.compile(r'''(?x)
+    [^\W\d]\w*  # alphanumeric identifier
     |
     ([1-9]\d* | 0)  # purely integer identifier
 ''')
@@ -59,13 +63,14 @@ def dollar_quote_literal(text: str) -> str:
     return quote + text + quote
 
 
-def needs_quoting(string: str, allow_reserved: bool) -> bool:
+def needs_quoting(string: str, allow_reserved: bool, allow_num: bool) -> bool:
     if not string or string.startswith('@') or '::' in string:
         # some strings are illegal as identifiers and as such don't
         # require quoting
         return False
 
-    isalnum = _re_ident.fullmatch(string)
+    r = _re_ident_or_num if allow_num else _re_ident
+    isalnum = r.fullmatch(string)
 
     string = string.lower()
 
@@ -85,8 +90,10 @@ def _quote_ident(string: str) -> str:
 
 
 def quote_ident(string: str, *,
-                force: bool = False, allow_reserved: bool = False) -> str:
-    if force or needs_quoting(string, allow_reserved):
+                force: bool = False,
+                allow_reserved: bool = False,
+                allow_num: bool = False) -> str:
+    if force or needs_quoting(string, allow_reserved, allow_num):
         return _quote_ident(string)
     else:
         return string


Test patch:
diff --git a/tests/test_edgeql_syntax.py b/tests/test_edgeql_syntax.py
--- a/tests/test_edgeql_syntax.py
+++ b/tests/test_edgeql_syntax.py
@@ -5223,6 +5223,13 @@ def test_edgeql_syntax_ddl_type_22(self):
         };
         """
 
+    def test_edgeql_syntax_ddl_type_23(self):
+        """
+        CREATE TYPE `123` {
+            CREATE PROPERTY `456` -> str;
+        };
+        """
+
     def test_edgeql_syntax_set_command_01(self):
         """
         SET MODULE default;