Ejemplo n.º 1
0
def strip_output_var(
        var: pgast.OutputVar,
        *,
        optional: typing.Optional[bool] = None,
        nullable: typing.Optional[bool] = None) -> pgast.OutputVar:

    if isinstance(var, pgast.TupleVar):
        elements = []

        for el in var.elements:
            if isinstance(el.name, str):
                val = pgast.ColumnRef(name=[el.name])
            else:
                val = strip_output_var(el.name)

            elements.append(
                pgast.TupleElement(path_id=el.path_id, name=el.name, val=val))

        result = pgast.TupleVar(elements, named=var.named)
    else:
        result = pgast.ColumnRef(
            name=[var.name[-1]],
            optional=optional if optional is not None else var.optional,
            nullable=nullable if nullable is not None else var.nullable,
        )

    return result
Ejemplo n.º 2
0
def _compile_set_in_singleton_mode(
        node: irast.Set, *, ctx: context.CompilerContextLevel) -> pgast.Base:
    if isinstance(node, irast.EmptySet):
        return pgast.Constant(value=None)
    elif node.expr is not None:
        return dispatch.compile(node.expr, ctx=ctx)
    else:
        if node.rptr:
            ptrcls = node.rptr.ptrcls
            source = node.rptr.source

            if not ptrcls.is_link_property():
                if source.rptr:
                    raise RuntimeError('unexpectedly long path in simple expr')

            colref = pgast.ColumnRef(
                name=[common.edgedb_name_to_pg_name(ptrcls.shortname)])
        elif isinstance(node.scls, s_scalars.ScalarType):
            colref = pgast.ColumnRef(
                name=[common.edgedb_name_to_pg_name(node.scls.name)])
        else:
            colref = pgast.ColumnRef(
                name=[common.edgedb_name_to_pg_name(node.scls.name)])

        return colref
Ejemplo n.º 3
0
def _new_mapped_pointer_rvar(
        ir_ptr: irast.Pointer, *,
        ctx: context.CompilerContextLevel) -> pgast.BaseRangeVar:
    ptrcls = ir_ptr.ptrcls
    ptr_rvar = dbobj.range_for_pointer(ir_ptr, env=ctx.env)
    # Set up references according to the link direction.
    if isinstance(ptrcls, s_links.Link):
        # XXX: fix this once Properties are Sources
        src_ptr_info = pg_types.get_pointer_storage_info(
            ptrcls.getptr(ctx.env.schema, 'source'), resolve_type=False,
            schema=ctx.env.schema)
        src_col = src_ptr_info.column_name
    else:
        src_col = 'source'

    source_ref = pgast.ColumnRef(name=[src_col], nullable=False)

    if isinstance(ptrcls, s_links.Link):
        # XXX: fix this once Properties are Sources
        tgt_ptr_info = pg_types.get_pointer_storage_info(
            ptrcls.getptr(ctx.env.schema, 'target'), resolve_type=False,
            schema=ctx.env.schema)
        tgt_col = tgt_ptr_info.column_name
    else:
        tgt_col = 'target'

    target_ref = pgast.ColumnRef(
        name=[tgt_col],
        nullable=not ptrcls.get_required(ctx.env.schema))

    if ir_ptr.direction == s_pointers.PointerDirection.Inbound:
        near_ref = target_ref
        far_ref = source_ref
    else:
        near_ref = source_ref
        far_ref = target_ref

    src_pid = ir_ptr.source.path_id
    tgt_pid = ir_ptr.target.path_id
    ptr_pid = tgt_pid.ptr_path()

    ptr_rvar.query.path_id = ptr_pid
    pathctx.put_rvar_path_bond(ptr_rvar, src_pid)
    pathctx.put_rvar_path_output(ptr_rvar, src_pid, aspect='identity',
                                 var=near_ref, env=ctx.env)
    pathctx.put_rvar_path_output(ptr_rvar, src_pid, aspect='value',
                                 var=near_ref, env=ctx.env)
    pathctx.put_rvar_path_output(ptr_rvar, tgt_pid, aspect='value',
                                 var=far_ref, env=ctx.env)

    if tgt_pid.is_objtype_path():
        pathctx.put_rvar_path_bond(ptr_rvar, tgt_pid)
        pathctx.put_rvar_path_output(ptr_rvar, tgt_pid, aspect='identity',
                                     var=far_ref, env=ctx.env)

    return ptr_rvar
Ejemplo n.º 4
0
def new_root_rvar(
        ir_set: irast.Set, *,
        ctx: context.CompilerContextLevel) -> pgast.BaseRangeVar:
    if not isinstance(ir_set.stype, s_objtypes.ObjectType):
        raise ValueError('cannot create root rvar for non-object path')

    set_rvar = dbobj.range_for_set(ir_set, env=ctx.env)
    pathctx.put_rvar_path_bond(set_rvar, ir_set.path_id)
    set_rvar.value_scope.add(ir_set.path_id)

    if ir_set.rptr and ir_set.rptr.is_inbound:
        ptrcls = ir_set.rptr.ptrcls
        ptr_info = pg_types.get_pointer_storage_info(
            ptrcls, resolve_type=False, link_bias=False,
            schema=ctx.env.schema)

        if ptr_info.table_type == 'ObjectType':
            # Inline link
            rref = pgast.ColumnRef(
                name=[ptr_info.column_name],
                nullable=not ptrcls.get_required(ctx.env.schema))
            pathctx.put_rvar_path_bond(
                set_rvar, ir_set.path_id.src_path())
            pathctx.put_rvar_path_output(
                set_rvar, ir_set.path_id.src_path(),
                aspect='identity', var=rref, env=ctx.env)

    return set_rvar
Ejemplo n.º 5
0
def get_path_serialized_output(rel: pgast.Query, path_id: irast.PathId, *,
                               env: context.Environment) -> pgast.OutputVar:
    # Serialized output is a special case, we don't
    # want this behaviour to be recursive, so it
    # must be kept outside of get_path_output() generic.
    aspect = 'serialized'

    result = rel.path_outputs.get((path_id, aspect))
    if result is not None:
        return result

    ref = get_path_serialized_or_value_var(rel, path_id, env=env)

    ref = output.serialize_expr(ref, path_id=path_id, env=env)
    alias = get_path_output_alias(path_id, aspect, env=env)

    restarget = pgast.ResTarget(name=alias, val=ref)
    if hasattr(rel, 'returning_list'):
        rel.returning_list.append(restarget)
    else:
        rel.target_list.append(restarget)

    result = pgast.ColumnRef(name=[alias], nullable=ref.nullable)
    rel.path_outputs[path_id, aspect] = result
    return result
