Ejemplo n.º 1
0
    def reduce_START_TRANSACTION_OptTransactionModeList(self, *kids):
        modes = kids[2].val

        isolation = None
        access = None
        deferrable = None

        for mode, mode_ctx in modes:
            if isinstance(mode, qltypes.TransactionIsolationLevel):
                if isolation is not None:
                    raise errors.EdgeQLSyntaxError(
                        f"only one isolation level can be specified",
                        context=mode_ctx)
                isolation = mode

            elif isinstance(mode, qltypes.TransactionAccessMode):
                if access is not None:
                    raise errors.EdgeQLSyntaxError(
                        f"only one access mode can be specified",
                        context=mode_ctx)
                access = mode

            else:
                assert isinstance(mode, qltypes.TransactionDeferMode)
                if deferrable is not None:
                    raise errors.EdgeQLSyntaxError(
                        f"deferrable mode can only be specified once",
                        context=mode_ctx)
                deferrable = mode

        self.val = qlast.StartTransaction(isolation=isolation,
                                          access=access,
                                          deferrable=deferrable)
Ejemplo n.º 2
0
    def _process_role_body(
        cls,
        cmd: sd.Command,
        schema: s_schema.Schema,
        astnode: qlast.DDLOperation,
        context: sd.CommandContext,
    ) -> None:
        password = cmd.get_attribute_value('password')
        if password is not None:
            if cmd.get_attribute_value('password_hash') is not None:
                raise errors.EdgeQLSyntaxError(
                    'cannot specify both `password` and `password_hash` in'
                    ' the same statement',
                    context=astnode.context,
                )
            salted_password = scram.build_verifier(password)
            cmd.set_attribute_value('password', salted_password)

        password_hash = cmd.get_attribute_value('password_hash')
        if password_hash is not None:
            try:
                scram.parse_verifier(password_hash)
            except ValueError as e:
                raise errors.InvalidValueError(
                    e.args[0],
                    context=astnode.context)
            cmd.set_attribute_value('password', password_hash)
Ejemplo n.º 3
0
def _ql_typeexpr_to_type(ql_t: qlast.TypeExpr, *,
                         ctx: context.ContextLevel) -> List[s_types.Type]:

    if isinstance(ql_t, qlast.TypeOf):
        with ctx.newscope(fenced=True, temporary=True) as subctx:
            ir_set = setgen.ensure_set(dispatch.compile(ql_t.expr, ctx=subctx),
                                       ctx=subctx)
            stype = setgen.get_set_type(ir_set, ctx=subctx)

        return [stype]

    elif isinstance(ql_t, qlast.TypeOp):
        if ql_t.op == '|':
            return (_ql_typeexpr_to_type(ql_t.left, ctx=ctx) +
                    _ql_typeexpr_to_type(ql_t.right, ctx=ctx))

        raise errors.UnsupportedFeatureError(
            f'type operator {ql_t.op!r} is not implemented',
            context=ql_t.context)

    elif isinstance(ql_t, qlast.TypeName):
        return [_ql_typename_to_type(ql_t, ctx=ctx)]

    else:
        raise errors.EdgeQLSyntaxError("Unexpected type expression",
                                       context=ql_t.context)
Ejemplo n.º 4
0
def _ql_typeexpr_to_type(ql_t: qlast.TypeExpr, *,
                         ctx: context.ContextLevel) -> List[s_types.Type]:

    if isinstance(ql_t, qlast.TypeOf):
        with ctx.new() as subctx:
            # Use an empty scope tree, to avoid polluting things pointlessly
            subctx.path_scope = irast.ScopeTreeNode()
            ir_set = dispatch.compile(ql_t.expr, ctx=subctx)
            stype = setgen.get_set_type(ir_set, ctx=subctx)

        return [stype]

    elif isinstance(ql_t, qlast.TypeOp):
        if ql_t.op == '|':
            return (_ql_typeexpr_to_type(ql_t.left, ctx=ctx) +
                    _ql_typeexpr_to_type(ql_t.right, ctx=ctx))

        raise errors.UnsupportedFeatureError(
            f'type operator {ql_t.op!r} is not implemented',
            context=ql_t.context)

    elif isinstance(ql_t, qlast.TypeName):
        return [_ql_typename_to_type(ql_t, ctx=ctx)]

    else:
        raise errors.EdgeQLSyntaxError("Unexpected type expression",
                                       context=ql_t.context)
Ejemplo n.º 5
0
    def _extract_target(self, target, cmds, context, *, overloaded=False):
        if target:
            return target, cmds

        for cmd in cmds:
            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':
                if target is not None:
                    raise errors.EdgeQLSyntaxError(
                        f'computable link with more than one expression',
                        context=context)
                target = cmd.value

        if not overloaded and target is None:
            raise errors.EdgeQLSyntaxError(
                f'computable link without expression', context=context)

        return target, cmds
Ejemplo n.º 6
0
def normalize(eql: str) -> Entry:
    try:
        return _normalize(eql)
    except TokenizerError as e:
        message, position = e.args
        hint = _derive_hint(eql, message, position)
        raise errors.EdgeQLSyntaxError(
            message, position=position, hint=hint) from e
