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 schema_constraint_to_backend_constraint( cls, subject, constraint, schema): assert constraint.get_subject(schema) is not None ir = ql_compiler.compile_ast_to_ir( constraint.get_finalexpr(schema).qlast, schema, anchors={qlast.Subject: subject}, ) terminal_refs = ir_utils.get_longest_paths(ir.expr.expr.result) ref_tables = cls._get_ref_storage_info(ir.schema, terminal_refs) if len(ref_tables) > 1: raise ValueError( 'backend: multi-table constraints are not currently supported') elif ref_tables: subject_db_name, refs = next(iter(ref_tables.items())) link_bias = refs[0][3].table_type == 'link' else: subject_db_name = common.get_backend_name( schema, subject, catenate=False) link_bias = False exclusive_expr_refs = cls._get_exclusive_refs(ir) pg_constr_data = { 'subject_db_name': subject_db_name, 'expressions': [] } exprs = pg_constr_data['expressions'] if exclusive_expr_refs: for ref in exclusive_expr_refs: exprdata = cls._edgeql_ref_to_pg_constr( subject, ref, schema, link_bias) exprs.append(exprdata) pg_constr_data['scope'] = 'relation' pg_constr_data['type'] = 'unique' pg_constr_data['subject_db_name'] = subject_db_name else: exprdata = cls._edgeql_ref_to_pg_constr( subject, ir, schema, link_bias) exprs.append(exprdata) pg_constr_data['subject_db_name'] = subject_db_name 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
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 _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 schema_constraint_to_backend_constraint( cls, subject, constraint, schema): 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, ), ) terminal_refs = ir_utils.get_longest_paths(ir.expr.expr.result) ref_tables = cls._get_ref_storage_info(ir.schema, terminal_refs) if len(ref_tables) > 1: raise ValueError( 'backend: multi-table constraints are not currently supported') 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_ir = qlcompiler.compile_ast_to_ir( constraint_origin.get_finalexpr(schema).qlast, schema, options=qlcompiler.CompilerOptions( anchors={qlast.Subject().name: origin_subject}, ), ) origin_terminal_refs = ir_utils.get_longest_paths( origin_ir.expr.expr.result) origin_ref_tables = cls._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
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