Repository: edgedb/edgedb

Issue:
Constraint violation error messages should be exposed in GraphQL
Currently, the GraphQL extension does not give very friendly errors when you try to do something that would violate a constraint.  For example, given this minimal schema:

```
using extension graphql;
module default {
	type ConstraintDemo {;
		property tenorless -> int16 {
			constraint max_value(10);
		}
	}
}
```

Trying to insert a ConstraintDemo with a tenorless of 11 via the CLI gives a nice error message:

```
edgedb> insert ConstraintDemo {tenorless := 11};
edgedb error: ConstraintViolationError: Maximum allowed value for tenorless is 10.
  Detail: Maximum allowed value for value of property 'tenorless' of object type 'default::ConstraintDemo' is 10.
```

But trying to do so through the GraphQL API does not:
```graphql
mutation {
  insert_ConstraintDemo(data: {tenorless: 11}) {
    id
  }
}
# returns
{
  "errors": [
    {
      "message": "QueryError: new row for relation \"81c174b3-ca67-11ec-90a9-7114d4b7def4\" violates check constraint \"a56dc4cd-ca67-11ec-a985-2df63feca51f;schemaconstr\""
    }
  ]
}
```


Discussion:

Resolving patch:
diff --git a/edb/graphql/translator.py b/edb/graphql/translator.py
--- a/edb/graphql/translator.py
+++ b/edb/graphql/translator.py
@@ -1183,15 +1183,23 @@ def _get_input_expr_for_pointer_mutaiton(
                 card = ptype.get_field_cardinality(fname)
                 if card is qltypes.SchemaCardinality.Many:
                     # Need to wrap the set into an "assert_distinct()".
+                    msg = f'objects provided for {fname!r} are not distinct'
                     compexpr = qlast.FunctionCall(
                         func='assert_distinct',
                         args=[compexpr],
+                        kwargs={
+                            'message': qlast.StringConstant.from_python(msg)
+                        }
                     )
                 else:
                     # Singleton object values need to be verified.
+                    msg = f'more than one object provided for {fname!r}'
                     compexpr = qlast.FunctionCall(
                         func='assert_single',
                         args=[compexpr],
+                        kwargs={
+                            'message': qlast.StringConstant.from_python(msg)
+                        }
                     )
 
             # Object types need to be wrapped in a DETACHED in
diff --git a/edb/graphql/types.py b/edb/graphql/types.py
--- a/edb/graphql/types.py
+++ b/edb/graphql/types.py
@@ -395,7 +395,8 @@ def edgedb_schema(self) -> s_schema.Schema:
     def graphql_schema(self) -> GraphQLSchema:
         return self._gql_schema
 
-    def get_gql_name(self, name: s_name.QualName) -> str:
+    @classmethod
+    def get_gql_name(cls, name: s_name.QualName) -> str:
         module, shortname = name.module, name.name
 
         # Adjust the shortname.
@@ -412,7 +413,7 @@ def get_gql_name(self, name: s_name.QualName) -> str:
             names = []
             for part in shortname[1:-1].split(' | '):
                 names.append(
-                    self.get_gql_name(s_name.QualName(*part.split(':', 1))))
+                    cls.get_gql_name(s_name.QualName(*part.split(':', 1))))
             shortname = '_OR_'.join(names)
 
         if module in {'default', 'std'}:
diff --git a/edb/server/compiler/errormech.py b/edb/server/compiler/errormech.py
--- a/edb/server/compiler/errormech.py
+++ b/edb/server/compiler/errormech.py
@@ -27,6 +27,8 @@
 from edb.common import value_dispatch
 from edb.common import uuidgen
 
+from edb.graphql import types as gql_types
+
 from edb.schema import name as sn
 from edb.schema import objtypes as s_objtypes
 from edb.schema import pointers as s_pointers
@@ -125,7 +127,55 @@ class ErrorDetails(NamedTuple):
     r'(?P<p>enum) (?P<v>edgedb([\w-]+)."(?P<id>[\w-]+)_domain")')
 
 
