Exemplo n.º 1
0
def get_params_symtable(
    params: FuncParameterList,
    schema: s_schema.Schema,
    *,
    inlined_defaults: bool,
) -> Dict[str, qlast.Expr]:

    anchors: Dict[str, qlast.Expr] = {}

    defaults_mask = qlast.TypeCast(
        expr=qlast.Parameter(name='__defaults_mask__', optional=False),
        type=qlast.TypeName(
            maintype=qlast.ObjectRef(
                module='std',
                name='bytes',
            ),
        ),
    )

    for pi, p in enumerate(params.get_in_canonical_order(schema)):
        p_shortname = p.get_parameter_name(schema)
        p_is_optional = p.get_typemod(schema) is not ft.TypeModifier.SINGLETON
        anchors[p_shortname] = qlast.TypeCast(
            expr=qlast.Parameter(
                name=p_shortname,
                optional=p_is_optional,
            ),
            type=utils.typeref_to_ast(schema, p.get_type(schema)),
        )

        p_default = p.get_default(schema)
        if p_default is None:
            continue

        if not inlined_defaults:
            continue

        anchors[p_shortname] = qlast.IfElse(
            condition=qlast.BinOp(
                left=qlast.FunctionCall(
                    func=('std', 'bytes_get_bit'),
                    args=[
                        defaults_mask,
                        qlast.IntegerConstant(value=str(pi)),
                    ]),
                op='=',
                right=qlast.IntegerConstant(value='0'),
            ),
            if_expr=anchors[p_shortname],
            else_expr=qlast._Optional(expr=p_default.qlast),
        )

    return anchors
Exemplo n.º 2
0
def get_param_anchors_for_callable(
    params: s_func.ParameterLikeList,
    schema: s_schema.Schema,
    *,
    inlined_defaults: bool,
) -> Tuple[Dict[str, irast.Parameter], List[qlast.AliasedExpr], ]:
    anchors = {}
    aliases = []

    if inlined_defaults:
        anchors['__defaults_mask__'] = irast.Parameter(
            name='__defaults_mask__',
            typeref=irtyputils.type_to_typeref(  # note: no cache
                schema,
                cast(s_scalars.ScalarType, schema.get('std::bytes')),
            ),
        )

    pg_params = s_func.PgParams.from_params(schema, params)
    for pi, p in enumerate(pg_params.params):
        p_shortname = p.get_shortname(schema)
        anchors[p_shortname] = irast.Parameter(
            name=p_shortname,
            typeref=irtyputils.type_to_typeref(schema, p.get_type(schema)))

        if p.get_default(schema) is None:
            continue

        if not inlined_defaults:
            continue

        aliases.append(
            qlast.AliasedExpr(
                alias=p_shortname,
                expr=qlast.
                IfElse(condition=qlast.BinOp(left=qlast.FunctionCall(
                    func=('std', 'bytes_get_bit'),
                    args=[
                        qlast.Path(
                            steps=[qlast.ObjectRef(name='__defaults_mask__')]),
                        qlast.IntegerConstant(value=str(pi)),
                    ]),
                                             right=qlast.IntegerConstant(
                                                 value='0'),
                                             op='='),
                       if_expr=qlast.Path(
                           steps=[qlast.ObjectRef(name=p_shortname)]),
                       else_expr=qlast._Optional(
                           expr=p.get_ql_default(schema)))))

    return anchors, aliases
Exemplo n.º 3
0
    def _visit_query(self, node):
        # populate input variables with defaults, where applicable
        if node.variable_definitions:
            self.visit(node.variable_definitions)

        # base Query needs to be configured specially
        base = self._context.gqlcore.get('Query')

        # special treatment of the selection_set, different from inner
        # recursion
        query = qlast.SelectQuery(
            result=qlast.Shape(
                expr=qlast.Path(
                    steps=[qlast.ObjectRef(name='Query',
                                           module='stdgraphql')]
                ),
                elements=[]
            ),
            limit=qlast.IntegerConstant(value='1')
        )

        self._context.fields.append({})
        self._context.path.append([Step(None, base)])
        query.result.elements = self.visit(node.selection_set)
        self._context.fields.pop()
        self._context.path.pop()

        return query
Exemplo n.º 4
0
def generate_config_query(schema) -> str:
    cfg = schema.get('cfg::Config')

    ref = qlast.ObjectRef(name='Config', module='cfg')
    query = qlast.SelectQuery(
        result=qlast.Shape(
            expr=qlast.Path(steps=[ref]),
            elements=qlcompiler.get_config_type_shape(schema, cfg, path=[ref]),
        ),
        limit=qlast.IntegerConstant(value='1', ),
    )

    return qlcodegen.generate_source(query)
Exemplo n.º 5
0
def get_param_anchors_for_callable(params, schema):
    anchors = {}
    aliases = []

    anchors['__defaults_mask__'] = irast.Parameter(
        name='__defaults_mask__',
        typeref=irtyputils.type_to_typeref(schema, schema.get('std::bytes')))

    pg_params = s_func.PgParams.from_params(schema, params)
    for pi, p in enumerate(pg_params.params):
        p_shortname = p.get_shortname(schema)
        anchors[p_shortname] = irast.Parameter(
            name=p_shortname,
            typeref=irtyputils.type_to_typeref(schema, p.get_type(schema)))

        if p.get_default(schema) is None:
            continue

        aliases.append(
            qlast.AliasedExpr(
                alias=p_shortname,
                expr=qlast.
                IfElse(condition=qlast.BinOp(left=qlast.FunctionCall(
                    func=('std', 'bytes_get_bit'),
                    args=[
                        qlast.Path(
                            steps=[qlast.ObjectRef(name='__defaults_mask__')]),
                        qlast.IntegerConstant(value=str(pi)),
                    ]),
                                             right=qlast.IntegerConstant(
                                                 value='0'),
                                             op='='),
                       if_expr=qlast.Path(
                           steps=[qlast.ObjectRef(name=p_shortname)]),
                       else_expr=qlast._Optional(
                           expr=p.get_ql_default(schema)))))

    return anchors, aliases
