Repository: edgedb/edgedb

Issue:
InternalServerError when restoring dump file
- EdgeDB Version: 1.0-alpha.3+dev.83.g9937f6b0
- OS Version: Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-1051-aws x86_64)

Steps to Reproduce:

1. Dump db (attached below) from 1.0-alpha.2+g16f75ece5.d20200122
2. Trying to restore into db on 1.0-alpha.3+dev.83.g9937f6b0 (or alpha.2) gives error: `InternalServerError: Object table dump data has extra fields`

Dump file: [movies.dump.header.gz](https://github.com/edgedb/edgedb/files/4357036/movies.dump.header.gz)
(I've removed the data blocks from the dump file to make it a bit smaller and it doesn't seem to affect the error, but I can send the whole dump if it helps, it's only data from a public dataset)



Discussion:
Interesting.  Can you show what your schema looks like please?
```
CREATE MODULE default IF NOT EXISTS;
CREATE TYPE default::Movie {
    CREATE SINGLE PROPERTY budget -> std::int64;
    CREATE SINGLE PROPERTY overview -> std::str;
    CREATE SINGLE PROPERTY release_date -> cal::local_date;
    CREATE SINGLE PROPERTY revenue -> std::int64;
    CREATE SINGLE PROPERTY runtime -> std::int16;
    CREATE SINGLE PROPERTY tagline -> std::str;
    CREATE SINGLE PROPERTY temp_id -> std::int64;
    CREATE REQUIRED SINGLE PROPERTY title -> std::str;
    CREATE SINGLE PROPERTY vote_average -> std::decimal;
};
CREATE FINAL SCALAR TYPE default::MovieStatus EXTENDING enum<'Released', 'Rumored', 'Post Production', 'Unknown'>;
ALTER TYPE default::Movie {
    CREATE REQUIRED SINGLE PROPERTY status -> default::MovieStatus;
};
CREATE TYPE default::Person {
    CREATE SINGLE PROPERTY genderCode -> std::int16;
    CREATE REQUIRED SINGLE PROPERTY name -> std::str;
    CREATE SINGLE PROPERTY temp_id -> std::int64;
};
ALTER TYPE default::Movie {
    CREATE MULTI LINK cast -> default::Person {
        CREATE SINGLE PROPERTY character_name -> std::str;
        CREATE SINGLE PROPERTY credit_order -> std::int16;
    };
};
CREATE TYPE default::Genre {
    CREATE REQUIRED SINGLE PROPERTY name -> std::str;
};
ALTER TYPE default::Movie {
    CREATE MULTI LINK genres -> default::Genre;
};
CREATE TYPE default::Language {
    CREATE REQUIRED SINGLE PROPERTY iso_code -> std::str {
        CREATE CONSTRAINT std::exclusive;
        CREATE CONSTRAINT std::regexp(r'[a-z]{2}');
        CREATE ANNOTATION std::description := 'ISO 639-1 language code';
    };
    CREATE REQUIRED SINGLE PROPERTY name -> std::str;
    CREATE REQUIRED SINGLE PROPERTY native_name -> std::str;
};
ALTER TYPE default::Movie {
    CREATE REQUIRED SINGLE LINK language -> default::Language;
};
CREATE TYPE default::ProductionCompany {
    CREATE REQUIRED SINGLE PROPERTY name -> std::str;
};
ALTER TYPE default::Movie {
    CREATE MULTI LINK production_companies -> default::ProductionCompany;
};
CREATE TYPE default::Country {
    CREATE REQUIRED SINGLE PROPERTY iso_code -> std::str {
        CREATE CONSTRAINT std::exclusive;
        CREATE CONSTRAINT std::regexp(r'[A-Z]{2}');
        CREATE ANNOTATION std::description := 'ISO 3166-1 two letter country code';
    };
    CREATE REQUIRED SINGLE PROPERTY name -> std::str;
};
ALTER TYPE default::Movie {
    CREATE MULTI LINK production_countries -> default::Country;
};
ALTER TYPE default::Movie {
    CREATE MULTI LINK spoken_languages -> default::Language;
};
```
I didn't think to check this before, but it also fails with the same error when trying to restore straight back into a db on alpha2, so the upgrade to alpha3 doesn't seem to be relevant to the bug.
It seems the bug only occurs when the camel cased property `genderCode` exists on the `Person` type, if that's dropped or changed to snake case, dump/restore works again.

#### Example testcase:
```
CREATE TYPE default::Example { 
    CREATE SINGLE PROPERTY camelCase -> std::int16; 
};
# Restore fails with `InternalServerError: Object table dump data has extra fields`
```

```
CREATE TYPE default::Example { 
    CREATE SINGLE PROPERTY snake_case -> std::int16; 
};
# Restore works as expected
```
Thanks for the hint, @jaclarke!

Resolving patch:
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
@@ -1631,7 +1631,7 @@ def _describe_object(
                     link_bias=True,
                 )
 
-                cols.append(pg_common.quote_ident(stor_info.column_name))
+                cols.append(stor_info.column_name)
 
                 props[ptr.get_shortname(schema).name] = ptr.get_target(schema)
 
@@ -1661,7 +1661,7 @@ def _describe_object(
                 )
 
                 if stor_info.table_type == 'ObjectType':
-                    cols.append(pg_common.quote_ident(stor_info.column_name))
+                    cols.append(stor_info.column_name)
                     shape.append(ptr)
 
                 link_stor_info = pg_types.get_pointer_storage_info(
@@ -1687,7 +1687,8 @@ def _describe_object(
         )
 
         stmt = (
-            f'COPY {table_name} ({", ".join(c for c in cols)}) '
+            f'COPY {table_name} '
+            f'({", ".join(pg_common.quote_ident(c) for c in cols)}) '
             f'TO STDOUT WITH BINARY'
         ).encode()
 
@@ -1761,7 +1762,7 @@ async def describe_database_restore(
                         link_bias=True,
                     )
 
-                    cols.append(pg_common.quote_ident(stor_info.column_name))
+                    cols.append(stor_info.column_name)
 
                 if set(desc_cols) != set(cols):
                     raise RuntimeError(
@@ -1783,8 +1784,7 @@ async def describe_database_restore(
                     )
 
                     if stor_info.table_type == 'ObjectType':
-                        cols.append(pg_common.quote_ident(
-                            stor_info.column_name))
+                        cols.append(stor_info.column_name)
 
                 if set(desc_cols) != set(cols):
                     raise RuntimeError(
@@ -1794,7 +1794,8 @@ async def describe_database_restore(
                 schema, obj, catenate=True)
 
             stmt = (
-                f'COPY {table_name} ({", ".join(c for c in desc_cols)}) '
+                f'COPY {table_name} '
+                f'({", ".join(pg_common.quote_ident(c) for c in desc_cols)}) '
                 f'FROM STDIN WITH BINARY'
             ).encode()
 


Test patch:
diff --git a/tests/schemas/dump02_default.esdl b/tests/schemas/dump02_default.esdl
new file mode 100644
--- /dev/null
+++ b/tests/schemas/dump02_default.esdl
@@ -0,0 +1,25 @@
+#
+# This source file is part of the EdgeDB open source project.
+#
+# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+type `S p a M` {
+    required property `🚀` -> int32;
+}
+
+type A {
+    required link `s p A m 🤞` -> `S p a M`;
+}
diff --git a/tests/schemas/dump02_setup.edgeql b/tests/schemas/dump02_setup.edgeql
new file mode 100644
--- /dev/null
+++ b/tests/schemas/dump02_setup.edgeql
@@ -0,0 +1,28 @@
+#
+# This source file is part of the EdgeDB open source project.
+#
+# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+SET MODULE default;
+
+INSERT `S p a M` {
+    `🚀` := 42
+};
+
+INSERT A {
+    `s p A m 🤞` := (SELECT `S p a M` FILTER .`🚀` = 42)
+};
diff --git a/tests/test_dump02.py b/tests/test_dump02.py
new file mode 100644
--- /dev/null
+++ b/tests/test_dump02.py
@@ -0,0 +1,90 @@
+#
+# This source file is part of the EdgeDB open source project.
+#
+# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+import os.path
+import tempfile
+
+from edb.testbase import server as tb
+
+
+class TestDump02(tb.QueryTestCase, tb.CLITestCaseMixin):
+
+    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',
+                                  'dump02_default.esdl')
+
+    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',
+                         'dump02_setup.edgeql')
+
+    ISOLATED_METHODS = False
+    SERIALIZED = True
+
+    async def test_dump02_basic(self):
+        await self.ensure_schema_data_integrity()
+
+    async def test_dump02_dump_restore(self):
+        assert type(self).__name__.startswith('Test')
+        # The name of the database created for this test case by
+        # the test runner:
+        dbname = type(self).__name__[4:].lower()
+
+        with tempfile.NamedTemporaryFile() as f:
+            self.run_cli('-d', dbname, 'dump', f.name)
+
+            await self.con.execute(f'CREATE DATABASE {dbname}_restored')
+            try:
+                self.run_cli('-d', f'{dbname}_restored', 'restore', f.name)
+                con2 = await self.connect(database=f'{dbname}_restored')
+            except Exception:
+                await self.con.execute(f'DROP DATABASE {dbname}_restored')
+                raise
+
+        oldcon = self.con
+        self.__class__.con = con2
+        try:
+            await self.ensure_schema_data_integrity()
+        finally:
+            self.__class__.con = oldcon
+            await con2.aclose()
+            await self.con.execute(f'DROP DATABASE {dbname}_restored')
+
+    async def ensure_schema_data_integrity(self):
+        tx = self.con.transaction()
+        await tx.start()
+        try:
+            await self._ensure_schema_data_integrity()
+        finally:
+            await tx.rollback()
+
+    async def _ensure_schema_data_integrity(self):
+        await self.assert_query_result(
+            r'''
+                SELECT A {
+                    `s p A m 🤞`: {
+                        `🚀`
+                    }
+                }
+            ''',
+            [
+                {
+                    's p A m 🤞': {
+                        '🚀': 42
+                    }
+                }
+            ]
+        )