-def translate_pgtype_inner(schema, msg):
+type_in_access_policy_re = re.compile(r'(\w+|`.+?`)::(\w+|`.+?`)')
+
+
+def gql_translate_pgtype_inner(schema, msg):
+    """Try to replace any internal pg type name with a GraphQL type name"""
+
+    # Mapping base types
+    def base_type_map(name):
+        result = gql_types.EDB_TO_GQL_SCALARS_MAP.get(
+            str(types.base_type_name_map_r.get(name))
+        )
+
+        if result is None:
+            return name
+        else:
+            return result.name
+
+    translated = pgtype_re.sub(
+        lambda r: base_type_map(r.group(0)),
+        msg,
+    )
+
+    if translated != msg:
+        return translated
+
+    def replace(r):
+        type_id = uuidgen.UUID(r.group('id'))
+        stype = schema.get_by_id(type_id, None)
+        gql_name = gql_types.GQLCoreSchema.get_gql_name(
+            stype.get_name(schema))
+        if stype:
+            return f'{r.group("p")} {gql_name!r}'
+        else:
+            return f'{r.group("p")} {r.group("v")}'
+
+    translated = enum_re.sub(replace, msg)
+
+    return translated
+
+
+def gql_replace_type_names_in_text(msg):
+    return type_in_access_policy_re.sub(
+        lambda m: gql_types.GQLCoreSchema.get_gql_name(
+            sn.QualName.from_string(m.group(0))),
+        msg,
+    )
+
+
+def eql_translate_pgtype_inner(schema, msg):
     """Try to replace any internal pg type name with an edgedb type name"""
     translated = pgtype_re.sub(
         lambda r: str(types.base_type_name_map_r.get(r.group(0), r.group(0))),
@@ -148,7 +198,7 @@ def replace(r):
     return translated
 
 
-def translate_pgtype(schema, msg):
+def translate_pgtype(schema, msg, from_graphql=False):
     """Try to translate a message that might refer to internal pg types.
 
     We *want* to replace internal pg type names with edgedb names, but only
@@ -160,7 +210,10 @@ def translate_pgtype(schema, msg):
     """
 
     leading, *rest = msg.split(':')
-    leading_translated = translate_pgtype_inner(schema, leading)
+    if from_graphql:
+        leading_translated = gql_translate_pgtype_inner(schema, leading)
+    else:
+        leading_translated = eql_translate_pgtype_inner(schema, leading)
     return ':'.join([leading_translated, *rest])
 
 
@@ -216,7 +269,7 @@ def get_generic_exception_from_err_details(err_details):
 #########################################################################
 
 
-def static_interpret_backend_error(fields):
+def static_interpret_backend_error(fields, from_graphql=False):
     err_details = get_error_details(fields)
     # handle some generic errors if possible
     err = get_generic_exception_from_err_details(err_details)
@@ -224,31 +277,47 @@ def static_interpret_backend_error(fields):
         return err
 
     return static_interpret_by_code(
-        err_details.code, err_details)
+        err_details.code, err_details, from_graphql=from_graphql)
 
 
 @value_dispatch.value_dispatch
