@@ -476,6 +476,32 @@ function MOI.is_valid(
476476 v_map = Variable. bridges (b):: Variable.Map
477477 return MOI. is_valid (v_map, ci)
478478 else
479+ # This return value has the potential to be a false positive: it
480+ # doesn't discriminate between constraints that the user added, and
481+ # constraints that a bridge added that were themselves bridged.
482+ #
483+ # "Fixing" this particular call has a number of wide reaching
484+ # effects because bridges need this to be "true" so that they can
485+ # query attributes of the constraint from `b`.
486+ #
487+ # In most cases a false positive doesn't matter, because we really
488+ # do support querying stuff about it. And also the user needs some
489+ # way of obtaining the correct index, which they won't have except
490+ # by luck/enumeration.
491+ #
492+ # The main place that this is problematic is when we come to delete
493+ # constraints, and in particular VariableIndex constraints, because we
494+ # triviallly have their `.value` field from the `.value` of the
495+ # VariableIndex.
496+ #
497+ # Instead of fixing everything though, we implement some extra
498+ # checks when deleting, and we leave the false-positive as-is for
499+ # now. If you, future reader, hit this comment while debugging, we
500+ # might need to revisit this decision.
501+ #
502+ # x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2696
503+ # x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817
504+ # x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818
479505 return haskey (Constraint. bridges (b), ci)
480506 end
481507 else
@@ -489,10 +515,10 @@ function _delete_variables_in_vector_of_variables_constraint(
489515 ci:: MOI.ConstraintIndex{MOI.VectorOfVariables,S} ,
490516) where {S}
491517 func = MOI. get (b, MOI. ConstraintFunction (), ci)
492- variables = copy (func. variables)
493- if vis == variables
518+ if vis == func. variables
494519 MOI. delete (b, ci)
495520 else
521+ variables = copy (func. variables)
496522 for vi in vis
497523 i = findfirst (isequal (vi), variables)
498524 if i != = nothing
@@ -504,34 +530,81 @@ function _delete_variables_in_vector_of_variables_constraint(
504530 end
505531 end
506532 end
533+ return
507534end
508535
536+ """
537+ _is_added_by_bridge(
538+ c_map,
539+ cache::Dict{Any,Set{Int64}},
540+ ci::MOI.ConstraintIndex{F,S},
541+ ) where {F,S}
542+
543+ Return `true` if `ci` was added by one of the bridges in `c_map`.
544+
545+ For performance reasons, we store the index values associated with
546+ `MOI.ListOfConstraintIndices{F,S}` in `cache` so that we don't have to keep
547+ looping through the bridges.
548+ """
549+ function _is_added_by_bridge (
550+ c_map,
551+ cache:: Dict{Any,Set{Int64}} ,
552+ ci:: MOI.ConstraintIndex{F,S} ,
553+ ) where {F,S}
554+ ret = get! (cache, (F, S)) do
555+ set = Set {Int64} ()
556+ for bridge in values (c_map)
557+ for ci in MOI. get (
558+ bridge,
559+ MOI. ListOfConstraintIndices {F,S} (),
560+ )
561+ push! (set, ci. value)
562+ end
563+ end
564+ return set
565+ end :: Set{Int64}
566+ return ci. value in ret
567+ end
568+
569+ """
570+ _delete_variables_in_variables_constraints(
571+ b::AbstractBridgeOptimizer,
572+ vis::Vector{MOI.VariableIndex},
573+ )
574+
575+ The point of this function is to delete constraints associated with the
576+ variables in `vis`.
577+
578+ ## Warning
579+
580+ Because of the false positive potential in
581+ `is_valid(::AbstractBridgeOptimizer, MOI.ConstraintIndex)`, we need to ensure
582+ that we delete constraints only if they were not added by a different constraint
583+ bridge, otherwise when we come to delete the parent constraint we'll hit a
584+ runtime error where we have already deleted part of the bridged constraint.
585+
586+ x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817
587+ x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818
588+ """
509589function _delete_variables_in_variables_constraints (
510590 b:: AbstractBridgeOptimizer ,
511591 vis:: Vector{MOI.VariableIndex} ,
512592)
513593 c_map = Constraint. bridges (b):: Constraint.Map
514- # Delete all `MOI.VectorOfVariables` constraints of these variables.
515- # We reverse for the same reason as for `VariableIndex` below.
516- # As the iterators are lazy, when the inner bridge constraint is deleted,
517- # it won't be part of the iteration.
518- for ci in
519- Iterators. reverse (Constraint. vector_of_variables_constraints (c_map))
520- _delete_variables_in_vector_of_variables_constraint (b, vis, ci)
521- end
522- # Delete all `MOI.VariableIndex` constraints of these variables.
594+ cache = Dict {Any,Set{Int64}} ()
523595 for vi in vis
524- # If a bridged `VariableIndex` constraints creates a second one,
525- # then we will delete the second one when deleting the first one hence we
526- # should not delete it again in this loop.
527- # For this, we reverse the order so that we encounter the first one first
528- # and we won't delete the second one since `MOI.is_valid(b, ci)` will be `false`.
529- for ci in Iterators. reverse (Constraint. variable_constraints (c_map, vi))
530- if MOI. is_valid (b, ci)
596+ for ci in Constraint. variable_constraints (c_map, vi)
597+ if ! _is_added_by_bridge (c_map, cache, ci)
531598 MOI. delete (b, ci)
532599 end
533600 end
534601 end
602+ for ci in Constraint. vector_of_variables_constraints (c_map)
603+ if ! _is_added_by_bridge (c_map, cache, ci)
604+ _delete_variables_in_vector_of_variables_constraint (b, vis, ci)
605+ end
606+ end
607+ return
535608end
536609
537610function _delete_variables_in_bridged_objective (
0 commit comments