Ejemplo n.º 7
0
 def _validate(self):
     on_target_delete = None
     for cmd in self.val.commands:
         if isinstance(cmd, qlast.OnTargetDelete):
             if on_target_delete:
                 raise errors.EdgeQLSyntaxError(
                     f"more than one 'on target delete' specification",
                     context=cmd.context)
             else:
                 on_target_delete = cmd
Ejemplo n.º 8
0
    def reduce_MODULE_ModuleName_SDLCommandBlock(self, *kids):
        # Check that top-level declarations DO NOT use fully-qualified
        # names and aren't nested module blocks.
        declarations = kids[2].val
        for decl in declarations:
            if isinstance(decl, qlast.ModuleDeclaration):
                raise errors.EdgeQLSyntaxError(
                    "nested module declaration is not allowed",
                    context=decl.context)
            elif decl.name.module is not None:
                raise errors.EdgeQLSyntaxError(
                    "fully-qualified name is not allowed in "
                    "a module declaration",
                    context=decl.name.context)

        self.val = qlast.ModuleDeclaration(
            # mirror what we do in CREATE MODULE
            name=qlast.ObjectRef(module=None, name='.'.join(kids[1].val)),
            declarations=declarations,
        )
Ejemplo n.º 9
0
    def _cmd_tree_from_ast(cls, schema, astnode, context):
        cmd = super()._cmd_tree_from_ast(schema, astnode, context)

        if not astnode.superuser and not context.testmode:
            raise errors.EdgeQLSyntaxError(
                'missing required SUPERUSER qualifier',
                context=astnode.context,
            )

        cmd.set_attribute_value('is_superuser', astnode.superuser)
        cls._process_role_body(cmd, schema, astnode, context)
        return cmd
Ejemplo n.º 10
0
def register_set_in_scope(ir_set: irast.Set,
                          *,
                          path_scope: irast.ScopeTreeNode = None,
                          ctx: context.CompilerContext) -> None:
    if path_scope is None:
        path_scope = ctx.path_scope

    try:
        path_scope.attach_path(ir_set.path_id)
    except irast.InvalidScopeConfiguration as e:
        raise errors.EdgeQLSyntaxError(e.args[0],
                                       context=ir_set.context) from e
Ejemplo n.º 11
0
def _ql_typeexpr_to_type(ql_t: qlast.TypeExpr, *,
                         ctx: context.ContextLevel) -> List[s_types.Type]:

    if isinstance(ql_t, qlast.TypeOf):
        with ctx.new() as subctx:
            # Use an empty scope tree, to avoid polluting things pointlessly
            subctx.path_scope = irast.ScopeTreeNode()
            subctx.expr_exposed = context.Exposure.UNEXPOSED
            ir_set = dispatch.compile(ql_t.expr, ctx=subctx)
            stype = setgen.get_set_type(ir_set, ctx=subctx)

        return [stype]

    elif isinstance(ql_t, qlast.TypeOp):
        if ql_t.op == '|':
            # We need to validate that type ops are applied only to
            # object types. So we check the base case here, when the
            # left or right operand is a single type, because if it's
            # a longer list, then we know that it was already composed
            # of "|" or "&", or it is the result of inference by
            # "typeof" and is a list of object types anyway.
            left = _ql_typeexpr_to_type(ql_t.left, ctx=ctx)
            right = _ql_typeexpr_to_type(ql_t.right, ctx=ctx)

            if len(left) == 1 and not left[0].is_object_type():
                raise errors.UnsupportedFeatureError(
                    f'cannot use type operator {ql_t.op!r} with non-object '
                    f'type {left[0].get_displayname(ctx.env.schema)}',
                    context=ql_t.left.context)
            if len(right) == 1 and not right[0].is_object_type():
                raise errors.UnsupportedFeatureError(
                    f'cannot use type operator {ql_t.op!r} with non-object '
                    f'type {right[0].get_displayname(ctx.env.schema)}',
                    context=ql_t.right.context)

            return left + right

        raise errors.UnsupportedFeatureError(
            f'type operator {ql_t.op!r} is not implemented',
            context=ql_t.context)

    elif isinstance(ql_t, qlast.TypeName):
        return [_ql_typename_to_type(ql_t, ctx=ctx)]

    else:
        raise errors.EdgeQLSyntaxError("Unexpected type expression",
                                       context=ql_t.context)
Ejemplo n.º 12
0
    def _cmd_tree_from_ast(
        cls,
        schema: s_schema.Schema,
        astnode: qlast.DDLOperation,
        context: sd.CommandContext,
    ) -> sd.Command:
        assert isinstance(astnode, qlast.CreateRole)
        cmd = super()._cmd_tree_from_ast(schema, astnode, context)

        if not astnode.superuser and not context.testmode:
            raise errors.EdgeQLSyntaxError(
                'missing required SUPERUSER qualifier',
                context=astnode.context,
            )

        cmd.set_attribute_value('superuser', astnode.superuser)
        cls._process_role_body(cmd, schema, astnode, context)
        return cmd