-def static_interpret_by_code(code, err_details):
+def static_interpret_by_code(
+    _code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     return errors.InternalServerError(err_details.message)
 
 
 @static_interpret_by_code.register_for_all(
     directly_mappable.keys())
-def _static_interpret_directly_mappable(code, err_details):
+def _static_interpret_directly_mappable(
+    code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     errcls = directly_mappable[code]
-    return errcls(err_details.message)
+
+    if from_graphql:
+        msg = gql_replace_type_names_in_text(err_details.message)
+    else:
+        msg = err_details.message
+
+    return errcls(msg)
 
 
 @static_interpret_by_code.register_for_all(constraint_errors)
-def _static_interpret_constraint_errors(code, err_details):
+def _static_interpret_constraint_errors(
+    code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     if code == pgerrors.ERROR_NOT_NULL_VIOLATION:
         if err_details.table_name or err_details.column_name:
             return SchemaRequired
         else:
             return errors.InternalServerError(err_details.message)
 
-    source = pointer = None
-
     for errtype, ere in constraint_res.items():
         m = ere.match(err_details.message)
         if m:
@@ -258,9 +327,7 @@ def _static_interpret_constraint_errors(code, err_details):
         return errors.InternalServerError(err_details.message)
 
     if error_type == 'cardinality':
-        return errors.CardinalityViolationError(
-            'cardinality violation',
-            source=source, pointer=pointer)
+        return errors.CardinalityViolationError('cardinality violation')
 
     elif error_type == 'link_target':
         if err_details.detail_json:
@@ -287,8 +354,13 @@ def _static_interpret_constraint_errors(code, err_details):
         return errors.UnknownLinkError(msg)
 
     elif error_type == 'link_target_del':
+        if from_graphql:
+            msg = gql_replace_type_names_in_text(err_details.message)
+        else:
+            msg = err_details.message
+
         return errors.ConstraintViolationError(
-            err_details.message, details=err_details.detail)
+            msg, details=err_details.detail)
 
     elif error_type == 'constraint':
         if err_details.constraint_name is None:
@@ -297,7 +369,7 @@ def _static_interpret_constraint_errors(code, err_details):
         constraint_id, _, _ = err_details.constraint_name.rpartition(';')
 
         try:
-            constraint_id = uuidgen.UUID(constraint_id)
+            uuidgen.UUID(constraint_id)
         except ValueError:
             return errors.InternalServerError(err_details.message)
 
@@ -310,7 +382,7 @@ def _static_interpret_constraint_errors(code, err_details):
         constraint_id, _, _ = err_details.constraint_name.rpartition('_')
 
         try:
-            constraint_id = uuidgen.UUID(constraint_id)
+            uuidgen.UUID(constraint_id)
         except ValueError:
             return errors.InternalServerError(err_details.message)
 
@@ -337,7 +409,11 @@ def _static_interpret_constraint_errors(code, err_details):
 
 
 @static_interpret_by_code.register_for_all(SCHEMA_CODES)
-def _static_interpret_schema_errors(code, err_details):
+def _static_interpret_schema_errors(
+    code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     if code == pgerrors.ERROR_INVALID_DATETIME_FORMAT:
         hint = None
         if err_details.detail_json:
@@ -353,7 +429,11 @@ def _static_interpret_schema_errors(code, err_details):
 
 @static_interpret_by_code.register(
     pgerrors.ERROR_INVALID_PARAMETER_VALUE)
-def _static_interpret_invalid_param_value(_code, err_details):
+def _static_interpret_invalid_param_value(
+    _code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     return errors.InvalidValueError(
         err_details.message,
         details=err_details.detail if err_details.detail else None
@@ -362,7 +442,11 @@ def _static_interpret_invalid_param_value(_code, err_details):
 
 @static_interpret_by_code.register(
     pgerrors.ERROR_WRONG_OBJECT_TYPE)
-def _static_interpret_wrong_object_type(_code, err_details):
+def _static_interpret_wrong_object_type(
+    _code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     if err_details.column_name:
         return SchemaRequired
 
@@ -379,7 +463,11 @@ def _static_interpret_wrong_object_type(_code, err_details):
 
 @static_interpret_by_code.register(
     pgerrors.ERROR_CARDINALITY_VIOLATION)
-def _static_interpret_cardinality_violation(_code, err_details):
+def _static_interpret_cardinality_violation(
+    _code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
 
     if (err_details.constraint_name == 'std::assert_single'
             or err_details.constraint_name == 'std::assert_exists'):
@@ -399,7 +487,11 @@ def _static_interpret_cardinality_violation(_code, err_details):
 
 @static_interpret_by_code.register(
     pgerrors.ERROR_FEATURE_NOT_SUPPORTED)
-def _static_interpret_feature_not_supported(_code, err_details):
+def _static_interpret_feature_not_supported(
+    _code: str,
+    err_details: ErrorDetails,
+    from_graphql: bool = False,
+):
     return errors.UnsupportedBackendFeatureError(err_details.message)
 
 
@@ -408,7 +500,7 @@ def _static_interpret_feature_not_supported(_code, err_details):
 #########################################################################
 
 
-def interpret_backend_error(schema, fields):
+def interpret_backend_error(schema, fields, from_graphql=False):
     # all generic errors are static and have been handled by this point
 
     err_details = get_error_details(fields)
@@ -416,11 +508,12 @@ def interpret_backend_error(schema, fields):
     if err_details.detail_json:
         hint = err_details.detail_json.get('hint')
 
-    return interpret_by_code(err_details.code, schema, err_details, hint)
+    return interpret_by_code(err_details.code, schema, err_details, hint,
+                             from_graphql=from_graphql)
 
 
 @value_dispatch.value_dispatch
-def interpret_by_code(code, schema, err_details, hint):
+def interpret_by_code(code, schema, err_details, hint, from_graphql=False):
     return errors.InternalServerError(err_details.message)
 
 
@@ -430,6 +523,7 @@ def _interpret_constraint_errors(
     schema: s_schema.Schema,
     err_details: ErrorDetails,
     hint: Optional[str],
+    from_graphql: bool = False,
 ):
     details = None
     if code == pgerrors.ERROR_NOT_NULL_VIOLATION:
@@ -460,6 +554,9 @@ def _interpret_constraint_errors(
                 if object_id is not None:
                     details = f'Failing object id is {str(object_id)!r}.'
 
+            if from_graphql:
+                pname = gql_replace_type_names_in_text(pname)
+
             return errors.MissingRequiredError(
                 f'missing value for required {pname}',
                 details=details,
@@ -504,12 +601,18 @@ def _interpret_constraint_errors(
         msg = constraint.format_error(schema)
         subject = constraint.get_subject(schema)
         vname = subject.get_verbosename(schema, with_parent=True)
-        subjtitle = f"value of {vname}"
-        details = constraint.format_error_message(schema, subjtitle)
+        details = constraint.format_error_message(schema, vname)
+
+        if from_graphql:
+            msg = gql_replace_type_names_in_text(msg)
+            details = gql_replace_type_names_in_text(details)
+
         return errors.ConstraintViolationError(msg, details=details)
     elif error_type == 'newconstraint':
         # If we're here, it means that we already validated that
         # schema_name, table_name and column_name all exist.
+        #
+        # NOTE: this should never occur in GraphQL mode.
         tabname = (err_details.schema_name, err_details.table_name)
         source = common.get_object_from_backend_name(
             schema, s_objtypes.ObjectType, tabname)
@@ -524,6 +627,9 @@ def _interpret_constraint_errors(
     elif error_type == 'scalar':
         assert match
         domain_name = match.group(1)
+        # NOTE: We don't attempt to change the name of the scalar type even if
+        # the error comes from GraphQL because we map custom scalars onto
+        # their underlying base types.
         stype_name = types.base_type_name_map_r.get(domain_name)
         if stype_name:
             if match.group(2) in range_constraints:
@@ -538,35 +644,75 @@ def _interpret_constraint_errors(
 
 
 @interpret_by_code.register(pgerrors.ERROR_INVALID_TEXT_REPRESENTATION)
-def _interpret_invalid_text_repr(code, schema, err_details, hint):
+def _interpret_invalid_text_repr(
+    code: str,
+    schema: s_schema.Schema,
+    err_details: ErrorDetails,
+    hint: Optional[str],
+    from_graphql: bool = False,
+):
     return errors.InvalidValueError(
-        translate_pgtype(schema, err_details.message))
+        translate_pgtype(schema, err_details.message,
+                         from_graphql=from_graphql)
+    )
 
 
 @interpret_by_code.register(pgerrors.ERROR_NUMERIC_VALUE_OUT_OF_RANGE)
-def _interpret_numeric_out_of_range(code, schema, err_details, hint):
+def _interpret_numeric_out_of_range(
+    code: str,
+    schema: s_schema.Schema,
+    err_details: ErrorDetails,
+    hint: Optional[str],
+    from_graphql: bool = False,
+):
     return errors.NumericOutOfRangeError(
-        translate_pgtype(schema, err_details.message))
+        translate_pgtype(schema, err_details.message,
+                         from_graphql=from_graphql)
+    )
 
 
 @interpret_by_code.register(pgerrors.ERROR_INVALID_DATETIME_FORMAT)
 @interpret_by_code.register(pgerrors.ERROR_DATETIME_FIELD_OVERFLOW)
-def _interpret_invalid_datetime(code, schema, err_details, hint):
+def _interpret_invalid_datetime(
+    code: str,
+    schema: s_schema.Schema,
+    err_details: ErrorDetails,
+    hint: Optional[str],
+    from_graphql: bool = False,
+):
     return errors.InvalidValueError(
-        translate_pgtype(schema, err_details.message),
-        hint=hint)
+        translate_pgtype(schema, err_details.message,
+                         from_graphql=from_graphql),
+        hint=hint,
+    )
 
 
 @interpret_by_code.register(pgerrors.ERROR_WRONG_OBJECT_TYPE)
-def _interpret_wrong_object_type(code, schema, err_details, hint):
-    if err_details.message == 'covariance error':
+def _interpret_wrong_object_type(
+    code: str,
+    schema: s_schema.Schema,
+    err_details: ErrorDetails,
+    hint: Optional[str],
+    from_graphql: bool = False,
+):
+    # NOTE: this should never occur in GraphQL mode due to schema/query
+    # validation.
+
+    if (
+        err_details.message == 'covariance error' and
+        err_details.column_name is not None and
+        err_details.table_name is not None
+    ):
         ptr = schema.get_by_id(uuidgen.UUID(err_details.column_name))
         wrong_obj = schema.get_by_id(uuidgen.UUID(err_details.table_name))
+        assert isinstance(ptr, (s_pointers.Pointer, s_pointers.PseudoPointer))
+        target = ptr.get_target(schema)
+        assert target is not None
 
         vn = ptr.get_verbosename(schema, with_parent=True)
         return errors.InvalidLinkTargetError(
             f"invalid target for {vn}: '{wrong_obj.get_name(schema)}'"
-            f" (expecting '{ptr.get_target(schema).get_name(schema)}')"
+            f" (expecting '{target.get_name(schema)}')"
         )
 
     return errors.InternalServerError(err_details.message)


Test patch:
diff --git a/tests/schemas/graphql.esdl b/tests/schemas/graphql.esdl
--- a/tests/schemas/graphql.esdl
+++ b/tests/schemas/graphql.esdl
@@ -113,6 +113,10 @@ type ScalarTest {
 
     property p_tuple -> tuple<int64, str>;
     property p_array_tuple -> array<tuple<str, bool>>;
+
+    property p_short_str -> str {
+        constraint max_len_value(5);
+    }
 }
 type BigIntTest {
     property value -> bigint;
@@ -179,4 +183,37 @@ type Combo {
     # Test type union, Setting and Profile share some inherited fields as well
     # as non-inherited ones.
     link data -> Setting | Profile;
-};
\ No newline at end of file
+};
+
+abstract constraint error_test_constraint extending expression {
+    errmessage :=
+        '{__subject__} cannot have val equal to the length of text field.';
+}
+
+# Used to test run-time errors.
+type ErrorTest {
+    required property text -> str {
+        # rewrite empty string as empty set
+        rewrite update using (
+            .text if len(.text) > 0 else <str>{}
+        );
+    }
+    required property val -> int64 {
+        # force values of 10 or more cause an error
+        rewrite update using (
+            .val if .val < 10 else .val // 0
+        );
+    }
+
+    property div_by_val := 1 / .val;
+    property re_text := re_match(.text, 'test');
+
+    access policy access_all
+        allow all
+        using (true);
+    access policy deny_negative_val
+        deny update write
+        using (.val < 0);
+
+    constraint error_test_constraint on (len(.text) != .val);
+}
\ No newline at end of file
diff --git a/tests/schemas/graphql_setup.edgeql b/tests/schemas/graphql_setup.edgeql
--- a/tests/schemas/graphql_setup.edgeql
+++ b/tests/schemas/graphql_setup.edgeql
@@ -108,7 +108,7 @@ INSERT Foo {
 
 INSERT ScalarTest {
     p_bool := True,
-    p_str := 'Hello',
+    p_str := 'Hello world',
     p_datetime := <datetime>'2018-05-07T20:01:22.306916+00:00',
     p_local_datetime := <cal::local_datetime>'2018-05-07T20:01:22.306916',
     p_local_date := <cal::local_date>'2018-05-07',
@@ -132,6 +132,8 @@ INSERT ScalarTest {
 
     p_tuple := (123, 'test'),
     p_array_tuple := [('hello', true), ('world', false)],
+
+    p_short_str := 'hello',
 };
 
 # Inheritance tests
@@ -201,4 +203,9 @@ INSERT Combo {
 INSERT Combo {
     name := 'combo 2',
     data := assert_single((SELECT Profile FILTER .name = 'Alice profile')),
+};
+
+INSERT ErrorTest {
+    text := ')error(',
+    val := 0,
 };
\ No newline at end of file
diff --git a/tests/test_http_graphql_mutation.py b/tests/test_http_graphql_mutation.py
--- a/tests/test_http_graphql_mutation.py
+++ b/tests/test_http_graphql_mutation.py
@@ -1522,6 +1522,152 @@ def test_graphql_mutation_insert_bad_02(self):
                 }
             """)
 
+    def test_graphql_mutation_insert_bad_03(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"objects provided for 'settings' are not distinct"
+        ):
+            self.graphql_query(r"""
+                mutation insert_UserGroup {
+                    insert_UserGroup(
+                        data: [{
+                            name: "Bad UserGroup03",
+                            settings: [{
+                                filter: {name: {like: "%"}}
+                            }, {
+                                filter: {name: {like: "perks"}}
+                            }]
+                        }]
+                    ) {
+                        name
+                        settings {
+                            name
+                            value
+                        }
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_04(self):
+        with self.assertRaisesRegex(
+            edgedb.CardinalityViolationError,
+            r"more than one object provided for 'profile'"
+        ):
+            self.graphql_query(r"""
+                mutation insert_User {
+                    insert_User(
+                        data: [{
+                            name: "Bad User04",
+                            active: true,
+                            age: 33,
+                            score: 33.33,
+                            profile: {
+                                filter: {name: {like: "%"}}
+                            },
+                        }]
+                    ) {
+                        name
+                        age
+                        score
+                        profile {
+                            name
+                        }
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_05(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"settings violates exclusivity constraint"
+        ):
+            self.graphql_query(r"""
+                mutation insert_UserGroup {
+                    insert_UserGroup(
+                        data: [{
+                            name: "Bad UserGroup05",
+                            settings: {
+                                filter: {name: {like: "perks"}}
+                            }
+                        }]
+                    ) {
+                        name
+                        settings {
+                            name
+                            value
+                        }
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_06(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"Minimum allowed value for positive_int_t is 0"
+        ):
+            self.graphql_query(r"""
+                mutation insert_ScalarTest {
+                    insert_ScalarTest(
+                        data: [{
+                            p_posint: -42,
+                        }]
+                    ) {
+                        p_posint
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_07(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"p_short_str must be no longer than 5 characters"
+        ):
+            self.graphql_query(r"""
+                mutation insert_ScalarTest {
+                    insert_ScalarTest(
+                        data: [{
+                            p_short_str: "too long",
+                        }]
+                    ) {
+                        p_short_str
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_08(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidValueError,
+            r"invalid input syntax for type String: '2023/05/07'"
+        ):
+            self.graphql_query(r"""
+                mutation insert_ScalarTest {
+                    insert_ScalarTest(
+                        data: [{
+                            p_local_date: "2023/05/07",
+                        }]
+                    ) {
+                        p_local_date
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_09(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidValueError,
+            r'String/String field value out of range: "2023-05-77"'
+        ):
+            self.graphql_query(r"""
+                mutation insert_ScalarTest {
+                    insert_ScalarTest(
+                        data: [{
+                            p_local_date: "2023-05-77",
+                        }]
+                    ) {
+                        p_local_date
+                    }
+                }
+            """)
+
     def test_graphql_mutation_insert_multiple_01(self):
         # Issue #1566
         # Test multiple mutations.
@@ -1670,7 +1816,7 @@ def test_graphql_mutation_insert_default_01(self):
     def test_graphql_mutation_update_scalars_01(self):
         orig_data = {
             'p_bool': True,
-            'p_str': 'Hello',
+            'p_str': 'Hello world',
             'p_datetime': '2018-05-07T20:01:22.306916+00:00',
             'p_local_datetime': '2018-05-07T20:01:22.306916',
             'p_local_date': '2018-05-07',
@@ -1849,12 +1995,14 @@ def test_graphql_mutation_update_scalars_01(self):
 
     def test_graphql_mutation_update_scalars_02(self):
         orig_data = {
-            'p_str': 'Hello',
+            'p_str': 'Hello world',
             'p_posint': 42,
+            'p_short_str': 'hello',
         }
         data = {
             'p_str': 'Update ScalarTest02',
             'p_posint': 9999,
+            'p_short_str': 'hi',
         }
 
         validation_query = rf"""
@@ -1870,6 +2018,7 @@ def test_graphql_mutation_update_scalars_02(self):
                 ) {{
                     p_str
                     p_posint
+                    p_short_str
                 }}
             }}
         """
@@ -1884,10 +2033,12 @@ def test_graphql_mutation_update_scalars_02(self):
                     data: {
                         p_str: {set: "Update ScalarTest02"},
                         p_posint: {set: 9999},
+                        p_short_str: {set: "hi"},
                     }
                 ) {
                     p_str
                     p_posint
+                    p_short_str
                 }
             }
         """, {
@@ -1902,15 +2053,18 @@ def test_graphql_mutation_update_scalars_02(self):
             mutation update_ScalarTest(
                 $p_str: String,
                 $p_posint: Int64,
+                $p_short_str: String,
             ) {
                 update_ScalarTest(
                     data: {
                         p_str: {set: $p_str},
                         p_posint: {set: $p_posint},
+                        p_short_str: {set: $p_short_str},
                     }
                 ) {
                     p_str
                     p_posint
+                    p_short_str
                 }
             }
         """, {
@@ -1924,7 +2078,7 @@ def test_graphql_mutation_update_scalars_02(self):
 
     def test_graphql_mutation_update_scalars_03(self):
         orig_data = {
-            'p_str': 'Hello',
+            'p_str': 'Hello world',
             'p_json': {"foo": [1, None, "bar"]},
         }
         data = {
@@ -3371,6 +3525,204 @@ def test_graphql_mutation_update_bad_03(self):
                 }
             """)
 
+    def test_graphql_mutation_update_bad_04(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"objects provided for 'settings' are not distinct"
+        ):
+            self.graphql_query(r"""
+                mutation update_UserGroup {
+                    update_UserGroup(
+                        filter: {
+                            name: {eq: "basic"}
+                        },
+                        data: {
+                            settings: {
+                                set: [{
+                                    filter: {name: {like: "%"}}
+                                }, {
+                                    filter: {name: {like: "perks"}}
+                                }]
+                            }
+                        }
+                    ) {
+                        name
+                        settings {
+                            name
+                            value
+                        }
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_update_bad_05(self):
+        with self.assertRaisesRegex(
+            edgedb.CardinalityViolationError,
+            r"more than one object provided for 'profile'"
+        ):
+            self.graphql_query(r"""
+                mutation update_User {
+                    update_User(
+                        filter: {name: {eq: "Alice"}},
+                        data: {
+                            profile: {
+                                set: {
+                                    filter: {name: {like: "%"}}
+                                }
+                            }
+                        }
+                    ) {
+                        name
+                        age
+                        score
+                        profile {
+                            name
+                        }
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_update_bad_06(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"settings violates exclusivity constraint"
+        ):
+            self.graphql_query(r"""
+                mutation update_UserGroup {
+                    update_UserGroup(
+                        filter: {name: {eq: "unused"}},
+                        data: {
+                            settings: {
+                                set : {filter: {name: {like: "perks"}}}
+                            }
+                        }
+                    ) {
+                        name
+                        settings {
+                            name
+                            value
+                        }
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_update_bad_07(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"Minimum allowed value for positive_int_t is 0"
+        ):
+            self.graphql_query(r"""
+                mutation update_ScalarTest {
+                    update_ScalarTest(
+                        filter: {p_str: {eq: "Hello world"}}
+                        data: {
+                            p_posint: {set: -42},
+                        }
+                    ) {
+                        p_posint
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_update_bad_08(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"p_short_str must be no longer than 5 characters"
+        ):
+            self.graphql_query(r"""
+                mutation update_ScalarTest {
+                    update_ScalarTest(
+                        filter: {p_str: {eq: "Hello world"}}
+                        data: {
+                            p_short_str: {set: "too long"},
+                        }
+                    ) {
+                        p_short_str
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_update_bad_09(self):
+        with self.assertRaisesRegex(
+            edgedb.NumericOutOfRangeError,
+            r"Int out of range"
+        ):
+            self.graphql_query(r"""
+                mutation update_ScalarTest {
+                    update_ScalarTest(
+                        filter: {p_str: {eq: "Hello world"}}
+                        data: {
+                            p_int32: {increment: 1234567890},
+                        }
+                    ) {
+                        p_int32
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_update_bad_10(self):
+        with self.assertRaisesRegex(
+            edgedb.AccessPolicyError,
+            r"access policy violation on update of ErrorTest"
+        ):
+            self.graphql_query(r"""
+                mutation update_ErrorTest {
+                    update_ErrorTest(
+                        data: {val: {set: -5}}
+                    ) {
+                        text
+                    }
+                }
+        """)
+
+    def test_graphql_mutation_update_bad_11(self):
+        # We expect a rewrite rule to cause a division by 0 error.
+        with self.assertRaisesRegex(
+            edgedb.DivisionByZeroError,
+            r"division by zero"
+        ):
+            self.graphql_query(r"""
+                mutation update_ErrorTest {
+                    update_ErrorTest(
+                        data: {val: {set: 15}}
+                    ) {
+                        text
+                    }
+                }
+        """)
+
+    def test_graphql_mutation_update_bad_12(self):
+        # We expect a rewrite rule to convert "" to empty set.
+        with self.assertRaisesRegex(
+            edgedb.MissingRequiredError,
+            r"missing value for required property 'text' of object "
+            r"type 'ErrorTest'"
+        ):
+            self.graphql_query(r"""
+                mutation update_ErrorTest {
+                    update_ErrorTest(
+                        data: {text: {set: ""}}
+                    ) {
+                        text
+                    }
+                }
+        """)
+
+    def test_graphql_mutation_update_bad_13(self):
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"ErrorTest cannot have val equal to the length of text field"
+        ):
+            self.graphql_query(r"""
+                mutation update_ErrorTest {
+                    update_ErrorTest(
+                        data: {val: {set: 7}}
+                    ) {
+                        text
+                    }
+                }
+        """)
+
     def test_graphql_mutation_update_multiple_01(self):
         # Issue #1566
         # Test multiple mutations.
@@ -3729,6 +4081,20 @@ def test_graphql_mutation_delete_alias_01(self):
             "SettingAlias": []
         })
 
+    def test_graphql_mutation_delete_bad_01(self):
+        # We expext a rewrite rule to cause a division by 0 error.
+        with self.assertRaisesRegex(
+            edgedb.ConstraintViolationError,
+            r"deletion of UserGroup .+is prohibited by link target policy"
+        ):
+            self.graphql_query(r"""
+                mutation delete_UserGroup {
+                    delete_UserGroup {
+                        name
+                    }
+                }
+        """)
+
     def test_graphql_mutation_bigint(self):
         self.assert_graphql_query_result(r"""
             mutation insert_BigIntTest {
diff --git a/tests/test_http_graphql_query.py b/tests/test_http_graphql_query.py
--- a/tests/test_http_graphql_query.py
+++ b/tests/test_http_graphql_query.py
@@ -1413,11 +1413,11 @@ def test_graphql_functional_arguments_21(self):
 
     def test_graphql_functional_arguments_22(self):
         with self.assertRaisesRegex(
-                edgedb.QueryError,
+                edgedb.InvalidValueError,
                 # this error message is subpar, but this is what we get
                 # from postgres, because we transfer bigint values to postgres
                 # as strings
-                r'invalid input syntax for type bigint: "aaaaa"',
+                r'invalid input syntax for type Int64: "aaaaa"',
                 # _line=5, _col=32,
         ):
             self.graphql_query(r"""
@@ -2545,7 +2545,7 @@ def test_graphql_functional_scalars_01(self):
         """, {
             "ScalarTest": [{
                 'p_bool': True,
-                'p_str': 'Hello',
+                'p_str': 'Hello world',
                 'p_datetime': '2018-05-07T20:01:22.306916+00:00',
                 'p_local_datetime': '2018-05-07T20:01:22.306916',
                 'p_local_date': '2018-05-07',
@@ -3268,8 +3268,8 @@ def test_graphql_functional_variables_32(self):
 
     def test_graphql_functional_variables_33(self):
         with self.assertRaisesRegex(
-                edgedb.QueryError,
-                r'expected JSON string'):
+                edgedb.InvalidValueError,
+                r'expected JSON string or null; got JSON number'):
 
             self.graphql_query(
                 r"""
@@ -3407,7 +3407,7 @@ def test_graphql_functional_variables_38(self):
     # error message expressed in terms of GraphQL types.
     def test_graphql_functional_variables_39(self):
         with self.assertRaisesRegex(
-                edgedb.QueryError,
+                edgedb.InvalidValueError,
                 r'expected JSON number.+got JSON string'):
             self.graphql_query(
                 r"""
@@ -4135,6 +4135,58 @@ def test_graphql_functional_type_union_01(self):
             ]
         })
 
+    def test_graphql_functional_edgedb_errors_01(self):
+        with self.assertRaisesRegex(
+            edgedb.DivisionByZeroError,
+            r"division by zero"
+        ):
+            self.graphql_query(r"""
+                query {
+                    ErrorTest {
+                        div_by_val
+                    }
+                }
+            """)
+
+    def test_graphql_functional_edgedb_errors_02(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidValueError,
+            r"LIMIT must not be negative"
+        ):
+            self.graphql_query(r"""
+                query {
+                    ErrorTest(first: -2) {
+                        text
+                    }
+                }
+            """)
+
+    def test_graphql_functional_edgedb_errors_03(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidValueError,
+            r"OFFSET must not be negative"
+        ):
+            self.graphql_query(r"""
+                query {
+                    ErrorTest(after: "-2") {
+                        text
+                    }
+                }
+            """)
+
+    def test_graphql_functional_edgedb_errors_04(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidValueError,
+            r"invalid regular expression"
+        ):
+            self.graphql_query(r"""
+                query {
+                    ErrorTest {
+                        re_text
+                    }
+                }
+            """)
+
     def test_graphql_globals_01(self):
         Q = r'''query { GlobalTest { gstr, garray, gid, gdef, gdef2 } }'''