Ejemplo n.º 6
0
def get_path_output_or_null(
        rel: pgast.Query, path_id: irast.PathId, *,
        aspect: str, env: context.Environment) -> \
        typing.Tuple[pgast.OutputVar, bool]:

    path_id = map_path_id(path_id, rel.view_path_id_map)

    ref = maybe_get_path_output(rel, path_id, aspect=aspect, env=env)
    if ref is not None:
        return ref, False

    alt_aspect = get_less_specific_aspect(path_id, aspect)
    if alt_aspect is not None:
        ref = maybe_get_path_output(rel, path_id, aspect=alt_aspect, env=env)
        if ref is not None:
            rel.path_outputs[path_id, aspect] = ref
            return ref, False

    alias = env.aliases.get('null')
    restarget = pgast.ResTarget(name=alias, val=pgast.Constant(val=None))

    if hasattr(rel, 'returning_list'):
        rel.returning_list.append(restarget)
    else:
        rel.target_list.append(restarget)

    ref = pgast.ColumnRef(name=[alias], nullable=True)
    rel.path_outputs[path_id, aspect] = ref

    return ref, True
Ejemplo n.º 7
0
def new_empty_rvar(
        ir_set: irast.EmptySet, *,
        ctx: context.CompilerContextLevel) -> pgast.BaseRangeVar:
    nullref_alias = ctx.env.aliases.get('e')
    val = typecomp.cast(pgast.Constant(val=None, nullable=True),
                        source_type=ir_set.scls, target_type=ir_set.scls,
                        force=True, env=ctx.env)

    nullrel = pgast.SelectStmt(
        target_list=[
            pgast.ResTarget(
                val=val,
                name=nullref_alias
            )
        ],
        nullable=True
    )
    rvar = dbobj.rvar_for_rel(nullrel, env=ctx.env)
    rvar.path_scope.add(ir_set.path_id)
    rvar.value_scope.add(ir_set.path_id)
    null_ref = pgast.ColumnRef(name=[nullref_alias], nullable=True)
    pathctx.put_rvar_path_output(rvar, ir_set.path_id, aspect='value',
                                 var=null_ref, env=ctx.env)
    if ir_set.path_id.is_objtype_path():
        pathctx.put_rvar_path_output(rvar, ir_set.path_id, aspect='identity',
                                     var=null_ref, env=ctx.env)
    return rvar
Ejemplo n.º 8
0
def top_output_as_value(stmt: pgast.Query, *,
                        env: context.Environment) -> pgast.Query:
    """Finalize output serialization on the top level."""

    if env.output_format == context.OutputFormat.JSON:
        # For JSON we just want to aggregate the whole thing
        # into a JSON array.
        subrvar = pgast.RangeSubselect(
            subquery=stmt,
            alias=pgast.Alias(aliasname=env.aliases.get('aggw')))

        stmt_res = stmt.target_list[0]

        if stmt_res.name is None:
            stmt_res.name = env.aliases.get('v')

        new_val = pgast.FuncCall(name=('json_agg', ),
                                 args=[pgast.ColumnRef(name=[stmt_res.name])])

        # XXX: nullability introspection is not reliable,
        #      remove `True or` once it is.
        if True or stmt_res.val.nullable:
            new_val = pgast.CoalesceExpr(
                args=[new_val, pgast.Constant(val='[]')])

        result = pgast.SelectStmt(target_list=[pgast.ResTarget(val=new_val)],
                                  from_clause=[subrvar])

        result.ctes = stmt.ctes
        stmt.ctes = []

        return result

    else:
        return stmt
Ejemplo n.º 9
0
def array_as_json_object(expr, *, stype, env):
    if stype.element_type.is_tuple():
        coldeflist = []
        json_args = []
        is_named = stype.element_type.named

        for n, st in stype.element_type.iter_subtypes():
            colname = env.aliases.get(str(n))
            if is_named:
                json_args.append(pgast.StringConstant(val=n))

            val = pgast.ColumnRef(name=[colname])
            if st.is_collection():
                val = coll_as_json_object(val, stype=st, env=env)

            json_args.append(val)

            coldeflist.append(
                pgast.ColumnDef(
                    name=colname,
                    typename=pgast.TypeName(
                        name=pgtypes.pg_type_from_object(env.schema, st))))

        if is_named:
            json_func = 'jsonb_build_object'
        else:
            json_func = 'jsonb_build_array'

        return pgast.SelectStmt(target_list=[
            pgast.ResTarget(
                val=pgast.FuncCall(name=('jsonb_agg', ),
                                   args=[
                                       pgast.FuncCall(
                                           name=(json_func, ),
                                           args=json_args,
                                       )
                                   ]),
                ser_safe=True,
            )
        ],
                                from_clause=[
                                    pgast.RangeFunction(
                                        alias=pgast.Alias(
                                            aliasname=env.aliases.get('q'), ),
                                        coldeflist=coldeflist,
                                        functions=[
                                            pgast.FuncCall(
                                                name=('unnest', ),
                                                args=[expr],
                                            )
                                        ])
                                ])
    else:
        return pgast.FuncCall(name=('to_jsonb', ),
                              args=[expr],
                              null_safe=True,
                              ser_safe=True)
Ejemplo n.º 10
0
def _compile_set_in_singleton_mode(
        node: irast.Set, *, ctx: context.CompilerContextLevel) -> pgast.Base:
    if isinstance(node, irast.EmptySet):
        return pgast.NullConstant()
    elif node.expr is not None:
        return dispatch.compile(node.expr, ctx=ctx)
    else:
        if node.rptr:
            ptrcls = node.rptr.ptrcls
            source = node.rptr.source

            if not ptrcls.is_link_property(ctx.env.schema):
                if source.rptr:
                    raise RuntimeError(
                        'unexpectedly long path in simple expr')

            ptr_stor_info = pg_types.get_pointer_storage_info(
                ptrcls, schema=ctx.env.schema, resolve_type=False)

            colref = pgast.ColumnRef(name=[ptr_stor_info.column_name])
        elif isinstance(node.stype, s_scalars.ScalarType):
            colref = pgast.ColumnRef(
                name=[
                    common.edgedb_name_to_pg_name(
                        node.stype.get_name(ctx.env.schema))
                ]
            )
        else:
            colref = pgast.ColumnRef(
                name=[
                    common.edgedb_name_to_pg_name(
                        node.stype.get_name(ctx.env.schema))
                ]
            )

        return colref