Exemplo n.º 6
0
def const_ast_from_python(val: Any) -> qlast.BaseConstant:
    if isinstance(val, str):
        return qlast.StringConstant.from_python(val)
    elif isinstance(val, bool):
        return qlast.BooleanConstant(value='true' if val else 'false')
    elif isinstance(val, int):
        if MIN_INT64 <= val <= MAX_INT64:
            return qlast.IntegerConstant(value=str(val))
        else:
            raise ValueError(f'int64 value out of range: {val}')
    elif isinstance(val, decimal.Decimal):
        return qlast.DecimalConstant(value=f'{val}n')
    elif isinstance(val, float):
        return qlast.FloatConstant(value=str(val))
    elif isinstance(val, bytes):
        return qlast.BytesConstant.from_python(value=val)
    else:
        raise ValueError(f'unexpected constant type: {type(val)!r}')
Exemplo n.º 7
0
def compile_SelectQuery(
        expr: qlast.SelectQuery, *, ctx: context.ContextLevel) -> irast.Set:
    with ctx.subquery() as sctx:
        stmt = irast.SelectStmt()
        init_stmt(stmt, expr, ctx=sctx, parent_ctx=ctx)
        if expr.implicit:
            # Make sure path prefix does not get blown away by
            # implicit subqueries.
            sctx.partial_path_prefix = ctx.partial_path_prefix
            stmt.implicit_wrapper = True

        if (
            (ctx.expr_exposed or sctx.stmt is ctx.toplevel_stmt)
            and ctx.implicit_limit
            and expr.limit is None
            and not ctx.inhibit_implicit_limit
        ):
            expr.limit = qlast.IntegerConstant(value=str(ctx.implicit_limit))

        stmt.result = compile_result_clause(
            expr.result,
            view_scls=ctx.view_scls,
            view_rptr=ctx.view_rptr,
            result_alias=expr.result_alias,
            view_name=ctx.toplevel_result_view_name,
            ctx=sctx)

        clauses.compile_where_clause(
            stmt, expr.where, ctx=sctx)

        stmt.orderby = clauses.compile_orderby_clause(
            expr.orderby, ctx=sctx)

        stmt.offset = clauses.compile_limit_offset_clause(
            expr.offset, ctx=sctx)

        stmt.limit = clauses.compile_limit_offset_clause(
            expr.limit, ctx=sctx)

        result = fini_stmt(stmt, expr, ctx=sctx, parent_ctx=ctx)

    return result
Exemplo n.º 8
0
    def _get_general_offset_limit(self, after, before, first, last):
        # convert any static values to corresponding qlast
        if after is not None:
            if isinstance(after, qlast.Base):
                after = qlast.TypeCast(type=qlast.TypeName(
                    maintype=qlast.ObjectRef(name='int64')),
                                       expr=after)
            else:
                after = qlast.BaseConstant.from_python(after)
        if before is not None:
            if isinstance(before, qlast.Base):
                before = qlast.TypeCast(type=qlast.TypeName(
                    maintype=qlast.ObjectRef(name='int64')),
                                        expr=before)
            else:
                before = qlast.BaseConstant.from_python(before)
        if first is not None and not isinstance(first, qlast.Base):
            first = qlast.BaseConstant.from_python(first)
        if last is not None and not isinstance(last, qlast.Base):
            last = qlast.BaseConstant.from_python(last)

        offset = limit = None
        # convert before, after, first and last into offset and limit
        if after is not None:
            # The +1 is to make 'after' into an appropriate index.
            #
            # 0--a--1--b--2--c--3-- ... we call element at
            # index 0 (or "element 0" for short), the element
            # immediately after the mark 0. So after "element
            # 0" really means after "index 1".
            offset = qlast.BinOp(left=after,
                                 op='+',
                                 right=qlast.IntegerConstant(value='1'))

        if before is not None:
            # limit = before - (after or 0)
            if after:
                limit = qlast.BinOp(left=before, op='-', right=after)
            else:
                limit = before

        if first is not None:
            if limit is None:
                limit = first
            else:
                limit = qlast.IfElse(if_expr=first,
                                     condition=qlast.BinOp(left=first,
                                                           op='<',
                                                           right=limit),
                                     else_expr=limit)

        if last is not None:
            if limit is not None:
                if offset:
                    offset = qlast.BinOp(left=offset,
                                         op='+',
                                         right=qlast.BinOp(left=limit,
                                                           op='-',
                                                           right=last))
                else:
                    offset = qlast.BinOp(left=limit, op='-', right=last)

                limit = qlast.IfElse(if_expr=last,
                                     condition=qlast.BinOp(left=last,
                                                           op='<',
                                                           right=limit),
                                     else_expr=limit)

            else:
                # FIXME: there wasn't any limit, so we can define last
                # in terms of offset alone without negative OFFSET
                # implementation
                raise g_errors.GraphQLTranslationError(
                    f'last translates to a negative OFFSET in '
                    f'EdgeQL which is currently unsupported')

        return offset, limit
Exemplo n.º 9
0
def compile_ForQuery(
        qlstmt: qlast.ForQuery, *, ctx: context.ContextLevel) -> irast.Set:
    with ctx.subquery() as sctx:
        stmt = irast.SelectStmt()
        init_stmt(stmt, qlstmt, ctx=sctx, parent_ctx=ctx)

        with sctx.newscope(fenced=True) as scopectx:
            iterator_ctx = None
            if (ctx.expr_exposed and ctx.iterator_ctx is not None
                    and ctx.iterator_ctx is not sctx):
                iterator_ctx = ctx.iterator_ctx

            if iterator_ctx is not None:
                iterator_scope_parent = iterator_ctx.path_scope
                path_id_ns = iterator_ctx.path_id_namespace
            else:
                iterator_scope_parent = sctx.path_scope
                path_id_ns = sctx.path_id_namespace

            iterator = qlstmt.iterator
            if isinstance(iterator, qlast.Set) and len(iterator.elements) == 1:
                iterator = iterator.elements[0]

            iterator_view = stmtctx.declare_view(
                iterator, qlstmt.iterator_alias,
                path_id_namespace=path_id_ns, ctx=scopectx)

            iterator_stmt = setgen.new_set_from_set(
                iterator_view, preserve_scope_ns=True, ctx=scopectx)

            if iterator_ctx is not None and iterator_ctx.stmt is not None:
                iterator_ctx.stmt.hoisted_iterators.append(iterator_stmt)

            stmt.iterator_stmt = iterator_stmt

            view_scope_info = scopectx.path_scope_map[iterator_view]
            iterator_scope = view_scope_info.path_scope

        pathctx.register_set_in_scope(
            iterator_stmt,
            path_scope=iterator_scope_parent,
            ctx=sctx,
        )
        # Iterator symbol is, by construction, outside of the scope
        # of the UNION argument, but is perfectly legal to be referenced
        # inside a factoring fence that is an immediate child of this
        # scope.
        iterator_scope_parent.factoring_whitelist.add(
            stmt.iterator_stmt.path_id)
        node = iterator_scope_parent.find_descendant(iterator_stmt.path_id)
        if node is not None:
            node.attach_subtree(iterator_scope)

        stmt.result = compile_result_clause(
            qlstmt.result,
            view_scls=ctx.view_scls,
            view_rptr=ctx.view_rptr,
            result_alias=qlstmt.result_alias,
            view_name=ctx.toplevel_result_view_name,
            forward_rptr=True,
            ctx=sctx)

        if ((ctx.expr_exposed or sctx.stmt is ctx.toplevel_stmt)
                and ctx.implicit_limit):
            stmt.limit = setgen.ensure_set(
                dispatch.compile(
                    qlast.IntegerConstant(value=str(ctx.implicit_limit)),
                    ctx=sctx,
                ),
                ctx=sctx,
            )

        result = fini_stmt(stmt, qlstmt, ctx=sctx, parent_ctx=ctx)

    return result