Ejemplo n.º 13
0
    def get_exception(self, native_err, context, token=None):
        msg = native_err.args[0]

        if isinstance(native_err, errors.EdgeQLSyntaxError):
            return native_err
        else:
            if msg.startswith('Unexpected token: '):
                token = token or getattr(native_err, 'token', None)

                if not token or token.kind() == 'EOF':
                    msg = 'Unexpected end of line'
                elif hasattr(token, 'val'):
                    msg = f'Unexpected {token.val!r}'
                elif token.kind() == 'NL':
                    msg = 'Unexpected end of line'
                else:
                    msg = f'Unexpected {token.text()!r}'

        return errors.EdgeQLSyntaxError(msg, context=context, token=token)
Ejemplo n.º 14
0
    def _cmd_tree_from_ast(
        cls,
        schema: s_schema.Schema,
        astnode: qlast.DDLOperation,
        context: sd.CommandContext,
    ) -> CreateDatabase:
        cmd = super()._cmd_tree_from_ast(schema, astnode, context)
        assert isinstance(cmd, CreateDatabase)

        assert isinstance(astnode, qlast.CreateDatabase)
        if astnode.template is not None:
            if not context.testmode:
                raise errors.EdgeQLSyntaxError(
                    f'unexpected {astnode.template.name!r}',
                    context=astnode.template.context,
                )
            cmd.template = astnode.template.name

        return cmd
Ejemplo n.º 15
0
    def _cmd_tree_from_ast(
        cls,
        schema: s_schema.Schema,
        astnode: qlast.DDLOperation,
        context: sd.CommandContext,
    ) -> sd.Command:

        cmd = super()._cmd_tree_from_ast(schema, astnode, context)
        assert isinstance(astnode, qlast.AlterFunction)

        if astnode.code is not None:
            if (
                astnode.code.language is not qlast.Language.EdgeQL or
                astnode.code.from_function is not None or
                astnode.code.from_expr
            ):
                raise errors.EdgeQLSyntaxError(
                    'altering function code is only supported for '
                    'pure EdgeQL functions',
                    context=astnode.context
                )

            nativecode_expr: Optional[qlast.Expr] = None
            if astnode.nativecode is not None:
                nativecode_expr = astnode.nativecode
            elif astnode.code.code is not None:
                nativecode_expr = qlparser.parse(astnode.code.code)

            if nativecode_expr is not None:
                nativecode = expr.Expression.from_ast(
                    nativecode_expr,
                    schema,
                    context.modaliases,
                )

                cmd.set_attribute_value(
                    'nativecode',
                    nativecode,
                )

        return cmd