Ejemplo n.º 11
0
def get_column(rvar: pgast.BaseRangeVar,
               colspec: typing.Union[str, pgast.ColumnRef],
               *,
               nullable: bool = None) -> pgast.ColumnRef:

    if isinstance(colspec, pgast.ColumnRef):
        colname = colspec.name[-1]
    else:
        colname = colspec

    ser_safe = False

    if nullable is None:
        if isinstance(rvar, pgast.RangeVar):
            # Range over a relation, we cannot infer nullability in
            # this context, so assume it's true.
            nullable = True

        elif isinstance(rvar, pgast.RangeSubselect):
            col_idx = find_column_in_subselect_rvar(rvar, colname)
            if astutils.is_set_op_query(rvar.subquery):
                nullables = []
                ser_safes = []
                astutils.for_each_query_in_set(
                    rvar.subquery, lambda q:
                    (nullables.append(q.target_list[col_idx].nullable),
                     ser_safes.append(q.target_list[col_idx].ser_safe)))
                nullable = any(nullables)
                ser_safe = all(ser_safes)
            else:
                rt = rvar.subquery.target_list[col_idx]
                nullable = rt.nullable
                ser_safe = rt.ser_safe

        elif isinstance(rvar, pgast.RangeFunction):
            # Range over a function.
            # TODO: look into the possibility of inspecting coldeflist.
            nullable = True

        elif isinstance(rvar, pgast.JoinExpr):
            raise RuntimeError(
                f'cannot find {colname!r} in unexpected {rvar!r} range var')

    name = [rvar.alias.aliasname, colname]

    return pgast.ColumnRef(name=name, nullable=nullable, ser_safe=ser_safe)
Ejemplo n.º 12
0
def get_path_output_or_null(
        rel: pgast.Query, path_id: irast.PathId, *,
        aspect: str, env: context.Environment) -> \
        typing.Tuple[pgast.OutputVar, bool]:
    try:
        ref = get_path_output(rel, path_id, aspect=aspect, env=env)
        is_null = False
    except LookupError:
        alias = env.aliases.get('null')
        restarget = pgast.ResTarget(name=alias, val=pgast.Constant(val=None))
        if hasattr(rel, 'returning_list'):
            rel.returning_list.append(restarget)
        else:
            rel.target_list.append(restarget)
        is_null = True
        ref = pgast.ColumnRef(name=[alias], nullable=True)

    return ref, is_null
Ejemplo n.º 13
0
def _get_rel_path_output(rel: pgast.BaseRelation,
                         path_id: irast.PathId,
                         *,
                         aspect: str,
                         ptr_info: typing.Optional[
                             pg_types.PointerStorageInfo] = None,
                         env: context.Environment) -> pgast.OutputVar:

    if path_id.is_objtype_path():
        if aspect == 'identity':
            aspect = 'value'

        if aspect != 'value':
            raise LookupError(
                f'invalid request for non-scalar path {path_id} {aspect}')

        if (path_id == rel.path_id or (rel.path_id.is_type_indirection_path()
                                       and path_id == rel.path_id.src_path())):
            path_id = irutils.get_id_path_id(path_id, schema=env.schema)
    else:
        if aspect == 'identity':
            raise LookupError(
                f'invalid request for scalar path {path_id} {aspect}')

    if path_id.rptr_dir() != s_pointers.PointerDirection.Outbound:
        raise LookupError(
            f'{path_id} is an inbound pointer and cannot be resolved '
            f'on a base relation')

    ptrcls = path_id.rptr()

    if ptrcls is None:
        raise ValueError(
            f'could not resolve trailing pointer class for {path_id}')

    ptr_info = pg_types.get_pointer_storage_info(ptrcls,
                                                 resolve_type=False,
                                                 link_bias=False)

    result = pgast.ColumnRef(name=[ptr_info.column_name],
                             nullable=rel.nullable or not ptrcls.required)
    rel.path_outputs[path_id, aspect] = result
    return result
Ejemplo n.º 14
0
def get_column(
        rvar: pgast.BaseRangeVar,
        colspec: typing.Union[str, pgast.ColumnRef], *,
        optional: bool=False, nullable: bool=None) -> pgast.ColumnRef:

    if isinstance(colspec, pgast.ColumnRef):
        colname = colspec.name[-1]
        if nullable is None:
            nullable = colspec.nullable
        optional = colspec.optional
    else:
        colname = colspec
        if nullable is None:
            # Assume the column is nullable unless told otherwise.
            nullable = True

    if rvar is None:
        name = [colname]
    else:
        name = [rvar.alias.aliasname, colname]

    return pgast.ColumnRef(name=name, nullable=nullable, optional=optional)
Ejemplo n.º 15
0
def get_column(rvar: pgast.BaseRangeVar,
               colspec: typing.Union[pgast.ColumnRef, str],
               *,
               optional: bool = False,
               nullable: bool = None) -> pgast.ColumnRef:

    if isinstance(colspec, pgast.ColumnRef):
        colname = colspec.name[-1]
        if nullable is None:
            nullable = rvar.nullable if rvar is not None else colspec.nullable
        optional = colspec.optional
    else:
        colname = colspec
        if nullable is None:
            nullable = rvar.nullable if rvar is not None else False
        optional = optional

    if rvar is None:
        name = [colname]
    else:
        name = [rvar.alias.aliasname, colname]

    return pgast.ColumnRef(name=name, nullable=nullable, optional=optional)
