def compile_path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set: """Create an ir.Set representing the given EdgeQL path expression.""" anchors = ctx.anchors if expr.partial: if ctx.partial_path_prefix is not None: path_tip = ctx.partial_path_prefix else: raise errors.QueryError( 'could not resolve partial path ', context=expr.context) extra_scopes = {} computables = [] path_sets = [] for i, step in enumerate(expr.steps): if isinstance(step, qlast.SpecialAnchor): path_tip = resolve_special_anchor(step, ctx=ctx) elif isinstance(step, qlast.ObjectRef): if i > 0: # pragma: no cover raise RuntimeError( 'unexpected ObjectRef as a non-first path item') refnode = None if not step.module and step.name not in ctx.aliased_views: # Check if the starting path label is a known anchor refnode = anchors.get(step.name) if refnode is not None: path_tip = new_set_from_set( refnode, preserve_scope_ns=True, ctx=ctx) else: stype = schemactx.get_schema_type( step, condition=lambda o: ( isinstance(o, s_types.Type) and (o.is_object_type() or o.is_view(ctx.env.schema)) ), label='object type or alias', srcctx=step.context, ctx=ctx, ) if (stype.get_expr_type(ctx.env.schema) is not None and stype.get_name(ctx.env.schema) not in ctx.view_nodes): # This is a schema-level view, as opposed to # a WITH-block or inline alias view. stype = stmtctx.declare_view_from_schema(stype, ctx=ctx) view_set = ctx.view_sets.get(stype) if view_set is not None: path_scope, path_scope_ns = ctx.path_scope_map[view_set] path_tip = new_set_from_set( view_set, preserve_scope_ns=path_scope_ns is not None, ctx=ctx, ) extra_scopes[path_tip] = path_scope.copy() else: path_tip = class_set(stype, ctx=ctx) view_scls = ctx.class_view_overrides.get(stype.id) if (view_scls is not None and view_scls != get_set_type(path_tip, ctx=ctx)): path_tip = ensure_set( path_tip, type_override=view_scls, ctx=ctx) elif isinstance(step, qlast.Ptr): # Pointer traversal step ptr_expr = step if ptr_expr.direction is not None: direction = s_pointers.PointerDirection(ptr_expr.direction) else: direction = s_pointers.PointerDirection.Outbound ptr_name = ptr_expr.ptr.name source: s_obj.Object ptr: s_pointers.PointerLike if ptr_expr.type == 'property': # Link property reference; the source is the # link immediately preceding this step in the path. if isinstance(path_tip.rptr.ptrref, irast.TypeIntersectionPointerRef): ind_prefix, ptrs = typegen.collapse_type_intersection_rptr( path_tip, ctx=ctx, ) prefix_type = get_set_type(ind_prefix.rptr.source, ctx=ctx) assert isinstance(prefix_type, s_sources.Source) prefix_ptr_name = ( next(iter(ptrs)).get_shortname(ctx.env.schema).name) ptr = schemactx.get_union_pointer( ptrname=prefix_ptr_name, source=prefix_type, direction=ind_prefix.rptr.direction, components=ptrs, ctx=ctx, ) else: ptr = typegen.ptrcls_from_ptrref( path_tip.rptr.ptrref, ctx=ctx) if isinstance(ptr, s_links.Link): source = ptr else: raise errors.QueryError( 'improper reference to link property on ' 'a non-link object', context=step.context, ) assert isinstance(ptr, s_links.Link) else: source = get_set_type(path_tip, ctx=ctx) with ctx.newscope(fenced=True, temporary=True) as subctx: if isinstance(source, s_abc.Tuple): path_tip = tuple_indirection_set( path_tip, source=source, ptr_name=ptr_name, source_context=step.context, ctx=subctx) else: path_tip = ptr_step_set( path_tip, source=source, ptr_name=ptr_name, direction=direction, ignore_computable=True, source_context=step.context, ctx=subctx) ptrcls = typegen.ptrcls_from_ptrref( path_tip.rptr.ptrref, ctx=ctx) if _is_computable_ptr(ptrcls, ctx=ctx): computables.append(path_tip) elif isinstance(step, qlast.TypeIntersection): arg_type = inference.infer_type(path_tip, ctx.env) if not isinstance(arg_type, s_objtypes.ObjectType): raise errors.QueryError( f'cannot apply type intersection operator to ' f'{arg_type.get_verbosename(ctx.env.schema)}: ' f'it is not an object type', context=step.context) if not isinstance(step.type, qlast.TypeName): raise errors.QueryError( f'complex type expressions are not supported here', context=step.context, ) typ = schemactx.get_schema_type(step.type.maintype, ctx=ctx) try: path_tip = type_intersection_set( path_tip, typ, optional=False, ctx=ctx) except errors.SchemaError as e: e.set_source_context(step.type.context) raise else: # Arbitrary expression if i > 0: # pragma: no cover raise RuntimeError( 'unexpected expression as a non-first path item') with ctx.newscope(fenced=True, temporary=True) as subctx: path_tip = ensure_set( dispatch.compile(step, ctx=subctx), ctx=subctx) if path_tip.path_id.is_type_intersection_path(): scope_set = path_tip.rptr.source else: scope_set = path_tip extra_scopes[scope_set] = subctx.path_scope for key_path_id in path_tip.path_id.iter_weak_namespace_prefixes(): mapped = ctx.view_map.get(key_path_id) if mapped is not None: path_tip = new_set( path_id=mapped.path_id, stype=get_set_type(path_tip, ctx=ctx), expr=mapped.expr, rptr=mapped.rptr, ctx=ctx) break if pathctx.path_is_banned(path_tip.path_id, ctx=ctx): dname = stype.get_displayname(ctx.env.schema) raise errors.QueryError( f'invalid reference to {dname}: ' f'self-referencing INSERTs are not allowed', hint=( f'Use DETACHED if you meant to refer to an ' f'uncorrelated {dname} set' ), context=step.context, ) path_sets.append(path_tip) path_tip.context = expr.context # Since we are attaching the computable scopes as siblings to # the subpaths they're computing, we must make sure that the # actual path head is not visible from inside the computable scope. # # Example: # type Tree { # multi link children -> Tree; # parent := .<children[IS Tree]; # } # `SELECT Tree.parent` should generate rougly the following scope tree: # # (test::Tree).>parent[IS test::Tree]: { # "BRANCH": { # "(test::Tree)" # }, # "FENCE": { # "ns@(test::Tree).<children": { # "(test::Tree) 0x7f30c7885d90" # } # }, # } # # Note that we use an unfenced BRANCH node to isolate the path head, # to make sure it is still properly factorable. We temporarily flip # the branch to be a full fence for the compilation of the computable. fence_points = frozenset(c.path_id for c in computables) fences = pathctx.register_set_in_scope( path_tip, fence_points=fence_points, ctx=ctx, ) for fence in fences: fence.fenced = True for ir_set in computables: scope = ctx.path_scope.find_descendant(ir_set.path_id) if scope is None: # The path is already in the scope, no point # in recompiling the computable expression. continue with ctx.new() as subctx: subctx.path_scope = scope comp_ir_set = computable_ptr_set(ir_set.rptr, ctx=subctx) i = path_sets.index(ir_set) if i != len(path_sets) - 1: path_sets[i + 1].rptr.source = comp_ir_set else: path_tip = comp_ir_set path_sets[i] = comp_ir_set for ir_set, scope in extra_scopes.items(): nodes = tuple( node for node in ctx.path_scope.find_descendants(ir_set.path_id) if node.parent_fence not in fences ) if not nodes: # The path portion not being a descendant means # that is is already present in the scope above us, # along with the view scope. continue assert len(nodes) == 1 nodes[0].fuse_subtree(scope) if ir_set.path_scope_id is None: pathctx.assign_set_scope(ir_set, nodes[0], ctx=ctx) for fence in fences: fence.fenced = False return path_tip
def compile_path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set: """Create an ir.Set representing the given EdgeQL path expression.""" anchors = ctx.anchors if expr.partial: if ctx.partial_path_prefix is not None: path_tip = ctx.partial_path_prefix else: raise errors.QueryError('could not resolve partial path ', context=expr.context) computables = [] path_sets = [] for i, step in enumerate(expr.steps): if isinstance(step, qlast.SpecialAnchor): path_tip = resolve_special_anchor(step, ctx=ctx) elif isinstance(step, qlast.ObjectRef): if i > 0: # pragma: no cover raise RuntimeError( 'unexpected ObjectRef as a non-first path item') refnode = None if (not step.module and s_name.UnqualName(step.name) not in ctx.aliased_views): # Check if the starting path label is a known anchor refnode = anchors.get(step.name) if refnode is not None: path_tip = new_set_from_set(refnode, preserve_scope_ns=True, ctx=ctx) else: stype = schemactx.get_schema_type( step, condition=lambda o: (isinstance(o, s_types.Type) and (o.is_object_type() or o.is_view(ctx.env.schema))), label='object type or alias', item_type=s_types.QualifiedType, srcctx=step.context, ctx=ctx, ) if (stype.get_expr_type(ctx.env.schema) is not None and stype.get_name(ctx.env.schema) not in ctx.view_nodes): # This is a schema-level view, as opposed to # a WITH-block or inline alias view. stype = stmtctx.declare_view_from_schema(stype, ctx=ctx) view_set = ctx.view_sets.get(stype) if view_set is not None: view_scope_info = ctx.path_scope_map[view_set] path_tip = new_set_from_set( view_set, preserve_scope_ns=(view_scope_info.pinned_path_id_ns is not None), is_binding=True, ctx=ctx, ) else: path_tip = class_set(stype, ctx=ctx) view_scls = ctx.class_view_overrides.get(stype.id) if (view_scls is not None and view_scls != get_set_type(path_tip, ctx=ctx)): path_tip = ensure_set(path_tip, type_override=view_scls, ctx=ctx) elif isinstance(step, qlast.Ptr): # Pointer traversal step ptr_expr = step if ptr_expr.direction is not None: direction = s_pointers.PointerDirection(ptr_expr.direction) else: direction = s_pointers.PointerDirection.Outbound ptr_name = ptr_expr.ptr.name source: s_obj.Object ptr: s_pointers.PointerLike if ptr_expr.type == 'property': # Link property reference; the source is the # link immediately preceding this step in the path. if path_tip.rptr is None: raise errors.EdgeQLSyntaxError( f"unexpected reference to link property {ptr_name!r} " "outside of a path expression", context=ptr_expr.ptr.context, ) if isinstance(path_tip.rptr.ptrref, irast.TypeIntersectionPointerRef): ind_prefix, ptrs = typegen.collapse_type_intersection_rptr( path_tip, ctx=ctx, ) assert ind_prefix.rptr is not None prefix_type = get_set_type(ind_prefix.rptr.source, ctx=ctx) assert isinstance(prefix_type, s_objtypes.ObjectType) if not ptrs: tip_type = get_set_type(path_tip, ctx=ctx) s_vn = prefix_type.get_verbosename(ctx.env.schema) t_vn = tip_type.get_verbosename(ctx.env.schema) pn = ind_prefix.rptr.ptrref.shortname.name if direction is s_pointers.PointerDirection.Inbound: s_vn, t_vn = t_vn, s_vn raise errors.InvalidReferenceError( f"property '{ptr_name}' does not exist because" f" there are no '{pn}' links between" f" {s_vn} and {t_vn}", context=ptr_expr.ptr.context, ) prefix_ptr_name = (next(iter(ptrs)).get_local_name( ctx.env.schema)) ptr = schemactx.get_union_pointer( ptrname=prefix_ptr_name, source=prefix_type, direction=ind_prefix.rptr.direction, components=ptrs, ctx=ctx, ) else: ptr = typegen.ptrcls_from_ptrref(path_tip.rptr.ptrref, ctx=ctx) if isinstance(ptr, s_links.Link): source = ptr else: raise errors.QueryError( 'improper reference to link property on ' 'a non-link object', context=step.context, ) else: source = get_set_type(path_tip, ctx=ctx) # If this is followed by type intersections, collect # them up, since we need them in ptr_step_set. upcoming_intersections = [] for j in range(i + 1, len(expr.steps)): nstep = expr.steps[j] if (isinstance(nstep, qlast.TypeIntersection) and isinstance(nstep.type, qlast.TypeName)): upcoming_intersections.append( schemactx.get_schema_type(nstep.type.maintype, ctx=ctx)) else: break if isinstance(source, s_types.Tuple): path_tip = tuple_indirection_set(path_tip, source=source, ptr_name=ptr_name, source_context=step.context, ctx=ctx) else: path_tip = ptr_step_set( path_tip, expr=step, source=source, ptr_name=ptr_name, direction=direction, upcoming_intersections=upcoming_intersections, ignore_computable=True, source_context=step.context, ctx=ctx) assert path_tip.rptr is not None ptrcls = typegen.ptrcls_from_ptrref(path_tip.rptr.ptrref, ctx=ctx) if _is_computable_ptr(ptrcls, ctx=ctx): computables.append(path_tip) elif isinstance(step, qlast.TypeIntersection): arg_type = inference.infer_type(path_tip, ctx.env) if not isinstance(arg_type, s_objtypes.ObjectType): raise errors.QueryError( f'cannot apply type intersection operator to ' f'{arg_type.get_verbosename(ctx.env.schema)}: ' f'it is not an object type', context=step.context) if not isinstance(step.type, qlast.TypeName): raise errors.QueryError( f'complex type expressions are not supported here', context=step.context, ) typ = schemactx.get_schema_type(step.type.maintype, ctx=ctx) try: path_tip = type_intersection_set(path_tip, typ, optional=False, ctx=ctx) except errors.SchemaError as e: e.set_source_context(step.type.context) raise else: # Arbitrary expression if i > 0: # pragma: no cover raise RuntimeError( 'unexpected expression as a non-first path item') # We need to fence this if the head is a mutating # statement, to make sure that the factoring allowlist # works right. is_subquery = isinstance(step, qlast.Statement) with ctx.newscope(fenced=is_subquery) as subctx: path_tip = ensure_set(dispatch.compile(step, ctx=subctx), ctx=subctx) # If the head of the path is a direct object # reference, wrap it in an expression set to give it a # new path id. This prevents the object path from being # spuriously visible to computable paths defined in a shape # at the root of a path. (See test_edgeql_select_tvariant_04 # for an example). if (path_tip.path_id.is_objtype_path() and not path_tip.path_id.is_view_path() and path_tip.path_id.src_path() is None): path_tip = expression_set(ensure_stmt(path_tip, ctx=subctx), ctx=subctx) if path_tip.path_id.is_type_intersection_path(): assert path_tip.rptr is not None scope_set = path_tip.rptr.source else: scope_set = path_tip scope_set = scoped_set(scope_set, ctx=subctx) for key_path_id in path_tip.path_id.iter_weak_namespace_prefixes(): mapped = ctx.view_map.get(key_path_id) if mapped is not None: path_tip = new_set(path_id=mapped.path_id, stype=get_set_type(path_tip, ctx=ctx), expr=mapped.expr, rptr=mapped.rptr, ctx=ctx) break if pathctx.path_is_banned(path_tip.path_id, ctx=ctx): dname = stype.get_displayname(ctx.env.schema) raise errors.QueryError( f'invalid reference to {dname}: ' f'self-referencing INSERTs are not allowed', hint=(f'Use DETACHED if you meant to refer to an ' f'uncorrelated {dname} set'), context=step.context, ) path_sets.append(path_tip) path_tip.context = expr.context # Since we are attaching the computable scopes as siblings to # the subpaths they're computing, we must make sure that the # actual path head is not visible from inside the computable scope. # # Example: # type Tree { # multi link children -> Tree; # parent := .<children[IS Tree]; # } # `SELECT Tree.parent` should generate rougly the following scope tree: # # (test::Tree).>parent[IS test::Tree]: { # "BRANCH": { # "(test::Tree)" # }, # "FENCE": { # "ns@(test::Tree).<children": { # "(test::Tree) 0x7f30c7885d90" # } # }, # } # # Note that we use an unfenced BRANCH node to isolate the path head, # to make sure it is still properly factorable. # The branch insertion is handled automatically by attach_path, and # we temporarily flip the branch to be a full fence for the compilation # of the computable. fences = pathctx.register_set_in_scope( path_tip, ctx=ctx, ) for fence in fences: fence.fenced = True for ir_set in computables: scope = ctx.path_scope.find_descendant(ir_set.path_id) if scope is None: scope = ctx.path_scope.find_visible(ir_set.path_id) # We skip recompiling if we can't find a scope for it. # This whole mechanism seems a little sketchy, unfortunately. if scope is None: continue with ctx.new() as subctx: subctx.path_scope = scope assert ir_set.rptr is not None comp_ir_set = computable_ptr_set(ir_set.rptr, ctx=subctx) i = path_sets.index(ir_set) if i != len(path_sets) - 1: prptr = path_sets[i + 1].rptr assert prptr is not None prptr.source = comp_ir_set else: path_tip = comp_ir_set path_sets[i] = comp_ir_set for fence in fences: fence.fenced = False return path_tip
def compile_path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set: """Create an ir.Set representing the given EdgeQL path expression.""" anchors = ctx.anchors if expr.partial: if ctx.partial_path_prefix is not None: path_tip = ctx.partial_path_prefix else: raise errors.QueryError('could not resolve partial path ', context=expr.context) extra_scopes = {} computables = [] path_sets = [] for i, step in enumerate(expr.steps): if isinstance(step, qlast.SpecialAnchor): path_tip = resolve_special_anchor(step, ctx=ctx) elif isinstance(step, qlast.ObjectRef): if i > 0: # pragma: no cover raise RuntimeError( 'unexpected ObjectRef as a non-first path item') refnode = None if not step.module and step.name not in ctx.aliased_views: # Check if the starting path label is a known anchor refnode = anchors.get(step.name) if refnode is not None: path_tip = new_set_from_set(refnode, preserve_scope_ns=True, ctx=ctx) else: stype = schemactx.get_schema_type( step, condition=lambda o: (o.is_object_type() or o.is_view(ctx.env.schema)), label='object type or view', srcctx=step.context, ctx=ctx, ) if (stype.get_view_type(ctx.env.schema) is not None and stype.get_name(ctx.env.schema) not in ctx.view_nodes): # This is a schema-level view, as opposed to # a WITH-block or inline alias view. stype = stmtctx.declare_view_from_schema(stype, ctx=ctx) view_set = ctx.view_sets.get(stype) if view_set is not None: path_tip = new_set_from_set(view_set, ctx=ctx) path_scope = ctx.path_scope_map[view_set] extra_scopes[path_tip] = path_scope.copy() else: path_tip = class_set(stype, ctx=ctx) view_scls = ctx.class_view_overrides.get(stype.id) if (view_scls is not None and view_scls != get_set_type(path_tip, ctx=ctx)): path_tip = ensure_set(path_tip, type_override=view_scls, ctx=ctx) elif isinstance(step, qlast.Ptr): # Pointer traversal step ptr_expr = step if ptr_expr.direction is not None: direction = s_pointers.PointerDirection(ptr_expr.direction) else: direction = s_pointers.PointerDirection.Outbound ptr_name = ptr_expr.ptr.name source: typing.Union[s_types.Type, s_pointers.PointerLike] if ptr_expr.type == 'property': # Link property reference; the source is the # link immediately preceding this step in the path. source = irtyputils.ptrcls_from_ptrref(path_tip.rptr.ptrref, schema=ctx.env.schema) else: source = get_set_type(path_tip, ctx=ctx) with ctx.newscope(fenced=True, temporary=True) as subctx: if isinstance(source, s_abc.Tuple): path_tip = tuple_indirection_set( path_tip, source=source, ptr_name=ptr_name, source_context=step.context, ctx=subctx) else: path_tip = ptr_step_set(path_tip, source=source, ptr_name=ptr_name, direction=direction, ignore_computable=True, source_context=step.context, ctx=subctx) ptrcls = irtyputils.ptrcls_from_ptrref( path_tip.rptr.ptrref, schema=ctx.env.schema) if _is_computable_ptr(ptrcls, ctx=ctx): computables.append(path_tip) elif isinstance(step, qlast.TypeIndirection): arg_type = inference.infer_type(path_tip, ctx.env) if not isinstance(arg_type, s_objtypes.ObjectType): raise errors.QueryError( f'invalid type filter operand: ' f'{arg_type.get_displayname(ctx.env.schema)} ' f'is not an object type', context=step.context) if not isinstance(step.type, qlast.TypeName): raise errors.QueryError( f'complex type expressions are not supported here', context=step.context, ) typ = schemactx.get_schema_type(step.type.maintype, ctx=ctx) if not isinstance(typ, s_objtypes.ObjectType): raise errors.QueryError( f'invalid type filter operand: ' f'{typ.get_displayname(ctx.env.schema)} is not ' f'an object type', context=step.type.context) # The expression already of the desired type, elide # the indirection. if arg_type != typ: path_tip = class_indirection_set(path_tip, typ, optional=False, ctx=ctx) else: # Arbitrary expression if i > 0: # pragma: no cover raise RuntimeError( 'unexpected expression as a non-first path item') with ctx.newscope(fenced=True, temporary=True) as subctx: path_tip = ensure_set(dispatch.compile(step, ctx=subctx), ctx=subctx) if path_tip.path_id.is_type_indirection_path(): scope_set = path_tip.rptr.source else: scope_set = path_tip extra_scopes[scope_set] = subctx.path_scope for key_path_id in path_tip.path_id.iter_weak_namespace_prefixes(): mapped = ctx.view_map.get(key_path_id) if mapped is not None: path_tip = new_set(path_id=mapped.path_id, stype=get_set_type(path_tip, ctx=ctx), expr=mapped.expr, rptr=mapped.rptr, ctx=ctx) break if pathctx.path_is_banned(path_tip.path_id, ctx=ctx): dname = stype.get_displayname(ctx.env.schema) raise errors.QueryError( f'invalid reference to {dname}: ' f'self-referencing INSERTs are not allowed', hint=(f'Use DETACHED if you meant to refer to an ' f'uncorrelated {dname} set'), context=step.context, ) path_sets.append(path_tip) path_tip.context = expr.context pathctx.register_set_in_scope(path_tip, ctx=ctx) for ir_set in computables: scope = ctx.path_scope.find_descendant(ir_set.path_id) if scope is None: # The path is already in the scope, no point # in recompiling the computable expression. continue with ctx.new() as subctx: subctx.path_scope = scope comp_ir_set = computable_ptr_set(ir_set.rptr, ctx=subctx) i = path_sets.index(ir_set) if i != len(path_sets) - 1: path_sets[i + 1].rptr.source = comp_ir_set else: path_tip = comp_ir_set path_sets[i] = comp_ir_set for ir_set, scope in extra_scopes.items(): node = ctx.path_scope.find_descendant(ir_set.path_id) if node is None: # The path portion not being a descendant means # that is is already present in the scope above us, # along with the view scope. continue fuse_scope_branch(ir_set, node, scope, ctx=ctx) if ir_set.path_scope_id is None: pathctx.assign_set_scope(ir_set, node, ctx=ctx) return path_tip