Skip to content

Commit

Permalink
Support adding constraints to object types in DDL (#1581)
Browse files Browse the repository at this point in the history
Work on #1164.
  • Loading branch information
msullivan committed Jul 27, 2020
1 parent 4ab945a commit 41e6bba
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
16 changes: 16 additions & 0 deletions docs/edgeql/ddl/objects.rst
Expand Up @@ -28,6 +28,7 @@ CREATE TYPE
CREATE ANNOTATION <annotation-name> := <value>
CREATE LINK <link-name> ...
CREATE PROPERTY <property-name> ...
CREATE CONSTRAINT <constraint-name> ...
CREATE INDEX ON <index-expr>

Description
Expand Down Expand Up @@ -88,6 +89,10 @@ The following subcommands are allowed in the ``CREATE TYPE`` block:
Define a new property for this object type. See
:eql:stmt:`CREATE PROPERTY` for details.

:eql:synopsis:`CREATE CONSTRAINT <constraint-name> ...`
Define a concrete constraint for this object type. See
:eql:stmt:`CREATE CONSTRAINT` for details.

:eql:synopsis:`CREATE INDEX ON <index-expr>`
Define a new :ref:`index <ref_datamodel_indexes>`
using *index-expr* for this object type. See
Expand Down Expand Up @@ -139,6 +144,9 @@ Change the definition of an
CREATE PROPERTY <property-name> ...
ALTER PROPERTY <property-name> ...
DROP PROPERTY <property-name> ...
CREATE CONSTRAINT <constraint-name> ...
ALTER CONSTRAINT <constraint-name> ...
DROP CONSTRAINT <constraint-name> ...
CREATE INDEX ON <index-expr>
DROP INDEX ON <index-expr>

Expand Down Expand Up @@ -212,6 +220,14 @@ The following subcommands are allowed in the ``ALTER TYPE`` block:
Remove a property item from this object type. See
:eql:stmt:`DROP PROPERTY` for details.

:eql:synopsis:`ALTER CONSTRAINT <constraint-name> ...`
Alter the definition of a constraint for this object type. See
:eql:stmt:`ALTER CONSTRAINT` for details.

:eql:synopsis:`DROP CONSTRAINT <constraint-name>;`
Remove a constraint from this object type. See
:eql:stmt:`DROP CONSTRAINT` for details.

:eql:synopsis:`DROP INDEX ON <index-expr>`
Remove an :ref:`index <ref_datamodel_indexes>` defined as *index-expr*
from this object type. See :eql:stmt:`DROP INDEX` for details.
Expand Down
5 changes: 5 additions & 0 deletions edb/edgeql/parser/grammar/ddl.py
Expand Up @@ -1348,6 +1348,8 @@ def reduce_DropLink(self, *kids):
AlterConcretePropertyStmt,
CreateConcreteLinkStmt,
AlterConcreteLinkStmt,
CreateConcreteConstraintStmt,
AlterConcreteConstraintStmt,
CreateIndexStmt,
AlterIndexStmt,
)
Expand Down Expand Up @@ -1397,6 +1399,9 @@ def reduce_CreateRegularObjectTypeStmt(self, *kids):
CreateConcreteLinkStmt,
AlterConcreteLinkStmt,
DropConcreteLinkStmt,
CreateConcreteConstraintStmt,
AlterConcreteConstraintStmt,
DropConcreteConstraintStmt,
CreateIndexStmt,
AlterIndexStmt,
DropIndexStmt,
Expand Down
23 changes: 23 additions & 0 deletions edb/schema/constraints.py
Expand Up @@ -38,6 +38,7 @@
from . import inheriting
from . import name as sn
from . import objects as so
from . import types as s_types
from . import pseudo as s_pseudo
from . import referencing
from . import utils
Expand Down Expand Up @@ -722,6 +723,7 @@ def _populate_concrete_constraint_attrs(
**kwargs: Any
) -> None:
from edb.ir import ast as ir_ast
from edb.ir import utils as ir_utils

constr_base = schema.get(name, type=Constraint)

Expand Down Expand Up @@ -825,6 +827,27 @@ def _populate_concrete_constraint_attrs(
context=expr_context
)