Ejemplo n.º 16
0
def cast(node: pgast.Base,
         *,
         source_type: s_obj.Object,
         target_type: s_obj.Object,
         force: bool = False,
         env: context.Environment) -> pgast.Base:

    if source_type.name == target_type.name and not force:
        return node

    schema = env.schema
    real_t = schema.get('std::anyreal')
    int_t = schema.get('std::anyint')
    json_t = schema.get('std::json')
    str_t = schema.get('std::str')
    datetime_t = schema.get('std::datetime')
    bool_t = schema.get('std::bool')

    if isinstance(target_type, s_types.Collection):
        if target_type.schema_name == 'array':

            if source_type.issubclass(json_t):
                # If we are casting a jsonb array to array, we do the
                # following transformation:
                # EdgeQL: <array<T>>MAP_VALUE
                # SQL:
                #      SELECT array_agg(j::T)
                #      FROM jsonb_array_elements(MAP_VALUE) AS j

                inner_cast = cast(pgast.ColumnRef(name=['j']),
                                  source_type=source_type,
                                  target_type=target_type.element_type,
                                  env=env)

                return pgast.SelectStmt(
                    target_list=[
                        pgast.ResTarget(val=pgast.FuncCall(
                            name=('array_agg', ), args=[inner_cast]))
                    ],
                    from_clause=[
                        pgast.RangeFunction(functions=[
                            pgast.FuncCall(name=('jsonb_array_elements', ),
                                           args=[node])
                        ],
                                            alias=pgast.Alias(aliasname='j'))
                    ])
            else:
                # EdgeQL: <array<int64>>['1', '2']
                # to SQL: ARRAY['1', '2']::int[]

                elem_pgtype = pg_types.pg_type_from_object(
                    schema, target_type.element_type, topbase=True)

                return pgast.TypeCast(arg=node,
                                      type_name=pgast.TypeName(
                                          name=elem_pgtype, array_bounds=[-1]))

    else:
        # `target_type` is not a collection.
        if (source_type.issubclass(datetime_t)
                and target_type.issubclass(str_t)):
            # Normalize datetime to text conversion to have the same
            # format as one would get by serializing to JSON.
            #
            # EdgeQL: <text><datetime>'2010-10-10';
            # To SQL: trim(to_json('2010-01-01'::timestamptz)::text, '"')
            return pgast.FuncCall(
                name=('trim', ),
                args=[
                    pgast.TypeCast(arg=pgast.FuncCall(name=('to_json', ),
                                                      args=[node]),
                                   type_name=pgast.TypeName(name=('text', ))),
                    pgast.Constant(val='"')
                ])

        elif source_type.issubclass(bool_t) and target_type.issubclass(int_t):
            # PostgreSQL 9.6 doesn't allow to cast 'boolean' to any integer
            # other than int32:
            #      SELECT 'true'::boolean::bigint;
            #      ERROR:  cannot cast type boolean to bigint
            # So we transform EdgeQL: <int64>BOOL
            # to SQL: BOOL::int::<targetint>
            return pgast.TypeCast(
                arg=pgast.TypeCast(arg=node,
                                   type_name=pgast.TypeName(name=('int', ))),
                type_name=pgast.TypeName(
                    name=pg_types.pg_type_from_scalar(schema, target_type)))

        elif source_type.issubclass(int_t) and target_type.issubclass(bool_t):
            # PostgreSQL 9.6 doesn't allow to cast any integer other
            # than int32 to 'boolean':
            #      SELECT 1::bigint::boolean;
            #      ERROR:  cannot cast type bigint to boolea
            # So we transform EdgeQL: <boolean>INT
            # to SQL: (INT != 0)
            return astutils.new_binop(node,
                                      pgast.Constant(val=0),
                                      op=ast.ops.NE)

        elif source_type.issubclass(json_t):
            if (target_type.issubclass(real_t)
                    or target_type.issubclass(bool_t)):
                # Simply cast to text and the to the target type.
                return cast(cast(node,
                                 source_type=source_type,
                                 target_type=str_t,
                                 env=env),
                            source_type=str_t,
                            target_type=target_type,
                            env=env)

            elif target_type.issubclass(str_t):
                # It's not possible to cast jsonb string to text directly,
                # so we do a trick:
                # EdgeQL: <str>JSONB_VAL
                # SQL: array_to_json(ARRAY[JSONB_VAL])->>0

                return astutils.new_binop(pgast.FuncCall(
                    name=('array_to_json', ),
                    args=[pgast.ArrayExpr(elements=[node])]),
                                          pgast.Constant(val=0),
                                          op='->>')

            elif target_type.issubclass(json_t):
                return pgast.TypeCast(
                    arg=node, type_name=pgast.TypeName(name=('jsonb', )))

        else:
            const_type = pg_types.pg_type_from_object(schema,
                                                      target_type,
                                                      topbase=True)

            return pgast.TypeCast(arg=node,
                                  type_name=pgast.TypeName(name=const_type))

    raise RuntimeError(
        f'could not cast {source_type.name} to {target_type.name}')
