3535import com .scalar .db .exception .storage .NoMutationException ;
3636import com .scalar .db .exception .storage .RetriableExecutionException ;
3737import com .scalar .db .io .Column ;
38+ import com .scalar .db .util .ScalarDbUtils ;
3839import java .sql .Connection ;
3940import java .sql .SQLException ;
4041import java .util .ArrayList ;
@@ -395,7 +396,19 @@ private List<Put> dividePutForSourceTables(Put put, VirtualTableInfo virtualTabl
395396 putBuilderForLeftSourceTable .condition (ConditionBuilder .putIf (leftExpressions ));
396397 }
397398 if (!rightExpressions .isEmpty ()) {
398- putBuilderForRightSourceTable .condition (ConditionBuilder .putIf (rightExpressions ));
399+ if (isAllIsNullOnRightColumnsInLeftOuterJoin (virtualTableInfo , rightExpressions )
400+ && JdbcOperationAttributes
401+ .isLeftOuterVirtualTablePutIfIsNullOnRightColumnsConversionEnabled (put )) {
402+ // In a LEFT_OUTER join, when all conditions on the right source table columns are
403+ // IS_NULL, we cannot distinguish whether we should check for the existence of a
404+ // right-side record with NULL values or for the case where the right-side record does
405+ // not exist at all. Therefore, this behavior is controlled by the operation attribute.
406+ // By default, we convert the condition to PutIfNotExists, assuming that the more common
407+ // use case is to check that the right-side record does not exist.
408+ putBuilderForRightSourceTable .condition (ConditionBuilder .putIfNotExists ());
409+ } else {
410+ putBuilderForRightSourceTable .condition (ConditionBuilder .putIf (rightExpressions ));
411+ }
399412 }
400413 }
401414 }
@@ -464,7 +477,25 @@ private List<Delete> divideDeleteForSourceTables(Delete delete, VirtualTableInfo
464477 deleteBuilderForLeftSourceTable .condition (ConditionBuilder .deleteIf (leftExpressions ));
465478 }
466479 if (!rightExpressions .isEmpty ()) {
467- deleteBuilderForRightSourceTable .condition (ConditionBuilder .deleteIf (rightExpressions ));
480+ if (isAllIsNullOnRightColumnsInLeftOuterJoin (virtualTableInfo , rightExpressions )
481+ && !JdbcOperationAttributes
482+ .isLeftOuterVirtualTableDeleteIfIsNullOnRightColumnsAllowed (delete )) {
483+ // In a LEFT_OUTER join, when all conditions on the right source table columns are
484+ // IS_NULL, we cannot distinguish whether we should check for the existence of a
485+ // right-side record with NULL values or for the case where the right-side record does
486+ // not exist at all. This makes the delete operation semantically ambiguous. Therefore,
487+ // this behavior is controlled by the operation attribute. By default, we disallow this
488+ // operation to prevent unintended behavior.
489+ assert delete .forNamespace ().isPresent () && delete .forTable ().isPresent ();
490+ throw new IllegalArgumentException (
491+ CoreError
492+ .DELETE_IF_IS_NULL_FOR_RIGHT_SOURCE_TABLE_NOT_ALLOWED_FOR_LEFT_OUTER_VIRTUAL_TABLES
493+ .buildMessage (
494+ ScalarDbUtils .getFullTableName (
495+ delete .forNamespace ().get (), delete .forTable ().get ())));
496+ } else {
497+ deleteBuilderForRightSourceTable .condition (ConditionBuilder .deleteIf (rightExpressions ));
498+ }
468499 }
469500 }
470501 }
@@ -474,6 +505,13 @@ private List<Delete> divideDeleteForSourceTables(Delete delete, VirtualTableInfo
474505 return Arrays .asList (deleteForLeftSourceTable , deleteForRightSourceTable );
475506 }
476507
508+ private boolean isAllIsNullOnRightColumnsInLeftOuterJoin (
509+ VirtualTableInfo virtualTableInfo , List <ConditionalExpression > rightExpressions ) {
510+ return virtualTableInfo .getJoinType () == VirtualTableJoinType .LEFT_OUTER
511+ && rightExpressions .stream ()
512+ .allMatch (e -> e .getOperator () == ConditionalExpression .Operator .IS_NULL );
513+ }
514+
477515 private void close (Connection connection ) {
478516 try {
479517 if (connection != null ) {
0 commit comments