Ejemplo n.º 16
0
    def get_exception(self, native_err, context, token=None):
        msg = native_err.args[0]
        details = None
        hint = None

        if isinstance(native_err, errors.EdgeQLSyntaxError):
            return native_err
        else:
            if msg.startswith('Unexpected token: '):
                token = token or getattr(native_err, 'token', None)
                token_kind = token.kind()
                ltok = self.parser._stack[-1][0]

                is_reserved = (
                    token.text().lower()
                    in gr_keywords.by_type[gr_keywords.RESERVED_KEYWORD])

                # Look at the parsing stack and use tokens and
                # non-terminals to infer the parser rule when the
                # error occurred.
                i, rule = self._get_rule()

                if not token or token_kind == 'EOF':
                    msg = 'Unexpected end of line'
                elif (rule == 'shape' and token_kind == 'IDENT'
                      and isinstance(ltok, parsing.Nonterm)):
                    # Make sure that the previous element in the stack
                    # is some kind of Nonterminal, because if it's
                    # not, this is probably not an issue of a missing
                    # COMMA.
                    hint = (f"It appears that a ',' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule == 'list of arguments' and
                      # The stack is like <NodeName> LPAREN <AnyIdentifier>
                      i == 1 and isinstance(
                          ltok,
                          (gr_exprs.AnyIdentifier, tokens.T_WITH,
                           tokens.T_SELECT, tokens.T_FOR, tokens.T_INSERT,
                           tokens.T_UPDATE, tokens.T_DELETE))):
                    hint = ("Missing parentheses around statement used "
                            "as an expression")
                    # We want the error context correspond to the
                    # statement keyword
                    context = ltok.context
                    token = None
                elif (rule == 'array slice' and
                      # The offending token was something that could
                      # make an expression
                      token_kind in {'IDENT', 'ICONST'}
                      and not isinstance(ltok, tokens.T_COLON)):
                    hint = (f"It appears that a ':' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule in {'list of arguments', 'tuple', 'array'} and
                      # The offending token was something that could
                      # make an expression
                      token_kind in {
                          'IDENT',
                          'TRUE',
                          'FALSE',
                          'ICONST',
                          'FCONST',
                          'NICONST',
                          'NFCONST',
                          'BCONST',
                          'SCONST',
                      } and not isinstance(ltok, tokens.T_COMMA)):
                    hint = (f"It appears that a ',' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule == 'definition' and token_kind == 'IDENT'):
                    # Something went wrong in a definition, so check
                    # if the last successful token is a keyword.
                    if (isinstance(ltok, gr_exprs.Identifier)
                            and ltok.val.upper() == 'INDEX'):
                        msg = (f"Expected 'ON', but got {token.text()!r} "
                               f"instead")
                    else:
                        msg = f'Unexpected {token.text()!r}'
                elif rule == 'for iterator':
                    msg = ("Missing parentheses around complex expression in "
                           "a FOR iterator clause")

                    if i > 0:
                        context = pctx.merge_context([
                            self.parser._stack[-i][0].context,
                            context,
                        ])
                    token = None
                elif hasattr(token, 'val'):
                    msg = f'Unexpected {token.val!r}'
                elif token_kind == 'NL':
                    msg = 'Unexpected end of line'
                elif is_reserved and not isinstance(ltok, gr_exprs.Expr):
                    # Another token followed by a reserved keyword:
                    # likely an attempt to use keyword as identifier
                    msg = f'Unexpected keyword {token.text()!r}'
                    details = (
                        f'Token {token.text()!r} is a reserved keyword and'
                        f' cannot be used as an identifier')
                    hint = (
                        f'Use a different identifier or quote the name with'
                        f' backticks: `{token.text()}`')
                else:
                    msg = f'Unexpected {token.text()!r}'

        return errors.EdgeQLSyntaxError(msg,
                                        details=details,
                                        hint=hint,
                                        context=context,
                                        token=token)
Ejemplo n.º 17
0
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:
                    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
                        ),
                        ctx=ctx,
                    )

                    extra_scopes[path_tip] = view_scope_info
                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,
                    )

                    prefix_type = get_set_type(ind_prefix.rptr.source, ctx=ctx)
                    assert isinstance(prefix_type, s_sources.Source)

                    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)
                        ptr = 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 '{ptr}' links between"
                            f" {s_vn} and {t_vn}",
                            context=ptr_expr.ptr.context,
                        )

                    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)

            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, source=source, ptr_name=ptr_name,
                    direction=direction,
                    ignore_computable=True,
                    source_context=step.context, ctx=ctx)

                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] = context.ScopeInfo(
                    path_scope=subctx.path_scope,
                    tentative_work=[
                        cb
                        for cb in subctx.tentative_work
                        if cb not in ctx.tentative_work
                    ],
                )

        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, scope_info 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_info.path_scope.copy())

        for cb in scope_info.tentative_work:
            stmtctx.at_stmt_fini(cb, ctx=ctx)

        scope_info.tentative_work[:] = []

        if ir_set.path_scope_id is None:
            pathctx.assign_set_scope(ir_set, nodes[0], 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 fence in fences:
        fence.fenced = False

    return path_tip
Ejemplo n.º 18
0
    def get_exception(self, native_err, context, token=None):
        msg = native_err.args[0]
        hint = None

        if isinstance(native_err, errors.EdgeQLSyntaxError):
            return native_err
        else:
            if msg.startswith('Unexpected token: '):
                token = token or getattr(native_err, 'token', None)
                token_kind = token.kind()
                ltok = self.parser._stack[-1][0]

                # Look at the parsing stack and use tokens and
                # non-terminals to infer the parser rule when the
                # error occurred.
                i, rule = self._get_rule()

                if not token or token_kind == 'EOF':
                    msg = 'Unexpected end of line'
                elif (rule == 'shape' and token_kind == 'IDENT'
                      and isinstance(ltok, parsing.Nonterm)):
                    # Make sure that the previous element in the stack
                    # is some kind of Nonterminal, because if it's
                    # not, this is probably not an issue of a missing
                    # COMMA.
                    hint = (f"It appears that a ',' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule == 'list of arguments' and
                      # The stack is like <NodeName> LPAREN <AnyIdentifier>
                      i == 1 and isinstance(
                          ltok,
                          (gr_exprs.AnyIdentifier, tokens.T_WITH,
                           tokens.T_SELECT, tokens.T_FOR, tokens.T_INSERT,
                           tokens.T_UPDATE, tokens.T_DELETE))):
                    hint = ("Statement used as an expression must be "
                            "enclosed in a set of parentheses")
                    # We want the error context correspond to the
                    # statement keyword
                    context = ltok.context
                    token = None
                elif (rule == 'array slice' and
                      # The offending token was something that could
                      # make an expression
                      token_kind in {'IDENT', 'ICONST'}
                      and not isinstance(ltok, tokens.T_COLON)):
                    hint = (f"It appears that a ':' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule in {'list of arguments', 'tuple', 'array'} and
                      # The offending token was something that could
                      # make an expression
                      token_kind in {
                          'IDENT',
                          'TRUE',
                          'FALSE',
                          'ICONST',
                          'FCONST',
                          'NICONST',
                          'NFCONST',
                          'BCONST',
                          'SCONST',
                      } and not isinstance(ltok, tokens.T_COMMA)):
                    hint = (f"It appears that a ',' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule == 'definition' and token_kind == 'IDENT'):
                    # Something went wrong in a definition, so check
                    # if the last successful token is a keyword.
                    if (isinstance(ltok, gr_exprs.Identifier)
                            and ltok.val.upper() == 'INDEX'):
                        msg = (f"Expected 'ON', but got {token.text()!r} "
                               f"instead")
                    else:
                        msg = f'Unexpected {token.text()!r}'
                elif rule == 'for iterator':
                    msg = (f"missing '{{' before {token.text()!r} "
                           f"in a FOR query")
                elif hasattr(token, 'val'):
                    msg = f'Unexpected {token.val!r}'
                elif token_kind == 'NL':
                    msg = 'Unexpected end of line'
                else:
                    msg = f'Unexpected {token.text()!r}'

        return errors.EdgeQLSyntaxError(msg,
                                        hint=hint,
                                        context=context,
                                        token=token)
Ejemplo n.º 19
0
def ast_to_typeref(node: qlast.TypeName,
                   *,
                   metaclass: typing.Optional[so.ObjectMeta] = None,
                   modaliases: typing.Dict[typing.Optional[str], str],
                   schema) -> so.ObjectRef:

    if node.subtypes is not None and node.maintype.name == 'enum':
        from . import scalars as s_scalars

        return s_scalars.AnonymousEnumTypeRef(
            name='std::anyenum',
            elements=[st.val.value for st in node.subtypes],
        )

    elif node.subtypes is not None:
        from . import types as s_types

        coll = s_types.Collection.get_class(node.maintype.name)

        if issubclass(coll, s_abc.Tuple):
            subtypes = collections.OrderedDict()
            # tuple declaration must either be named or unnamed, but not both
            named = None
            unnamed = None
            for si, st in enumerate(node.subtypes):
                if st.name:
                    named = True
                    type_name = st.name
                else:
                    unnamed = True
                    type_name = str(si)

                if named is not None and unnamed is not None:
                    raise errors.EdgeQLSyntaxError(
                        f'mixing named and unnamed tuple declaration '
                        f'is not supported',
                        context=node.subtypes[0].context,
                    )

                subtypes[type_name] = ast_to_typeref(st,
                                                     modaliases=modaliases,
                                                     schema=schema)

            try:
                return coll.from_subtypes(schema, subtypes,
                                          {'named': bool(named)})
            except errors.SchemaError as e:
                # all errors raised inside are pertaining to subtypes, so
                # the context should point to the first subtype
                e.set_source_context(node.subtypes[0].context)
                raise e

        else:
            subtypes = []
            for st in node.subtypes:
                subtypes.append(
                    ast_to_typeref(st, modaliases=modaliases, schema=schema))

            try:
                return coll.from_subtypes(schema, subtypes)
            except errors.SchemaError as e:
                e.set_source_context(node.context)
                raise e

    elif isinstance(node.maintype, qlast.AnyType):
        from . import pseudo as s_pseudo
        return s_pseudo.AnyObjectRef()

    elif isinstance(node.maintype, qlast.AnyTuple):
        from . import pseudo as s_pseudo
        return s_pseudo.AnyTupleRef()

    return ast_objref_to_objref(node.maintype,
                                modaliases=modaliases,
                                metaclass=metaclass,
                                schema=schema)
Ejemplo n.º 20
0
def ast_to_typeref(node: qlast.TypeName,
                   *,
                   metaclass: Optional[so.ObjectMeta] = None,
                   modaliases: Mapping[Optional[str], str],
                   schema: s_schema.Schema) -> so.Object:

    if node.subtypes is not None and isinstance(node.maintype,
                                                qlast.ObjectRef) \
            and node.maintype.name == 'enum':
        from . import scalars as s_scalars

        return s_scalars.AnonymousEnumTypeRef(
            name='std::anyenum',
            elements=[
                st.val.value
                for st in cast(List[qlast.TypeExprLiteral], node.subtypes)
            ],
        )

    elif node.subtypes is not None:
        from . import types as s_types

        assert isinstance(node.maintype, qlast.ObjectRef)
        coll = s_types.Collection.get_class(node.maintype.name)

        if issubclass(coll, s_types.Tuple):
            # Note: if we used abc Tuple here, then we would need anyway
            # to assert it is an instance of s_types.Tuple to make mypy happy
            # (rightly so, because later we use from_subtypes method)

            subtypes: Dict[str, so.Object] \
                = collections.OrderedDict()
            # tuple declaration must either be named or unnamed, but not both
            named = None
            unnamed = None
            for si, st in enumerate(node.subtypes):
                if st.name:
                    named = True
                    type_name = st.name
                else:
                    unnamed = True
                    type_name = str(si)

                if named is not None and unnamed is not None:
                    raise errors.EdgeQLSyntaxError(
                        f'mixing named and unnamed tuple declaration '
                        f'is not supported',
                        context=node.subtypes[0].context,
                    )

                subtypes[type_name] = ast_to_typeref(cast(qlast.TypeName, st),
                                                     modaliases=modaliases,
                                                     metaclass=metaclass,
                                                     schema=schema)

            try:
                return coll.from_subtypes(
                    schema, cast(Mapping[str, s_types.Type], subtypes),
                    {'named': bool(named)})
            except errors.SchemaError as e:
                # all errors raised inside are pertaining to subtypes, so
                # the context should point to the first subtype
                e.set_source_context(node.subtypes[0].context)
                raise e

        else:
            subtypes_list: List[so.Object] = []
            for st in node.subtypes:
                subtypes_list.append(
                    ast_to_typeref(cast(qlast.TypeName, st),
                                   modaliases=modaliases,
                                   metaclass=metaclass,
                                   schema=schema))

            try:
                return coll.from_subtypes(
                    schema, cast(Sequence[s_types.Type], subtypes_list))
            except errors.SchemaError as e:
                e.set_source_context(node.context)
                raise e

    elif isinstance(node.maintype, qlast.AnyType):
        from . import pseudo as s_pseudo
        return s_pseudo.AnyObjectRef()

    elif isinstance(node.maintype, qlast.AnyTuple):
        from . import pseudo as s_pseudo
        return s_pseudo.AnyTupleRef()

    assert isinstance(node.maintype, qlast.ObjectRef)

    return ast_objref_to_objref(node.maintype,
                                modaliases=modaliases,
                                metaclass=metaclass,
                                schema=schema)
Ejemplo n.º 21
0
def _normalize_view_ptr_expr(
        shape_el: qlast.ShapeElement,
        view_scls: s_nodes.Node,
        *,
        path_id: irast.PathId,
        path_id_namespace: typing.Optional[irast.WeakNamespace] = None,
        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_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 = view_scls
    qlexpr = None

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

    if plen == 1:
        # regular shape
        lexpr = steps[0]
        is_linkprop = lexpr.type == 'property'
        if is_linkprop:
            if view_rptr is None:
                raise errors.QueryError(
                    'invalid reference to link property '
                    'in top level shape',
                    context=lexpr.context)
            ptrsource = view_rptr.ptrcls
        source = qlast.Source()
    elif plen == 2 and isinstance(steps[0], qlast.TypeIndirection):
        # Source type indirection: [IS Type].foo
        source = qlast.Path(steps=[
            qlast.Source(),
            steps[0],
        ])
        lexpr = steps[1]
        ptype = steps[0].type
        ptrsource = schemactx.get_schema_type(ptype.maintype, ctx=ctx)
        is_polymorphic = True
    else:  # pragma: no cover
        raise RuntimeError(
            f'unexpected path length in view shape: {len(steps)}')

    ptrname = lexpr.ptr.name

    compexpr = 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)

    if compexpr is None:
        ptrcls = setgen.resolve_ptr(ptrsource, ptrname, ctx=ctx)
        if is_polymorphic:
            ptrcls = schemactx.derive_view(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.Name(
            module='__',
            name=ptrcls.get_shortname(ctx.env.schema).name,
        )

        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):

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

            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

        if target_typexpr is not None:
            ptr_target = schemactx.get_schema_type(target_typexpr.maintype,
                                                   ctx=ctx)
        else:
            ptr_target = ptrcls.get_target(ctx.env.schema)

        if base_ptrcls in ctx.pending_cardinality:
            # We do not know the parent's pointer cardinality yet.
            ptr_cardinality = None
            ctx.pointer_derivation_map[base_ptrcls].append(ptrcls)
            stmtctx.pend_pointer_cardinality_inference(
                ptrcls=ptrcls,
                specified_card=shape_el.cardinality,
                from_parent=True,
                source_ctx=shape_el.context,
                ctx=ctx)
        else:
            ptr_cardinality = base_ptrcls.get_cardinality(ctx.env.schema)

        implicit_tid = has_implicit_tid(
            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,
                                                 target=ptrcls.get_target(
                                                     ctx.env.schema),
                                                 ns=ctx.path_id_namespace,
                                                 ctx=ctx)

            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.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,
                                           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,
                                           ctx=ctx)

    else:
        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, ctx=ctx)

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

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

        else:
            # Otherwise, assume no pointer inheritance.
            # Every computable is a new pointer derived from
            # std::link or std::property.  There is one exception:
            # pointer aliases (Foo {some := Foo.other}), where `foo`
            # gets derived from `Foo.other`.  This logic is applied
            # in compile_query_subject() by populating the base_ptrcls.
            base_ptrcls = ptrcls = None

            ptr_name = sn.Name(
                module='__',
                name=ptrname,
            )

        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
            shape_expr_ctx.partial_path_prefix = setgen.class_set(
                view_scls, path_id=path_id, ctx=shape_expr_ctx)

            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)

            irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx)

            irexpr.context = compexpr.context

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

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

        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)
            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 ptr_target.issubclass(ctx.env.schema, base_target)):
                    qlexpr = astutils.ensure_qlstmt(
                        qlast.TypeCast(
                            type=astutils.type_to_ql_typeref(
                                base_target, schema=ctx.env.schema),
                            expr=compexpr,
                        ))
                    ptr_target = base_target

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

                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:
        if is_linkprop:
            src_scls = view_rptr.ptrcls
        else:
            src_scls = view_scls

        if ptr_target.is_object_type():
            base = ctx.env.get_track_schema_object('std::link')
        else:
            base = ctx.env.get_track_schema_object('std::property')

        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=[src_scls.get_name(ctx.env.schema)],
            ctx=ctx)

        existing = ctx.env.schema.get(derived_name, None)
        if existing is not None:
            existing_target = existing.get_target(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:
                target_rptr_set = (ptr_target.get_rptr(ctx.env.schema)
                                   is not None)

                if target_rptr_set:
                    ctx.env.schema = ptr_target.set_field_value(
                        ctx.env.schema,
                        'rptr',
                        None,
                    )

                ctx.env.schema = existing.delete(ctx.env.schema)
                ptrcls = schemactx.derive_view(derive_from,
                                               src_scls,
                                               ptr_target,
                                               is_insert=is_insert,
                                               is_update=is_update,
                                               derived_name=derived_name,
                                               inheritance_merge=False,
                                               ctx=ctx)

                if target_rptr_set:
                    ctx.env.schema = ptr_target.set_field_value(
                        ctx.env.schema,
                        'rptr',
                        ptrcls,
                    )
        else:
            ptrcls = schemactx.derive_view(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)

    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] = (qlexpr, ctx, path_id, path_id_namespace)
        ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'computable',
                                                True)

    if not is_mutation:
        if ptr_cardinality is None:
            if ptrcls not in ctx.pending_cardinality:
                if qlexpr is not None:
                    from_parent = False
                elif ptrcls is not base_ptrcls:
                    ctx.pointer_derivation_map[base_ptrcls].append(ptrcls)
                    from_parent = True
                else:
                    from_parent = False

                stmtctx.pend_pointer_cardinality_inference(
                    ptrcls=ptrcls,
                    specified_card=shape_el.cardinality,
                    from_parent=from_parent,
                    source_ctx=shape_el.context,
                    ctx=ctx)

            ctx.env.schema = ptrcls.set_field_value(ctx.env.schema,
                                                    'cardinality', None)
        else:
            ctx.env.schema = ptrcls.set_field_value(ctx.env.schema,
                                                    'cardinality',
                                                    ptr_cardinality)

    if ptrcls.is_protected_pointer(ctx.env.schema) and qlexpr is not None:
        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)

    return ptrcls
