def _cmd_tree_from_ast(cls, schema, astnode, context): cmd = super()._cmd_tree_from_ast(schema, astnode, context) if isinstance(astnode, qlast.CreateConcreteConstraint): if astnode.delegated: cmd.set_attribute_value('delegated', astnode.delegated) args = cls._constraint_args_from_ast(schema, astnode, context) if args: cmd.add(sd.AlterObjectProperty(property='args', new_value=args)) elif isinstance(astnode, qlast.CreateConstraint): params = cls._get_param_desc_from_ast(schema, context.modaliases, astnode) for param in params: if param.get_kind(schema) is ft.ParameterKind.NAMED_ONLY: raise errors.InvalidConstraintDefinitionError( 'named only parameters are not allowed ' 'in this context', context=astnode.context) if param.get_default(schema) is not None: raise errors.InvalidConstraintDefinitionError( 'constraints do not support parameters ' 'with defaults', context=astnode.context) if cmd.get_attribute_value('return_type') is None: cmd.add( sd.AlterObjectProperty(property='return_type', new_value=utils.reduce_to_typeref( schema, schema.get('std::bool')))) if cmd.get_attribute_value('return_typemod') is None: cmd.add( sd.AlterObjectProperty( property='return_typemod', new_value=ft.TypeModifier.SINGLETON, )) # 'subjectexpr' can be present in either astnode type if astnode.subjectexpr: orig_text = cls.get_orig_expr_text(schema, astnode, 'subjectexpr') subjectexpr = s_expr.Expression.from_ast( astnode.subjectexpr, schema, context.modaliases, orig_text=orig_text, ) cmd.add( sd.AlterObjectProperty(property='subjectexpr', new_value=subjectexpr)) cls._validate_subcommands(astnode) return cmd
def validate_alter( self, schema: s_schema.Schema, context: sd.CommandContext, ) -> None: super().validate_alter(schema, context) self_delegated = self.get_attribute_value('delegated') if not self_delegated: return concrete_bases = [ b for b in self.scls.get_bases(schema).objects(schema) if not b.generic(schema) and not b.get_delegated(schema) ] if concrete_bases: tgt_repr = self.scls.get_verbosename(schema, with_parent=True) bases_repr = ', '.join( b.get_subject(schema).get_verbosename(schema, with_parent=True) for b in concrete_bases) raise errors.InvalidConstraintDefinitionError( f'cannot redefine {tgt_repr} as delegated:' f' it is defined as non-delegated in {bases_repr}', context=self.source_context, )
def normalize_constraint_expr(cls, schema, module_aliases, expr, *, subject=None, constraint_name, expr_context=None, enforce_boolean=False): if subject is None: subject = cls._dummy_subject(schema) edgeql_tree, ir_result = cls._normalize_constraint_expr( schema, module_aliases, expr, subject) if enforce_boolean: bool_t = schema.get('std::bool') expr_type = ir_result.stype if not expr_type.issubclass(schema, bool_t): raise errors.InvalidConstraintDefinitionError( f'{constraint_name} constraint expression expected ' f'to return a bool value, got ' f'{expr_type.get_name(schema).name!r}', context=expr_context) expr = edgeql.generate_source(edgeql_tree, pretty=False) return expr
def _validate_subcommands(cls, astnode): # check that 'subject' and 'subjectexpr' are not set as attributes for command in astnode.commands: if cls._is_special_name(command.name): raise errors.InvalidConstraintDefinitionError( f'{command.name.name} is not a valid constraint attribute', context=command.context)
def _validate_subcommands(cls, astnode): # check that 'subject' and 'subjectexpr' are not set as annotations for command in astnode.commands: cname = command.name if cname in {'subject', 'subjectexpr'}: raise errors.InvalidConstraintDefinitionError( f'{cname} is not a valid constraint annotation', context=command.context)
def _init_constraints(self, constraints): for constraint, decl in constraints.items(): attrs = {a.name.name: a.value for a in decl.fields} assert 'subject' not in attrs # TODO: Add proper validation assert 'subjectexpr' not in attrs # TODO: Add proper validation expr = attrs.pop('expr', None) if expr is not None: self._schema = constraint.set_field_value( self._schema, 'expr', s_expr.Expression.from_ast(expr, self._schema, self._mod_aliases), ) subjexpr = decl.subject if subjexpr is not None: self._schema = constraint.set_field_value( self._schema, 'subjectexpr', s_expr.Expression.from_ast(subjexpr, self._schema, self._mod_aliases), ) self._schema, params = s_func.FuncParameterList.from_ast( self._schema, decl, self._mod_aliases, func_fqname=constraint.get_name(self._schema)) for param in params.objects(self._schema): p_kind = param.get_kind(self._schema) if p_kind is qltypes.ParameterKind.NAMED_ONLY: raise errors.InvalidConstraintDefinitionError( 'named only parameters are not allowed ' 'in this context', context=decl.context) if param.get_default(self._schema) is not None: raise errors.InvalidConstraintDefinitionError( 'constraints do not support parameters ' 'with defaults', context=decl.context) self._schema = constraint.set_field_value( self._schema, 'params', params)
def _validate_subcommands( cls, astnode: qlast.DDLOperation, ) -> None: # check that 'subject' and 'subjectexpr' are not set as annotations for command in astnode.commands: assert isinstance(command, (qlast.NamedDDL, qlast.BaseSetField)) cname = command.name if cname in {'subject', 'subjectexpr'}: raise errors.InvalidConstraintDefinitionError( f'{cname} is not a valid constraint annotation', context=command.context)
def compile_Parameter(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Set: if ctx.env.options.func_params is not None: if ctx.env.options.schema_object_context is s_constr.Constraint: raise errors.InvalidConstraintDefinitionError( f'dollar-prefixed "$parameters" cannot be used here', context=expr.context) else: raise errors.InvalidFunctionDefinitionError( f'dollar-prefixed "$parameters" cannot be used here', context=expr.context) raise errors.QueryError(f'missing a type cast before the parameter', context=expr.context)
def visit_Parameter(self, node): raise errors.InvalidConstraintDefinitionError( f'dollar-prefixed "$parameters" are not supported in constraints')
def get_concrete_constraint_attrs( cls, schema, subject, *, name, subjectexpr=None, sourcectx=None, args=[], modaliases=None, **kwargs): from edb.edgeql import parser as qlparser from edb.edgeql import utils as qlutils constr_base = schema.get(name, module_aliases=modaliases) module_aliases = {} orig_subjectexpr = subjectexpr orig_subject = subject base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr') if subjectexpr is None: subjectexpr = base_subjectexpr elif (base_subjectexpr is not None and subjectexpr.text != base_subjectexpr.text): raise errors.InvalidConstraintDefinitionError( 'subjectexpr is already defined for ' + f'{str(name)!r}') if subjectexpr is not None: subject_ql = subjectexpr.qlast if subject_ql is None: subject_ql = qlparser.parse(subjectexpr.text, module_aliases) subject = subject_ql expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr') if not expr: raise errors.InvalidConstraintDefinitionError( f'missing constraint expression in {name!r}') expr_ql = qlparser.parse(expr.text, module_aliases) if not args: args = constr_base.get_field_value(schema, 'args') attrs = dict(kwargs) inherited = dict() if orig_subjectexpr is not None: attrs['subjectexpr'] = orig_subjectexpr else: base_subjectexpr = constr_base.get_subjectexpr(schema) if base_subjectexpr is not None: attrs['subjectexpr'] = base_subjectexpr errmessage = attrs.get('errmessage') if not errmessage: errmessage = constr_base.get_errmessage(schema) inherited['errmessage'] = True attrs['errmessage'] = errmessage if subject is not orig_subject: # subject has been redefined qlutils.inline_anchors(expr_ql, anchors={qlast.Subject: subject}) subject = orig_subject args_map = None if args: args_ql = [ qlast.Path(steps=[qlast.Subject()]), ] args_ql.extend( qlparser.parse(arg.text, module_aliases) for arg in args ) args_map = qlutils.index_parameters( args_ql, parameters=constr_base.get_params(schema), schema=schema) qlutils.inline_parameters(expr_ql, args_map) args_map = {name: edgeql.generate_source(val, pretty=False) for name, val in args_map.items()} args_map['__subject__'] = '{__subject__}' attrs['errmessage'] = attrs['errmessage'].format(**args_map) inherited.pop('errmessage', None) attrs['args'] = args if expr == '__subject__': expr_context = sourcectx else: expr_context = None final_expr = s_expr.Expression.compiled( s_expr.Expression.from_ast(expr_ql, schema, module_aliases), schema=schema, modaliases=module_aliases, anchors={qlast.Subject: subject}, ) bool_t = schema.get('std::bool') expr_type = final_expr.irast.stype if not expr_type.issubclass(schema, bool_t): raise errors.InvalidConstraintDefinitionError( f'{name} constraint expression expected ' f'to return a bool value, got ' f'{expr_type.get_name(schema).name!r}', 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 attrs['params'] = constr_base.get_params(schema) return constr_base, attrs, inherited
def _populate_concrete_constraint_attrs( self, schema: s_schema.Schema, context: sd.CommandContext, subject_obj: Optional[so.Object], *, name: sn.QualName, subjectexpr: Optional[s_expr.Expression] = None, sourcectx: Optional[c_parsing.ParserContext] = None, args: Any = None, **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) orig_subjectexpr = subjectexpr orig_subject = subject_obj base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr') if subjectexpr is None: subjectexpr = base_subjectexpr elif (base_subjectexpr is not None and subjectexpr.text != base_subjectexpr.text): raise errors.InvalidConstraintDefinitionError( f'subjectexpr is already defined for {name}') if (isinstance(subject_obj, s_scalars.ScalarType) and constr_base.get_is_aggregate(schema)): raise errors.InvalidConstraintDefinitionError( f'{constr_base.get_verbosename(schema)} may not ' f'be used on scalar types') if subjectexpr is not None: subject_ql = subjectexpr.qlast subject = subject_ql else: subject = subject_obj expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr') if not expr: raise errors.InvalidConstraintDefinitionError( f'missing constraint expression in {name}') # Re-parse instead of using expr.qlast, because we mutate # the AST below. expr_ql = qlparser.parse(expr.text) if not args: args = constr_base.get_field_value(schema, 'args') attrs = dict(kwargs) inherited = dict() if orig_subjectexpr is not None: attrs['subjectexpr'] = orig_subjectexpr else: base_subjectexpr = constr_base.get_subjectexpr(schema) if base_subjectexpr is not None: attrs['subjectexpr'] = base_subjectexpr inherited['subjectexpr'] = True errmessage = attrs.get('errmessage') if not errmessage: errmessage = constr_base.get_errmessage(schema) inherited['errmessage'] = True attrs['errmessage'] = errmessage if subject is not orig_subject: # subject has been redefined assert isinstance(subject, qlast.Base) qlutils.inline_anchors(expr_ql, anchors={qlast.Subject().name: subject}) subject = orig_subject if args: args_ql: List[qlast.Base] = [ qlast.Path(steps=[qlast.Subject()]), ] args_ql.extend(arg.qlast for arg in args) args_map = qlutils.index_parameters( args_ql, parameters=constr_base.get_params(schema), schema=schema, ) qlutils.inline_parameters(expr_ql, args_map) attrs['args'] = args assert subject is not None path_prefix_anchor = (qlast.Subject().name if isinstance( subject, s_types.Type) else None) final_expr = s_expr.Expression.compiled( s_expr.Expression.from_ast(expr_ql, schema, {}), schema=schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: subject}, path_prefix_anchor=path_prefix_anchor, apply_query_rewrites=not context.stdmode, ), ) bool_t = schema.get('std::bool', type=s_scalars.ScalarType) assert isinstance(final_expr.irast, ir_ast.Statement) expr_type = final_expr.irast.stype if not expr_type.issubclass(schema, bool_t): raise errors.InvalidConstraintDefinitionError( f'{name} constraint expression expected ' f'to return a bool value, got ' f'{expr_type.get_verbosename(schema)}', context=sourcectx) if subjectexpr is not None: if (isinstance(subject_obj, s_types.Type) and subject_obj.is_object_type()): singletons = frozenset({subject_obj}) else: singletons = frozenset() final_subjectexpr = s_expr.Expression.compiled( subjectexpr, schema=schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: subject}, path_prefix_anchor=path_prefix_anchor, singletons=singletons, apply_query_rewrites=not context.stdmode, ), ) assert isinstance(final_subjectexpr.irast, ir_ast.Statement) refs = ir_utils.get_longest_paths(final_expr.irast) has_multi = False for ref in refs: while ref.rptr: rptr = ref.rptr if rptr.ptrref.dir_cardinality.is_multi(): has_multi = True if (not isinstance(rptr.ptrref, ir_ast.TupleIndirectionPointerRef) and rptr.ptrref.source_ptr is None and rptr.source.rptr is not None): raise errors.InvalidConstraintDefinitionError( "constraints cannot contain paths with more " "than one hop", context=sourcectx) ref = rptr.source if has_multi and len(refs) > 1: raise errors.InvalidConstraintDefinitionError( "cannot reference multiple links or properties in a " "constraint where at least one link or property is MULTI", context=sourcectx) if has_multi and ir_utils.contains_set_of_op( final_subjectexpr.irast): raise errors.InvalidConstraintDefinitionError( "cannot use aggregate functions or operators " "in a non-aggregating constraint", context=sourcectx) attrs['return_type'] = constr_base.get_return_type(schema) attrs['return_typemod'] = constr_base.get_return_typemod(schema) attrs['finalexpr'] = final_expr attrs['params'] = constr_base.get_params(schema) attrs['abstract'] = False for k, v in attrs.items(): self.set_attribute_value(k, v, inherited=bool(inherited.get(k)))
def _init_constraints(self, constraints): for constraint, decl in constraints.items(): attrs = {a.name.name: a.value for a in decl.fields} assert 'subject' not in attrs # TODO: Add proper validation assert 'subjectexpr' not in attrs # TODO: Add proper validation self._schema, params = s_func.FuncParameterList.from_ast( self._schema, decl, self._mod_aliases, func_fqname=constraint.get_name(self._schema), prepend=[ qlast.FuncParam( name='__subject__', type=qlast.TypeName(maintype=qlast.AnyType(), ), typemod=qltypes.TypeModifier.SINGLETON, kind=qltypes.ParameterKind.POSITIONAL, default=None, ), ], ) for param in params.objects(self._schema): p_kind = param.get_kind(self._schema) if p_kind is qltypes.ParameterKind.NAMED_ONLY: raise errors.InvalidConstraintDefinitionError( 'named only parameters are not allowed ' 'in this context', context=decl.context) if param.get_default(self._schema) is not None: raise errors.InvalidConstraintDefinitionError( 'constraints do not support parameters ' 'with defaults', context=decl.context) anchors, _ = qlcompiler.get_param_anchors_for_callable( params, self._schema) expr = attrs.pop('expr', None) if expr is not None: self._schema = constraint.set_field_value( self._schema, 'expr', s_expr.Expression.compiled( s_expr.Expression.from_ast(expr, self._schema, self._mod_aliases), schema=self._schema, modaliases=self._mod_aliases, anchors=anchors, func_params=params, allow_generic_type_output=True, parent_object_type=type(constraint), ), ) subjexpr = decl.subject if subjexpr is not None: self._schema = constraint.set_field_value( self._schema, 'subjectexpr', s_expr.Expression.compiled( s_expr.Expression.from_ast(subjexpr, self._schema, self._mod_aliases), schema=self._schema, modaliases=self._mod_aliases, anchors=anchors, func_params=params, allow_generic_type_output=True, parent_object_type=type(constraint), ), ) self._schema = constraint.set_field_value(self._schema, 'params', params)
def _populate_concrete_constraint_attrs( self, schema: s_schema.Schema, subject_obj: Optional[so.Object], *, name: str, subjectexpr: Optional[s_expr.Expression] = None, sourcectx: Optional[c_parsing.ParserContext] = None, args: Any = None, **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) orig_subjectexpr = subjectexpr orig_subject = subject_obj base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr') if subjectexpr is None: subjectexpr = base_subjectexpr elif (base_subjectexpr is not None and subjectexpr.text != base_subjectexpr.text): raise errors.InvalidConstraintDefinitionError( f'subjectexpr is already defined for {name!r}') if (isinstance(subject_obj, s_scalars.ScalarType) and constr_base.get_is_aggregate(schema)): raise errors.InvalidConstraintDefinitionError( f'{constr_base.get_verbosename(schema)} may not ' f'be used on scalar types') if subjectexpr is not None: subject_ql = subjectexpr.qlast subject = subject_ql else: subject = subject_obj expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr') if not expr: raise errors.InvalidConstraintDefinitionError( f'missing constraint expression in {name!r}') # Re-parse instead of using expr.qlast, because we mutate # the AST below. expr_ql = qlparser.parse(expr.text) if not args: args = constr_base.get_field_value(schema, 'args') attrs = dict(kwargs) inherited = dict() if orig_subjectexpr is not None: attrs['subjectexpr'] = orig_subjectexpr else: base_subjectexpr = constr_base.get_subjectexpr(schema) if base_subjectexpr is not None: attrs['subjectexpr'] = base_subjectexpr inherited['subjectexpr'] = True errmessage = attrs.get('errmessage') if not errmessage: errmessage = constr_base.get_errmessage(schema) inherited['errmessage'] = True attrs['errmessage'] = errmessage if subject is not orig_subject: # subject has been redefined assert isinstance(subject, qlast.Base) qlutils.inline_anchors(expr_ql, anchors={qlast.Subject().name: subject}) subject = orig_subject if args: args_ql: List[qlast.Base] = [ qlast.Path(steps=[qlast.Subject()]), ] args_ql.extend(arg.qlast for arg in args) args_map = qlutils.index_parameters( args_ql, parameters=constr_base.get_params(schema), schema=schema, ) qlutils.inline_parameters(expr_ql, args_map) attrs['args'] = args if expr == '__subject__': expr_context = sourcectx else: expr_context = None assert subject is not None final_expr = s_expr.Expression.compiled( s_expr.Expression.from_ast(expr_ql, schema, {}), schema=schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: subject}, ), ) bool_t: s_scalars.ScalarType = schema.get('std::bool') assert isinstance(final_expr.irast, ir_ast.Statement) expr_type = final_expr.irast.stype if not expr_type.issubclass(schema, bool_t): raise errors.InvalidConstraintDefinitionError( f'{name} constraint expression expected ' f'to return a bool value, got ' f'{expr_type.get_verbosename(schema)}', 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 attrs['params'] = constr_base.get_params(schema) attrs['is_abstract'] = False for k, v in attrs.items(): self.set_attribute_value(k, v, inherited=bool(inherited.get(k)))
def _populate_concrete_constraint_attrs( self, schema: s_schema.Schema, context: sd.CommandContext, subject_obj: Optional[so.Object], *, name: sn.QualName, subjectexpr: Optional[s_expr.Expression] = None, subjectexpr_inherited: bool = False, sourcectx: Optional[c_parsing.ParserContext] = None, args: Any = None, **kwargs: Any ) -> None: from edb.ir import ast as ir_ast from edb.ir import utils as ir_utils from . import pointers as s_pointers from . import links as s_links from . import scalars as s_scalars bases = self.get_resolved_attribute_value( 'bases', schema=schema, context=context, ) if not bases: bases = self.scls.get_bases(schema) constr_base = bases.objects(schema)[0] # If we have a concrete base, then we should inherit all of # these attrs through the normal inherit_fields() mechanisms, # and populating them ourselves will just mess up # inherited_fields. if not constr_base.generic(schema): return orig_subjectexpr = subjectexpr orig_subject = subject_obj base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr') if subjectexpr is None: subjectexpr = base_subjectexpr elif (base_subjectexpr is not None and subjectexpr.text != base_subjectexpr.text): raise errors.InvalidConstraintDefinitionError( f'subjectexpr is already defined for {name}' ) if (isinstance(subject_obj, s_scalars.ScalarType) and constr_base.get_is_aggregate(schema)): raise errors.InvalidConstraintDefinitionError( f'{constr_base.get_verbosename(schema)} may not ' f'be used on scalar types' ) if subjectexpr is not None: subject_ql = subjectexpr.qlast subject = subject_ql else: subject = subject_obj expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr') if not expr: raise errors.InvalidConstraintDefinitionError( f'missing constraint expression in {name}') # Re-parse instead of using expr.qlast, because we mutate # the AST below. expr_ql = qlparser.parse(expr.text) if not args: args = constr_base.get_field_value(schema, 'args') attrs = dict(kwargs) inherited = dict() if orig_subjectexpr is not None: attrs['subjectexpr'] = orig_subjectexpr inherited['subjectexpr'] = subjectexpr_inherited else: base_subjectexpr = constr_base.get_subjectexpr(schema) if base_subjectexpr is not None: attrs['subjectexpr'] = base_subjectexpr inherited['subjectexpr'] = True errmessage = attrs.get('errmessage') if not errmessage: errmessage = constr_base.get_errmessage(schema) inherited['errmessage'] = True attrs['errmessage'] = errmessage if subject is not orig_subject: # subject has been redefined assert isinstance(subject, qlast.Base) qlutils.inline_anchors( expr_ql, anchors={qlast.Subject().name: subject}) subject = orig_subject if args: args_ql: List[qlast.Base] = [ qlast.Path(steps=[qlast.Subject()]), ] args_ql.extend(arg.qlast for arg in args) args_map = qlutils.index_parameters( args_ql, parameters=constr_base.get_params(schema), schema=schema, ) qlutils.inline_parameters(expr_ql, args_map) attrs['args'] = args assert subject is not None final_expr = s_expr.Expression.compiled( s_expr.Expression.from_ast(expr_ql, schema, {}), schema=schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: subject}, path_prefix_anchor=qlast.Subject().name, apply_query_rewrites=not context.stdmode, ), ) bool_t = schema.get('std::bool', type=s_scalars.ScalarType) assert isinstance(final_expr.irast, ir_ast.Statement) expr_type = final_expr.irast.stype if not expr_type.issubclass(schema, bool_t): raise errors.InvalidConstraintDefinitionError( f'{name} constraint expression expected ' f'to return a bool value, got ' f'{expr_type.get_verbosename(schema)}', context=sourcectx ) if subjectexpr is not None: assert isinstance(subject_obj, (s_types.Type, s_pointers.Pointer)) singletons = frozenset({subject_obj}) final_subjectexpr = s_expr.Expression.compiled( subjectexpr, schema=schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: subject}, path_prefix_anchor=qlast.Subject().name, singletons=singletons, apply_query_rewrites=not context.stdmode, ), ) assert isinstance(final_subjectexpr.irast, ir_ast.Statement) refs = ir_utils.get_longest_paths(final_expr.irast) has_multi = False for ref in refs: while ref.rptr: rptr = ref.rptr if rptr.dir_cardinality.is_multi(): has_multi = True # We don't need to look further than the subject, # which is always valid. (And which is a singleton # in a constraint expression if it is itself a # singleton, regardless of other parts of the path.) if ( isinstance(rptr.ptrref, ir_ast.PointerRef) and rptr.ptrref.id == subject_obj.id ): break if (not isinstance(rptr.ptrref, ir_ast.TupleIndirectionPointerRef) and rptr.ptrref.source_ptr is None and rptr.source.rptr is not None): if isinstance(subject, s_links.Link): raise errors.InvalidConstraintDefinitionError( "link constraints may not access " "the link target", context=sourcectx ) else: raise errors.InvalidConstraintDefinitionError( "constraints cannot contain paths with more " "than one hop", context=sourcectx ) ref = rptr.source if has_multi and len(refs) > 1: raise errors.InvalidConstraintDefinitionError( "cannot reference multiple links or properties in a " "constraint where at least one link or property is MULTI", context=sourcectx ) if has_multi and ir_utils.contains_set_of_op( final_subjectexpr.irast): raise errors.InvalidConstraintDefinitionError( "cannot use aggregate functions or operators " "in a non-aggregating constraint", context=sourcectx ) attrs['finalexpr'] = final_expr attrs['params'] = constr_base.get_params(schema) inherited['params'] = True attrs['abstract'] = False for k, v in attrs.items(): self.set_attribute_value(k, v, inherited=bool(inherited.get(k)))
def get_concrete_constraint_attrs(cls, schema, subject, *, name, subjectexpr=None, sourcectx=None, args=[], modaliases=None, **kwargs): from edb.edgeql import utils as edgeql_utils from edb.edgeql import parser as edgeql_parser constr_base = schema.get(name, module_aliases=modaliases) module_aliases = {} orig_subject = subject base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr') if subjectexpr is None: subjectexpr = base_subjectexpr elif (base_subjectexpr is not None and subjectexpr.text != base_subjectexpr.text): raise errors.InvalidConstraintDefinitionError( 'subjectexpr is already defined for ' + f'{str(name)!r}') if subjectexpr is not None: subject, _ = cls._normalize_constraint_expr( schema, {}, subjectexpr.text, subject) expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr') if not expr: raise errors.InvalidConstraintDefinitionError( f'missing constraint expression in {name!r}') expr_ql = edgeql_parser.parse(expr.text, module_aliases) if not args: args = constr_base.get_field_value(schema, 'args') attrs = dict(kwargs) args_map = None if args: args_ql = [ edgeql_parser.parse(arg.text, module_aliases) for arg in args ] args_map = edgeql_utils.index_parameters( args_ql, parameters=constr_base.get_params(schema), schema=schema) edgeql_utils.inline_parameters(expr_ql, args_map) args_map = { name: edgeql.generate_source(val, pretty=False) for name, val in args_map.items() } errmessage = attrs.get('errmessage') if not errmessage: errmessage = constr_base.get_errmessage(schema) attrs['errmessage'] = errmessage.format( __subject__='{__subject__}', **args_map) attrs['args'] = args if expr == '__subject__': expr_context = sourcectx else: expr_context = None if subject is not orig_subject: # subject has been redefined subject_anchor = qlast.SubExpr( expr=subject, anchors={qlast.Subject: orig_subject}) else: subject_anchor = subject expr_text = cls.normalize_constraint_expr(schema, module_aliases, expr_ql, subject=subject_anchor, constraint_name=name, enforce_boolean=True, expr_context=expr_context) attrs['finalexpr'] = s_expr.Expression(text=expr_text) return constr_base, attrs
def get_concrete_constraint_attrs( cls, schema: s_schema.Schema, subject: Optional[so.Object], *, name: str, subjectexpr: Optional[s_expr.Expression] = None, sourcectx: Optional[c_parsing.ParserContext] = None, args: Any = None, modaliases: Optional[Mapping[Optional[str], str]] = None, **kwargs: Any ) -> Tuple[Any, Dict[str, Any], Dict[str, bool]]: # constr_base, attrs, inherited from edb.edgeql import parser as qlparser from edb.edgeql import utils as qlutils from edb.ir import ast as ir_ast constr_base: Constraint = schema.get(name, module_aliases=modaliases) module_aliases: Mapping[Optional[str], str] = {} orig_subjectexpr = subjectexpr orig_subject = subject base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr') if subjectexpr is None: subjectexpr = base_subjectexpr elif (base_subjectexpr is not None and subjectexpr.text != base_subjectexpr.text): raise errors.InvalidConstraintDefinitionError( 'subjectexpr is already defined for ' + f'{str(name)!r}') if subjectexpr is not None: subject_ql = subjectexpr.qlast if subject_ql is None: subject_ql = qlparser.parse(subjectexpr.text, module_aliases) subject = subject_ql expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr') if not expr: raise errors.InvalidConstraintDefinitionError( f'missing constraint expression in {name!r}') expr_ql = qlparser.parse(expr.text, module_aliases) if not args: args = constr_base.get_field_value(schema, 'args') attrs = dict(kwargs) inherited = dict() if orig_subjectexpr is not None: attrs['subjectexpr'] = orig_subjectexpr else: base_subjectexpr = constr_base.get_subjectexpr(schema) if base_subjectexpr is not None: attrs['subjectexpr'] = base_subjectexpr inherited['subjectexpr'] = True errmessage = attrs.get('errmessage') if not errmessage: errmessage = constr_base.get_errmessage(schema) inherited['errmessage'] = True attrs['errmessage'] = errmessage if subject is not orig_subject: # subject has been redefined assert isinstance(subject, qlast.Base) qlutils.inline_anchors( expr_ql, anchors={qlast.Subject().name: subject}) subject = orig_subject if args: args_map = None args_ql: List[qlast.Base] = [ qlast.Path(steps=[qlast.Subject()]), ] args_ql.extend( qlparser.parse(arg.text, module_aliases) for arg in args ) args_map = qlutils.index_parameters( args_ql, parameters=constr_base.get_params(schema), schema=schema) qlutils.inline_parameters(expr_ql, args_map) attrs['args'] = args if expr == '__subject__': expr_context = sourcectx else: expr_context = None assert subject is not None final_expr = s_expr.Expression.compiled( s_expr.Expression.from_ast(expr_ql, schema, module_aliases), schema=schema, options=qlcompiler.CompilerOptions( modaliases=module_aliases, anchors={qlast.Subject().name: subject}, ), ) bool_t: s_scalars.ScalarType = schema.get('std::bool') assert isinstance(final_expr.irast, ir_ast.Statement) expr_type = final_expr.irast.stype if not expr_type.issubclass(schema, bool_t): raise errors.InvalidConstraintDefinitionError( f'{name} constraint expression expected ' f'to return a bool value, got ' f'{expr_type.get_verbosename(schema)}', 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 attrs['params'] = constr_base.get_params(schema) attrs['is_abstract'] = False return constr_base, attrs, inherited
def validate_create( self, schema: s_schema.Schema, context: sd.CommandContext, ) -> None: super().validate_create(schema, context) if self.get_referrer_context(context) is not None: # The checks below apply only to abstract constraints. return base_params: Optional[s_func.FuncParameterList] = None base_with_params: Optional[Constraint] = None bases = self.get_resolved_attribute_value( 'bases', schema=schema, context=context, ) for base in bases.objects(schema): params = base.get_params(schema) if params and len(params) > 1: # All constraints have __subject__ parameter # auto-injected, hence the "> 1" check. if base_params is not None: raise errors.InvalidConstraintDefinitionError( f'{self.get_verbosename()} ' f'extends multiple constraints ' f'with parameters', context=self.source_context, ) base_params = params base_with_params = base if base_params: assert base_with_params is not None params = self._get_params(schema, context) if not params or len(params) == 1: # All constraints have __subject__ parameter # auto-injected, hence the "== 1" check. raise errors.InvalidConstraintDefinitionError( f'{self.get_verbosename()} ' f'must define parameters to reflect parameters of ' f'the {base_with_params.get_verbosename(schema)} ' f'it extends', context=self.source_context, ) if len(params) < len(base_params): raise errors.InvalidConstraintDefinitionError( f'{self.get_verbosename()} ' f'has fewer parameters than the ' f'{base_with_params.get_verbosename(schema)} ' f'it extends', context=self.source_context, ) # Skipping the __subject__ param for base_param, param in zip( base_params.objects(schema)[1:], params.objects(schema)[1:]): param_name = param.get_parameter_name(schema) base_param_name = base_param.get_parameter_name(schema) if param_name != base_param_name: raise errors.InvalidConstraintDefinitionError( f'the {param_name!r} parameter of the ' f'{self.get_verbosename()} ' f'must be renamed to {base_param_name!r} ' f'to match the signature of the base ' f'{base_with_params.get_verbosename(schema)} ', context=self.source_context, ) param_type = param.get_type(schema) base_param_type = base_param.get_type(schema) if (not base_param_type.is_polymorphic(schema) and param_type.is_polymorphic(schema)): raise errors.InvalidConstraintDefinitionError( f'the {param_name!r} parameter of the ' f'{self.get_verbosename()} cannot ' f'be of generic type because the corresponding ' f'parameter of the ' f'{base_with_params.get_verbosename(schema)} ' f'it extends has a concrete type', context=self.source_context, ) if (not base_param_type.is_polymorphic(schema) and not param_type.is_polymorphic(schema) and not param_type.implicitly_castable_to( base_param_type, schema)): raise errors.InvalidConstraintDefinitionError( f'the {param_name!r} parameter of the ' f'{self.get_verbosename()} has type of ' f'{param_type.get_displayname(schema)} that ' f'is not implicitly castable to the ' f'corresponding parameter of the ' f'{base_with_params.get_verbosename(schema)} with ' f'type {base_param_type.get_displayname(schema)}', context=self.source_context, )
def _cmd_tree_from_ast( cls, schema: s_schema.Schema, astnode: qlast.DDLOperation, context: sd.CommandContext, ) -> CreateConstraint: cmd = super()._cmd_tree_from_ast(schema, astnode, context) if isinstance(astnode, qlast.CreateConcreteConstraint): if astnode.delegated: cmd.set_attribute_value('delegated', astnode.delegated) args = cls._constraint_args_from_ast(schema, astnode, context) if args: cmd.set_attribute_value('args', args) elif isinstance(astnode, qlast.CreateConstraint): params = cls._get_param_desc_from_ast(schema, context.modaliases, astnode) for param in params: if param.get_kind(schema) is ft.ParameterKind.NamedOnlyParam: raise errors.InvalidConstraintDefinitionError( 'named only parameters are not allowed ' 'in this context', context=astnode.context) if param.get_default(schema) is not None: raise errors.InvalidConstraintDefinitionError( 'constraints do not support parameters ' 'with defaults', context=astnode.context) if cmd.get_attribute_value('return_type') is None: cmd.set_attribute_value( 'return_type', schema.get('std::bool'), ) if cmd.get_attribute_value('return_typemod') is None: cmd.set_attribute_value( 'return_typemod', ft.TypeModifier.SingletonType, ) assert isinstance( astnode, (qlast.CreateConstraint, qlast.CreateConcreteConstraint)) # 'subjectexpr' can be present in either astnode type if astnode.subjectexpr: orig_text = cls.get_orig_expr_text(schema, astnode, 'subjectexpr') if (orig_text is not None and context.compat_ver_is_before( (1, 0, verutils.VersionStage.ALPHA, 6))): # Versions prior to a6 used a different expression # normalization strategy, so we must renormalize the # expression. expr_ql = qlcompiler.renormalize_compat( astnode.subjectexpr, orig_text, schema=schema, localnames=context.localnames, ) else: expr_ql = astnode.subjectexpr subjectexpr = s_expr.Expression.from_ast( expr_ql, schema, context.modaliases, context.localnames, ) cmd.set_attribute_value( 'subjectexpr', subjectexpr, ) cls._validate_subcommands(astnode) assert isinstance(cmd, CreateConstraint) return cmd
def _cmd_tree_from_ast( cls, schema: s_schema.Schema, astnode: qlast.DDLOperation, context: sd.CommandContext, ) -> CreateConstraint: cmd = super()._cmd_tree_from_ast(schema, astnode, context) if isinstance(astnode, qlast.CreateConcreteConstraint): if astnode.delegated: cmd.set_attribute_value('delegated', astnode.delegated) args = cls._constraint_args_from_ast(schema, astnode, context) if args: cmd.set_attribute_value('args', args) elif isinstance(astnode, qlast.CreateConstraint): params = cls._get_param_desc_from_ast(schema, context.modaliases, astnode) for param in params: if param.get_kind(schema) is ft.ParameterKind.NAMED_ONLY: raise errors.InvalidConstraintDefinitionError( 'named only parameters are not allowed ' 'in this context', context=astnode.context) if param.get_default(schema) is not None: raise errors.InvalidConstraintDefinitionError( 'constraints do not support parameters ' 'with defaults', context=astnode.context) if cmd.get_attribute_value('return_type') is None: cmd.set_attribute_value( 'return_type', schema.get('std::bool'), ) if cmd.get_attribute_value('return_typemod') is None: cmd.set_attribute_value( 'return_typemod', ft.TypeModifier.SINGLETON, ) assert isinstance( astnode, (qlast.CreateConstraint, qlast.CreateConcreteConstraint)) # 'subjectexpr' can be present in either astnode type if astnode.subjectexpr: orig_text = cls.get_orig_expr_text(schema, astnode, 'subjectexpr') subjectexpr = s_expr.Expression.from_ast( astnode.subjectexpr, schema, context.modaliases, orig_text=orig_text, ) cmd.set_attribute_value( 'subjectexpr', subjectexpr, ) cls._validate_subcommands(astnode) assert isinstance(cmd, CreateConstraint) return cmd
def schema_constraint_to_backend_constraint(cls, subject, constraint, schema, context, source_context): assert constraint.get_subject(schema) is not None constraint_origin = cls._get_constraint_origin(schema, constraint) if constraint_origin != constraint: origin_subject = constraint_origin.get_subject(schema) else: origin_subject = subject path_prefix_anchor = (qlast.Subject().name if isinstance( subject, s_types.Type) else None) ir = qlcompiler.compile_ast_to_ir( constraint.get_finalexpr(schema).qlast, schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: subject}, path_prefix_anchor=path_prefix_anchor, apply_query_rewrites=not context.stdmode, ), ) terminal_refs = ir_utils.get_longest_paths(ir.expr.expr.result) ref_tables = get_ref_storage_info(ir.schema, terminal_refs) if len(ref_tables) > 1: raise errors.InvalidConstraintDefinitionError( f'Constraint {constraint.get_displayname(schema)} on ' f'{subject.get_displayname(schema)} is not supported ' f'because it would depend on multiple objects', context=source_context, ) elif ref_tables: subject_db_name, _ = next(iter(ref_tables.items())) else: subject_db_name = common.get_backend_name(schema, subject, catenate=False) exclusive_expr_refs = cls._get_exclusive_refs(ir) pg_constr_data = { 'subject_db_name': subject_db_name, 'expressions': [], 'origin_expressions': [], } if constraint_origin != constraint: origin_path_prefix_anchor = (qlast.Subject().name if isinstance( origin_subject, s_types.Type) else None) origin_ir = qlcompiler.compile_ast_to_ir( constraint_origin.get_finalexpr(schema).qlast, schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: origin_subject}, path_prefix_anchor=origin_path_prefix_anchor, apply_query_rewrites=not context.stdmode, ), ) origin_terminal_refs = ir_utils.get_longest_paths( origin_ir.expr.expr.result) origin_ref_tables = get_ref_storage_info(origin_ir.schema, origin_terminal_refs) if origin_ref_tables: origin_subject_db_name, _ = (next( iter(origin_ref_tables.items()))) else: origin_subject_db_name = common.get_backend_name( schema, origin_subject, catenate=False, ) origin_exclusive_expr_refs = cls._get_exclusive_refs(origin_ir) pg_constr_data['origin_subject_db_name'] = origin_subject_db_name else: origin_exclusive_expr_refs = None pg_constr_data['origin_subject_db_name'] = subject_db_name if exclusive_expr_refs: for ref in exclusive_expr_refs: exprdata = cls._edgeql_ref_to_pg_constr( subject, origin_subject, ref, schema) pg_constr_data['expressions'].append(exprdata) if origin_exclusive_expr_refs: for ref in origin_exclusive_expr_refs: exprdata = cls._edgeql_ref_to_pg_constr( subject, origin_subject, ref, schema) pg_constr_data['origin_expressions'].append(exprdata) else: pg_constr_data['origin_expressions'] = ( pg_constr_data['expressions']) pg_constr_data['scope'] = 'relation' pg_constr_data['type'] = 'unique' else: exprdata = cls._edgeql_ref_to_pg_constr(subject, origin_subject, ir, schema) pg_constr_data['expressions'].append(exprdata) pg_constr_data['scope'] = 'row' pg_constr_data['type'] = 'check' if isinstance(constraint.get_subject(schema), s_scalars.ScalarType): constraint = SchemaDomainConstraint(subject=subject, constraint=constraint, pg_constr_data=pg_constr_data, schema=schema) else: constraint = SchemaTableConstraint(subject=subject, constraint=constraint, pg_constr_data=pg_constr_data, schema=schema) return constraint