Example #1
0
def compile_inheritance_conflict_selects(
    stmt: irast.MutatingStmt,
    conflict: irast.MutatingStmt,
    typ: s_objtypes.ObjectType,
    subject_type: s_objtypes.ObjectType,
    *, ctx: context.ContextLevel,
) -> List[irast.OnConflictClause]:
    """Compile the selects needed to resolve multiple DML to related types

    Generate a SELECT that finds all objects of type `typ` that conflict with
    the insert `stmt`. The backend will use this to explicitly check that
    no conflicts exist, and raise an error if they do.

    This is needed because we mostly use triggers to enforce these
    cross-type exclusive constraints, and they use a snapshot
    beginning at the start of the statement.
    """
    pointers = _get_exclusive_ptr_constraints(typ, ctx=ctx)
    exclusive = ctx.env.schema.get('std::exclusive', type=s_constr.Constraint)
    obj_constrs = [
        constr for constr in
        typ.get_constraints(ctx.env.schema).objects(ctx.env.schema)
        if constr.issubclass(ctx.env.schema, exclusive)
    ]

    shape_ptrs = set()
    for elem, op in stmt.subject.shape:
        assert elem.rptr is not None
        if op != qlast.ShapeOp.MATERIALIZE:
            shape_ptrs.add(elem.rptr.ptrref.shortname.name)

    # This is a little silly, but for *this* we need to do one per
    # constraint (so that we can properly identify which constraint
    # failed in the error messages)
    entries: List[Tuple[s_constr.Constraint, ConstraintPair]] = []
    for name, (ptr, ptr_constrs) in pointers.items():
        for ptr_constr in ptr_constrs:
            # For updates, we only need to emit the check if we actually
            # modify a pointer used by the constraint. For inserts, though
            # everything must be in play, since constraints can depend on
            # nonexistence also.
            if (
                _constr_matters(ptr_constr, ctx)
                and (
                    isinstance(stmt, irast.InsertStmt)
                    or (_get_needed_ptrs(typ, (), [name], ctx)[0] & shape_ptrs)
                )
            ):
                entries.append((ptr_constr, ({name: (ptr, [ptr_constr])}, [])))
    for obj_constr in obj_constrs:
        # See note above about needed ptrs check
        if (
            _constr_matters(obj_constr, ctx)
            and (
                isinstance(stmt, irast.InsertStmt)
                or (_get_needed_ptrs(
                    typ, [obj_constr], (), ctx)[0] & shape_ptrs)
            )
        ):
            entries.append((obj_constr, ({}, [obj_constr])))

    # For updates, we need to pull from the actual result overlay,
    # since the final row can depend on things not in the query.
    fake_dml_set = None
    if isinstance(stmt, irast.UpdateStmt):
        fake_subject = qlast.DetachedExpr(expr=qlast.Path(steps=[
            s_utils.name_to_ast_ref(subject_type.get_name(ctx.env.schema))]))

        fake_dml_set = dispatch.compile(fake_subject, ctx=ctx)

    clauses = []
    for cnstr, (p, o) in entries:
        select_ir, _, _ = compile_conflict_select(
            stmt, typ,
            for_inheritance=True,
            fake_dml_set=fake_dml_set,
            constrs=p,
            obj_constrs=o,
            parser_context=stmt.context, ctx=ctx)
        if isinstance(select_ir, irast.EmptySet):
            continue
        cnstr_ref = irast.ConstraintRef(id=cnstr.id)
        clauses.append(
            irast.OnConflictClause(
                constraint=cnstr_ref, select_ir=select_ir, always_check=False,
                else_ir=None, else_fail=conflict,
                update_query_set=fake_dml_set)
        )
    return clauses
