def compile_Coalesce(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: if all(isinstance(a, qlast.Set) and not a.elements for a in expr.args): return irutils.new_empty_set(ctx.schema, alias=ctx.aliases.get('e')) with ctx.newscope() as newctx: leftmost_arg = larg = setgen.ensure_set(dispatch.compile(expr.args[0], ctx=newctx), ctx=newctx) for rarg_ql in expr.args[1:]: with newctx.new() as nestedscopectx: with nestedscopectx.newscope(fenced=True) as fencectx: rarg = setgen.scoped_set(dispatch.compile(rarg_ql, ctx=fencectx), ctx=fencectx) coalesce = irast.Coalesce(left=larg, right=rarg) larg = setgen.generated_set(coalesce, ctx=nestedscopectx) # Make sure any empty set types are properly resolved # before entering them into the scope tree. irutils.infer_type(larg, schema=ctx.schema) pathctx.register_set_in_scope(leftmost_arg, ctx=ctx) pathctx.mark_path_as_optional(leftmost_arg.path_id, ctx=ctx) return larg
def compile_equivalence_op(expr: qlast.BinOp, *, ctx: context.ContextLevel) -> irast.EquivalenceOp: # A ?= B ≣ EQUIV(OPTIONAL any A, OPTIONAL any B) -> std::bool # Definition: # | {a = b | ∀ (a, b) ∈ A ⨯ B}, iff A != ∅ ∧ B != ∅ # | {True}, iff A = B = ∅ # | {False}, iff A != ∅ ∧ B = ∅ # | {False}, iff A = ∅ ∧ B != ∅ # # A ?!= B ≣ NEQUIV(OPTIONAL any A, OPTIONAL any B) -> std::bool # Definition: # | {a != b | ∀ (a, b) ∈ A ⨯ B}, iff A != ∅ ∧ B != ∅ # | {False}, iff A = B = ∅ # | {True}, iff A != ∅ ∧ B = ∅ # | {True}, iff A = ∅ ∧ B != ∅ left = setgen.ensure_set(dispatch.compile(expr.left, ctx=ctx), ctx=ctx) right = setgen.ensure_set(dispatch.compile(expr.right, ctx=ctx), ctx=ctx) result = irast.EquivalenceOp(left=left, right=right, op=expr.op) # Make sure any empty set types are properly resolved # before entering them into the scope tree. irutils.infer_type(result, schema=ctx.schema) pathctx.register_set_in_scope(left, ctx=ctx) pathctx.mark_path_as_optional(left.path_id, ctx=ctx) pathctx.register_set_in_scope(right, ctx=ctx) pathctx.mark_path_as_optional(right.path_id, ctx=ctx) return result
def compile_IfElse(expr: qlast.IfElse, *, ctx: context.ContextLevel) -> irast.Base: condition = setgen.ensure_set(dispatch.compile(expr.condition, ctx=ctx), ctx=ctx) ql_if_expr = expr.if_expr ql_else_expr = expr.else_expr with ctx.newscope(fenced=True) as scopectx: if_expr = dispatch.compile(ql_if_expr, ctx=scopectx) with ctx.newscope(fenced=True) as scopectx: else_expr = dispatch.compile(ql_else_expr, ctx=scopectx) if_expr_type = irutils.infer_type(if_expr, ctx.schema) else_expr_type = irutils.infer_type(else_expr, ctx.schema) result = s_utils.get_class_nearest_common_ancestor( [if_expr_type, else_expr_type]) if result is None: raise errors.EdgeQLError( 'if/else clauses must be of related types, got: {}/{}'.format( if_expr_type.name, else_expr_type.name), context=expr.context) return setgen.generated_set(irast.IfElseExpr(if_expr=if_expr, else_expr=else_expr, condition=condition), ctx=ctx)
def fini_expression(ir: irast.Base, *, ctx: context.ContextLevel) -> irast.Command: for ir_set in ctx.all_sets: if ir_set.path_id.namespace: ir_set.path_id = ir_set.path_id.strip_weak_namespaces() if isinstance(ir, irast.Command): # IR is already a Command return ir if ctx.path_scope is not None: # Simple expressions have no scope. for node in ctx.path_scope.get_all_path_nodes(include_subpaths=True): if node.path_id.namespace: node.path_id = node.path_id.strip_weak_namespaces() cardinality = pathctx.infer_cardinality(ir, ctx=ctx) else: cardinality = irast.Cardinality.ONE result = irast.Statement( expr=ir, params=ctx.arguments, views=ctx.view_nodes, source_map=ctx.source_map, scope_tree=ctx.path_scope, cardinality=cardinality, view_shapes=ctx.class_shapes, ) irutils.infer_type(result, schema=ctx.schema) return result
def _cast_expr(ql_type: qlast.TypeName, ir_expr: irast.Base, *, source_context: parsing.ParserContext, ctx: context.ContextLevel) -> irast.Base: try: orig_type = irutils.infer_type(ir_expr, ctx.schema) except errors.EdgeQLError: # It is possible that the source expression is unresolved # if the expr is an empty set (or a coalesce of empty sets). orig_type = None if isinstance(orig_type, s_types.Tuple): # For tuple-to-tuple casts we generate a new tuple # to simplify things on sqlgen side. new_type = typegen.ql_typeref_to_type(ql_type, ctx=ctx) if not isinstance(new_type, s_types.Tuple): raise errors.EdgeQLError(f'cannot cast tuple to {new_type.name}', context=source_context) if len(orig_type.element_types) != len(new_type.element_types): raise errors.EdgeQLError( f'cannot cast to {new_type.name}: ' f'number of elements is not the same', context=source_context) new_names = list(new_type.element_types) elements = [] for i, n in enumerate(orig_type.element_types): val = setgen.generated_set(irast.TupleIndirection(expr=ir_expr, name=n), ctx=ctx) val.path_id = irutils.tuple_indirection_path_id( ir_expr.path_id, n, orig_type.element_types[n]) val_type = irutils.infer_type(val, ctx.schema) new_el_name = new_names[i] if val_type != new_type.element_types[new_el_name]: # Element cast val = _cast_expr(ql_type.subtypes[i], val, ctx=ctx, source_context=source_context) elements.append(irast.TupleElement(name=new_el_name, val=val)) return irast.Tuple(named=new_type.named, elements=elements) elif isinstance(ir_expr, irast.EmptySet): # For the common case of casting an empty set, we simply # generate a new EmptySet node of the requested type. scls = typegen.ql_typeref_to_type(ql_type, ctx=ctx) return irutils.new_empty_set(ctx.schema, scls=scls, alias=ir_expr.path_id.target.name.name) else: typ = typegen.ql_typeref_to_ir_typeref(ql_type, ctx=ctx) return setgen.ensure_set(irast.TypeCast(expr=ir_expr, type=typ), ctx=ctx)
def normalize_constraint_expr(cls, schema, module_aliases, expr, *, subject=None, constraint, expr_context=None, enforce_boolean=False): from edb.lang.ir import utils as irutils if subject is None: subject = cls._dummy_subject() edgeql_tree, ir_result = cls._normalize_constraint_expr( schema, module_aliases, expr, subject) if enforce_boolean: bool_t = schema.get('std::bool') expr_type = irutils.infer_type(ir_result, schema) if not expr_type.issubclass(bool_t): raise s_errors.SchemaDefinitionError( f'{constraint.displayname} constraint expression expected ' f'to return a bool value, got {expr_type.name.name!r}', context=expr_context) expr = edgeql.generate_source(edgeql_tree, pretty=False) # XXX: check that expr has boolean result return expr
def fini_stmt( irstmt: irast.Base, qlstmt: qlast.Statement, *, ctx: context.ContextLevel, parent_ctx: context.ContextLevel) -> irast.Set: irstmt.cardinality = qlstmt.cardinality view_name = parent_ctx.toplevel_result_view_name t = irutils.infer_type(irstmt, ctx.schema) if t.name == view_name: # The view statement did contain a view declaration and # generated a view class with the requested name. view = t path_id = pathctx.get_path_id(view, ctx=parent_ctx) elif view_name is not None: # The view statement did _not_ contain a view declaration, # but we still want the correct path_id. view = schemactx.derive_view(t, derived_name=view_name, ctx=parent_ctx) path_id = pathctx.get_path_id(view, ctx=parent_ctx) else: view = None path_id = None result = setgen.scoped_set(irstmt, path_id=path_id, ctx=ctx) if view is not None: parent_ctx.view_sets[view] = result result.scls = view return result
def compile_UpdateQuery( expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: with ctx.subquery() as ictx: stmt = irast.UpdateStmt() init_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx) subject = dispatch.compile(expr.subject, ctx=ictx) subj_type = irutils.infer_type(subject, ictx.schema) if not isinstance(subj_type, s_objtypes.ObjectType): raise errors.EdgeQLError( f'cannot update non-ObjectType objects', context=expr.subject.context ) stmt.subject = compile_query_subject( subject, shape=expr.shape, view_rptr=ctx.view_rptr, compile_views=True, result_alias=expr.subject_alias, is_update=True, ctx=ictx) stmt.result = setgen.class_set( stmt.subject.scls.material_type(), path_id=stmt.subject.path_id, ctx=ctx) stmt.where = clauses.compile_where_clause( expr.where, ctx=ictx) result = fini_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx) return result
def compile_DeleteQuery( expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: with ctx.subquery() as ictx: stmt = irast.DeleteStmt() init_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx) # DELETE Expr is a delete(SET OF X), so we need a scope fence. with ictx.newscope(fenced=True) as scopectx: subject = setgen.scoped_set( dispatch.compile(expr.subject, ctx=scopectx), ctx=scopectx) subj_type = irutils.infer_type(subject, ictx.schema) if not isinstance(subj_type, s_objtypes.ObjectType): raise errors.EdgeQLError( f'cannot delete non-ObjectType objects', context=expr.subject.context ) stmt.subject = compile_query_subject( subject, shape=None, result_alias=expr.subject_alias, ctx=ictx) stmt.result = setgen.class_set( stmt.subject.scls.material_type(), path_id=stmt.subject.path_id, ctx=ctx) result = fini_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx) return result
def compile_UnaryOp(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Set: if expr.op == qlast.DISTINCT: return compile_distinct_op(expr, ctx=ctx) operand = dispatch.compile(expr.operand, ctx=ctx) if astutils.is_exists_expr_set(operand): operand.expr.negated = not operand.expr.negated return operand unop = irast.UnaryOp(expr=operand, op=expr.op) result_type = irutils.infer_type(unop, ctx.schema) real_t = ctx.schema.get('std::anyreal') if (isinstance(operand.expr, irast.Constant) and result_type.issubclass(real_t)): # Fold the operation to constant if possible if expr.op == ast.ops.UMINUS: return setgen.ensure_set(irast.Constant(value=-operand.expr.value, type=result_type), ctx=ctx) elif expr.op == ast.ops.UPLUS: return operand return setgen.generated_set(unop, ctx=ctx)
def compile_Array(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: elements = [dispatch.compile(e, ctx=ctx) for e in expr.elements] # check that none of the elements are themselves arrays for el, expr_el in zip(elements, expr.elements): if isinstance(irutils.infer_type(el, ctx.schema), s_types.Array): raise errors.EdgeQLError(f'nested arrays are not supported', context=expr_el.context) return setgen.generated_set(irast.Array(elements=elements), ctx=ctx)
def try_fold_arithmetic_binop( op: ast.ops.Operator, left: irast.Set, right: irast.Set, *, ctx: context.ContextLevel) -> typing.Optional[irast.Set]: """Try folding an arithmetic expr into a constant.""" schema = ctx.schema real_t = schema.get('std::anyreal') float_t = schema.get('std::anyfloat') int_t = schema.get('std::anyint') left_type = irutils.infer_type(left, schema) right_type = irutils.infer_type(right, schema) if not left_type.issubclass(real_t) or not right_type.issubclass(real_t): return result_type = left_type if right_type.issubclass(float_t): result_type = right_type left = left.expr right = right.expr if op == ast.ops.ADD: value = left.value + right.value elif op == ast.ops.SUB: value = left.value - right.value elif op == ast.ops.MUL: value = left.value * right.value elif op == ast.ops.DIV: if left_type.issubclass(int_t) and right_type.issubclass(int_t): value = left.value // right.value else: value = left.value / right.value elif op == ast.ops.POW: value = left.value**right.value elif op == ast.ops.MOD: value = left.value % right.value else: value = None if value is not None: return setgen.ensure_set(irast.Constant(value=value, type=result_type), ctx=ctx)
def try_fold_binop(binop: irast.BinOp, *, ctx: context.ContextLevel) -> typing.Optional[irast.Set]: """Try folding a binary operator expression.""" schema = ctx.schema real_t = schema.get('std::anyreal') result_type = irutils.infer_type(binop, schema) folded = None left = binop.left right = binop.right op = binop.op if (isinstance(left.expr, irast.Constant) and isinstance(right.expr, irast.Constant)): # Left and right nodes are constants. if isinstance(op, ast.ops.ComparisonOperator): folded = try_fold_comparison_binop(op, left, right, ctx=ctx) elif result_type.issubclass(real_t): folded = try_fold_arithmetic_binop(op, left, right, ctx=ctx) elif op in {ast.ops.ADD, ast.ops.MUL}: # Let's check if we have (CONST + (OTHER_CONST + X)) # tree, which can be optimized to ((CONST + OTHER_CONST) + X) my_const = left other_binop = right if isinstance(right.expr, irast.Constant): my_const, other_binop = other_binop, my_const if (isinstance(my_const.expr, irast.Constant) and isinstance(other_binop.expr, irast.BinOp) and other_binop.expr.op == op): other_const = other_binop.expr.left other_binop_node = other_binop.expr.right if isinstance(other_binop_node.expr, irast.Constant): other_binop_node, other_const = \ other_const, other_binop_node if isinstance(other_const.expr, irast.Constant): new_const = try_fold_arithmetic_binop(op, other_const, my_const, ctx=ctx) if new_const is not None: folded_binop = irast.BinOp(left=new_const, right=other_binop_node, op=op) folded = setgen.ensure_set(folded_binop, ctx=ctx) return folded
def _command_for_ast_node(cls, astnode, schema, context): classname = cls._classname_from_ast(astnode, context, schema) if isinstance(astnode, qlast.CreateView): expr = cls._get_view_expr(astnode) ir = cls._compile_view_expr(expr, classname, schema, context) scls = irutils.infer_type(ir, schema) else: scls = schema.get(classname) if isinstance(scls, s_scalars.ScalarType): mapping = cls._scalar_cmd_map else: mapping = cls._objtype_cmd_map return mapping[type(astnode)]
def compile_type_check_op(expr: qlast.IsOp, *, ctx: context.ContextLevel) -> irast.TypeCheckOp: # <Expr> IS <TypeExpr> left = dispatch.compile(expr.left, ctx=ctx) ltype = irutils.infer_type(left, ctx.schema) left = setgen.ptr_step_set(left, source=ltype, ptr_name=('std', '__type__'), direction=s_pointers.PointerDirection.Outbound, source_context=expr.context, ctx=ctx) pathctx.register_set_in_scope(left, ctx=ctx) right = typegen.ql_typeref_to_ir_typeref(expr.right, ctx=ctx) return irast.TypeCheckOp(left=left, right=right, op=expr.op)
def process_func_args( expr: qlast.FunctionCall, funcname: sn.Name, *, ctx: context.ContextLevel) \ -> typing.Tuple[ typing.List[irast.Base], # args typing.Dict[str, irast.Base], # kwargs typing.List[s_types.Type]]: # arg_types args = [] kwargs = {} arg_types = [] for ai, a in enumerate(expr.args): arg_ql = a.arg if a.sort or a.filter: arg_ql = astutils.ensure_qlstmt(arg_ql) if a.filter: arg_ql.where = astutils.extend_qlbinop(arg_ql.where, a.filter) if a.sort: arg_ql.orderby = a.sort + arg_ql.orderby with ctx.newscope(fenced=True) as fencectx: # We put on a SET OF fence preemptively in case this is # a SET OF arg, which we don't know yet due to polymorphic # matching. arg = setgen.scoped_set( dispatch.compile(arg_ql, ctx=fencectx), ctx=fencectx) if a.name: kwargs[a.name] = arg aname = a.name else: args.append(arg) aname = ai arg_type = irutils.infer_type(arg, ctx.schema) if arg_type is None: raise errors.EdgeQLError( f'could not resolve the type of argument ' f'${aname} of function {funcname}', context=a.context) arg_types.append(arg_type) return args, kwargs, arg_types
def _handle_view_op(cls, cmd, astnode, context, schema): from edb.lang.ir import utils as irutils view_expr = cls._maybe_get_view_expr(astnode) if view_expr is not None: ir = cls._compile_view_expr(view_expr, cmd.classname, schema, context) rt = irutils.infer_type(ir, schema) if rt.is_view(): # The expression itself declares a view, use it. rt.name = cmd.classname view_schema = cls._view_schema_from_ir(cmd.classname, ir, schema) if isinstance(astnode, qlast.AlterObjectType): prev = schema.get(cmd.classname) prev_ir = cls._compile_view_expr(prev.expr, cmd.classname, schema, context) prev_view_schema = cls._view_schema_from_ir( cmd.classname, prev_ir, schema) else: prev_view_schema = cls._view_schema_from_ir( cmd.classname, None, schema) derived_delta = sd.delta_schemas(view_schema, prev_view_schema, include_derived=True) if rt.is_view(): for op in list(derived_delta.get_subcommands()): if op.classname == cmd.classname: for subop in op.get_subcommands(): if isinstance(subop, sd.AlterObjectProperty): cmd.discard_attribute(subop.property) cmd.add(subop) derived_delta.discard(op) cmd.update(derived_delta.get_subcommands()) cmd.discard_attribute('view_type') cmd.add( sd.AlterObjectProperty(property='view_type', new_value=s_types.ViewType.Select)) return cmd
def compile_FunctionCall( expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: with ctx.new() as fctx: if isinstance(expr.func, str): funcname = expr.func else: funcname = sn.Name(expr.func[1], expr.func[0]) funcs = fctx.schema.get_functions( funcname, module_aliases=fctx.modaliases) if funcs is None: raise errors.EdgeQLError( f'could not resolve function name {funcname}', context=expr.context) fctx.in_func_call = True args, kwargs, arg_types = process_func_args(expr, funcname, ctx=fctx) fatal_array_check = len(funcs) == 1 for funcobj in funcs: if check_function(expr, funcname, funcobj, arg_types, fatal_array_check=fatal_array_check): break else: raise errors.EdgeQLError( f'could not find a function variant {funcname}', context=expr.context) fixup_param_scope(funcobj, args, kwargs, ctx=fctx) node = irast.FunctionCall(func=funcobj, args=args, kwargs=kwargs) if funcobj.initial_value is not None: rtype = irutils.infer_type(node, fctx.schema) iv_ql = qlast.TypeCast( expr=qlparser.parse_fragment(funcobj.initial_value), type=typegen.type_to_ql_typeref(rtype) ) node.initial_value = dispatch.compile(iv_ql, ctx=fctx) ir_set = setgen.ensure_set(node, ctx=ctx) return ir_set
def compile_type_check_op( expr: qlast.BinOp, *, ctx: context.ContextLevel) -> irast.TypeCheckOp: # <Expr> IS <Type> left = dispatch.compile(expr.left, ctx=ctx) with ctx.new() as subctx: subctx.path_as_type = True right = dispatch.compile(expr.right, ctx=subctx) ltype = irutils.infer_type(left, ctx.schema) left = setgen.ptr_step_set( left, source=ltype, ptr_name=('std', '__type__'), direction=s_pointers.PointerDirection.Outbound, source_context=expr.context, ctx=ctx) pathctx.register_set_in_scope(left, ctx=ctx) right = typegen.process_type_ref_expr(right) return irast.TypeCheckOp(left=left, right=right, op=expr.op)
def new_expression_set(ir_expr, path_id=None, alias=None, typehint: typing.Optional[irast.TypeRef] = None, *, ctx: context.ContextLevel) -> irast.Set: if isinstance(ir_expr, irast.EmptySet) and typehint is not None: ir_expr = irast.TypeCast(expr=ir_expr, type=typehint) result_type = irutils.infer_type(ir_expr, ctx.schema) if path_id is None: path_id = getattr(ir_expr, 'path_id', None) if not path_id: if alias is None: raise ValueError('either path_id or alias are required') path_id = get_expression_path_id(result_type, alias, ctx=ctx) return new_set(path_id=path_id, scls=result_type, expr=ir_expr, ctx=ctx)
def compile_TypeFilter(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: # Expr[IS Type] expressions. with ctx.new() as scopectx: arg = setgen.ensure_set(dispatch.compile(expr.expr, ctx=scopectx), ctx=scopectx) arg_type = irutils.infer_type(arg, ctx.schema) if not isinstance(arg_type, s_objtypes.ObjectType): raise errors.EdgeQLError( f'invalid type filter operand: {arg_type.name} ' f'is not an object type', context=expr.expr.context) typ = schemactx.get_schema_type(expr.type.maintype, ctx=ctx) if not isinstance(typ, s_objtypes.ObjectType): raise errors.EdgeQLError( f'invalid type filter operand: {typ.name} is not an object type', context=expr.type.context) return setgen.class_indirection_set(arg, typ, optional=False, ctx=ctx)
def _normalize_ptr_default(self, expr, source, ptr, ptrdecl): module_aliases = {None: source.name.module} ir, _, expr_text = edgeql.utils.normalize_tree( expr, self._schema, modaliases=module_aliases, anchors={qlast.Source: source}) self_set = ast.find_children( ir, lambda n: getattr(n, 'anchor', None) == qlast.Source, terminate_early=True) try: expr_type = ir_utils.infer_type(ir, self._schema) except edgeql.EdgeQLError as e: raise s_err.SchemaError( 'could not determine the result type of the default ' 'expression on {!s}.{!s}'.format(source.name, ptr.shortname), context=expr.context) from e ptr.default = expr_text ptr.normalize_defaults() if ptr.is_pure_computable(): # Pure computable without explicit target. # Fixup pointer target and target property. ptr.target = expr_type if isinstance(ptr, s_links.Link): if not isinstance(expr_type, s_objtypes.ObjectType): raise s_err.SchemaDefinitionError( f'invalid link target, expected object type, got ' f'{expr_type.__class__.__name__}', context=ptrdecl.expr.context) else: if not isinstance(expr_type, (s_scalars.ScalarType, s_types.Collection)): raise s_err.SchemaDefinitionError( f'invalid property target, expected primitive type, ' f'got {expr_type.__class__.__name__}', context=ptrdecl.expr.context) if isinstance(ptr, s_links.Link): pname = s_name.Name('std::target') tgt_prop = ptr.pointers[pname] tgt_prop.target = expr_type cardinality = self._get_literal_attribute(ptrdecl, 'cardinality') if cardinality is not None: raise s_err.SchemaError( 'computable links must not define explicit cardinality', context=expr.context) singletons = set() if self_set is not None: singletons.add(self_set.path_id) cardinality = \ ir_inference.infer_cardinality(ir, singletons, self._schema) if cardinality == qlast.Cardinality.MANY: ptr.cardinality = s_pointers.PointerCardinality.ManyToMany else: ptr.cardinality = s_pointers.PointerCardinality.ManyToOne if (not isinstance(expr_type, s_types.Type) or (ptr.target is not None and not expr_type.issubclass(ptr.target))): raise s_err.SchemaError( 'default value query must yield a single result of ' 'type {!r}'.format(ptr.target.name), context=expr.context) if not isinstance(ptr.target, s_scalars.ScalarType): many_mapping = (s_pointers.PointerCardinality.ManyToOne, s_pointers.PointerCardinality.ManyToMany) if ptr.cardinality not in many_mapping: raise s_err.SchemaError( 'type links with query defaults ' 'must have either a "*1" or "**" cardinality', context=expr.context)
def _infer_type(expr: irast.Base, *, ctx: context.CompilerContextLevel) -> s_obj.Object: return irutils.infer_type(expr, schema=ctx.env.schema)
def _cmd_tree_from_ast(cls, astnode, context, schema): from edb.lang.edgeql import utils as ql_utils from edb.lang.ir import ast as irast from edb.lang.ir import inference as ir_inference from edb.lang.ir import utils as ir_utils from . import objtypes as s_objtypes cmd = super()._cmd_tree_from_ast(astnode, context, schema) if isinstance(astnode, qlast.CreateConcreteLink): cmd.add( sd.AlterObjectProperty(property='required', new_value=astnode.is_required)) # "source" attribute is set automatically as a refdict back-attr parent_ctx = context.get(LinkSourceCommandContext) source_name = parent_ctx.op.classname target_type = None if len(astnode.targets) > 1: cmd.add( sd.AlterObjectProperty( property='spectargets', new_value=so.ObjectList([ utils.ast_to_typeref(t, modaliases=context.modaliases, schema=schema) for t in astnode.targets ]))) target_name = sources.Source.gen_virt_parent_name( (sn.Name(module=t.maintype.module, name=t.maintype.name) for t in astnode.targets), module=source_name.module) target = so.ObjectRef(classname=target_name) create_virt_parent = s_objtypes.CreateObjectType( classname=target_name, metaclass=s_objtypes.ObjectType) create_virt_parent.update( (sd.AlterObjectProperty( property='bases', new_value=so.ObjectList([ so.ObjectRef( classname=sn.Name(module='std', name='Object')) ])), sd.AlterObjectProperty(property='name', new_value=target_name), sd.AlterObjectProperty(property='is_virtual', new_value=True))) alter_db_ctx = context.get(s_db.DatabaseCommandContext) for cc in alter_db_ctx.op.get_subcommands( type=s_objtypes.CreateObjectType): if cc.classname == create_virt_parent.classname: break else: alter_db_ctx.op.add(create_virt_parent) else: target_expr = astnode.targets[0] if isinstance(target_expr, qlast.TypeName): target = utils.ast_to_typeref( target_expr, modaliases=context.modaliases, schema=schema) else: # computable source = schema.get(source_name, default=None) if source is None: raise s_err.SchemaDefinitionError( f'cannot define link computables in CREATE TYPE', hint='Perform a CREATE TYPE without the link ' 'followed by ALTER TYPE defining the ' 'computable', context=target_expr.context) ir, _, target_expr = ql_utils.normalize_tree( target_expr, schema, anchors={qlast.Source: source}) try: target_type = ir_utils.infer_type(ir, schema) except edgeql.EdgeQLError as e: raise s_err.SchemaDefinitionError( 'could not determine the result type of ' 'computable expression', context=target_expr.context) from e target = utils.reduce_to_typeref(target_type) cmd.add( sd.AlterObjectProperty(property='default', new_value=target_expr)) cmd.add( sd.AlterObjectProperty(property='computable', new_value=True)) singletons = {irast.PathId(source)} cardinality = ir_inference.infer_cardinality( ir, singletons, schema) if cardinality == qlast.Cardinality.ONE: link_card = pointers.PointerCardinality.ManyToOne else: link_card = pointers.PointerCardinality.ManyToMany cmd.add( sd.AlterObjectProperty(property='cardinality', new_value=link_card)) if (isinstance(target, so.ObjectRef) and target.classname == source_name): # Special case for loop links. Since the target # is the same as the source, we know it's a proper # type. pass else: if target_type is None: target_type = utils.resolve_typeref(target, schema=schema) if not isinstance(target_type, s_objtypes.ObjectType): raise s_err.SchemaDefinitionError( f'invalid link target, expected object type, got ' f'{target_type.__class__.__name__}', context=astnode.targets[0].context) cmd.add(sd.AlterObjectProperty(property='target', new_value=target)) base_prop_name = sn.Name('std::source') s_name = lproperties.Property.get_specialized_name( base_prop_name, cmd.classname) src_prop_name = sn.Name(name=s_name, module=cmd.classname.module) src_prop = lproperties.CreateProperty( classname=src_prop_name, metaclass=lproperties.Property) src_prop.update(( sd.AlterObjectProperty(property='name', new_value=src_prop_name), sd.AlterObjectProperty( property='bases', new_value=[so.ObjectRef(classname=base_prop_name)]), sd.AlterObjectProperty( property='source', new_value=so.ObjectRef(classname=cmd.classname)), sd.AlterObjectProperty( property='target', new_value=so.ObjectRef(classname=source_name)), sd.AlterObjectProperty(property='required', new_value=True), sd.AlterObjectProperty(property='readonly', new_value=True), )) cmd.add(src_prop) base_prop_name = sn.Name('std::target') s_name = lproperties.Property.get_specialized_name( base_prop_name, cmd.classname) tgt_prop_name = sn.Name(name=s_name, module=cmd.classname.module) tgt_prop = lproperties.CreateProperty( classname=tgt_prop_name, metaclass=lproperties.Property) tgt_prop.update(( sd.AlterObjectProperty(property='name', new_value=tgt_prop_name), sd.AlterObjectProperty( property='bases', new_value=[so.ObjectRef(classname=base_prop_name)]), sd.AlterObjectProperty( property='source', new_value=so.ObjectRef(classname=cmd.classname)), sd.AlterObjectProperty(property='target', new_value=target), sd.AlterObjectProperty(property='required', new_value=False), sd.AlterObjectProperty(property='readonly', new_value=True), )) cmd.add(tgt_prop) cls._parse_default(cmd) return cmd
def _normalize_view_ptr_expr( shape_el: qlast.ShapeElement, view_scls: s_nodes.Node, *, path_id: irast.PathId, is_insert: bool = False, is_update: bool = False, view_rptr: typing.Optional[context.ViewRPtr] = None, ctx: context.CompilerContext) -> s_pointers.Pointer: steps = shape_el.expr.steps is_linkprop = False is_mutation = is_insert or is_update # Pointers may be qualified by the explicit source # class, which is equivalent to Expr[IS Type]. is_polymorphic = len(steps) == 2 scls = view_scls.peel_view() ptrsource = scls qlexpr = None if is_polymorphic: source = qlast.TypeFilter(expr=qlast.Path(steps=[qlast.Source()]), type=qlast.TypeName(maintype=steps[0])) lexpr = steps[1] ptrsource = schemactx.get_schema_type(steps[0], ctx=ctx) elif len(steps) == 1: lexpr = steps[0] is_linkprop = lexpr.type == 'property' if is_linkprop: if view_rptr is None: raise errors.EdgeQLError( 'invalid reference to link property ' 'in top level shape', context=lexpr.context) if view_rptr.ptrcls is None: derive_ptrcls(view_rptr, target_scls=view_scls, ctx=ctx) ptrsource = scls = view_rptr.ptrcls source = qlast.Source() else: raise RuntimeError( f'unexpected path length in view shape: {len(steps)}') ptrname = (lexpr.ptr.module, lexpr.ptr.name) ptrcls_is_derived = False compexpr = shape_el.compexpr if compexpr is None and is_insert and shape_el.elements: # Nested insert short form: # INSERT Foo { bar: Spam { name := 'name' }} # Expand to: # INSERT Foo { bar := (INSERT Spam { name := 'name' }) } if lexpr.target is not None: ptr_target = schemactx.get_schema_type(lexpr.target, ctx=ctx) else: ptr_target = None base_ptrcls = ptrcls = setgen.resolve_ptr( ptrsource, ptrname, s_pointers.PointerDirection.Outbound, target=ptr_target, ctx=ctx) compexpr = qlast.InsertQuery(subject=qlast.Path(steps=[ qlast.ObjectRef(name=ptrcls.target.name.name, module=ptrcls.target.name.module) ]), shape=shape_el.elements) if compexpr is None: if lexpr.target is not None: ptr_target = schemactx.get_schema_type(lexpr.target, ctx=ctx) else: ptr_target = None base_ptrcls = ptrcls = setgen.resolve_ptr( ptrsource, ptrname, s_pointers.PointerDirection.Outbound, target=ptr_target, ctx=ctx) base_ptr_is_computable = ptrcls in ctx.source_map if ptr_target is not None and ptr_target != base_ptrcls.target: # This happens when a union type target is narrowed by an # [IS Type] construct. Since the derived pointer will have # the correct target, we don't need to do anything, but # remove the [IS] qualifier to prevent recursion. lexpr.target = None else: ptr_target = ptrcls.target if ptrcls in ctx.pending_cardinality: # We do not know the parent's pointer cardinality yet. ptr_cardinality = None else: ptr_cardinality = ptrcls.cardinality if shape_el.elements: sub_view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, is_insert=is_insert, is_update=is_update) sub_path_id = path_id.extend(ptrcls, target=ptrcls.target) ctx.path_scope.attach_path(sub_path_id) if is_update: for subel in shape_el.elements or []: is_prop = (isinstance(subel.expr.steps[0], qlast.Ptr) and subel.expr.steps[0].type == 'property') if not is_prop: raise errors.EdgeQLError( 'only references to link properties are allowed ' 'in nested UPDATE shapes', context=subel.context) ptr_target = _process_view(scls=ptr_target, path_id=sub_path_id, view_rptr=sub_view_rptr, elements=shape_el.elements, is_update=True, ctx=ctx) else: ptr_target = _process_view(scls=ptr_target, path_id=sub_path_id, view_rptr=sub_view_rptr, elements=shape_el.elements, ctx=ctx) ptrcls = sub_view_rptr.derived_ptrcls if ptrcls is None: ptrcls_is_derived = False ptrcls = sub_view_rptr.ptrcls else: ptrcls_is_derived = True if (shape_el.where or shape_el.orderby or shape_el.offset or shape_el.limit or base_ptr_is_computable or is_polymorphic): if qlexpr is None: qlexpr = qlast.Path(steps=[source, lexpr]) qlexpr = astutils.ensure_qlstmt(qlexpr) qlexpr.where = shape_el.where qlexpr.orderby = shape_el.orderby qlexpr.offset = shape_el.offset qlexpr.limit = shape_el.limit else: try: base_ptrcls = ptrcls = setgen.resolve_ptr( ptrsource, ptrname, s_pointers.PointerDirection.Outbound, ctx=ctx) ptr_name = ptrcls.shortname except errors.EdgeQLReferenceError: if is_mutation: raise base_ptrcls = ptrcls = None ptr_module = (ptrname[0] or ctx.derived_target_module or scls.name.module) ptr_name = sn.SchemaName(module=ptr_module, name=ptrname[1]) qlexpr = astutils.ensure_qlstmt(compexpr) with ctx.newscope(fenced=True) as shape_expr_ctx: # Put current pointer class in context, so # that references to link properties in sub-SELECT # can be resolved. This is necessary for proper # evaluation of link properties on computable links, # most importantly, in INSERT/UPDATE context. shape_expr_ctx.view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, ptrcls_name=ptr_name, ptrcls_is_linkprop=is_linkprop, is_insert=is_insert, is_update=is_update) shape_expr_ctx.path_scope.unnest_fence = True if is_mutation: shape_expr_ctx.expr_exposed = True irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx) irexpr.context = compexpr.context if base_ptrcls is None: base_ptrcls = ptrcls = shape_expr_ctx.view_rptr.ptrcls derived_ptrcls = shape_expr_ctx.view_rptr.derived_ptrcls if derived_ptrcls is not None: ptrcls_is_derived = True ptrcls = derived_ptrcls ptr_cardinality = None ptr_target = irutils.infer_type(irexpr, ctx.schema) if ptr_target is None: msg = 'cannot determine expression result type' raise errors.EdgeQLError(msg, context=shape_el.context) if is_mutation and not ptr_target.assignment_castable_to( base_ptrcls.target, schema=ctx.schema): # Validate that the insert/update expression is # of the correct class. lname = f'({ptrsource.name}).{ptrcls.shortname.name}' expected = [repr(str(base_ptrcls.target.name))] raise edgedb_error.InvalidPointerTargetError( f'invalid target for link {str(lname)!r}: ' f'{str(ptr_target.name)!r} (expecting ' f'{" or ".join(expected)})') if qlexpr is not None or ptr_target is not ptrcls.target: if not ptrcls_is_derived: if is_linkprop: rptrcls = view_rptr.derived_ptrcls if rptrcls is None: rptrcls = derive_ptrcls(view_rptr, target_scls=view_scls, ctx=ctx) src_scls = rptrcls else: src_scls = view_scls ptrcls = schemactx.derive_view(ptrcls, src_scls, ptr_target, is_insert=is_insert, is_update=is_update, derived_name_quals=[view_scls.name], ctx=ctx) if qlexpr is not None: ctx.source_map[ptrcls] = (qlexpr, ctx) ptrcls.computable = True if not is_mutation: if ptr_cardinality is None: if compexpr is not None: ctx.pending_cardinality.add(ptrcls) elif ptrcls is not base_ptrcls: ctx.pointer_derivation_map[base_ptrcls].append(ptrcls) ptrcls.cardinality = None else: ptrcls.cardinality = ptr_cardinality if ptrcls.is_protected_pointer() and qlexpr is not None: msg = f'cannot assign to {ptrcls.shortname.name}' raise errors.EdgeQLError(msg, context=shape_el.context) return ptrcls