Ejemplo n.º 22
0
    def get_exception(self, native_err, context, token=None):
        msg = native_err.args[0]
        hint = None

        if isinstance(native_err, errors.EdgeQLSyntaxError):
            return native_err
        else:
            if msg.startswith('Unexpected token: '):
                token = token or getattr(native_err, 'token', None)
                token_kind = token.kind()
                ltok = self.parser._stack[-1][0]

                # Look at the parsing stack and use tokens and
                # non-terminals to infer the parser rule when the
                # error occurred.
                rule = ''
                # The last valid token was a closing
                # brace/parent/bracket, so we need to find a match for
                # it before deciding what rule context we're in.
                need_match = isinstance(
                    ltok,
                    (tokens.T_RBRACE, tokens.T_RPAREN, tokens.T_RBRACKET))
                for i, (el, _) in enumerate(reversed(self.parser._stack)):
                    if isinstance(el, tokens.Token):
                        # We'll need the element right before "{", "[", or "(".
                        prevel = self.parser._stack[-2 - i][0]

                        if isinstance(el, tokens.T_LBRACE):
                            if need_match and isinstance(
                                    ltok, tokens.T_RBRACE):
                                # This is matched, while we're looking
                                # for unmatched braces.
                                need_match = False
                                continue

                            elif isinstance(prevel, gr_commondl.OptExtending):
                                # This is some SDL/DDL
                                rule = 'definition'
                            elif (
                                    isinstance(prevel, gr_exprs.Expr) or
                                (isinstance(prevel, tokens.T_COLON)
                                 and isinstance(self.parser._stack[-3 - i][0],
                                                gr_exprs.ShapePointer))):
                                # This is some kind of shape.
                                rule = 'shape'
                            break
                        elif isinstance(el, tokens.T_LPAREN):
                            if need_match and isinstance(
                                    ltok, tokens.T_RPAREN):
                                # This is matched, while we're looking
                                # for unmatched parentheses.
                                need_match = False
                                continue
                            # This could be an argument list or a tuple.
                            elif isinstance(prevel, gr_exprs.NodeName):
                                rule = 'list of arguments'
                            else:
                                rule = 'tuple'
                            break
                        elif isinstance(el, tokens.T_LBRACKET):
                            if need_match and isinstance(
                                    ltok, tokens.T_RBRACKET):
                                # This is matched, while we're looking
                                # for unmatched brackets.
                                need_match = False
                                continue
                            # This is either an array literal or
                            # array index.
                            elif isinstance(prevel, gr_exprs.Expr):
                                rule = 'array slice'
                            else:
                                rule = 'array'
                            break

                if not token or token_kind == 'EOF':
                    msg = 'Unexpected end of line'
                elif (rule == 'shape' and token_kind == 'IDENT'
                      and isinstance(ltok, parsing.Nonterm)):
                    # Make sure that the previous element in the stack
                    # is some kind of Nonterminal, because if it's
                    # not, this is probably not an issue of a missing
                    # COMMA.
                    hint = (f"It appears that a ',' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule == 'list of arguments' and
                      # The stack is like <NodeName> LPAREN <AnyIdentifier>
                      i == 1 and isinstance(
                          ltok,
                          (gr_exprs.AnyIdentifier, tokens.T_WITH,
                           tokens.T_SELECT, tokens.T_FOR, tokens.T_INSERT,
                           tokens.T_UPDATE, tokens.T_DELETE))):
                    hint = ("Statement used as an expression must be "
                            "enclosed in a set of parentheses")
                    # We want the error context correspond to the
                    # statement keyword
                    context = ltok.context
                    token = None
                elif (rule == 'array slice' and
                      # The offending token was something that could
                      # make an expression
                      token_kind in {'IDENT', 'ICONST'}
                      and not isinstance(ltok, tokens.T_COLON)):
                    hint = (f"It appears that a ':' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule in {'list of arguments', 'tuple', 'array'} and
                      # The offending token was something that could
                      # make an expression
                      token_kind in {
                          'IDENT',
                          'TRUE',
                          'FALSE',
                          'ICONST',
                          'FCONST',
                          'NICONST',
                          'NFCONST',
                          'BCONST',
                          'SCONST',
                      } and not isinstance(ltok, tokens.T_COMMA)):
                    hint = (f"It appears that a ',' is missing in {a(rule)} "
                            f"before {token.text()!r}")
                elif (rule == 'definition' and token_kind == 'IDENT'):
                    # Something went wrong in a definition, so check
                    # if the last successful token is a keyword.
                    if (isinstance(ltok, gr_exprs.Identifier)
                            and ltok.val.upper() == 'INDEX'):
                        msg = (f"Expected 'ON', but got {token.text()!r} "
                               f"instead")
                    else:
                        msg = f'Unexpected {token.text()!r}'
                elif hasattr(token, 'val'):
                    msg = f'Unexpected {token.val!r}'
                elif token_kind == 'NL':
                    msg = 'Unexpected end of line'
                else:
                    msg = f'Unexpected {token.text()!r}'

        return errors.EdgeQLSyntaxError(msg,
                                        hint=hint,
                                        context=context,
                                        token=token)