Example #2
0
def compile_insert_unless_conflict_on(
    stmt: irast.InsertStmt,
    typ: s_objtypes.ObjectType,
    constraint_spec: qlast.Expr,
    else_branch: Optional[qlast.Expr],
    *, ctx: context.ContextLevel,
) -> irast.OnConflictClause:

    with ctx.new() as constraint_ctx:
        constraint_ctx.partial_path_prefix = stmt.subject

        # We compile the name here so we can analyze it, but we don't do
        # anything else with it.
        cspec_res = dispatch.compile(constraint_spec, ctx=constraint_ctx)

    # We accept a property, link, or a list of them in the form of a
    # tuple.
    if cspec_res.rptr is None and isinstance(cspec_res.expr, irast.Tuple):
        cspec_args = [elem.val for elem in cspec_res.expr.elements]
    else:
        cspec_args = [cspec_res]

    for cspec_arg in cspec_args:
        if not cspec_arg.rptr:
            raise errors.QueryError(
                'UNLESS CONFLICT argument must be a property, link, '
                'or tuple of properties and links',
                context=constraint_spec.context,
            )

        if cspec_arg.rptr.source.path_id != stmt.subject.path_id:
            raise errors.QueryError(
                'UNLESS CONFLICT argument must be a property of the '
                'type being inserted',
                context=constraint_spec.context,
            )

    schema = ctx.env.schema

    ptrs = []
    exclusive_constr = schema.get('std::exclusive', type=s_constr.Constraint)
    for cspec_arg in cspec_args:
        assert cspec_arg.rptr is not None
        schema, ptr = (
            typeutils.ptrcls_from_ptrref(cspec_arg.rptr.ptrref, schema=schema))
        if not isinstance(ptr, s_pointers.Pointer):
            raise errors.QueryError(
                'UNLESS CONFLICT property must be a property',
                context=constraint_spec.context,
            )

        ptr = ptr.get_nearest_non_derived_parent(schema)
        ptr_card = ptr.get_cardinality(schema)
        if not ptr_card.is_single():
            raise errors.QueryError(
                'UNLESS CONFLICT property must be a SINGLE property',
                context=constraint_spec.context,
            )

        ptrs.append(ptr)

    obj_constrs = inference.cardinality.get_object_exclusive_constraints(
        typ, set(ptrs), ctx.env)

    field_constrs = []
    if len(ptrs) == 1:
        field_constrs = [
            c for c in ptrs[0].get_constraints(schema).objects(schema)
            if c.issubclass(schema, exclusive_constr)]

    all_constrs = list(obj_constrs) + field_constrs
    if len(all_constrs) != 1:
        raise errors.QueryError(
            'UNLESS CONFLICT property must have a single exclusive constraint',
            context=constraint_spec.context,
        )

    ds = {ptr.get_shortname(schema).name: (ptr, field_constrs)
          for ptr in ptrs}
    select_ir, always_check, from_anc = compile_conflict_select(
        stmt, typ, constrs=ds, obj_constrs=list(obj_constrs),
        parser_context=stmt.context, ctx=ctx)

    # Compile an else branch
    else_ir = None
    if else_branch:
        # TODO: We should support this, but there is some semantic and
        # implementation trickiness.
        if from_anc:
            raise errors.UnsupportedFeatureError(
                'UNLESS CONFLICT can not use ELSE when constraint is from a '
                'parent type',
                details=(
                    f"The existing object can't be exposed in the ELSE clause "
                    f"because it may not have type {typ.get_name(schema)}"),
                context=constraint_spec.context,
            )

        # The ELSE needs to be able to reference the subject in an
        # UPDATE, even though that would normally be prohibited.
        ctx.path_scope.factoring_allowlist.add(stmt.subject.path_id)

        # Compile else
        else_ir = dispatch.compile(
            astutils.ensure_qlstmt(else_branch), ctx=ctx)
        assert isinstance(else_ir, irast.Set)

    return irast.OnConflictClause(
        constraint=irast.ConstraintRef(id=all_constrs[0].id),
        select_ir=select_ir,
        always_check=always_check,
        else_ir=else_ir
    )
