From 9798846ddd7cc6192dea4f2de9376a1d5414e718 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Thu, 13 Dec 2018 18:43:20 -0500 Subject: [PATCH] Make attributes non-inheritable by default Automatic inheritance of attributes is sometimes inconvenient or undesirable. A "description" attribute, for example, is unlikely to be useful when inherited. Thus, we make all attributes non-inheritable by default, with an option to opt-in to automatic inheritance with the new `INHERITABLE` qualifier for `CREATE ATTRIBUTE`: CREATE INHERITABLE ATTRIBUTE foo; Issue: #306 --- edb/lang/edgeql/ast.py | 1 + edb/lang/edgeql/codegen.py | 7 +- edb/lang/edgeql/parser/grammar/ddl.py | 11 +++ edb/lang/edgeql/parser/grammar/keywords.py | 1 + edb/lang/schema/ast.py | 2 +- edb/lang/schema/attributes.py | 57 +++++++++++- edb/lang/schema/codegen.py | 10 +- edb/lang/schema/constraints.py | 2 +- edb/lang/schema/declarative.py | 2 + edb/lang/schema/inheriting.py | 13 +++ edb/lang/schema/objects.py | 74 ++++++++++----- .../schema/parser/grammar/declarations.py | 25 +++-- edb/lang/schema/parser/grammar/keywords.py | 1 + edb/lib/schema.eql | 4 +- .../pgsql/datasources/schema/attributes.py | 6 +- edb/server/pgsql/intromech.py | 5 +- edb/server/pgsql/metaschema.py | 93 ++++++++++--------- tests/test_edgeql_ddl.py | 65 ++++++++++++- tests/test_edgeql_syntax.py | 11 +++ tests/test_schema.py | 24 +++++ tests/test_schema_syntax.py | 12 ++- 21 files changed, 331 insertions(+), 95 deletions(-) diff --git a/edb/lang/edgeql/ast.py b/edb/lang/edgeql/ast.py index 75300df40e..002040690e 100644 --- a/edb/lang/edgeql/ast.py +++ b/edb/lang/edgeql/ast.py @@ -617,6 +617,7 @@ class DropRole(DropObject): class CreateAttribute(CreateExtendingObject): type: typing.Optional[TypeExpr] + inheritable: bool class DropAttribute(DropObject): diff --git a/edb/lang/edgeql/codegen.py b/edb/lang/edgeql/codegen.py index 13407d6b40..4ab9f0014b 100644 --- a/edb/lang/edgeql/codegen.py +++ b/edb/lang/edgeql/codegen.py @@ -839,8 +839,11 @@ def visit_AlterObjectProperty(self, node): def visit_CreateAttribute(self, node): after_name = lambda: self._ddl_visit_bases(node) - self._visit_CreateObject(node, 'ABSTRACT ATTRIBUTE', - after_name=after_name) + if node.inheritable: + tag = 'ABSTRACT INHERITABLE ATTRIBUTE' + else: + tag = 'ABSTRACT ATTRIBUTE' + self._visit_CreateObject(node, tag, after_name=after_name) def visit_DropAttribute(self, node): self._visit_DropObject(node, 'ABSTRACT ATTRIBUTE') diff --git a/edb/lang/edgeql/parser/grammar/ddl.py b/edb/lang/edgeql/parser/grammar/ddl.py index 1dd8f821b3..2e0cc99c46 100644 --- a/edb/lang/edgeql/parser/grammar/ddl.py +++ b/edb/lang/edgeql/parser/grammar/ddl.py @@ -852,6 +852,17 @@ def reduce_CreateAttribute(self, *kids): name=kids[3].val, bases=kids[4].val, commands=kids[5].val, + inheritable=False, + ) + + def reduce_CreateInheritableAttribute(self, *kids): + r"""%reduce CREATE ABSTRACT INHERITABLE ATTRIBUTE + NodeName OptExtending OptCreateCommandsBlock""" + self.val = qlast.CreateAttribute( + name=kids[4].val, + bases=kids[5].val, + commands=kids[6].val, + inheritable=True, ) diff --git a/edb/lang/edgeql/parser/grammar/keywords.py b/edb/lang/edgeql/parser/grammar/keywords.py index 492f20e85c..8a5cc25a8a 100644 --- a/edb/lang/edgeql/parser/grammar/keywords.py +++ b/edb/lang/edgeql/parser/grammar/keywords.py @@ -55,6 +55,7 @@ "implicit", "index", "infix", + "inheritable", "inherited", "into", "last", diff --git a/edb/lang/schema/ast.py b/edb/lang/schema/ast.py index 511f20bea3..714548928a 100644 --- a/edb/lang/schema/ast.py +++ b/edb/lang/schema/ast.py @@ -109,7 +109,7 @@ class ScalarTypeDeclaration(Declaration): class AttributeDeclaration(Declaration): - type: qlast.TypeName + inheritable: bool = False class ObjectTypeDeclaration(Declaration): diff --git a/edb/lang/schema/attributes.py b/edb/lang/schema/attributes.py index 8d7c257620..4daeb539a9 100644 --- a/edb/lang/schema/attributes.py +++ b/edb/lang/schema/attributes.py @@ -18,6 +18,8 @@ # +import typing + from edb.lang.edgeql import ast as qlast from edb.lang.edgeql import errors as qlerrors @@ -35,6 +37,9 @@ class Attribute(inheriting.InheritingObject): name = so.SchemaField( sn.Name, inheritable=False, compcoef=0.2) + inheritable = so.SchemaField( + bool, default=False, compcoef=0.2) + class AttributeValue(inheriting.InheritingObject): @@ -47,6 +52,9 @@ class AttributeValue(inheriting.InheritingObject): value = so.SchemaField( str, compcoef=0.909) + inheritable = so.SchemaField( + bool, default=False, compcoef=0.2) + def __str__(self): return '<{}: at 0x{:x}>'.format(self.__class__.__name__, id(self)) @@ -57,6 +65,7 @@ class AttributeSubject(so.Object): attributes_refs = so.RefDict( attr='attributes', local_attr='own_attributes', + non_inheritable_attr='non_inheritable_attributes', ref_cls=AttributeValue) attributes = so.SchemaField( @@ -69,6 +78,11 @@ class AttributeSubject(so.Object): inheritable=False, ephemeral=True, coerce=True, default=so.ObjectIndexByShortname) + non_inheritable_attributes = so.SchemaField( + so.ObjectIndexByShortname, compcoef=0.909, + inheritable=False, ephemeral=True, coerce=True, + default=so.ObjectIndexByShortname) + def add_attribute(self, schema, attribute, replace=False): schema = self.add_classref( schema, 'attributes', attribute, replace=replace) @@ -78,18 +92,24 @@ def del_attribute(self, schema, attribute_name): shortname = sn.shortname_from_fullname(attribute_name) return self.del_classref(schema, 'attributes', shortname) - def get_attribute(self, schema, name): - return self.get_attributes(schema).get(schema, name, None) + def get_attribute(self, schema, name: str) -> typing.Optional[str]: + attrval = self.get_attributes(schema).get(schema, name, None) + return attrval.get_value(schema) if attrval is not None else None def set_attribute(self, schema, attr: Attribute, value: str): attrname = attr.get_name(schema) existing = self.get_own_attributes(schema).get(schema, attrname, None) + if existing is None: + existing = self.get_non_inheritable_attributes(schema).get( + schema, attrname, None) if existing is None: my_name = self.get_name(schema) ann = sn.get_specialized_name(attrname, my_name) an = sn.Name(name=ann, module=my_name.module) schema, av = AttributeValue.create_in_schema( - schema, name=an, value=value) + schema, name=an, value=value, + subject=self, attribute=attr, + inheritable=attr.get_inheritable(schema)) schema = self.add_attribute(schema, av) else: schema, updated = existing.set_field_value('value', value) @@ -110,6 +130,24 @@ class AttributeCommand(sd.ObjectCommand, schema_metaclass=Attribute, class CreateAttribute(AttributeCommand, sd.CreateObject): astnode = qlast.CreateAttribute + @classmethod + def _cmd_tree_from_ast(cls, schema, astnode, context): + cmd = super()._cmd_tree_from_ast(schema, astnode, context) + cmd.update(( + sd.AlterObjectProperty( + property='inheritable', + new_value=astnode.inheritable, + ), + )) + + return cmd + + def _apply_field_ast(self, schema, context, node, op): + if op.property == 'inheritable': + node.inheritable = op.new_value + else: + super()._apply_field_ast(schema, context, node, op) + class AlterAttribute(AttributeCommand, sd.AlterObject): pass @@ -195,6 +233,10 @@ def _cmd_tree_from_ast(cls, schema, astnode, context): sd.AlterObjectProperty( property='value', new_value=value + ), + sd.AlterObjectProperty( + property='inheritable', + new_value=attr.get_inheritable(schema), ) )) @@ -209,6 +251,8 @@ def _apply_field_ast(self, schema, context, node, op): pass elif op.property == 'subject': pass + elif op.property == 'inheritable': + pass else: super()._apply_field_ast(schema, context, node, op) @@ -221,12 +265,15 @@ def apply(self, schema, context): name = sn.shortname_from_fullname(self.classname) attrs = attrsubj.scls.get_own_attributes(schema) attribute = attrs.get(schema, name, None) + if attribute is None: + attrs = attrsubj.scls.get_non_inheritable_attributes(schema) + attribute = attrs.get(schema, name, None) + if attribute is None: schema, attribute = super().apply(schema, context) schema = self.add_attribute(schema, attribute, attrsubj.scls) else: - schema, attribute = sd.AlterObject.apply( - self, schema, context) + schema, attribute = sd.AlterObject.apply(self, schema, context) return schema, attribute diff --git a/edb/lang/schema/codegen.py b/edb/lang/schema/codegen.py index dfb9dae09c..50b2b9ee21 100644 --- a/edb/lang/schema/codegen.py +++ b/edb/lang/schema/codegen.py @@ -187,15 +187,11 @@ def visit_ScalarTypeDeclaration(self, node): self._visit_Declaration(node) def visit_AttributeDeclaration(self, node): - def after_name(node): - if node.type: - self.write(' ') - self.visit(node.type) - self.write(' ') - if node.abstract: self.write('abstract ') - self._visit_Declaration(node, after_name=after_name) + if node.inheritable: + self.write('inheritable ') + self._visit_Declaration(node) def visit_ObjectTypeDeclaration(self, node): self._visit_qualifier(node) diff --git a/edb/lang/schema/constraints.py b/edb/lang/schema/constraints.py index 76b69fe606..36c2d50d32 100644 --- a/edb/lang/schema/constraints.py +++ b/edb/lang/schema/constraints.py @@ -283,7 +283,7 @@ def format_error_message(self, schema): subjname = subject.get_shortname(schema) subjtitle = subjname.name else: - subjtitle = titleattr.value + subjtitle = titleattr formatted = errmsg.format(__subject__=subjtitle) diff --git a/edb/lang/schema/declarative.py b/edb/lang/schema/declarative.py index f620b8f25f..2fab5e1c4c 100644 --- a/edb/lang/schema/declarative.py +++ b/edb/lang/schema/declarative.py @@ -105,6 +105,8 @@ def load_module(self, module_name, decl_ast): objcls_kw['is_abstract'] = decl.delegated if hasattr(decl, 'final'): objcls_kw['is_final'] = decl.final + if hasattr(decl, 'inheritable'): + objcls_kw['inheritable'] = decl.inheritable if objcls is s_constr.Constraint: objcls_kw['return_type'] = self._schema.get('std::bool') diff --git a/edb/lang/schema/inheriting.py b/edb/lang/schema/inheriting.py index 0e72a8f214..0a85737104 100644 --- a/edb/lang/schema/inheriting.py +++ b/edb/lang/schema/inheriting.py @@ -454,6 +454,19 @@ def merge(self, *objs, schema, dctx=None): if other_coll is None: continue + if refdict.non_inheritable_attr: + non_inh_coll = obj.get_explicit_field_value( + schema, refdict.non_inheritable_attr, None) + + if non_inh_coll: + other_coll = type(other_coll).create(schema, { + v for k, v in other_coll.items(schema) + if not non_inh_coll.has(schema, k) + }) + + if not other_coll: + continue + if this_coll is None: schema = self.set_field_value( schema, refdict.attr, other_coll) diff --git a/edb/lang/schema/objects.py b/edb/lang/schema/objects.py index 6da78b4689..846861301f 100644 --- a/edb/lang/schema/objects.py +++ b/edb/lang/schema/objects.py @@ -253,6 +253,7 @@ class RefDict(struct.Struct): local_attr = struct.Field(str) attr = struct.Field(str) + non_inheritable_attr = struct.Field(str, default=None) backref_attr = struct.Field(str, default='subject') requires_explicit_inherit = struct.Field(bool, default=False) ref_cls = struct.Field(type) @@ -853,28 +854,11 @@ def delta(cls, old, new, *, context=None, old_schema, new_schema): else: full_delta = alter_delta = delta - old_idx_key = lambda o: o.get_name(old_schema) - new_idx_key = lambda o: o.get_name(new_schema) - for refdict in cls.get_refdicts(): - local_attr = refdict.local_attr - - if old: - oldcoll = old.get_field_value(old_schema, local_attr) - oldcoll_idx = ordered.OrderedIndex( - oldcoll.objects(old_schema), key=old_idx_key) - else: - oldcoll_idx = {} - - if new: - newcoll = new.get_field_value(new_schema, local_attr) - newcoll_idx = ordered.OrderedIndex( - newcoll.objects(new_schema), key=new_idx_key) - else: - newcoll_idx = {} - - cls.delta_sets(oldcoll_idx, newcoll_idx, alter_delta, context, - old_schema=old_schema, new_schema=new_schema) + cls._delta_refdict( + old, new, delta=alter_delta, + refdict=refdict, context=context, + old_schema=old_schema, new_schema=new_schema) if alter_delta is not full_delta: if alter_delta.has_subcommands(): @@ -884,10 +868,46 @@ def delta(cls, old, new, *, context=None, old_schema, new_schema): return full_delta + @classmethod + def _delta_refdict(cls, old, new, *, delta, refdict, context, + old_schema, new_schema): + + old_idx_key = lambda o: o.get_name(old_schema) + new_idx_key = lambda o: o.get_name(new_schema) + + def _delta_subdict(attr): + if old: + oldcoll = old.get_field_value(old_schema, attr) + oldcoll_idx = ordered.OrderedIndex( + oldcoll.objects(old_schema), key=old_idx_key) + else: + oldcoll_idx = {} + + if new: + newcoll = new.get_field_value(new_schema, attr) + newcoll_idx = ordered.OrderedIndex( + newcoll.objects(new_schema), key=new_idx_key) + else: + newcoll_idx = {} + + cls.delta_sets(oldcoll_idx, newcoll_idx, delta, context, + old_schema=old_schema, new_schema=new_schema) + + _delta_subdict(refdict.local_attr) + if refdict.non_inheritable_attr: + _delta_subdict(refdict.non_inheritable_attr) + def add_classref(self, schema, collection, obj, replace=False): refdict = type(self).get_refdict(collection) attr = refdict.attr - local_attr = refdict.local_attr + + if (refdict.non_inheritable_attr + and type(obj).get_field('inheritable') is not None + and not obj.get_inheritable(schema)): + local_attr = refdict.non_inheritable_attr + else: + local_attr = refdict.local_attr + colltype = type(self).get_field(local_attr).type local_coll = self.get_explicit_field_value(schema, local_attr, None) @@ -916,6 +936,12 @@ def del_classref(self, schema, collection, key): refdict = type(self).get_refdict(collection) attr = refdict.attr local_attr = refdict.local_attr + non_inh_attr = refdict.non_inheritable_attr + + if non_inh_attr is not None: + non_inh_coll = self.get_field_value(schema, non_inh_attr) + else: + non_inh_coll = None local_coll = self.get_field_value(schema, local_attr) all_coll = self.get_field_value(schema, attr) @@ -924,6 +950,10 @@ def del_classref(self, schema, collection, key): schema, local_coll = local_coll.delete(schema, [key]) schema = self.set_field_value(schema, local_attr, local_coll) + if non_inh_coll and non_inh_coll.has(schema, key): + schema, non_inh_coll = non_inh_coll.delete(schema, [key]) + schema = self.set_field_value(schema, non_inh_attr, non_inh_coll) + if all_coll and all_coll.has(schema, key): schema, all_coll = all_coll.delete(schema, [key]) schema = self.set_field_value(schema, attr, all_coll) diff --git a/edb/lang/schema/parser/grammar/declarations.py b/edb/lang/schema/parser/grammar/declarations.py index f2d99428cf..2a64d72b53 100644 --- a/edb/lang/schema/parser/grammar/declarations.py +++ b/edb/lang/schema/parser/grammar/declarations.py @@ -373,21 +373,34 @@ def reduce_SCALAR_TYPE_NameAndExtends_DeclarationSpecsBlob(self, *kids): ) +class OptInheritable(Nonterm): + def reduce_empty(self): + self.val = False + + def reduce_INHERITABLE(self, *kids): + self.val = True + + class AttributeDeclaration(Nonterm): - def reduce_ATTRIBUTE_NameAndExtends_NL(self, *kids): - np: NameWithParents = kids[1].val + def reduce_OptInheritable_ATTRIBUTE_NameAndExtends_NL(self, *kids): + np: NameWithParents = kids[2].val self.val = esast.AttributeDeclaration( name=np.name, - extends=np.extends) + extends=np.extends, + inheritable=kids[0].val + ) - def reduce_ATTRIBUTE_NameAndExtends_DeclarationSpecsBlob(self, *kids): - np: NameWithParents = kids[1].val + def reduce_attribute_with_specs(self, *kids): + """%reduce OptInheritable ATTRIBUTE NameAndExtends + DeclarationSpecsBlob + """ + np: NameWithParents = kids[2].val self.val = esast.AttributeDeclaration( name=np.name, extends=np.extends, **_process_decl_body( - kids[2].val, + kids[3].val, ( esast.Attribute, esast.Field diff --git a/edb/lang/schema/parser/grammar/keywords.py b/edb/lang/schema/parser/grammar/keywords.py index f515bc17bb..a4dd0f2a95 100644 --- a/edb/lang/schema/parser/grammar/keywords.py +++ b/edb/lang/schema/parser/grammar/keywords.py @@ -40,6 +40,7 @@ "final", "from", "index", + "inheritable", "inherited", "link", "multi", diff --git a/edb/lib/schema.eql b/edb/lib/schema.eql index aabbe56219..b87c1255ba 100644 --- a/edb/lib/schema.eql +++ b/edb/lib/schema.eql @@ -92,7 +92,9 @@ CREATE TYPE schema::Delta EXTENDING schema::Object { }; -CREATE TYPE schema::Attribute EXTENDING schema::Object; +CREATE TYPE schema::Attribute EXTENDING schema::Object { + CREATE PROPERTY schema::inheritable -> std::bool; +}; CREATE ABSTRACT LINK schema::attributes { diff --git a/edb/server/pgsql/datasources/schema/attributes.py b/edb/server/pgsql/datasources/schema/attributes.py index e7afbabe81..f076ce5881 100644 --- a/edb/server/pgsql/datasources/schema/attributes.py +++ b/edb/server/pgsql/datasources/schema/attributes.py @@ -29,7 +29,8 @@ async def fetch( return await conn.fetch(""" SELECT a.id AS id, - a.name AS name + a.name AS name, + a.inheritable AS inheritable FROM edgedb.attribute a WHERE @@ -54,7 +55,8 @@ async def fetch_values( AS subject_name, edgedb._resolve_type_name(a.attribute) AS attribute_name, - a.value AS value + a.value AS value, + a.inheritable AS inheritable FROM edgedb.AttributeValue a WHERE diff --git a/edb/server/pgsql/intromech.py b/edb/server/pgsql/intromech.py index fec5878115..00c61b6f2e 100644 --- a/edb/server/pgsql/intromech.py +++ b/edb/server/pgsql/intromech.py @@ -755,6 +755,7 @@ async def read_attributes(self, schema, only_modules, exclude_modules): schema, id=r['id'], name=name, + inheritable=r['inheritable'], ) return schema @@ -780,7 +781,9 @@ async def read_attribute_values( name=name, subject=subject, attribute=attribute, - value=value) + value=value, + inheritable=r['inheritable'], + ) schema = subject.add_attribute(schema, attribute) diff --git a/edb/server/pgsql/metaschema.py b/edb/server/pgsql/metaschema.py index e8a6daf25f..0663263368 100644 --- a/edb/server/pgsql/metaschema.py +++ b/edb/server/pgsql/metaschema.py @@ -1593,51 +1593,14 @@ def _get_link_view(mcls, schema_cls, field, ptr, refdict, schema): pn = ptr.get_shortname(schema) if refdict: - if (issubclass(mcls, s_inheriting.InheritingObject) or - mcls is s_obj.Object): + if issubclass(mcls, s_inheriting.InheritingObject): + schematab = 'edgedb.{}'.format(mcls.__name__) - if mcls is s_obj.Object: - schematab = 'edgedb.InheritingObject' - else: - schematab = 'edgedb.{}'.format(mcls.__name__) - - link_query = ''' - SELECT DISTINCT ON ((cls.id, r.bases[1])) - cls.id AS {src}, - r.id AS {tgt} - FROM - (SELECT - s.id AS id, - ancestry.ancestor AS ancestor, - ancestry.depth AS depth - FROM - {schematab} s - LEFT JOIN LATERAL - UNNEST(s.mro) WITH ORDINALITY - AS ancestry(ancestor, depth) ON true - - UNION ALL - SELECT - s.id AS id, - s.id AS ancestor, - 0 AS depth - FROM - {schematab} s - ) AS cls - - INNER JOIN {reftab} r - ON (((r.{refattr}).types[1]).maintype = cls.ancestor) - ORDER BY - (cls.id, r.bases[1]), cls.depth - '''.format( - schematab=schematab, - reftab='edgedb.{}'.format(refdict.ref_cls.__name__), - refattr=q(refdict.backref_attr), - src=dbname(sn.Name('std::source')), - tgt=dbname(sn.Name('std::target')), - ) + non_inh_link_query = None else: - link_query = ''' + schematab = 'edgedb.InheritingObject' + + non_inh_link_query = ''' SELECT (({refattr}).types[1]).maintype AS {src}, id AS {tgt} @@ -1650,6 +1613,50 @@ def _get_link_view(mcls, schema_cls, field, ptr, refdict, schema): tgt=dbname(sn.Name('std::target')), ) + inh_link_query = ''' + SELECT DISTINCT ON ((cls.id, r.bases[1])) + cls.id AS {src}, + r.id AS {tgt} + FROM + (SELECT + s.id AS id, + ancestry.ancestor AS ancestor, + ancestry.depth AS depth + FROM + {schematab} s + LEFT JOIN LATERAL + UNNEST(s.mro) WITH ORDINALITY + AS ancestry(ancestor, depth) ON true + + UNION ALL + SELECT + s.id AS id, + s.id AS ancestor, + 0 AS depth + FROM + {schematab} s + ) AS cls + + INNER JOIN {reftab} r + ON (((r.{refattr}).types[1]).maintype = cls.ancestor) + ORDER BY + (cls.id, r.bases[1]), cls.depth + '''.format( + schematab=schematab, + reftab='edgedb.{}'.format(refdict.ref_cls.__name__), + refattr=q(refdict.backref_attr), + src=dbname(sn.Name('std::source')), + tgt=dbname(sn.Name('std::target')), + ) + + if non_inh_link_query: + link_query = ( + f'({inh_link_query})\nUNION\n' + f'({non_inh_link_query})' + ) + else: + link_query = inh_link_query + if pn.name == 'attributes': link_query = ''' SELECT diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py index e775fa3383..06462ae50f 100644 --- a/tests/test_edgeql_ddl.py +++ b/tests/test_edgeql_ddl.py @@ -1298,7 +1298,6 @@ async def test_edgeql_ddl_property_computable_bad_01(self): }; ''') - @unittest.expectedFailure async def test_edgeql_ddl_attribute_01(self): await self.con.execute(""" CREATE ABSTRACT ATTRIBUTE test::attr1; @@ -1308,6 +1307,21 @@ async def test_edgeql_ddl_attribute_01(self): }; """) + await self.assert_query_result(''' + WITH MODULE schema + SELECT ScalarType { + attributes: { + name, + @value, + } + } + FILTER + .name = 'test::TestAttrType1'; + + ''', [ + [{"attributes": [{"name": "test::attr1", "@value": "aaaa"}]}] + ]) + await self.con.execute(""" CREATE MIGRATION test::mig1 TO eschema $$ abstract attribute attr2 @@ -1325,7 +1339,7 @@ async def test_edgeql_ddl_attribute_01(self): attributes: { name, @value, - } FILTER .name = 'test::attr2' + } } FILTER .name = 'test::TestAttrType1'; @@ -1334,7 +1348,6 @@ async def test_edgeql_ddl_attribute_01(self): [{"attributes": [{"name": "test::attr2", "@value": "aaaa"}]}] ]) - @unittest.expectedFailure async def test_edgeql_ddl_attribute_02(self): await self.con.execute(""" CREATE ABSTRACT ATTRIBUTE test::attr1; @@ -1370,6 +1383,52 @@ async def test_edgeql_ddl_attribute_02(self): [{"attributes": [{"name": "test::attr2", "@value": "aaaa"}]}] ]) + async def test_edgeql_ddl_attribute_03(self): + await self.con.execute(""" + CREATE ABSTRACT ATTRIBUTE test::noninh; + CREATE ABSTRACT INHERITABLE ATTRIBUTE test::inh; + + CREATE TYPE test::TestAttr1 { + SET ATTRIBUTE test::noninh := 'no inherit'; + SET ATTRIBUTE test::inh := 'inherit me'; + }; + + CREATE TYPE test::TestAttr2 EXTENDING test::TestAttr1; + """) + + await self.assert_query_result(''' + WITH MODULE schema + SELECT ObjectType { + attributes: { + name, + inheritable, + @value, + } ORDER BY .name + } + FILTER + .name LIKE 'test::TestAttr%' + ORDER BY + .name; + + ''', [ + [{ + "attributes": [{ + "name": "test::inh", + "inheritable": True, + "@value": "inherit me", + }, { + "name": "test::noninh", + "@value": "no inherit", + }] + }, { + "attributes": [{ + "name": "test::inh", + "inheritable": True, + "@value": "inherit me", + }] + }] + ]) + async def test_edgeql_ddl_anytype_01(self): with self.assertRaisesRegex( client_errors.SchemaError, diff --git a/tests/test_edgeql_syntax.py b/tests/test_edgeql_syntax.py index 192d81971b..a4323e1c64 100644 --- a/tests/test_edgeql_syntax.py +++ b/tests/test_edgeql_syntax.py @@ -2684,6 +2684,17 @@ def test_edgeql_syntax_ddl_attribute_02(self): CREATE ABSTRACT ATTRIBUTE std::paramtypes EXTENDING std::baseattr; """ + def test_edgeql_syntax_ddl_attribute_03(self): + """ + CREATE ABSTRACT INHERITABLE ATTRIBUTE std::paramtypes; + """ + + def test_edgeql_syntax_ddl_attribute_04(self): + """ + CREATE ABSTRACT INHERITABLE ATTRIBUTE std::paramtypes + EXTENDING std::foo; + """ + def test_edgeql_syntax_ddl_constraint_01(self): """ CREATE ABSTRACT CONSTRAINT std::enum(VARIADIC p: anytype) diff --git a/tests/test_schema.py b/tests/test_schema.py index b8d7f424ee..ddf176f43e 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -216,3 +216,27 @@ def test_schema_refs_01(self): foo_target, # and also a target of its @target property }) ) + + def test_schema_attribute_inheritance(self): + schema = self.load_schema(""" + abstract attribute noninh + abstract inheritable attribute inh + + type Object1: + attribute noninh := 'bar' + attribute inh := 'inherit me' + + type Object2 extending Object1 + """) + + Object1 = schema.get('test::Object1') + Object2 = schema.get('test::Object2') + + self.assertEqual(Object1.get_attribute(schema, 'test::noninh'), 'bar') + # Attributes are non-inheritable by default + self.assertIsNone(Object2.get_attribute(schema, 'test::noninh')) + + self.assertEqual( + Object1.get_attribute(schema, 'test::inh'), 'inherit me') + self.assertEqual( + Object2.get_attribute(schema, 'test::inh'), 'inherit me') diff --git a/tests/test_schema_syntax.py b/tests/test_schema_syntax.py index 17f6c20c76..4ee6cf2719 100644 --- a/tests/test_schema_syntax.py +++ b/tests/test_schema_syntax.py @@ -1202,9 +1202,19 @@ def test_eschema_syntax_attribute_11(self): abstract attribute as extending foo """ + def test_eschema_syntax_attribute_12(self): + """ + abstract inheritable attribute foo + """ + + def test_eschema_syntax_attribute_13(self): + """ + abstract inheritable attribute foo extending bar + """ + @tb.must_fail(error.SchemaSyntaxError, r"Unexpected 'extending'", line=2, col=41) - def test_eschema_syntax_attribute_12(self): + def test_eschema_syntax_attribute_14(self): """ abstract attribute as extending extending foo """