Ejemplo n.º 17
0
def process_link_update(
        ir_stmt: irast.MutatingStmt, ir_expr: irast.Base, props_only: bool,
        wrapper: pgast.Query, dml_cte: pgast.CommonTableExpr,
        iterator_cte: pgast.CommonTableExpr, *,
        ctx: context.CompilerContextLevel) -> typing.Optional[pgast.Query]:
    """Perform updates to a link relation as part of a DML statement.

    :param ir_stmt:
        IR of the statement.
    :param ir_expr:
        IR of the INSERT/UPDATE body element.
    :param props_only:
        Whether this link update only touches link properties.
    :param wrapper:
        Top-level SQL query.
    :param dml_cte:
        CTE representing the SQL INSERT or UPDATE to the main
        relation of the Object.
    :param iterator_cte:
        CTE representing the iterator range in the FOR clause of the
        EdgeQL DML statement.
    """
    toplevel = ctx.toplevel_stmt

    edgedb_ptr_tab = pgast.RangeVar(
        relation=pgast.Relation(schemaname='edgedb', name='pointer'),
        alias=pgast.Alias(aliasname=ctx.env.aliases.get(hint='ptr')))

    ltab_alias = edgedb_ptr_tab.alias.aliasname

    rptr = ir_expr.rptr
    ptrcls = rptr.ptrcls
    target_is_scalar = isinstance(ptrcls.target, s_scalars.ScalarType)

    path_id = rptr.source.path_id.extend(ptrcls, rptr.direction,
                                         rptr.target.scls)

    # The links in the dml class shape have been derived,
    # but we must use the correct specialized link class for the
    # base material type.
    mptrcls = ptrcls.material_type()

    # Lookup link class id by link name.
    lname_to_id = pgast.CommonTableExpr(query=pgast.SelectStmt(
        from_clause=[edgedb_ptr_tab],
        target_list=[
            pgast.ResTarget(val=pgast.ColumnRef(name=[ltab_alias, 'id']))
        ],
        where_clause=astutils.new_binop(
            lexpr=pgast.ColumnRef(name=[ltab_alias, 'name']),
            rexpr=pgast.Constant(val=mptrcls.name),
            op=ast.ops.EQ)),
                                        name=ctx.env.aliases.get(hint='lid'))

    lname_to_id_rvar = pgast.RangeVar(relation=lname_to_id)
    toplevel.ctes.append(lname_to_id)

    target_rvar = dbobj.range_for_ptrcls(mptrcls,
                                         '>',
                                         include_overlays=False,
                                         env=ctx.env)
    target_alias = target_rvar.alias.aliasname

    target_tab_name = (target_rvar.relation.schemaname,
                       target_rvar.relation.name)

    tab_cols = dbobj.cols_for_pointer(mptrcls, env=ctx.env)

    dml_cte_rvar = pgast.RangeVar(
        relation=dml_cte,
        alias=pgast.Alias(aliasname=ctx.env.aliases.get('m')))

    col_data = {
        'ptr_item_id':
        pgast.ColumnRef(name=[lname_to_id.name, 'id']),
        'std::source':
        pathctx.get_rvar_path_identity_var(dml_cte_rvar,
                                           ir_stmt.subject.path_id,
                                           env=ctx.env)
    }

    # Drop all previous link records for this source.
    delcte = pgast.CommonTableExpr(query=pgast.DeleteStmt(
        relation=target_rvar,
        where_clause=astutils.new_binop(
            lexpr=col_data['std::source'],
            op=ast.ops.EQ,
            rexpr=pgast.ColumnRef(name=[target_alias, 'std::source'])),
        using_clause=[dml_cte_rvar],
        returning_list=[
            pgast.ResTarget(val=pgast.ColumnRef(
                name=[target_alias, pgast.Star()]))
        ]),
                                   name=ctx.env.aliases.get(hint='d'))

    pathctx.put_path_value_rvar(delcte.query,
                                path_id.ptr_path(),
                                target_rvar,
                                env=ctx.env)

    # Record the effect of this removal in the relation overlay
    # context to ensure that the RETURNING clause potentially
    # referencing this link yields the expected results.
    overlays = ctx.env.rel_overlays[ptrcls.shortname]
    overlays.append(('except', delcte))
    toplevel.ctes.append(delcte)

    # Turn the IR of the expression on the right side of :=
    # into a subquery returning records for the link table.
    data_cte, specified_cols = process_link_values(ir_stmt,
                                                   ir_expr,
                                                   target_tab_name,
                                                   tab_cols,
                                                   col_data,
                                                   dml_cte_rvar,
                                                   [lname_to_id_rvar],
                                                   props_only,
                                                   target_is_scalar,
                                                   iterator_cte,
                                                   ctx=ctx)

    toplevel.ctes.append(data_cte)

    data_select = pgast.SelectStmt(
        target_list=[
            pgast.ResTarget(val=pgast.ColumnRef(
                name=[data_cte.name, pgast.Star()]))
        ],
        from_clause=[pgast.RangeVar(relation=data_cte)])

    # Inserting rows into the link table may produce cardinality
    # constraint violations, since the INSERT into the link table
    # is executed in the snapshot where the above DELETE from
    # the link table is not visible.  Hence, we need to use
    # the ON CONFLICT clause to resolve this.
    conflict_cols = ['std::source', 'std::target', 'ptr_item_id']
    conflict_inference = []
    conflict_exc_row = []

    for col in conflict_cols:
        conflict_inference.append(pgast.ColumnRef(name=[col]))
        conflict_exc_row.append(pgast.ColumnRef(name=['excluded', col]))

    conflict_data = pgast.SelectStmt(
        target_list=[
            pgast.ResTarget(val=pgast.ColumnRef(
                name=[data_cte.name, pgast.Star()]))
        ],
        from_clause=[pgast.RangeVar(relation=data_cte)],
        where_clause=astutils.new_binop(
            lexpr=pgast.ImplicitRowExpr(args=conflict_inference),
            rexpr=pgast.ImplicitRowExpr(args=conflict_exc_row),
            op='='))

    cols = [pgast.ColumnRef(name=[col]) for col in specified_cols]
    updcte = pgast.CommonTableExpr(
        name=ctx.env.aliases.get(hint='i'),
        query=pgast.InsertStmt(
            relation=target_rvar,
            select_stmt=data_select,
            cols=cols,
            on_conflict=pgast.OnConflictClause(
                action='update',
                infer=pgast.InferClause(index_elems=conflict_inference),
                target_list=[
                    pgast.MultiAssignRef(columns=cols, source=conflict_data)
                ]),
            returning_list=[
                pgast.ResTarget(val=pgast.ColumnRef(name=[pgast.Star()]))
            ]))

    pathctx.put_path_value_rvar(updcte.query,
                                path_id.ptr_path(),
                                target_rvar,
                                env=ctx.env)

    # Record the effect of this insertion in the relation overlay
    # context to ensure that the RETURNING clause potentially
    # referencing this link yields the expected results.
    overlays = ctx.env.rel_overlays[ptrcls.shortname]
    overlays.append(('union', updcte))

    toplevel.ctes.append(updcte)

    return data_cte