Example #3
0
def compile_insert_unless_conflict_on(
    stmt: irast.InsertStmt,
    insert_subject: qlast.Path,
    constraint_spec: qlast.Expr,
    else_branch: Optional[qlast.Expr],
    *, ctx: context.ContextLevel,
) -> irast.OnConflictClause:

    with ctx.new() as constraint_ctx:
        constraint_ctx.partial_path_prefix = stmt.subject

        # We compile the name here so we can analyze it, but we don't do
        # anything else with it.
        cspec_res = setgen.ensure_set(dispatch.compile(
            constraint_spec, ctx=constraint_ctx), ctx=constraint_ctx)

    if not cspec_res.rptr:
        raise errors.QueryError(
            'UNLESS CONFLICT argument must be a property',
            context=constraint_spec.context,
        )

    if cspec_res.rptr.source.path_id != stmt.subject.path_id:
        raise errors.QueryError(
            'UNLESS CONFLICT argument must be a property of the '
            'type being inserted',
            context=constraint_spec.context,
        )

    schema = ctx.env.schema
    schema, typ = typeutils.ir_typeref_to_type(schema, stmt.subject.typeref)
    assert isinstance(typ, s_objtypes.ObjectType)
    real_typ = typ.get_nearest_non_derived_parent(schema)

    schema, ptr = (
        typeutils.ptrcls_from_ptrref(cspec_res.rptr.ptrref,
                                     schema=schema))
    if not isinstance(ptr, s_pointers.Pointer):
        raise errors.QueryError(
            'UNLESS CONFLICT property must be a property',
            context=constraint_spec.context,
        )

    ptr = ptr.get_nearest_non_derived_parent(schema)
    ptr_card = ptr.get_cardinality(schema)
    if not ptr_card.is_single():
        raise errors.QueryError(
            'UNLESS CONFLICT property must be a SINGLE property',
            context=constraint_spec.context,
        )

    exclusive_constr = schema.get('std::exclusive', type=s_constr.Constraint)
    ex_cnstrs = [c for c in ptr.get_constraints(schema).objects(schema)
                 if c.issubclass(schema, exclusive_constr)]

    if len(ex_cnstrs) != 1:
        raise errors.QueryError(
            'UNLESS CONFLICT property must have a single exclusive constraint',
            context=constraint_spec.context,
        )

    module_id = schema.get_global(
        s_mod.Module, ptr.get_name(schema).module).id

    field_name = cspec_res.rptr.ptrref.shortname
    ds = {field_name.name: (ptr, ex_cnstrs)}
    select_ir = compile_insert_unless_conflict_select(
        stmt, insert_subject, real_typ, constrs=ds, obj_constrs=[],
        parser_context=stmt.context, ctx=ctx)

    # Compile an else branch
    else_ir = None
    if else_branch:
        # The ELSE needs to be able to reference the subject in an
        # UPDATE, even though that would normally be prohibited.
        ctx.path_scope.factoring_allowlist.add(stmt.subject.path_id)

        # Compile else
        else_ir = dispatch.compile(
            astutils.ensure_qlstmt(else_branch), ctx=ctx)
        assert isinstance(else_ir, irast.Set)

    return irast.OnConflictClause(
        constraint=irast.ConstraintRef(
            id=ex_cnstrs[0].id, module_id=module_id),
        select_ir=select_ir,
        else_ir=else_ir
    )
Example #4
0
def compile_insert_unless_conflict(
    stmt: irast.InsertStmt,
    insert_subject: qlast.Path,
    constraint_spec: qlast.Expr,
    else_branch: Optional[qlast.Expr],
    *,
    ctx: context.ContextLevel,
) -> irast.OnConflictClause:

    with ctx.new() as constraint_ctx:
        constraint_ctx.partial_path_prefix = stmt.subject

        # We compile the name here so we can analyze it, but we don't do
        # anything else with it.
        cspec_res = setgen.ensure_set(dispatch.compile(constraint_spec,
                                                       ctx=constraint_ctx),
                                      ctx=constraint_ctx)

    if not cspec_res.rptr:
        raise errors.QueryError(
            'ON CONFLICT argument must be a property',
            context=constraint_spec.context,
        )

    if cspec_res.rptr.source.path_id != stmt.subject.path_id:
        raise errors.QueryError(
            'ON CONFLICT argument must be a property of the '
            'type being inserted',
            context=constraint_spec.context,
        )

    schema = ctx.env.schema
    schema, ptr = (typeutils.ptrcls_from_ptrref(cspec_res.rptr.ptrref,
                                                schema=schema))
    if not isinstance(ptr, s_pointers.Pointer):
        raise errors.QueryError(
            'ON CONFLICT property must be a property',
            context=constraint_spec.context,
        )

    ptr = ptr.get_nearest_non_derived_parent(schema)
    if ptr.get_cardinality(schema) != qltypes.SchemaCardinality.ONE:
        raise errors.QueryError(
            'ON CONFLICT property must be a SINGLE property',
            context=constraint_spec.context,
        )

    exclusive_constr: s_constr.Constraint = schema.get('std::exclusive')
    ex_cnstrs = [
        c for c in ptr.get_constraints(schema).objects(schema)
        if c.issubclass(schema, exclusive_constr)
    ]

    if len(ex_cnstrs) != 1:
        raise errors.QueryError(
            'ON CONFLICT property must have a single exclusive constraint',
            context=constraint_spec.context,
        )

    module_id = schema.get_global(s_mod.Module, ptr.get_name(schema).module).id

    field_name = cspec_res.rptr.ptrref.shortname

    # Find the IR corresponding to our field
    # FIXME: Is there a better way to do this?
    for elem, _ in stmt.subject.shape:
        if elem.rptr.ptrref.shortname == field_name:
            key = elem.expr
            break
    else:
        raise errors.QueryError(
            'INSERT ON CONFLICT property requires matching shape',
            context=constraint_spec.context,
        )

    # FIXME: This reuse of the source
    ctx.anchors = ctx.anchors.copy()
    source_alias = ctx.aliases.get('a')
    ctx.anchors[source_alias] = setgen.ensure_set(key, ctx=ctx)
    anchor = qlast.Path(steps=[qlast.ObjectRef(name=source_alias)])

    ctx.env.schema = schema

    # Compile an else branch
    else_info = None
    if else_branch:
        # Produce a query that finds the conflicting objects
        nobe = qlast.SelectQuery(
            result=insert_subject,
            where=qlast.BinOp(op='=', left=constraint_spec, right=anchor),
        )
        select_ir = dispatch.compile(nobe, ctx=ctx)
        select_ir = setgen.scoped_set(select_ir, force_reassign=True, ctx=ctx)
        assert isinstance(select_ir, irast.Set)

        # The ELSE needs to be able to reference the subject in an
        # UPDATE, even though that would normally be prohibited.
        ctx.path_scope.factoring_allowlist.add(stmt.subject.path_id)

        # Compile else
        else_ir = dispatch.compile(astutils.ensure_qlstmt(else_branch),
                                   ctx=ctx)
        assert isinstance(else_ir, irast.Set)
        else_info = irast.OnConflictElse(select_ir, else_ir)

    return irast.OnConflictClause(
        irast.ConstraintRef(id=ex_cnstrs[0].id, module_id=module_id),
        else_info)
