diff --git a/edb/buildmeta.py b/edb/buildmeta.py index 368e91e8d00..62d2d3f8cb1 100644 --- a/edb/buildmeta.py +++ b/edb/buildmeta.py @@ -55,7 +55,7 @@ # Increment this whenever the database layout or stdlib changes. -EDGEDB_CATALOG_VERSION = 2024_03_27_00_01 +EDGEDB_CATALOG_VERSION = 2024_03_28_00_00 EDGEDB_MAJOR_VERSION = 6 diff --git a/edb/edgeql/ast.py b/edb/edgeql/ast.py index 62f46c6490a..c3679d1a5fe 100644 --- a/edb/edgeql/ast.py +++ b/edb/edgeql/ast.py @@ -1281,6 +1281,7 @@ class ConcreteIndexCommand(IndexCommand): kwargs: typing.Dict[str, Expr] = ast.field(factory=dict) expr: Expr except_expr: typing.Optional[Expr] = None + deferred: typing.Optional[bool] = None class CreateConcreteIndex(ConcreteIndexCommand, CreateObject): diff --git a/edb/edgeql/codegen.py b/edb/edgeql/codegen.py index f0ae873086f..62073e6ad8e 100644 --- a/edb/edgeql/codegen.py +++ b/edb/edgeql/codegen.py @@ -1414,6 +1414,13 @@ def _process_special_set(self, node: qlast.SetField) -> List[str]: keywords.extend(('SET', 'OWNED')) else: keywords.extend(('DROP', 'OWNED')) + elif fname == 'deferred': + if node.value is None: + keywords.extend(('RESET', 'DEFERRED')) + elif self._eval_bool_expr(node.value): + keywords.extend(('SET', 'DEFERRED')) + else: + keywords.extend(('DROP', 'DEFERRED')) else: raise EdgeQLSourceGeneratorError( 'unknown special field: {!r}'.format(fname)) @@ -1993,22 +2000,6 @@ def after_name() -> None: self._ddl_visit_bases(node) - if node.commands or node.code: - self.write(' {') - self._block_ws(1) - commands = self._ddl_clean_up_commands(node.commands) - self.visit_list(commands, terminator=';') - self.new_lines = 1 - - if node.code: - self._write_keywords('USING', node.code.language) - self.write(edgeql_quote.dollar_quote_literal( - node.code.code)) - self.write(';') - - self._block_ws(-1) - self.write('}') - self._visit_CreateObject(node, 'ABSTRACT INDEX', after_name=after_name) def visit_AlterIndex(self, node: qlast.AlterIndex) -> None: @@ -2024,9 +2015,13 @@ def visit_IndexCode(self, node: qlast.IndexCode) -> None: def visit_CreateConcreteIndex( self, node: qlast.CreateConcreteIndex ) -> None: + keywords = ['DEFERRED', 'INDEX'] if node.deferred else ['INDEX'] self._visit_CreateObject( - node, 'INDEX', named=node.name.name != 'idx', - after_name=lambda: self._after_index(node)) + node, + *keywords, + named=node.name.name != 'idx', + after_name=lambda: self._after_index(node), + ) def visit_AlterConcreteIndex(self, node: qlast.AlterConcreteIndex) -> None: self._visit_AlterObject( diff --git a/edb/edgeql/parser/grammar/commondl.py b/edb/edgeql/parser/grammar/commondl.py index 753ddcf45f8..14a63c2b4e3 100644 --- a/edb/edgeql/parser/grammar/commondl.py +++ b/edb/edgeql/parser/grammar/commondl.py @@ -184,6 +184,14 @@ def reduce_OnExpr(self, expr): pass +class OptDeferred(Nonterm): + def reduce_empty(self): + self.val = None + + def reduce_DEFERRED(self, _): + self.val = True + + class OptExceptExpr(Nonterm): def reduce_empty(self): self.val = None diff --git a/edb/edgeql/parser/grammar/ddl.py b/edb/edgeql/parser/grammar/ddl.py index 19f9dece395..894aeadaefe 100644 --- a/edb/edgeql/parser/grammar/ddl.py +++ b/edb/edgeql/parser/grammar/ddl.py @@ -1543,49 +1543,70 @@ def reduce_DropIndex(self, *kids): ) -commands_block( - 'AlterConcreteIndex', - SetFieldStmt, - ResetFieldStmt, - AlterOwnedStmt, - CreateAnnotationValueStmt, - AlterAnnotationValueStmt, - DropAnnotationValueStmt, - opt=False) - - # # CREATE CONCRETE INDEX # class CreateConcreteIndexStmt(Nonterm, commondl.ProcessIndexMixin): - def reduce_CREATE_INDEX_OnExpr_OptExceptExpr_OptCreateCommandsBlock( - self, *kids - ): + def reduce_CreateConcreteDefaultIndex(self, *kids): + r"""%reduce CREATE OptDeferred INDEX OnExpr OptExceptExpr + OptCreateCommandsBlock + """ self.val = qlast.CreateConcreteIndex( name=qlast.ObjectRef(module='__', name='idx'), - expr=kids[2].val, - except_expr=kids[3].val, - commands=kids[4].val, + expr=kids[3].val, + except_expr=kids[4].val, + deferred=kids[1].val, + commands=kids[5].val, ) def reduce_CreateConcreteIndex(self, *kids): - r"""%reduce CREATE INDEX NodeName \ - OptIndexExtArgList OnExpr OptExceptExpr \ - OptCreateCommandsBlock \ + r"""%reduce CREATE OptDeferred INDEX NodeName + OptIndexExtArgList OnExpr OptExceptExpr + OptCreateCommandsBlock """ - kwargs = self._process_arguments(kids[3].val) + kwargs = self._process_arguments(kids[4].val) self.val = qlast.CreateConcreteIndex( - name=kids[2].val, + name=kids[3].val, kwargs=kwargs, - expr=kids[4].val, - except_expr=kids[5].val, - commands=kids[6].val, + expr=kids[5].val, + except_expr=kids[6].val, + deferred=kids[1].val, + commands=kids[7].val, ) # # ALTER CONCRETE INDEX # + +class AlterDeferredStmt(Nonterm): + def reduce_DROP_DEFERRED(self, *kids): + self.val = qlast.SetField( + name='deferred', + value=qlast.BooleanConstant(value='false'), + special_syntax=True, + ) + + def reduce_SET_DEFERRED(self, *kids): + self.val = qlast.SetField( + name='deferred', + value=qlast.BooleanConstant(value='true'), + special_syntax=True, + ) + + +commands_block( + 'AlterConcreteIndex', + SetFieldStmt, + ResetFieldStmt, + AlterOwnedStmt, + AlterDeferredStmt, + CreateAnnotationValueStmt, + AlterAnnotationValueStmt, + DropAnnotationValueStmt, + opt=False) + + class AlterConcreteIndexStmt(Nonterm, commondl.ProcessIndexMixin): def reduce_AlterConcreteIndex(self, *kids): r"""%reduce ALTER INDEX OnExpr OptExceptExpr \ diff --git a/edb/edgeql/parser/grammar/sdl.py b/edb/edgeql/parser/grammar/sdl.py index 186dbdc9105..d7ad524755f 100644 --- a/edb/edgeql/parser/grammar/sdl.py +++ b/edb/edgeql/parser/grammar/sdl.py @@ -659,8 +659,10 @@ def reduce_CreateIndex_CreateFunctionArgs(self, *kids): class ConcreteIndexDeclarationBlock(Nonterm, commondl.ProcessIndexMixin): - def reduce_INDEX_OnExpr_OptExceptExpr_CreateConcreteIndexSDLCommandsBlock( - self, *kids): + def reduce_CreateConcreteAnonymousIndex(self, *kids): + r"""%reduce INDEX OnExpr OptExceptExpr + CreateConcreteIndexSDLCommandsBlock + """ _, on_expr, except_expr, commands = kids self.val = qlast.CreateConcreteIndex( name=qlast.ObjectRef(module='__', name='idx'), @@ -669,6 +671,19 @@ def reduce_INDEX_OnExpr_OptExceptExpr_CreateConcreteIndexSDLCommandsBlock( commands=commands.val, ) + def reduce_CreateConcreteAnonymousDeferredIndex(self, *kids): + r"""%reduce DEFERRED INDEX OnExpr OptExceptExpr + CreateConcreteIndexSDLCommandsBlock + """ + _, _, on_expr, except_expr, commands = kids + self.val = qlast.CreateConcreteIndex( + name=qlast.ObjectRef(module='__', name='idx'), + expr=on_expr.val, + except_expr=except_expr.val, + deferred=True, + commands=commands.val, + ) + def reduce_CreateConcreteIndex(self, *kids): r"""%reduce INDEX NodeName \ OnExpr OptExceptExpr \ @@ -682,6 +697,20 @@ def reduce_CreateConcreteIndex(self, *kids): commands=commands.val, ) + def reduce_CreateConcreteDeferredIndex(self, *kids): + r"""%reduce DEFERRED INDEX NodeName \ + OnExpr OptExceptExpr \ + CreateConcreteIndexSDLCommandsBlock \ + """ + _, _, name, on_expr, except_expr, commands = kids + self.val = qlast.CreateConcreteIndex( + name=name.val, + expr=on_expr.val, + except_expr=except_expr.val, + deferred=True, + commands=commands.val, + ) + def reduce_CreateConcreteIndexWithArgs(self, *kids): r"""%reduce INDEX NodeName IndexExtArgList \ OnExpr OptExceptExpr \ @@ -697,6 +726,22 @@ def reduce_CreateConcreteIndexWithArgs(self, *kids): commands=commands.val, ) + def reduce_CreateConcreteDeferredIndexWithArgs(self, *kids): + r"""%reduce DEFERRED INDEX NodeName IndexExtArgList \ + OnExpr OptExceptExpr \ + CreateConcreteIndexSDLCommandsBlock \ + """ + _, _, name, arg_list, on_expr, except_expr, commands = kids + kwargs = self._process_arguments(arg_list.val) + self.val = qlast.CreateConcreteIndex( + name=name.val, + kwargs=kwargs, + expr=on_expr.val, + except_expr=except_expr.val, + deferred=True, + commands=commands.val, + ) + class ConcreteIndexDeclarationShort(Nonterm, commondl.ProcessIndexMixin): def reduce_INDEX_OnExpr_OptExceptExpr(self, *kids): @@ -707,9 +752,17 @@ def reduce_INDEX_OnExpr_OptExceptExpr(self, *kids): except_expr=except_expr.val, ) + def reduce_DEFERRED_INDEX_OnExpr_OptExceptExpr(self, *kids): + _, _, on_expr, except_expr = kids + self.val = qlast.CreateConcreteIndex( + name=qlast.ObjectRef(module='__', name='idx'), + expr=on_expr.val, + except_expr=except_expr.val, + deferred=True, + ) + def reduce_CreateConcreteIndex(self, *kids): - r"""%reduce INDEX NodeName \ - OnExpr OptExceptExpr \ + r"""%reduce INDEX NodeName OnExpr OptExceptExpr """ _, name, on_expr, except_expr = kids self.val = qlast.CreateConcreteIndex( @@ -718,6 +771,17 @@ def reduce_CreateConcreteIndex(self, *kids): except_expr=except_expr.val, ) + def reduce_CreateConcreteDeferredIndex(self, *kids): + r"""%reduce DEFERRED INDEX NodeName OnExpr OptExceptExpr + """ + _, _, name, on_expr, except_expr = kids + self.val = qlast.CreateConcreteIndex( + name=name.val, + expr=on_expr.val, + except_expr=except_expr.val, + deferred=True, + ) + def reduce_CreateConcreteIndexWithArgs(self, *kids): r"""%reduce INDEX NodeName IndexExtArgList \ OnExpr OptExceptExpr \ @@ -731,6 +795,20 @@ def reduce_CreateConcreteIndexWithArgs(self, *kids): except_expr=except_expr.val, ) + def reduce_CreateConcreteDeferredIndexWithArgs(self, *kids): + r"""%reduce DEFERRED INDEX NodeName IndexExtArgList + OnExpr OptExceptExpr + """ + _, _, name, arg_list, on_expr, except_expr = kids + kwargs = self._process_arguments(arg_list.val) + self.val = qlast.CreateConcreteIndex( + name=name.val, + kwargs=kwargs, + expr=on_expr.val, + except_expr=except_expr.val, + deferred=True, + ) + # # Mutation rewrites diff --git a/edb/edgeql/qltypes.py b/edb/edgeql/qltypes.py index ca89ba106f7..5a26e273489 100644 --- a/edb/edgeql/qltypes.py +++ b/edb/edgeql/qltypes.py @@ -190,6 +190,18 @@ def is_duplicate(self) -> bool: return self is Multiplicity.DUPLICATE +class IndexDeferrability(s_enum.OrderedEnumMixin, s_enum.StrEnum): + Prohibited = 'Prohibited' + Permitted = 'Permitted' + Required = 'Required' + + def is_deferrable(self) -> bool: + return ( + self is IndexDeferrability.Required + or self is IndexDeferrability.Permitted + ) + + class AccessPolicyAction(s_enum.StrEnum): Allow = 'Allow' Deny = 'Deny' diff --git a/edb/lib/schema.edgeql b/edb/lib/schema.edgeql index b2bfb03b903..cc745e21d7f 100644 --- a/edb/lib/schema.edgeql +++ b/edb/lib/schema.edgeql @@ -64,6 +64,9 @@ CREATE SCALAR TYPE schema::RewriteKind CREATE SCALAR TYPE schema::MigrationGeneratedBy EXTENDING enum; +CREATE SCALAR TYPE schema::IndexDeferrability + EXTENDING enum; + # Base type for all schema entities. CREATE ABSTRACT TYPE schema::Object EXTENDING std::BaseObject { CREATE REQUIRED PROPERTY name -> std::str; @@ -267,6 +270,8 @@ CREATE TYPE schema::Index { CREATE PROPERTY expr -> std::str; CREATE PROPERTY except_expr -> std::str; + CREATE PROPERTY deferrability -> schema::IndexDeferrability; + CREATE PROPERTY deferred -> std::bool; CREATE MULTI LINK params EXTENDING schema::ordered -> schema::Parameter { ON TARGET DELETE ALLOW; }; diff --git a/edb/schema/delta.py b/edb/schema/delta.py index 7367e7e7e93..30dd9b0d5fa 100644 --- a/edb/schema/delta.py +++ b/edb/schema/delta.py @@ -2413,6 +2413,7 @@ def _apply_fields_ast( not fop.new_inherited or context.descriptive_mode or self.ast_ignore_ownership() + or self.ast_ignore_field_ownership(fop.property) ) and ( fop.old_value != new_value @@ -2906,6 +2907,10 @@ def ast_ignore_ownership(self) -> bool: """Whether to force generating an AST even though it isn't owned""" return False + def ast_ignore_field_ownership(self, field: str) -> bool: + """Whether to force generating an AST even though it isn't owned""" + return False + class ObjectCommandContext(CommandContextToken[ObjectCommand[so.Object_T]]): diff --git a/edb/schema/indexes.py b/edb/schema/indexes.py index 10749e8bd43..75266da6e21 100644 --- a/edb/schema/indexes.py +++ b/edb/schema/indexes.py @@ -63,7 +63,10 @@ def is_index_valid_for_type( - index: Index, expr_type: s_types.Type, schema: s_schema.Schema + index: Index, + expr_type: s_types.Type, + schema: s_schema.Schema, + context: sd.CommandContext, ) -> bool: # HACK: currently this helper just hardcodes the permitted index & type # combinations, but this should be inferred based on index definition. @@ -148,6 +151,13 @@ def is_index_valid_for_type( schema.get('std::str', type=s_scalars.ScalarType), ) + if context.testmode and index_name == 'default::test': + # For functional tests of abstract indexes. + return expr_type.issubclass( + schema, + schema.get('std::str', type=s_scalars.ScalarType), + ) + return False @@ -165,6 +175,95 @@ def is_subclass_or_tuple( return ty.issubclass(schema, parent) +def _merge_deferrability( + a: qltypes.IndexDeferrability, + b: qltypes.IndexDeferrability, +) -> qltypes.IndexDeferrability: + if a is b: + return a + else: + if a is qltypes.IndexDeferrability.Prohibited: + raise ValueError(f"{a} and {b} are incompatible") + elif a is qltypes.IndexDeferrability.Permitted: + return b + else: + return a + + +def merge_deferrability( + idx: Index, + bases: List[Index], + field_name: str, + *, + ignore_local: bool = False, + schema: s_schema.Schema, +) -> Optional[qltypes.IndexDeferrability]: + """Merge function for abstract index deferrability.""" + + return utils.merge_reduce( + idx, + bases, + field_name=field_name, + ignore_local=ignore_local, + schema=schema, + f=_merge_deferrability, + type=qltypes.IndexDeferrability, + ) + + +def merge_deferred( + idx: Index, + bases: List[Index], + field_name: str, + *, + ignore_local: bool = False, + schema: s_schema.Schema, +) -> Optional[bool]: + """Merge function for the DEFERRED qualifier on indexes.""" + + if idx.is_non_concrete(schema): + return None + + if bases: + deferrability = next(iter(bases)).get_deferrability(schema) + else: + deferrability = qltypes.IndexDeferrability.Prohibited + + local_deferred = idx.get_explicit_local_field_value( + schema, field_name, None) + + idx_repr = idx.get_verbosename(schema, with_parent=True) + + if ignore_local: + return deferrability is qltypes.IndexDeferrability.Required + elif local_deferred is None: + # No explicit local declaration, derive from abstract index + # deferrability. + if deferrability is qltypes.IndexDeferrability.Required: + raise errors.SchemaDefinitionError( + f"{idx_repr} must be declared as deferred" + ) + else: + return False + else: + if ( + local_deferred + and deferrability is qltypes.IndexDeferrability.Prohibited + ): + raise errors.SchemaDefinitionError( + f"{idx_repr} cannot be declared as deferred" + ) + elif ( + not local_deferred + and deferrability is qltypes.IndexDeferrability.Required + ): + raise errors.SchemaDefinitionError( + f"{idx_repr} must be declared as deferred" + ) + + return local_deferred # type: ignore + + class Index( referencing.ReferencedInheritingObject, so.InheritingObject, # Help reflection figure out the right db MRO @@ -206,6 +305,7 @@ class Index( default=None, compcoef=None, inheritable=False, + allow_ddl_set=True, ) # These can appear in abstract indexes extending an existing one in order @@ -236,6 +336,26 @@ class Index( ddl_identity=True, ) + deferrability = so.SchemaField( + qltypes.IndexDeferrability, + default=qltypes.IndexDeferrability.Prohibited, + coerce=True, + compcoef=0.909, + merge_fn=merge_deferrability, + allow_ddl_set=True, + ) + + deferred = so.SchemaField( + bool, + default=False, + compcoef=0.909, + special_ddl_syntax=True, + describe_visibility=( + so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED + ), + merge_fn=merge_deferred, + ) + def __repr__(self) -> str: cls = self.__class__ return '<{}.{} {!r} at 0x{:x}>'.format( @@ -593,6 +713,11 @@ def get_ast_attr_for_field( ) -> Optional[str]: if field in ('kwargs', 'expr', 'except_expr'): return field + elif ( + field == 'deferred' + and astnode is qlast.CreateConcreteIndex + ): + return field else: return super().get_ast_attr_for_field(field, astnode) @@ -699,6 +824,28 @@ def get_dummy_expr_field_value( else: raise NotImplementedError(f'unhandled field {field.name!r}') + def canonicalize_attributes( + self, + schema: s_schema.Schema, + context: sd.CommandContext, + ) -> s_schema.Schema: + schema = super().canonicalize_attributes(schema, context) + + referrer_ctx = self.get_referrer_context(context) + if referrer_ctx is not None: + # Concrete index + deferrability = self.get_attribute_value("deferrability") + if deferrability is not None: + raise errors.SchemaDefinitionError( + "deferrability can only be specified on abstract indexes", + span=self.get_attribute_span("deferrability"), + ) + return schema + + def ast_ignore_field_ownership(self, field: str) -> bool: + """Whether to force generating an AST even though field isn't owned""" + return field == "deferred" + class CreateIndex( IndexCommand, @@ -791,6 +938,13 @@ def _cmd_tree_from_ast( ), ) + if astnode.deferred is not None: + cmd.set_attribute_value( + 'deferred', + astnode.deferred, + span=astnode.span, + ) + return cmd @classmethod @@ -823,6 +977,7 @@ def as_inherited_ref_ast( kwargs=qlkwargs, expr=expr_ql, except_expr=except_expr_ql, + deferred=parent.get_deferred(schema), ) @classmethod @@ -1031,7 +1186,9 @@ def validate_object( ) expr_type = comp_expr.irast.stype - if not is_index_valid_for_type(root, expr_type, comp_expr.schema): + if not is_index_valid_for_type( + root, expr_type, comp_expr.schema, context, + ): hint = None if str(name) == 'fts::index': hint = ( diff --git a/edb/schema/pointers.py b/edb/schema/pointers.py index 77c79add4bc..af8bb5990e6 100644 --- a/edb/schema/pointers.py +++ b/edb/schema/pointers.py @@ -34,6 +34,7 @@ import collections.abc import enum import json +import operator from edb import errors @@ -211,7 +212,7 @@ def merge_required( field_name=field_name, ignore_local=ignore_local, schema=schema, - f=max, + f=operator.or_, type=bool, ) elif local_required: diff --git a/edb/schema/utils.py b/edb/schema/utils.py index f7cb78af2bf..2b7e394ff8c 100644 --- a/edb/schema/utils.py +++ b/edb/schema/utils.py @@ -801,21 +801,36 @@ def merge_reduce( *, ignore_local: bool, schema: s_schema.Schema, - f: Callable[[List[T]], T], + f: Callable[[T, T], T], type: Type[T], ) -> Optional[T]: - values = [] + values: list[tuple[T, str]] = [] if not ignore_local: ours = target.get_explicit_local_field_value(schema, field_name, None) if ours is not None: - values.append(ours) + vn = target.get_verbosename(schema, with_parent=True) + values.append((ours, vn)) for source in sources: theirs = source.get_explicit_field_value(schema, field_name, None) if theirs is not None: - values.append(theirs) + vn = source.get_verbosename(schema, with_parent=True) + values.append((theirs, vn)) if values: - return f(values) + val = values[0][0] + desc = values[0][1] + cdn = target.get_schema_class_displayname() + for other_val, other_desc in values[1:]: + try: + val = f(val, other_val) + except Exception: + raise errors.SchemaDefinitionError( + f'invalid {cdn} definition: {field_name} is defined ' + f'as {val} in {desc}, but is defined as {other_val} ' + f'in {other_desc}, which is incompatible' + ) + + return val else: return None diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py index ace3579fcff..a86f3183e76 100644 --- a/tests/test_edgeql_ddl.py +++ b/tests/test_edgeql_ddl.py @@ -12946,6 +12946,130 @@ async def test_edgeql_ddl_abstract_index_01(self): drop abstract index test; ''') + async def test_edgeql_ddl_deferred_index_01(self): + with self.assertRaisesRegex( + edgedb.SchemaDefinitionError, + r"cannot be declared as deferred", + _line=8, _col=21 + ): + await self.con.execute(''' + create abstract index test() { + set code := ' ((__col__) NULLS FIRST)'; + }; + + create type Foo { + create property bar -> str; + create deferred index test on (.bar); + }; + ''') + + async def test_edgeql_ddl_deferred_index_02(self): + with self.assertRaisesRegex( + edgedb.SchemaDefinitionError, + r"must be declared as deferred", + ): + await self.con.execute(''' + create abstract index test() { + set code := ' ((__col__) NULLS FIRST)'; + set deferrability := 'Required'; + }; + + create type Foo { + create property bar -> str; + create index test on (.bar); + }; + ''') + + async def test_edgeql_ddl_deferred_index_03(self): + with self.assertRaisesRegex( + edgedb.SchemaDefinitionError, + r"cannot be declared as deferred", + _line=12, + _col=59, + ): + await self.con.execute(''' + create abstract index test() { + set code := ' ((__col__) NULLS FIRST)'; + set deferrability := 'Prohibited'; + }; + + create type Foo { + create property bar -> str; + create index test on (.bar); + }; + + alter type Foo alter index test on (.bar) set deferred; + ''') + + async def test_edgeql_ddl_deferred_index_04(self): + with self.assertRaisesRegex( + edgedb.SchemaDefinitionError, + r"must be declared as deferred", + _line=12, + _col=59, + ): + await self.con.execute(''' + create abstract index test() { + set code := ' ((__col__) NULLS FIRST)'; + set deferrability := 'Required'; + }; + + create type Foo { + create property bar -> str; + create deferred index test on (.bar); + }; + + alter type Foo alter index test on (.bar) drop deferred; + ''') + + async def test_edgeql_ddl_deferred_index_05(self): + with self.assertRaisesRegex( + edgedb.SchemaDefinitionError, + r"deferrability can only be specified on abstract indexes", + _line=5, + _col=25, + ): + await self.con.execute(''' + create type Foo { + create property bar -> str; + create index on (.bar) { + set deferrability := 'Permitted'; + }; + }; + ''') + + async def test_edgeql_ddl_deferred_index_06(self): + await self.con.execute(''' + create abstract index test() { + set code := ' ((__col__) NULLS FIRST)'; + set deferrability := 'Permitted'; + }; + + create type Foo { + create property bar -> str; + create deferred index test on (.bar); + }; + ''') + + await self.assert_query_result( + ''' + SELECT schema::ObjectType { + name, + indexes: { + deferred, + deferrability, + } + } FILTER .name = 'default::Foo' + ''', + [{ + 'name': 'default::Foo', + 'indexes': [{ + 'deferred': True, + 'deferrability': 'Permitted', + }] + }] + ) + async def test_edgeql_ddl_errors_01(self): await self.con.execute(''' CREATE TYPE Err1 { diff --git a/tests/test_edgeql_syntax.py b/tests/test_edgeql_syntax.py index 4265f2d66ce..210aa2c063d 100644 --- a/tests/test_edgeql_syntax.py +++ b/tests/test_edgeql_syntax.py @@ -5944,6 +5944,29 @@ def test_edgeql_syntax_ddl_index_11(self): }; """ + def test_edgeql_syntax_ddl_index_12(self): + """ + CREATE TYPE Foo { + CREATE DEFERRED INDEX myindex0 ON (.bar); + + CREATE DEFERRED INDEX + myindex1(a := 13, b := 'ab', conf := [4, 3, 2]) ON (.baz); + + CREATE DEFERRED INDEX myindex2(num := 13, val := 'ab') + ON (.foo); + + CREATE DEFERRED INDEX ON (.bar); + }; + """ + + def test_edgeql_syntax_ddl_index_13(self): + """ + ALTER TYPE Foo { + ALTER INDEX myindex0 ON (.bar) SET DEFERRED; + ALTER INDEX ON (.bar) DROP DEFERRED; + }; + """ + def test_edgeql_syntax_ddl_global_01(self): """ CREATE GLOBAL Foo := (SELECT User); diff --git a/tests/test_schema.py b/tests/test_schema.py index 8d514a3f27f..d206616ca0a 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -7812,6 +7812,29 @@ def test_schema_migrations_equivalence_rename_abs_ptr_02(self): }; """]) + def test_schema_migrations_deferred_index_01(self): + self._assert_migration_equivalence([r""" + abstract index test() { + code := ' ((__col__) NULLS FIRST)'; + deferrability := 'Permitted'; + }; + + type Foo { + property bar -> str; + deferred index test on (.bar); + }; + """, r""" + abstract index test() { + code := ' ((__col__) NULLS FIRST)'; + deferrability := 'Permitted'; + }; + + type Foo { + property bar -> str; + index test on (.bar); + }; + """]) + def test_schema_migrations_drop_parent_01(self): self._assert_migration_equivalence([r""" type Parent { diff --git a/tests/test_schema_syntax.py b/tests/test_schema_syntax.py index 113d47dac0f..2cae87f4643 100644 --- a/tests/test_schema_syntax.py +++ b/tests/test_schema_syntax.py @@ -443,6 +443,9 @@ def test_eschema_syntax_type_23(self): }; constraint exclusive on (.asdf) except (.baz); index on (.asdf) except (.baz); + deferred index on (.asdf) except (.baz); + deferred index bar on (.foo); + deferred index bar on (.foo) except (.bar); }; }; """