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