Exemplo n.º 10
0
def _normalize_view_ptr_expr(
        shape_el: qlast.ShapeElement,
        view_scls: s_objtypes.ObjectType, *,
        path_id: irast.PathId,
        path_id_namespace: Optional[irast.WeakNamespace]=None,
        is_insert: bool=False,
        is_update: bool=False,
        from_default: bool=False,
        view_rptr: Optional[context.ViewRPtr]=None,
        ctx: context.ContextLevel) -> s_pointers.Pointer:
    steps = shape_el.expr.steps
    is_linkprop = False
    is_polymorphic = False
    is_mutation = is_insert or is_update
    # Pointers may be qualified by the explicit source
    # class, which is equivalent to Expr[IS Type].
    plen = len(steps)
    ptrsource: s_sources.Source = view_scls
    qlexpr: Optional[qlast.Expr] = None
    target_typexpr = None
    source: qlast.Base
    base_ptrcls_is_alias = False

    if plen >= 2 and isinstance(steps[-1], qlast.TypeIntersection):
        # Target type intersection: foo: Type
        target_typexpr = steps[-1].type
        plen -= 1
        steps = steps[:-1]

    if plen == 1:
        # regular shape
        lexpr = steps[0]
        assert isinstance(lexpr, qlast.Ptr)
        is_linkprop = lexpr.type == 'property'
        if is_linkprop:
            if view_rptr is None or view_rptr.ptrcls is None:
                raise errors.QueryError(
                    'invalid reference to link property '
                    'in top level shape', context=lexpr.context)
            assert isinstance(view_rptr.ptrcls, s_links.Link)
            ptrsource = view_rptr.ptrcls
        source = qlast.Source()
    elif plen == 2 and isinstance(steps[0], qlast.TypeIntersection):
        # Source type intersection: [IS Type].foo
        source = qlast.Path(steps=[
            qlast.Source(),
            steps[0],
        ])
        lexpr = steps[1]
        ptype = steps[0].type
        if not isinstance(ptype, qlast.TypeName):
            raise errors.QueryError(
                'complex type expressions are not supported here',
                context=ptype.context,
            )
        source_spec = schemactx.get_schema_type(ptype.maintype, ctx=ctx)
        if not isinstance(source_spec, s_objtypes.ObjectType):
            raise errors.QueryError(
                f'expected object type, got '
                f'{source_spec.get_verbosename(ctx.env.schema)}',
                context=ptype.context,
            )
        ptrsource = source_spec
        is_polymorphic = True
    else:  # pragma: no cover
        raise RuntimeError(
            f'unexpected path length in view shape: {len(steps)}')

    assert isinstance(lexpr, qlast.Ptr)
    ptrname = lexpr.ptr.name

    compexpr: Optional[qlast.Expr] = shape_el.compexpr
    if compexpr is None and is_insert and shape_el.elements:
        # Short shape form in INSERT, e.g
        #     INSERT Foo { bar: Spam { name := 'name' }}
        # is prohibited.
        raise errors.EdgeQLSyntaxError(
            "unexpected ':'", context=steps[-1].context)

    ptrcls: Optional[s_pointers.Pointer]

    if compexpr is None:
        ptrcls = setgen.resolve_ptr(
            ptrsource, ptrname, track_ref=lexpr, ctx=ctx)
        if is_polymorphic:
            ptrcls = schemactx.derive_ptr(
                ptrcls, view_scls,
                is_insert=is_insert,
                is_update=is_update,
                ctx=ctx)

        base_ptrcls = ptrcls.get_bases(ctx.env.schema).first(ctx.env.schema)
        base_ptr_is_computable = base_ptrcls in ctx.source_map
        ptr_name = sn.QualName(
            module='__',
            name=ptrcls.get_shortname(ctx.env.schema).name,
        )

        base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx)
        base_is_singleton = False
        if base_cardinality is not None and base_cardinality.is_known():
            base_is_singleton = base_cardinality.is_single()

        if (
            shape_el.where
            or shape_el.orderby
            or shape_el.offset
            or shape_el.limit
            or base_ptr_is_computable
            or is_polymorphic
            or target_typexpr is not None
            or (ctx.implicit_limit and not base_is_singleton)
        ):

            if target_typexpr is None:
                qlexpr = qlast.Path(steps=[source, lexpr])
            else:
                qlexpr = qlast.Path(steps=[
                    source,
                    lexpr,
                    qlast.TypeIntersection(type=target_typexpr),
                ])

            qlexpr = astutils.ensure_qlstmt(qlexpr)
            assert isinstance(qlexpr, qlast.SelectQuery)
            qlexpr.where = shape_el.where
            qlexpr.orderby = shape_el.orderby

            if shape_el.offset or shape_el.limit:
                qlexpr = qlast.SelectQuery(result=qlexpr, implicit=True)
                qlexpr.offset = shape_el.offset
                qlexpr.limit = shape_el.limit

            if (
                (ctx.expr_exposed or ctx.stmt is ctx.toplevel_stmt)
                and not qlexpr.limit
                and ctx.implicit_limit
                and not base_is_singleton
            ):
                qlexpr.limit = qlast.IntegerConstant(
                    value=str(ctx.implicit_limit),
                )

        if target_typexpr is not None:
            assert isinstance(target_typexpr, qlast.TypeName)
            intersector_type = schemactx.get_schema_type(
                target_typexpr.maintype, ctx=ctx)

            int_result = schemactx.apply_intersection(
                ptrcls.get_target(ctx.env.schema),  # type: ignore
                intersector_type,
                ctx=ctx,
            )

            ptr_target = int_result.stype
        else:
            _ptr_target = ptrcls.get_target(ctx.env.schema)
            assert _ptr_target
            ptr_target = _ptr_target

        ptr_cardinality = base_cardinality
        if ptr_cardinality is None or not ptr_cardinality.is_known():
            # We do not know the parent's pointer cardinality yet.
            ctx.env.pointer_derivation_map[base_ptrcls].append(ptrcls)
            ctx.env.pointer_specified_info[ptrcls] = (
                shape_el.cardinality, shape_el.required, shape_el.context)

        implicit_tid = has_implicit_type_computables(
            ptr_target,
            is_mutation=is_mutation,
            ctx=ctx,
        )

        if shape_el.elements or implicit_tid:
            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 = pathctx.extend_path_id(
                path_id,
                ptrcls=base_ptrcls,
                ns=ctx.path_id_namespace,
                ctx=ctx)

            ctx.path_scope.attach_path(sub_path_id,
                                       context=shape_el.context)

            if not isinstance(ptr_target, s_objtypes.ObjectType):
                raise errors.QueryError(
                    f'shapes cannot be applied to '
                    f'{ptr_target.get_verbosename(ctx.env.schema)}',
                    context=shape_el.context,
                )

            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.QueryError(
                            'only references to link properties are allowed '
                            'in nested UPDATE shapes', context=subel.context)

                ptr_target = _process_view(
                    stype=ptr_target, path_id=sub_path_id,
                    path_id_namespace=path_id_namespace,
                    view_rptr=sub_view_rptr,
                    elements=shape_el.elements, is_update=True,
                    parser_context=shape_el.context,
                    ctx=ctx)
            else:
                ptr_target = _process_view(
                    stype=ptr_target, path_id=sub_path_id,
                    path_id_namespace=path_id_namespace,
                    view_rptr=sub_view_rptr,
                    elements=shape_el.elements,
                    parser_context=shape_el.context,
                    ctx=ctx)

    else:
        base_ptrcls = ptrcls = None

        if (is_mutation
                and ptrname not in ctx.special_computables_in_mutation_shape):
            # If this is a mutation, the pointer must exist.
            ptrcls = setgen.resolve_ptr(
                ptrsource, ptrname, track_ref=lexpr, ctx=ctx)

            base_ptrcls = ptrcls.get_bases(
                ctx.env.schema).first(ctx.env.schema)

            ptr_name = sn.QualName(
                module='__',
                name=ptrcls.get_shortname(ctx.env.schema).name,
            )

        else:
            ptr_name = sn.QualName(
                module='__',
                name=ptrname,
            )

            try:
                ptrcls = setgen.resolve_ptr(
                    ptrsource,
                    ptrname,
                    track_ref=False,
                    ctx=ctx,
                )

                base_ptrcls = ptrcls.get_bases(
                    ctx.env.schema).first(ctx.env.schema)
            except errors.InvalidReferenceError:
                # This is a NEW computable pointer, it's fine.
                pass

        qlexpr = astutils.ensure_qlstmt(compexpr)

        if ((ctx.expr_exposed or ctx.stmt is ctx.toplevel_stmt)
                and ctx.implicit_limit
                and isinstance(qlexpr, qlast.OffsetLimitMixin)
                and not qlexpr.limit):
            qlexpr.limit = qlast.IntegerConstant(value=str(ctx.implicit_limit))

        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.defining_view = view_scls
            shape_expr_ctx.path_scope.unnest_fence = True
            shape_expr_ctx.partial_path_prefix = setgen.class_set(
                view_scls.get_bases(ctx.env.schema).first(ctx.env.schema),
                path_id=path_id, ctx=shape_expr_ctx)
            prefix_rptrref = path_id.rptr()
            if prefix_rptrref is not None:
                # Source path seems to contain multiple steps,
                # so set up a rptr for abbreviated link property
                # paths.
                src_path_id = path_id.src_path()
                assert src_path_id is not None
                ctx.env.schema, src_t = irtyputils.ir_typeref_to_type(
                    shape_expr_ctx.env.schema,
                    src_path_id.target,
                )
                prefix_rptr = irast.Pointer(
                    source=setgen.class_set(
                        src_t,
                        path_id=src_path_id,
                        ctx=shape_expr_ctx,
                    ),
                    target=shape_expr_ctx.partial_path_prefix,
                    ptrref=prefix_rptrref,
                    direction=s_pointers.PointerDirection.Outbound,
                )
                shape_expr_ctx.partial_path_prefix.rptr = prefix_rptr

            if is_mutation and ptrcls is not None:
                shape_expr_ctx.expr_exposed = True
                shape_expr_ctx.empty_result_type_hint = \
                    ptrcls.get_target(ctx.env.schema)

            shape_expr_ctx.stmt_metadata[qlexpr] = context.StatementMetadata(
                iterator_target=True,
            )
            irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx)

            if (
                shape_el.operation.op is qlast.ShapeOp.APPEND
                or shape_el.operation.op is qlast.ShapeOp.SUBTRACT
            ):
                if not is_update:
                    op = (
                        '+=' if shape_el.operation.op is qlast.ShapeOp.APPEND
                        else '-='
                    )
                    raise errors.EdgeQLSyntaxError(
                        f"unexpected '{op}'",
                        context=shape_el.operation.context,
                    )

            irexpr.context = compexpr.context

            if base_ptrcls is None:
                base_ptrcls = shape_expr_ctx.view_rptr.base_ptrcls
                base_ptrcls_is_alias = shape_expr_ctx.view_rptr.ptrcls_is_alias

            if ptrcls is not None:
                ctx.env.schema = ptrcls.set_field_value(
                    ctx.env.schema, 'owned', True)

        ptr_cardinality = None
        ptr_target = inference.infer_type(irexpr, ctx.env)

        if (
            isinstance(ptr_target, s_types.Collection)
            and not ctx.env.orig_schema.get_by_id(ptr_target.id, default=None)
        ):
            # Record references to implicitly defined collection types,
            # so that the alias delta machinery can pick them up.
            ctx.env.created_schema_objects.add(ptr_target)

        anytype = ptr_target.find_any(ctx.env.schema)
        if anytype is not None:
            raise errors.QueryError(
                'expression returns value of indeterminate type',
                context=ctx.env.type_origins.get(anytype),
            )

        # Validate that the insert/update expression is
        # of the correct class.
        if is_mutation and ptrcls is not None:
            base_target = ptrcls.get_target(ctx.env.schema)
            assert base_target is not None
            if ptr_target.assignment_castable_to(
                    base_target,
                    schema=ctx.env.schema):
                # Force assignment casts if the target type is not a
                # subclass of the base type and the cast is not to an
                # object type.
                if not (
                    base_target.is_object_type()
                    or s_types.is_type_compatible(
                        base_target, ptr_target, schema=ctx.env.schema
                    )
                ):
                    qlexpr = astutils.ensure_qlstmt(qlast.TypeCast(
                        type=typegen.type_to_ql_typeref(base_target, ctx=ctx),
                        expr=compexpr,
                    ))
                    ptr_target = base_target

            else:
                expected = [
                    repr(str(base_target.get_displayname(ctx.env.schema)))
                ]

                ercls: Type[errors.EdgeDBError]
                if ptrcls.is_property(ctx.env.schema):
                    ercls = errors.InvalidPropertyTargetError
                else:
                    ercls = errors.InvalidLinkTargetError

                ptr_vn = ptrcls.get_verbosename(ctx.env.schema,
                                                with_parent=True)

                raise ercls(
                    f'invalid target for {ptr_vn}: '
                    f'{str(ptr_target.get_displayname(ctx.env.schema))!r} '
                    f'(expecting {" or ".join(expected)})'
                )

    if qlexpr is not None or ptrcls is None:
        src_scls: s_sources.Source

        if is_linkprop:
            # Proper checking was done when is_linkprop is defined.
            assert view_rptr is not None
            assert isinstance(view_rptr.ptrcls, s_links.Link)
            src_scls = view_rptr.ptrcls
        else:
            src_scls = view_scls

        if ptr_target.is_object_type():
            base = ctx.env.get_track_schema_object(
                sn.QualName('std', 'link'), expr=None)
        else:
            base = ctx.env.get_track_schema_object(
                sn.QualName('std', 'property'), expr=None)

        if base_ptrcls is not None:
            derive_from = base_ptrcls
        else:
            derive_from = base

        derived_name = schemactx.derive_view_name(
            base_ptrcls,
            derived_name_base=ptr_name,
            derived_name_quals=[str(src_scls.get_name(ctx.env.schema))],
            ctx=ctx,
        )

        existing = ctx.env.schema.get(
            derived_name, default=None, type=s_pointers.Pointer)
        if existing is not None:
            existing_target = existing.get_target(ctx.env.schema)
            assert existing_target is not None
            if ctx.recompiling_schema_alias:
                ptr_cardinality = existing.get_cardinality(ctx.env.schema)
            if ptr_target == existing_target:
                ptrcls = existing
            elif ptr_target.implicitly_castable_to(
                    existing_target, ctx.env.schema):
                ctx.env.schema = existing.set_target(
                    ctx.env.schema, ptr_target)
                ptrcls = existing
            else:
                vnp = existing.get_verbosename(
                    ctx.env.schema, with_parent=True)

                t1_vn = existing_target.get_verbosename(ctx.env.schema)
                t2_vn = ptr_target.get_verbosename(ctx.env.schema)

                if compexpr is not None:
                    source_context = compexpr.context
                else:
                    source_context = shape_el.expr.steps[-1].context
                raise errors.SchemaError(
                    f'cannot redefine {vnp} as {t2_vn}',
                    details=f'{vnp} is defined as {t1_vn}',
                    context=source_context,
                )
        else:
            ptrcls = schemactx.derive_ptr(
                derive_from, src_scls, ptr_target,
                is_insert=is_insert,
                is_update=is_update,
                derived_name=derived_name,
                ctx=ctx)

    elif ptrcls.get_target(ctx.env.schema) != ptr_target:
        ctx.env.schema = ptrcls.set_target(ctx.env.schema, ptr_target)

    assert ptrcls is not None

    if qlexpr is None:
        # This is not a computable, just a pointer
        # to a nested shape.  Have it reuse the original
        # pointer name so that in `Foo.ptr.name` and
        # `Foo { ptr: {name}}` are the same path.
        path_id_name = base_ptrcls.get_name(ctx.env.schema)
        ctx.env.schema = ptrcls.set_field_value(
            ctx.env.schema, 'path_id_name', path_id_name
        )

    if qlexpr is not None:
        ctx.source_map[ptrcls] = irast.ComputableInfo(
            qlexpr=qlexpr,
            context=ctx,
            path_id=path_id,
            path_id_ns=path_id_namespace,
            shape_op=shape_el.operation.op,
        )

    if compexpr is not None or is_polymorphic:
        ctx.env.schema = ptrcls.set_field_value(
            ctx.env.schema,
            'computable',
            True,
        )

        ctx.env.schema = ptrcls.set_field_value(
            ctx.env.schema,
            'owned',
            True,
        )

    if ptr_cardinality is not None:
        ctx.env.schema = ptrcls.set_field_value(
            ctx.env.schema, 'cardinality', ptr_cardinality)
    else:
        if qlexpr is None and ptrcls is not base_ptrcls:
            ctx.env.pointer_derivation_map[base_ptrcls].append(ptrcls)

        base_cardinality = None
        base_required = False
        if base_ptrcls is not None and not base_ptrcls_is_alias:
            base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx)
            base_required = base_ptrcls.get_required(ctx.env.schema)

        if base_cardinality is None or not base_cardinality.is_known():
            specified_cardinality = shape_el.cardinality
            specified_required = shape_el.required
        else:
            specified_cardinality = base_cardinality
            specified_required = base_required

            if (shape_el.cardinality is not None
                    and base_ptrcls is not None
                    and shape_el.cardinality != base_cardinality):
                base_src = base_ptrcls.get_source(ctx.env.schema)
                assert base_src is not None
                base_src_name = base_src.get_verbosename(ctx.env.schema)
                raise errors.SchemaError(
                    f'cannot redefine the cardinality of '
                    f'{ptrcls.get_verbosename(ctx.env.schema)}: '
                    f'it is defined as {base_cardinality.as_ptr_qual()!r} '
                    f'in the base {base_src_name}',
                    context=compexpr and compexpr.context,
                )
            # The required flag may be inherited from the base
            specified_required = shape_el.required or base_required

        ctx.env.pointer_specified_info[ptrcls] = (
            specified_cardinality, specified_required, shape_el.context)

        ctx.env.schema = ptrcls.set_field_value(
            ctx.env.schema, 'cardinality', qltypes.SchemaCardinality.Unknown)

    if (
        ptrcls.is_protected_pointer(ctx.env.schema)
        and qlexpr is not None
        and not from_default
        and not ctx.env.options.allow_writing_protected_pointers
    ):
        ptrcls_sn = ptrcls.get_shortname(ctx.env.schema)
        if is_polymorphic:
            msg = (f'cannot access {ptrcls_sn.name} on a polymorphic '
                   f'shape element')
        else:
            msg = f'cannot assign to {ptrcls_sn.name}'
        raise errors.QueryError(msg, context=shape_el.context)

    if is_update and ptrcls.get_readonly(ctx.env.schema):
        raise errors.QueryError(
            f'cannot update {ptrcls.get_verbosename(ctx.env.schema)}: '
            f'it is declared as read-only',
            context=compexpr and compexpr.context,
        )

    return ptrcls