Ejemplo n.º 18
0
def init_dml_stmt(
        ir_stmt: irast.MutatingStmt, dml_stmt: pgast.DML, *,
        ctx: context.CompilerContextLevel,
        parent_ctx: context.CompilerContextLevel) \
        -> typing.Tuple[pgast.Query, pgast.CommonTableExpr,
                        pgast.CommonTableExpr]:
    """Prepare the common structure of the query representing a DML stmt.

    :param ir_stmt:
        IR of the statement.
    :param dml_stmt:
        SQL DML node instance.

    :return:
        A (*wrapper*, *dml_cte*, *range_cte*) tuple, where *wrapper* the
        the wrapping SQL statement, *dml_cte* is the CTE representing the
        SQL DML operation in the main relation of the Object, and
        *range_cte* is the CTE for the subset affected by the statement.
        *range_cte* is None for INSERT statmenets.
    """
    wrapper = ctx.rel

    clauses.init_stmt(ir_stmt, ctx, parent_ctx)

    target_ir_set = ir_stmt.subject

    dml_stmt.relation = dbobj.range_for_set(ir_stmt.subject,
                                            include_overlays=False,
                                            env=ctx.env)
    pathctx.put_path_value_rvar(dml_stmt,
                                target_ir_set.path_id,
                                dml_stmt.relation,
                                env=ctx.env)
    pathctx.put_path_source_rvar(dml_stmt,
                                 target_ir_set.path_id,
                                 dml_stmt.relation,
                                 env=ctx.env)
    dml_stmt.path_scope.add(target_ir_set.path_id)

    dml_cte = pgast.CommonTableExpr(query=dml_stmt,
                                    name=ctx.env.aliases.get(hint='m'))

    if isinstance(ir_stmt, (irast.UpdateStmt, irast.DeleteStmt)):
        # UPDATE and DELETE operate over a range, so generate
        # the corresponding CTE and connect it to the DML query.
        range_cte = get_dml_range(ir_stmt, dml_stmt, ctx=ctx)

        range_rvar = pgast.RangeVar(
            relation=range_cte,
            alias=pgast.Alias(aliasname=ctx.env.aliases.get(hint='range')))

        relctx.pull_path_namespace(target=dml_stmt, source=range_rvar, ctx=ctx)

        # Auxillary relations are always joined via the WHERE
        # clause due to the structure of the UPDATE/DELETE SQL statments.
        id_col = common.edgedb_name_to_pg_name('std::id')
        dml_stmt.where_clause = astutils.new_binop(
            lexpr=pgast.ColumnRef(
                name=[dml_stmt.relation.alias.aliasname, id_col]),
            op=ast.ops.EQ,
            rexpr=pathctx.get_rvar_path_identity_var(range_rvar,
                                                     target_ir_set.path_id,
                                                     env=ctx.env))

        # UPDATE has "FROM", while DELETE has "USING".
        if hasattr(dml_stmt, 'from_clause'):
            dml_stmt.from_clause.append(range_rvar)
        else:
            dml_stmt.using_clause.append(range_rvar)

    else:
        range_cte = None

    # Due to the fact that DML statements are structured
    # as a flat list of CTEs instead of nested range vars,
    # the top level path scope must be empty.  The necessary
    # range vars will be injected explicitly in all rels that
    # need them.
    ctx.path_scope.clear()

    pathctx.put_path_value_rvar(dml_stmt,
                                ir_stmt.subject.path_id,
                                dml_stmt.relation,
                                env=ctx.env)

    pathctx.put_path_source_rvar(dml_stmt,
                                 ir_stmt.subject.path_id,
                                 dml_stmt.relation,
                                 env=ctx.env)

    dml_rvar = pgast.RangeVar(
        relation=dml_cte,
        alias=pgast.Alias(aliasname=parent_ctx.env.aliases.get('d')))

    relctx.include_rvar(wrapper, dml_rvar, ir_stmt.subject.path_id, ctx=ctx)

    pathctx.put_path_bond(wrapper, ir_stmt.subject.path_id)

    return wrapper, dml_cte, dml_rvar, range_cte