Ejemplo n.º 23
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
Ejemplo n.º 24
0
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
Ejemplo n.º 25
0
def ast_to_type_shell(
    node: qlast.TypeName,
    *,
    metaclass: Optional[Type[s_types.Type]] = None,
    modaliases: Mapping[Optional[str], str],
    schema: s_schema.Schema,
) -> s_types.TypeShell:

    if (node.subtypes is not None
            and isinstance(node.maintype, qlast.ObjectRef)
            and node.maintype.name == 'enum'):
        from . import scalars as s_scalars

        return s_scalars.AnonymousEnumTypeShell(elements=[
            st.val.value
            for st in cast(List[qlast.TypeExprLiteral], node.subtypes)
        ], )

    elif node.subtypes is not None:
        from . import types as s_types

        assert isinstance(node.maintype, qlast.ObjectRef)
        coll = s_types.Collection.get_class(node.maintype.name)

        if issubclass(coll, s_types.Tuple):
            # Note: if we used abc Tuple here, then we would need anyway
            # to assert it is an instance of s_types.Tuple to make mypy happy
            # (rightly so, because later we use from_subtypes method)

            subtypes: Dict[str, s_types.TypeShell] = {}
            # tuple declaration must either be named or unnamed, but not both
            named = None
            unnamed = None
            for si, st in enumerate(node.subtypes):
                if st.name:
                    named = True
                    type_name = st.name
                else:
                    unnamed = True
                    type_name = str(si)

                if named is not None and unnamed is not None:
                    raise errors.EdgeQLSyntaxError(
                        f'mixing named and unnamed tuple declaration '
                        f'is not supported',
                        context=node.subtypes[0].context,
                    )

                subtypes[type_name] = ast_to_type_shell(
                    cast(qlast.TypeName, st),
                    modaliases=modaliases,
                    metaclass=metaclass,
                    schema=schema,
                )

            try:
                return coll.create_shell(
                    schema,
                    subtypes=subtypes,
                    typemods={'named': bool(named)},
                )
            except errors.SchemaError as e:
                # all errors raised inside are pertaining to subtypes, so
                # the context should point to the first subtype
                e.set_source_context(node.subtypes[0].context)
                raise e

        elif issubclass(coll, s_types.Array):

            subtypes_list: List[s_types.TypeShell] = []
            for st in node.subtypes:
                subtypes_list.append(
                    ast_to_type_shell(
                        cast(qlast.TypeName, st),
                        modaliases=modaliases,
                        metaclass=metaclass,
                        schema=schema,
                    ))

            if len(subtypes_list) != 1:
                raise errors.SchemaError(
                    f'unexpected number of subtypes,'
                    f' expecting 1, got {len(subtypes_list)}',
                    context=node.context,
                )

            if isinstance(subtypes_list[0], s_types.ArrayTypeShell):
                raise errors.UnsupportedFeatureError(
                    'nested arrays are not supported',
                    context=node.subtypes[0].context,
                )

            try:
                return coll.create_shell(
                    schema,
                    subtypes=subtypes_list,
                )
            except errors.SchemaError as e:
                e.set_source_context(node.context)
                raise e

    elif isinstance(node.maintype, qlast.AnyType):
        from . import pseudo as s_pseudo
        return s_pseudo.PseudoTypeShell(name='anytype')

    elif isinstance(node.maintype, qlast.AnyTuple):
        from . import pseudo as s_pseudo
        return s_pseudo.PseudoTypeShell(name='anytuple')

    assert isinstance(node.maintype, qlast.ObjectRef)

    return ast_objref_to_type_shell(
        node.maintype,
        modaliases=modaliases,
        metaclass=metaclass,
        schema=schema,
    )