Exemplo n.º 11
0
def compile_func_to_ir(func, schema, *,
                       anchors=None,
                       security_context=None,
                       modaliases=None,
                       implicit_id_in_shapes=False,
                       implicit_tid_in_shapes=False):
    """Compile an EdgeQL function into EdgeDB IR."""

    if debug.flags.edgeql_compile:
        debug.header('EdgeQL Function')
        debug.print(func.get_code(schema))

    trees = ql_parser.parse_block(func.get_code(schema) + ';')
    if len(trees) != 1:
        raise errors.InvalidFunctionDefinitionError(
            'functions can only contain one statement')

    tree = trees[0]
    if modaliases:
        ql_parser.append_module_aliases(tree, modaliases)

    if anchors is None:
        anchors = {}

    anchors['__defaults_mask__'] = irast.Parameter(
        name='__defaults_mask__',
        typeref=irtyputils.type_to_typeref(schema, schema.get('std::bytes')))

    func_params = func.get_params(schema)
    pg_params = s_func.PgParams.from_params(schema, func_params)
    for pi, p in enumerate(pg_params.params):
        p_shortname = p.get_shortname(schema)
        anchors[p_shortname] = irast.Parameter(
            name=p_shortname,
            typeref=irtyputils.type_to_typeref(schema, p.get_type(schema)))

        if p.get_default(schema) is None:
            continue

        tree.aliases.append(
            qlast.AliasedExpr(
                alias=p_shortname,
                expr=qlast.IfElse(
                    condition=qlast.BinOp(
                        left=qlast.FunctionCall(
                            func=('std', 'bytes_get_bit'),
                            args=[
                                qlast.FuncArg(
                                    arg=qlast.Path(steps=[
                                        qlast.ObjectRef(
                                            name='__defaults_mask__')
                                    ])),
                                qlast.FuncArg(
                                    arg=qlast.IntegerConstant(value=str(pi)))
                            ]),
                        right=qlast.IntegerConstant(value='0'),
                        op='='),
                    if_expr=qlast.Path(
                        steps=[qlast.ObjectRef(name=p_shortname)]),
                    else_expr=qlast._Optional(expr=p.get_ql_default(schema)))))

    ir = compile_ast_to_ir(
        tree, schema, anchors=anchors, func=func,
        security_context=security_context, modaliases=modaliases,
        implicit_id_in_shapes=implicit_id_in_shapes,
        implicit_tid_in_shapes=implicit_tid_in_shapes)

    return ir