Ejemplo n.º 19
0
def process_insert_body(ir_stmt: irast.MutatingStmt, wrapper: pgast.Query,
                        insert_cte: pgast.CommonTableExpr,
                        insert_rvar: pgast.BaseRangeVar, *,
                        ctx: context.CompilerContextLevel) -> None:
    """Generate SQL DML CTEs from an InsertStmt IR.

    :param ir_stmt:
        IR of the statement.
    :param wrapper:
        Top-level SQL query.
    :param insert_cte:
        CTE representing the SQL INSERT to the main relation of the Object.
    """
    cols = [pgast.ColumnRef(name=['std::__type__'])]
    select = pgast.SelectStmt(target_list=[])
    values = select.target_list

    # The main INSERT query of this statement will always be
    # present to insert at least the std::id and std::__type__
    # links.
    insert_stmt = insert_cte.query

    insert_stmt.cols = cols
    insert_stmt.select_stmt = select

    if ir_stmt.parent_stmt is not None:
        iterator_set = ir_stmt.parent_stmt.iterator_stmt
    else:
        iterator_set = None

    if iterator_set is not None:
        with ctx.substmt() as ictx:
            ictx.path_scope = ictx.path_scope.new_child()
            ictx.path_scope[iterator_set.path_id] = ictx.rel
            clauses.compile_iterator_expr(ictx.rel, iterator_set, ctx=ictx)
            ictx.rel.path_id = iterator_set.path_id
            pathctx.put_path_bond(ictx.rel, iterator_set.path_id)
            iterator_cte = pgast.CommonTableExpr(
                query=ictx.rel, name=ctx.env.aliases.get('iter'))
            ictx.toplevel_stmt.ctes.append(iterator_cte)
        iterator_rvar = dbobj.rvar_for_rel(iterator_cte, env=ctx.env)
        relctx.include_rvar(select,
                            iterator_rvar,
                            path_id=ictx.rel.path_id,
                            ctx=ctx)
        iterator_id = pathctx.get_path_identity_var(select,
                                                    iterator_set.path_id,
                                                    env=ctx.env)
    else:
        iterator_cte = None
        iterator_id = None

    values.append(
        pgast.ResTarget(val=pgast.SelectStmt(
            target_list=[pgast.ResTarget(val=pgast.ColumnRef(name=['id']))],
            from_clause=[
                pgast.RangeVar(relation=pgast.Relation(name='objecttype',
                                                       schemaname='edgedb'))
            ],
            where_clause=astutils.new_binop(
                op=ast.ops.EQ,
                lexpr=pgast.ColumnRef(name=['name']),
                rexpr=pgast.Constant(val=ir_stmt.subject.scls.shortname)))))

    external_inserts = []
    tuple_elements = []
    parent_link_props = []

    with ctx.newrel() as subctx:
        subctx.rel = select
        subctx.rel_hierarchy[select] = insert_stmt

        subctx.expr_exposed = False
        subctx.shape_format = context.ShapeFormat.FLAT

        if iterator_cte is not None:
            subctx.path_scope = ctx.path_scope.new_child()
            subctx.path_scope[iterator_cte.query.path_id] = select

        # Process the Insert IR and separate links that go
        # into the main table from links that are inserted into
        # a separate link table.
        for shape_el in ir_stmt.subject.shape:
            rptr = shape_el.rptr
            ptrcls = rptr.ptrcls.material_type()

            if (ptrcls.is_link_property()
                    and rptr.source.path_id != ir_stmt.subject.path_id):
                parent_link_props.append(shape_el)
                continue

            ptr_info = pg_types.get_pointer_storage_info(
                ptrcls,
                schema=subctx.env.schema,
                resolve_type=True,
                link_bias=False)

            props_only = False

            # First, process all local link inserts.
            if ptr_info.table_type == 'ObjectType':
                props_only = True
                field = pgast.ColumnRef(name=[ptr_info.column_name])
                cols.append(field)

                insvalue = insert_value_for_shape_element(insert_stmt,
                                                          wrapper,
                                                          ir_stmt,
                                                          shape_el,
                                                          iterator_id,
                                                          ptr_info=ptr_info,
                                                          ctx=subctx)

                tuple_el = astutils.tuple_element_for_shape_el(shape_el, field)
                tuple_elements.append(tuple_el)
                values.append(pgast.ResTarget(val=insvalue))

            ptr_info = pg_types.get_pointer_storage_info(ptrcls,
                                                         resolve_type=False,
                                                         link_bias=True)

            if ptr_info and ptr_info.table_type == 'link':
                external_inserts.append((shape_el, props_only))

        if iterator_cte is not None:
            cols.append(pgast.ColumnRef(name=['__edb_token']))

            values.append(pgast.ResTarget(val=iterator_id))

            pathctx.put_path_identity_var(insert_stmt,
                                          iterator_set.path_id,
                                          cols[-1],
                                          force=True,
                                          env=subctx.env)

            pathctx.put_path_bond(insert_stmt, iterator_set.path_id)

    toplevel = ctx.toplevel_stmt
    toplevel.ctes.append(insert_cte)

    # Process necessary updates to the link tables.
    for shape_el, props_only in external_inserts:
        process_link_update(ir_stmt,
                            shape_el,
                            props_only,
                            wrapper,
                            insert_cte,
                            iterator_cte,
                            ctx=ctx)

    if parent_link_props:
        prop_elements = []

        with ctx.newscope() as scopectx:
            scopectx.rel = wrapper

            for shape_el in parent_link_props:
                rptr = shape_el.rptr
                scopectx.path_scope[rptr.source.path_id] = wrapper
                pathctx.put_path_rvar_if_not_exists(wrapper,
                                                    rptr.source.path_id,
                                                    insert_rvar,
                                                    aspect='value',
                                                    env=scopectx.env)
                dispatch.compile(shape_el, ctx=scopectx)
                tuple_el = astutils.tuple_element_for_shape_el(shape_el, None)
                prop_elements.append(tuple_el)

        valtuple = pgast.TupleVar(elements=prop_elements, named=True)
        pathctx.put_path_value_var(wrapper,
                                   ir_stmt.subject.path_id,
                                   valtuple,
                                   force=True,
                                   env=ctx.env)
Ejemplo n.º 20
0
def get_path_output(rel: pgast.BaseRelation,
                    path_id: irast.PathId,
                    *,
                    aspect: str,
                    ptr_info: typing.Optional[
                        pg_types.PointerStorageInfo] = None,
                    env: context.Environment) -> pgast.OutputVar:

    view_path_id_map = getattr(rel, 'view_path_id_map', None)
    if view_path_id_map:
        path_id = map_path_id(path_id, view_path_id_map)

    result = rel.path_outputs.get((path_id, aspect))
    if result is not None:
        return result

    if isinstance(rel, pgast.Relation):
        return _get_rel_path_output(rel,
                                    path_id,
                                    aspect=aspect,
                                    ptr_info=ptr_info,
                                    env=env)
    else:
        ref = get_path_var(rel, path_id, aspect=aspect, env=env)

    other_output = find_path_output(rel, path_id, ref, env=env)
    if other_output is not None:
        rel.path_outputs[path_id, aspect] = other_output
        return other_output

    if isinstance(ref, pgast.TupleVar):
        elements = []
        for el in ref.elements:
            el_path_id = reverse_map_path_id(el.path_id, rel.view_path_id_map)
            element = get_path_output(rel, el_path_id, aspect=aspect, env=env)
            elements.append(
                pgast.TupleElement(path_id=el_path_id, name=element))
        result = pgast.TupleVar(elements=elements, named=ref.named)

    else:
        if astutils.is_set_op_query(rel):
            assert isinstance(ref, pgast.ColumnRef)
            result = dbobj.get_column(None, ref)
        else:
            alias = get_path_output_alias(path_id, aspect, env=env)

            restarget = pgast.ResTarget(name=alias, val=ref)
            if hasattr(rel, 'returning_list'):
                rel.returning_list.append(restarget)
            else:
                rel.target_list.append(restarget)

            if isinstance(ref, pgast.ColumnRef):
                nullable = ref.nullable
                optional = ref.optional
            else:
                nullable = rel.nullable
                optional = None

            result = pgast.ColumnRef(name=[alias],
                                     nullable=nullable,
                                     optional=optional)

    rel.path_outputs[path_id, aspect] = result
    return result