Example #5
0
def handle_conditional_insert(
    expr: qlast.InsertQuery,
    rhs: irast.InsertStmt,
    lhs_set: Union[irast.Expr, irast.Set],
    *,
    ctx: context.ContextLevel,
) -> irast.ConstraintRef:
    def error(s: str) -> NoReturn:
        raise errors.QueryError(
            f'Invalid conditional INSERT statement: {s}',
            context=expr.context,
        )

    schema = ctx.env.schema

    if not isinstance(lhs_set, irast.Set):
        error("left hand side is not SELECT")
    lhs_set = irutils.unwrap_set(lhs_set)
    if not isinstance(lhs_set.expr, irast.SelectStmt):
        error("left hand side is not SELECT")
    lhs = lhs_set.expr

    if lhs.result.path_id != rhs.subject.path_id:
        error("types do not match")

    if lhs.where:
        filtered_ptrs = inference.cardinality.extract_filters(
            lhs.result,
            lhs.where,
            scope_tree=ctx.path_scope,
            singletons=(),
            env=ctx.env,
            strict=True)
    else:
        filtered_ptrs = None

    # TODO: Can we support some >1 cases?
    if not filtered_ptrs or len(filtered_ptrs) > 1:
        error("does not contain exactly one FILTER clause")
        return None

    exclusive_constr: s_constr.Constraint = schema.get('std::exclusive')

    shape_props = {}
    for shape_set, _ in rhs.subject.shape:
        # We need to go through to the base_ptr to get at the
        # underlying type (instead of the shape's subtype)
        base_ptr = shape_set.rptr.ptrref.base_ptr
        if (not isinstance(base_ptr, irast.PointerRef)
                or not isinstance(shape_set.expr, irast.SelectStmt)):
            continue
        schema, pptr = typeutils.ptrcls_from_ptrref(base_ptr, schema=schema)
        shape_props[pptr] = shape_set.expr.result, base_ptr

    ptr, ptr_set = filtered_ptrs[0]

    ptr = ptr.get_nearest_non_derived_parent(schema)
    if ptr not in shape_props:
        error("property in FILTER clause does not match INSERT")
    result, rptr = shape_props[ptr]

    if not simple_stmt_eq(ptr_set.expr, result.expr, schema):
        error("value in FILTER clause does not match INSERT")

    ex_cnstrs = [
        c for c in ptr.get_constraints(schema).objects(schema)
        if c.issubclass(schema, exclusive_constr)
        and not c.get_subjectexpr(schema)
    ]

    if len(ex_cnstrs) != 1 or not ptr.is_property(schema):
        error("FILTER is not on an exclusive property")

    ex_cnstr = ex_cnstrs[0]

    module_id = schema.get_global(s_mod.Module, ptr.get_name(schema).module).id

    ctx.env.schema = schema
    return irast.ConstraintRef(id=ex_cnstr.id, module_id=module_id)