Exemplo n.º 12
0
 def reduce_ICONST(self, *kids):
     self.val = qlast.IntegerConstant(value=kids[0].val)
Exemplo n.º 13
0
def finalize_args(
    bound_call: polyres.BoundCall, *,
    actual_typemods: Sequence[ft.TypeModifier] = (),
    is_polymorphic: bool = False,
    ctx: context.ContextLevel,
) -> Tuple[List[irast.CallArg], List[ft.TypeModifier]]:

    args: List[irast.CallArg] = []
    typemods = []

    for i, barg in enumerate(bound_call.args):
        param = barg.param
        arg = barg.val
        if param is None:
            # defaults bitmask
            args.append(irast.CallArg(expr=arg))
            typemods.append(ft.TypeModifier.SINGLETON)
            continue

        if actual_typemods:
            param_mod = actual_typemods[i]
        else:
            param_mod = param.get_typemod(ctx.env.schema)

        typemods.append(param_mod)

        if param_mod is not ft.TypeModifier.SET_OF:
            arg_scope = pathctx.get_set_scope(arg, ctx=ctx)
            param_shortname = param.get_parameter_name(ctx.env.schema)

            # Arg was wrapped for scope fencing purposes,
            # but that fence has been removed above, so unwrap it.
            orig_arg = arg
            arg = irutils.unwrap_set(arg)

            if (param_mod is ft.TypeModifier.OPTIONAL or
                    param_shortname in bound_call.null_args):

                if arg_scope is not None:
                    # Due to the construction of relgen, the (unfenced)
                    # subscope is necessary to shield LHS paths from the outer
                    # query to prevent path binding which may break OPTIONAL.
                    branch = arg_scope.unfence()

                pathctx.register_set_in_scope(arg, ctx=ctx)
                pathctx.mark_path_as_optional(arg.path_id, ctx=ctx)

                if arg_scope is not None:
                    pathctx.assign_set_scope(arg, branch, ctx=ctx)

            elif arg_scope is not None:
                arg_scope.collapse()
                if arg is orig_arg:
                    pathctx.assign_set_scope(arg, None, ctx=ctx)
        else:
            if (is_polymorphic
                    and ctx.expr_exposed
                    and ctx.implicit_limit
                    and isinstance(arg.expr, irast.SelectStmt)
                    and arg.expr.limit is None):
                arg.expr.limit = setgen.ensure_set(
                    dispatch.compile(
                        qlast.IntegerConstant(value=str(ctx.implicit_limit)),
                        ctx=ctx,
                    ),
                    ctx=ctx,
                )

        paramtype = barg.param_type
        param_kind = param.get_kind(ctx.env.schema)
        if param_kind is ft.ParameterKind.VARIADIC:
            # For variadic params, paramtype would be array<T>,
            # and we need T to cast the arguments.
            assert isinstance(paramtype, s_types.Array)
            paramtype = list(paramtype.get_subtypes(ctx.env.schema))[0]

        # Check if we need to cast the argument value before passing
        # it to the callable.
        compatible = schemactx.is_type_compatible(
            paramtype,
            barg.valtype,
            ctx=ctx,
        )

        if not compatible:
            # The callable form was chosen via an implicit cast,
            # cast the arguments so that the backend has no
            # wiggle room to apply its own (potentially different)
            # casting.
            arg = casts.compile_cast(
                arg, paramtype, srcctx=None, ctx=ctx)

        call_arg = irast.CallArg(expr=arg, cardinality=None)
        stmtctx.get_expr_cardinality_later(
            target=call_arg, field='cardinality', irexpr=arg, ctx=ctx)

        args.append(call_arg)

    return args, typemods