if (subjectexpr is not None and isinstance(subject_obj, s_types.Type)
and subject_obj.is_object_type()):
final_subjectexpr = s_expr.Expression.compiled(
subjectexpr,
schema=schema,
options=qlcompiler.CompilerOptions(
anchors={qlast.Subject().name: subject},
singletons=frozenset({subject_obj}),
),
)
assert isinstance(final_subjectexpr.irast, ir_ast.Statement)

if final_subjectexpr.irast.cardinality.is_multi():
refs = ir_utils.get_longest_paths(final_expr.irast)
if len(refs) > 1:
raise errors.InvalidConstraintDefinitionError(
"Constraint with multi cardinality may not "
"reference multiple links or properties",
context=expr_context
)

attrs['return_type'] = constr_base.get_return_type(schema)
attrs['return_typemod'] = constr_base.get_return_typemod(schema)
attrs['finalexpr'] = final_expr
Expand Down
101 changes: 101 additions & 0 deletions tests/test_constraints.py
Expand Up @@ -933,6 +933,107 @@ async def test_constraints_ddl_06(self):
};
""")

async def test_constraints_ddl_07(self):
await self.con.execute("""
CREATE TYPE test::ObjCnstr {
CREATE PROPERTY first_name -> str;
CREATE PROPERTY last_name -> str;
CREATE CONSTRAINT exclusive on (__subject__.first_name);
};
""")

await self.con.execute("""
INSERT test::ObjCnstr { first_name := "foo", last_name := "bar" }
""")

with self.assertRaisesRegex(
edgedb.ConstraintViolationError,
"ObjCnstr violates exclusivity constraint"):
await self.con.execute("""
INSERT test::ObjCnstr {
first_name := "foo", last_name := "baz" }
""")

await self.con.execute("""
ALTER TYPE test::ObjCnstr {
DROP CONSTRAINT exclusive on (__subject__.first_name);
}
""")

await self.con.execute("""
ALTER TYPE test::ObjCnstr {
CREATE CONSTRAINT exclusive
on ((__subject__.first_name, __subject__.last_name));
}
""")

await self.con.execute("""
ALTER TYPE test::ObjCnstr {
ALTER CONSTRAINT exclusive
on ((__subject__.first_name, __subject__.last_name)) {
SET errmessage := "nope!";
}
}
""")

# This one should work now
await self.con.execute("""
INSERT test::ObjCnstr { first_name := "foo", last_name := "baz" }
""")

with self.assertRaisesRegex(
edgedb.ConstraintViolationError,
"nope!"):
await self.con.execute("""
INSERT test::ObjCnstr {
first_name := "foo", last_name := "bar" }
""")

async def test_constraints_ddl_08(self):
async with self._run_and_rollback():
await self.con.execute("""
CREATE TYPE test::ObjCnstr2 {
CREATE MULTI PROPERTY first_name -> str;
CREATE MULTI PROPERTY last_name -> str;
CREATE CONSTRAINT exclusive on (__subject__.first_name);
};
""")

with self.assertRaisesRegex(
edgedb.InvalidConstraintDefinitionError,
"Constraint with multi cardinality may not "
"reference multiple links or properties"):
await self.con.execute("""
ALTER TYPE test::ObjCnstr2 {
CREATE CONSTRAINT exclusive
on ((__subject__.first_name, __subject__.last_name));
};
""")

async def test_constraints_ddl_09(self):
async with self._run_and_rollback():
await self.con.execute("""
CREATE TYPE test::Label {
CREATE PROPERTY text -> str;
};
CREATE TYPE test::ObjCnstr3 {
CREATE LINK label -> test::Label;
CREATE CONSTRAINT exclusive on (__subject__.label);
};
INSERT test::ObjCnstr3 {
label := (INSERT test::Label {text := "obj_test" })
};
""")

with self.assertRaisesRegex(
edgedb.ConstraintViolationError,
"ObjCnstr3 violates exclusivity constraint"):
await self.con.execute("""
INSERT test::ObjCnstr3 {
label := (SELECT test::Label
FILTER .text = "obj_test" LIMIT 1) };
""")

async def test_constraints_ddl_function(self):
await self.con.execute('''\
CREATE FUNCTION test::comp_func(s: str) -> str {
Expand Down

0 comments on commit 41e6bba

Please sign in to comment.