Ejemplo n.º 21
0
def range_for_ptrcls(
        ptrcls: s_links.Link, direction: s_pointers.PointerDirection, *,
        include_overlays: bool=True,
        env: context.Environment) -> pgast.BaseRangeVar:
    """"Return a Range subclass corresponding to a given ptr step.

    If `ptrcls` is a generic link, then a simple RangeVar is returned,
    otherwise the return value may potentially be a UNION of all tables
    corresponding to a set of specialized links computed from the given
    `ptrcls` taking source inheritance into account.
    """
    linkname = ptrcls.shortname
    endpoint = ptrcls.source

    tgt_col = pgtypes.get_pointer_storage_info(
        ptrcls, resolve_type=False, link_bias=True).column_name

    cols = [
        'std::source',
        tgt_col
    ]

    set_ops = []

    ptrclses = set()

    for source in {endpoint} | set(endpoint.descendants(env.schema)):
        # Sift through the descendants to see who has this link
        try:
            src_ptrcls = source.pointers[linkname].material_type()
        except KeyError:
            # This source has no such link, skip it
            continue
        else:
            if src_ptrcls in ptrclses:
                # Seen this link already
                continue
            ptrclses.add(src_ptrcls)

        table = table_from_ptrcls(src_ptrcls, env=env)

        qry = pgast.SelectStmt()
        qry.from_clause.append(table)
        qry.rptr_rvar = table

        # Make sure all property references are pulled up properly
        for colname in cols:
            selexpr = pgast.ColumnRef(
                name=[table.alias.aliasname, colname])
            qry.target_list.append(
                pgast.ResTarget(val=selexpr, name=colname))

        set_ops.append(('union', qry))

        overlays = env.rel_overlays.get(src_ptrcls.shortname)
        if overlays and include_overlays:
            for op, cte in overlays:
                rvar = pgast.RangeVar(
                    relation=cte,
                    alias=pgast.Alias(
                        aliasname=env.aliases.get(cte.name)
                    )
                )

                qry = pgast.SelectStmt(
                    target_list=[
                        pgast.ResTarget(
                            val=pgast.ColumnRef(
                                name=[col]
                            )
                        )
                        for col in cols
                    ],
                    from_clause=[rvar],
                )
                set_ops.append((op, qry))

    rvar = range_from_queryset(set_ops, ptrcls, env=env)
    return rvar
Ejemplo n.º 22
0
def _get_path_output(rel: pgast.BaseRelation,
                     path_id: irast.PathId,
                     *,
                     aspect: str,
                     allow_nullable: bool = True,
                     ptr_info: typing.Optional[
                         pg_types.PointerStorageInfo] = None,
                     env: context.Environment) -> pgast.OutputVar:

    result = rel.path_outputs.get((path_id, aspect))
    if result is not None:
        return result

    if is_terminal_relation(rel):
        return _get_rel_path_output(rel,
                                    path_id,
                                    aspect=aspect,
                                    ptr_info=ptr_info,
                                    env=env)
    else:
        ref = get_path_var(rel, path_id, aspect=aspect, env=env)

    other_output = find_path_output(rel, path_id, ref, env=env)
    if other_output is not None:
        rel.path_outputs[path_id, aspect] = other_output
        return other_output

    if isinstance(ref, pgast.TupleVar):
        elements = []
        for el in ref.elements:
            el_path_id = reverse_map_path_id(el.path_id, rel.view_path_id_map)

            try:
                # Similarly to get_path_var(), check for outer path_id
                # first for tuple serialized var disambiguation.
                element = _get_path_output(rel,
                                           el_path_id,
                                           aspect=aspect,
                                           allow_nullable=False,
                                           env=env)
            except LookupError:
                element = get_path_output(rel,
                                          el_path_id,
                                          aspect=aspect,
                                          allow_nullable=False,
                                          env=env)

            elements.append(
                pgast.TupleElement(path_id=el_path_id, name=element))

        result = pgast.TupleVar(elements=elements, named=ref.named)

    else:
        if astutils.is_set_op_query(rel):
            assert isinstance(ref, pgast.ColumnRef)
            result = dbobj.get_column(None, ref)
        else:
            alias = get_path_output_alias(path_id, aspect, env=env)

            restarget = pgast.ResTarget(name=alias, val=ref)
            if hasattr(rel, 'returning_list'):
                rel.returning_list.append(restarget)
            else:
                rel.target_list.append(restarget)

            nullable = is_nullable(ref, env=env)

            if isinstance(ref, pgast.ColumnRef):
                optional = ref.optional
            else:
                optional = None

            if nullable and not allow_nullable:
                var = get_path_var(rel, path_id, aspect=aspect, env=env)
                rel.where_clause = astutils.extend_binop(
                    rel.where_clause, pgast.NullTest(arg=var, negated=True))
                nullable = False

            result = pgast.ColumnRef(name=[alias],
                                     nullable=nullable,
                                     optional=optional)

    rel.path_outputs[path_id, aspect] = result
    return result
Ejemplo n.º 23
0
def _get_rel_path_output(rel: pgast.BaseRelation,
                         path_id: irast.PathId,
                         *,
                         aspect: str,
                         ptr_info: typing.Optional[
                             pg_types.PointerStorageInfo] = None,
                         env: context.Environment) -> pgast.OutputVar:

    if path_id.is_objtype_path():
        if aspect == 'identity':
            aspect = 'value'

        if aspect != 'value':
            raise LookupError(
                f'invalid request for non-scalar path {path_id} {aspect}')

        if (path_id == rel.path_id or (rel.path_id.is_type_indirection_path()
                                       and path_id == rel.path_id.src_path())):
            path_id = irutils.get_id_path_id(path_id, schema=env.schema)
    else:
        if aspect == 'identity':
            raise LookupError(
                f'invalid request for scalar path {path_id} {aspect}')

        elif aspect == 'serialized':
            aspect = 'value'

    var = rel.path_outputs.get((path_id, aspect))
    if var is not None:
        return var

    ptrcls = path_id.rptr()
    rptr_dir = path_id.rptr_dir()

    if (rptr_dir is not None
            and rptr_dir != s_pointers.PointerDirection.Outbound):
        raise LookupError(
            f'{path_id} is an inbound pointer and cannot be resolved '
            f'on a base relation')

    if isinstance(rel, pgast.NullRelation):
        if ptrcls is not None:
            target = ptrcls.target
        else:
            target = path_id.target

        if ptr_info is not None:
            name = ptr_info.column_name
        else:
            name = env.aliases.get('v')

        val = typecomp.cast(pgast.Constant(val=None, nullable=True),
                            source_type=target,
                            target_type=target,
                            force=True,
                            env=env)

        rel.target_list.append(pgast.ResTarget(name=name, val=val))
        result = pgast.ColumnRef(name=[name], nullable=True)
    else:
        if ptrcls is None:
            raise ValueError(
                f'could not resolve trailing pointer class for {path_id}')

        ptr_info = pg_types.get_pointer_storage_info(ptrcls,
                                                     resolve_type=False,
                                                     link_bias=False)

        result = pgast.ColumnRef(name=[ptr_info.column_name],
                                 nullable=not ptrcls.required)
    rel.path_outputs[path_id, aspect] = result
    return result