Exemplo n.º 14
0
 def visit_IntValue(self, node):
     return qlast.IntegerConstant(value=node.value)
Exemplo n.º 15
0
def finalize_args(
    bound_call: polyres.BoundCall,
    *,
    actual_typemods: Sequence[ft.TypeModifier] = (),
    guessed_typemods: Dict[Union[int, str], ft.TypeModifier],
    is_polymorphic: bool = False,
    ctx: context.ContextLevel,
) -> Tuple[List[irast.CallArg], List[ft.TypeModifier]]:

    args: List[irast.CallArg] = []
    typemods = []

    for i, barg in enumerate(bound_call.args):
        param = barg.param
        arg = barg.val
        arg_type_path_id: Optional[irast.PathId] = None
        if param is None:
            # defaults bitmask
            args.append(irast.CallArg(expr=arg))
            typemods.append(ft.TypeModifier.SingletonType)
            continue

        if actual_typemods:
            param_mod = actual_typemods[i]
        else:
            param_mod = param.get_typemod(ctx.env.schema)

        typemods.append(param_mod)

        if param_mod is not ft.TypeModifier.SetOfType:
            param_shortname = param.get_parameter_name(ctx.env.schema)

            if param_shortname in bound_call.null_args:
                pathctx.register_set_in_scope(arg, optional=True, ctx=ctx)

            # If we guessed the argument was optional but it wasn't,
            # try to go back and make it *not* optional.
            elif (param_mod is ft.TypeModifier.SingletonType
                  and barg.arg_id is not None
                  and param_mod is not guessed_typemods[barg.arg_id]):
                for child in ctx.path_scope.children:
                    if (child.path_id == arg.path_id
                            or (arg.path_scope_id is not None
                                and child.unique_id == arg.path_scope_id)):
                        child.optional = False

            # Object type arguments to functions may be overloaded, so
            # we populate a path id field so that we can also pass the
            # type as an argument on the pgsql side. If the param type
            # is "anytype", though, then it can't be overloaded on
            # that argument.
            arg_type = setgen.get_set_type(arg, ctx=ctx)
            if (isinstance(arg_type, s_objtypes.ObjectType) and barg.param
                    and not barg.param.get_type(ctx.env.schema).is_any(
                        ctx.env.schema)):
                arg_type_path_id = pathctx.extend_path_id(
                    arg.path_id,
                    ptrcls=arg_type.getptr(ctx.env.schema,
                                           sn.UnqualName('__type__')),
                    ctx=ctx,
                )
        else:
            is_array_agg = (isinstance(bound_call.func, s_func.Function)
                            and (bound_call.func.get_shortname(ctx.env.schema)
                                 == sn.QualName('std', 'array_agg')))

            if (
                    # Ideally, we should implicitly slice all array values,
                    # but in practice, the vast majority of large arrays
                    # will come from array_agg, and so we only care about
                    # that.
                    is_array_agg and ctx.expr_exposed and
                    ctx.implicit_limit and
                    isinstance(arg.expr, irast.SelectStmt) and
                    arg.expr.limit is None and not ctx.inhibit_implicit_limit):
                arg.expr.limit = dispatch.compile(
                    qlast.IntegerConstant(value=str(ctx.implicit_limit)),
                    ctx=ctx,
                )

        paramtype = barg.param_type
        param_kind = param.get_kind(ctx.env.schema)
        if param_kind is ft.ParameterKind.VariadicParam:
            # For variadic params, paramtype would be array<T>,
            # and we need T to cast the arguments.
            assert isinstance(paramtype, s_types.Array)
            paramtype = list(paramtype.get_subtypes(ctx.env.schema))[0]

        # Check if we need to cast the argument value before passing
        # it to the callable.
        compatible = s_types.is_type_compatible(
            paramtype,
            barg.valtype,
            schema=ctx.env.schema,
        )

        if not compatible:
            # The callable form was chosen via an implicit cast,
            # cast the arguments so that the backend has no
            # wiggle room to apply its own (potentially different)
            # casting.
            arg = casts.compile_cast(arg, paramtype, srcctx=None, ctx=ctx)

        args.append(
            irast.CallArg(expr=arg,
                          expr_type_path_id=arg_type_path_id,
                          is_default=barg.is_default))

    return args, typemods
