Skip to content

Commit 70ee49d

Browse files
committed
Fix REPLACE() null handling for Access/Jet SQL
Wrap REPLACE calls with IIF to ensure NULL propagation when any argument is NULL, preventing "Type mismatch" errors in Access. Substitute safe placeholders for NULL arguments to avoid runtime errors. Update tests to match new SQL output.
1 parent 3f18d11 commit 70ee49d

2 files changed

Lines changed: 76 additions & 10 deletions

File tree

src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,72 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
10101010
return sqlFunctionExpression;
10111011
}
10121012

1013+
if (sqlFunctionExpression.Name.Equals("REPLACE", StringComparison.OrdinalIgnoreCase) &&
1014+
sqlFunctionExpression.Arguments is { Count: 3 })
1015+
{
1016+
// Access VBA's Replace() throws "Type mismatch" when ANY argument is NULL rather than
1017+
// propagating NULL as relational semantics require. Access IIF is also non-short-circuit
1018+
// (evaluates both branches), so a simple IIF wrapper doesn't prevent the crash.
1019+
// Solution: outer IIF returns NULL when any nullable arg IS NULL, while inner IIFs
1020+
// substitute safe non-NULL placeholders so REPLACE never actually receives NULL.
1021+
static SqlExpression? GetNullableTarget(SqlExpression arg) => arg switch
1022+
{
1023+
ColumnExpression { IsNullable: true } col => col,
1024+
SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression { IsNullable: true } inner } => inner,
1025+
SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: SqlFunctionExpression { IsNullable: true } inner } => inner,
1026+
_ => null
1027+
};
1028+
1029+
var arg0Check = GetNullableTarget(sqlFunctionExpression.Arguments[0]);
1030+
var arg1Check = GetNullableTarget(sqlFunctionExpression.Arguments[1]);
1031+
var arg2Check = GetNullableTarget(sqlFunctionExpression.Arguments[2]);
1032+
1033+
if (arg0Check != null || arg1Check != null || arg2Check != null)
1034+
{
1035+
Sql.Append("IIF(");
1036+
var nullChecks = new SqlExpression?[] { arg0Check, arg1Check, arg2Check }
1037+
.Where(c => c != null).ToList();
1038+
for (int i = 0; i < nullChecks.Count; i++)
1039+
{
1040+
if (i > 0) Sql.Append(" OR ");
1041+
Visit(nullChecks[i]!);
1042+
Sql.Append(" IS NULL");
1043+
}
1044+
Sql.Append(", NULL, REPLACE(");
1045+
1046+
// Arg 0 (expression): '' prevents Type mismatch if NULL slips past outer IIF
1047+
if (arg0Check != null)
1048+
{
1049+
Sql.Append("IIF("); Visit(arg0Check); Sql.Append(" IS NULL, '', ");
1050+
Visit(sqlFunctionExpression.Arguments[0]); Sql.Append(")");
1051+
}
1052+
else Visit(sqlFunctionExpression.Arguments[0]);
1053+
1054+
Sql.Append(", ");
1055+
1056+
// Arg 1 (find): CHR(1) is a safe non-empty placeholder unlikely to appear in data
1057+
if (arg1Check != null)
1058+
{
1059+
Sql.Append("IIF("); Visit(arg1Check); Sql.Append(" IS NULL, CHR(1), ");
1060+
Visit(sqlFunctionExpression.Arguments[1]); Sql.Append(")");
1061+
}
1062+
else Visit(sqlFunctionExpression.Arguments[1]);
1063+
1064+
Sql.Append(", ");
1065+
1066+
// Arg 2 (replacewith): CHR(1) placeholder; result is discarded by outer IIF anyway
1067+
if (arg2Check != null)
1068+
{
1069+
Sql.Append("IIF("); Visit(arg2Check); Sql.Append(" IS NULL, CHR(1), ");
1070+
Visit(sqlFunctionExpression.Arguments[2]); Sql.Append(")");
1071+
}
1072+
else Visit(sqlFunctionExpression.Arguments[2]);
1073+
1074+
Sql.Append("))");
1075+
return sqlFunctionExpression;
1076+
}
1077+
}
1078+
10131079
if (sqlFunctionExpression.Name.Equals("MID", StringComparison.OrdinalIgnoreCase) &&
10141080
sqlFunctionExpression.Arguments is { Count: > 2 })
10151081
{

test/EFCore.Jet.FunctionalTests/Query/NullSemanticsQueryJetTest.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3144,17 +3144,17 @@ public override async Task Null_semantics_applied_when_comparing_two_functions_w
31443144
await base.Null_semantics_applied_when_comparing_two_functions_with_multiple_nullable_arguments(async);
31453145

31463146
AssertSql(
3147-
$"""
3148-
SELECT `e`.`Id`
3149-
FROM `Entities1` AS `e`
3150-
WHERE (REPLACE(`e`.`NullableStringA`, `e`.`NullableStringB`, `e`.`NullableStringC`) = `e`.`NullableStringA`) OR (REPLACE(`e`.`NullableStringA`, `e`.`NullableStringB`, `e`.`NullableStringC`) IS NULL AND `e`.`NullableStringA` IS NULL)
3151-
""",
3147+
"""
3148+
SELECT `e`.`Id`
3149+
FROM `Entities1` AS `e`
3150+
WHERE IIF(`e`.`NullableStringA` IS NULL OR `e`.`NullableStringB` IS NULL OR `e`.`NullableStringC` IS NULL, NULL, REPLACE(IIF(`e`.`NullableStringA` IS NULL, '', `e`.`NullableStringA`), IIF(`e`.`NullableStringB` IS NULL, CHR(1), `e`.`NullableStringB`), IIF(`e`.`NullableStringC` IS NULL, CHR(1), `e`.`NullableStringC`))) = `e`.`NullableStringA` OR ((`e`.`NullableStringA` IS NULL OR `e`.`NullableStringB` IS NULL OR `e`.`NullableStringC` IS NULL) AND `e`.`NullableStringA` IS NULL)
3151+
""",
31523152
//
3153-
$"""
3154-
SELECT `e`.`Id`
3155-
FROM `Entities1` AS `e`
3156-
WHERE ((REPLACE(`e`.`NullableStringA`, `e`.`NullableStringB`, `e`.`NullableStringC`) <> `e`.`NullableStringA`) OR (REPLACE(`e`.`NullableStringA`, `e`.`NullableStringB`, `e`.`NullableStringC`) IS NULL OR `e`.`NullableStringA` IS NULL)) AND (REPLACE(`e`.`NullableStringA`, `e`.`NullableStringB`, `e`.`NullableStringC`) IS NOT NULL OR `e`.`NullableStringA` IS NOT NULL)
3157-
""");
3153+
"""
3154+
SELECT `e`.`Id`
3155+
FROM `Entities1` AS `e`
3156+
WHERE (IIF(`e`.`NullableStringA` IS NULL OR `e`.`NullableStringB` IS NULL OR `e`.`NullableStringC` IS NULL, NULL, REPLACE(IIF(`e`.`NullableStringA` IS NULL, '', `e`.`NullableStringA`), IIF(`e`.`NullableStringB` IS NULL, CHR(1), `e`.`NullableStringB`), IIF(`e`.`NullableStringC` IS NULL, CHR(1), `e`.`NullableStringC`))) <> `e`.`NullableStringA` OR `e`.`NullableStringA` IS NULL OR `e`.`NullableStringB` IS NULL OR `e`.`NullableStringC` IS NULL OR `e`.`NullableStringA` IS NULL) AND ((`e`.`NullableStringA` IS NOT NULL AND `e`.`NullableStringB` IS NOT NULL AND `e`.`NullableStringC` IS NOT NULL) OR `e`.`NullableStringA` IS NOT NULL)
3157+
""");
31583158
}
31593159

31603160
public override async Task Null_semantics_coalesce(bool async)

0 commit comments

Comments
 (0)