Exemplo n.º 16
0
def finalize_args(
    bound_call: polyres.BoundCall, *,
    arg_ctxs: Dict[irast.Set, context.ContextLevel],
    actual_typemods: Sequence[ft.TypeModifier] = (),
    is_polymorphic: bool = False,
    ctx: context.ContextLevel,
) -> Tuple[List[irast.CallArg], List[ft.TypeModifier]]:

    args: List[irast.CallArg] = []
    typemods = []

    for i, barg in enumerate(bound_call.args):
        param = barg.param
        arg = barg.val
        if param is None:
            # defaults bitmask
            args.append(irast.CallArg(expr=arg))
            typemods.append(ft.TypeModifier.SingletonType)
            continue

        if actual_typemods:
            param_mod = actual_typemods[i]
        else:
            param_mod = param.get_typemod(ctx.env.schema)

        typemods.append(param_mod)

        orig_arg = arg
        arg_ctx = arg_ctxs.get(orig_arg)
        arg_scope = pathctx.get_set_scope(arg, ctx=ctx)
        if param_mod is not ft.TypeModifier.SetOfType:
            param_shortname = param.get_parameter_name(ctx.env.schema)

            # Arg was wrapped for scope fencing purposes,
            # but that fence has been removed above, so unwrap it.
            arg = irutils.unwrap_set(arg)

            if (param_mod is ft.TypeModifier.OptionalType or
                    param_shortname in bound_call.null_args):

                process_path_log(arg_ctx, arg_scope)

                if arg_scope is not None:
                    # Due to the construction of relgen, the (unfenced)
                    # subscope is necessary to shield LHS paths from the outer
                    # query to prevent path binding which may break OPTIONAL.
                    arg_scope.mark_as_optional()
                    branch = arg_scope.unfence()

                pathctx.register_set_in_scope(arg, optional=True, ctx=ctx)

                if arg_scope is not None:
                    pathctx.assign_set_scope(arg, branch, ctx=ctx)

            elif arg_scope is not None:
                arg_scope.collapse()
                if arg is orig_arg:
                    pathctx.assign_set_scope(arg, None, ctx=ctx)
        else:
            process_path_log(arg_ctx, arg_scope)
            is_array_agg = (
                isinstance(bound_call.func, s_func.Function)
                and (
                    bound_call.func.get_shortname(ctx.env.schema)
                    == sn.QualName('std', 'array_agg')
                )
            )

            if (
                # Ideally, we should implicitly slice all array values,
                # but in practice, the vast majority of large arrays
                # will come from array_agg, and so we only care about
                # that.
                is_array_agg
                and ctx.expr_exposed
                and ctx.implicit_limit
                and isinstance(arg.expr, irast.SelectStmt)
                and arg.expr.limit is None
                and not ctx.inhibit_implicit_limit
            ):
                arg.expr.limit = setgen.ensure_set(
                    dispatch.compile(
                        qlast.IntegerConstant(value=str(ctx.implicit_limit)),
                        ctx=ctx,
                    ),
                    ctx=ctx,
                )

        paramtype = barg.param_type
        param_kind = param.get_kind(ctx.env.schema)
        if param_kind is ft.ParameterKind.VariadicParam:
            # For variadic params, paramtype would be array<T>,
            # and we need T to cast the arguments.
            assert isinstance(paramtype, s_types.Array)
            paramtype = list(paramtype.get_subtypes(ctx.env.schema))[0]

        # Check if we need to cast the argument value before passing
        # it to the callable.
        compatible = s_types.is_type_compatible(
            paramtype, barg.valtype, schema=ctx.env.schema,
        )

        if not compatible:
            # The callable form was chosen via an implicit cast,
            # cast the arguments so that the backend has no
            # wiggle room to apply its own (potentially different)
            # casting.
            arg = casts.compile_cast(
                arg, paramtype, srcctx=None, ctx=ctx)

        args.append(irast.CallArg(expr=arg, cardinality=None))

        # If we have any logged paths left over and our enclosing
        # context is logging paths, propagate them up.
        if arg_ctx and arg_ctx.path_log and ctx.path_log is not None:
            ctx.path_log.extend(arg_ctx.path_log)

    return args, typemods
Exemplo n.º 17
0
def compile_ForQuery(
        qlstmt: qlast.ForQuery, *, ctx: context.ContextLevel) -> irast.Set:
    with ctx.subquery() as sctx:
        stmt = irast.SelectStmt(context=qlstmt.context)
        init_stmt(stmt, qlstmt, ctx=sctx, parent_ctx=ctx)

        # As an optimization, if the iterator is a singleton set, use
        # the element directly.
        iterator = qlstmt.iterator
        if isinstance(iterator, qlast.Set) and len(iterator.elements) == 1:
            iterator = iterator.elements[0]

        # Compile the iterator
        iterator_ctx = None
        if (ctx.expr_exposed and ctx.iterator_ctx is not None
                and ctx.iterator_ctx is not sctx):
            iterator_ctx = ctx.iterator_ctx

        ictx = iterator_ctx or sctx

        contains_dml = qlutils.contains_dml(qlstmt.result)
        # If the body contains DML, then we need to prohibit
        # correlation between the iterator and the enclosing
        # query, since the correlation imposes compilation issues
        # we aren't willing to tackle.
        if contains_dml:
            ictx.path_scope.factoring_allowlist.update(
                ctx.iterator_path_ids)
        iterator_view = stmtctx.declare_view(
            iterator,
            s_name.UnqualName(qlstmt.iterator_alias),
            factoring_fence=contains_dml,
            path_id_namespace=ictx.path_id_namespace,
            ctx=ictx,
        )

        iterator_stmt = setgen.new_set_from_set(
            iterator_view, preserve_scope_ns=True, ctx=sctx)
        stmt.iterator_stmt = iterator_stmt

        iterator_type = setgen.get_set_type(iterator_stmt, ctx=ctx)
        anytype = iterator_type.find_any(ctx.env.schema)
        if anytype is not None:
            raise errors.QueryError(
                'FOR statement has iterator of indeterminate type',
                context=ctx.env.type_origins.get(anytype),
            )

        if iterator_ctx is not None and iterator_ctx.stmt is not None:
            iterator_ctx.stmt.hoisted_iterators.append(iterator_stmt)

        view_scope_info = sctx.path_scope_map[iterator_view]

        pathctx.register_set_in_scope(
            iterator_stmt,
            path_scope=ictx.path_scope,
            ctx=sctx,
        )

        # Iterator symbol is, by construction, outside of the scope
        # of the UNION argument, but is perfectly legal to be referenced
        # inside a factoring fence that is an immediate child of this
        # scope.
        ictx.path_scope.factoring_allowlist.add(
            stmt.iterator_stmt.path_id)
        sctx.iterator_path_ids |= {stmt.iterator_stmt.path_id}
        node = ictx.path_scope.find_descendant(iterator_stmt.path_id)
        if node is not None:
            # See above about why we need a factoring fence.
            # We need to do this again when we move the branch so
            # as to preserve the fencing.
            # Do this by sticking the iterator subtree onto a branch
            # with a factoring fence.
            if contains_dml:
                node = node.attach_branch()
                node.factoring_fence = True
                node = node.attach_branch()

            node.attach_subtree(view_scope_info.path_scope,
                                context=iterator.context)

        # Compile the body
        with sctx.newscope(fenced=True) as bctx:
            stmt.result = setgen.scoped_set(
                compile_result_clause(
                    qlstmt.result,
                    view_scls=ctx.view_scls,
                    view_rptr=ctx.view_rptr,
                    result_alias=qlstmt.result_alias,
                    view_name=ctx.toplevel_result_view_name,
                    forward_rptr=True,
                    ctx=bctx,
                ),
                ctx=bctx,
            )

        # Inject an implicit limit if appropriate
        if ((ctx.expr_exposed or sctx.stmt is ctx.toplevel_stmt)
                and ctx.implicit_limit):
            stmt.limit = setgen.ensure_set(
                dispatch.compile(
                    qlast.IntegerConstant(value=str(ctx.implicit_limit)),
                    ctx=sctx,
                ),
                ctx=sctx,
            )

        result = fini_stmt(stmt, qlstmt, ctx=sctx, parent_ctx=ctx)

    return result