Skip to content

Commit

Permalink
Patch null constant handling in update expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
artiomchi committed Nov 24, 2024
1 parent 5aa4b59 commit c5191a7
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public override string GenerateCommand(
result.Append(CultureInfo.InvariantCulture, $"MERGE INTO {tableName} t USING (");
foreach (var item in entities.Select((e, ind) => new {e, ind}))
{
result.Append(" SELECT ");
result.Append("SELECT ");
result.Append(string.Join(", ", item.e.Select(ec => string.Join(" AS ", ExpandValue(ec.Value), EscapeName(ec.ColumnName)))));
result.Append(" FROM dual");
if (entities.Count > 1 && item.ind != entities.Count - 1)
Expand All @@ -51,21 +51,18 @@ public override string GenerateCommand(
}
result.Append(") s ON (");
result.Append(string.Join(" AND ", joinColumns.Select(j => $"t.{EscapeName(j.ColumnName)} = s.{EscapeName(j.ColumnName)}")));
result.Append(") ");
result.Append(" WHEN NOT MATCHED THEN INSERT (");
result.Append(") WHEN NOT MATCHED THEN INSERT (");
result.Append(string.Join(", ", entities.First().Where(e => e.AllowInserts).Select(e => EscapeName(e.ColumnName))));
result.Append(") VALUES (");
result.Append(string.Join(", ", entities.First().Where(e => e.AllowInserts).Select(e => $"s.{EscapeName(e.ColumnName)}")));
result.Append(") ");
result.Append(')');
if (updateExpressions is not null)
{
result.Append("WHEN MATCHED ");

result.Append("THEN UPDATE SET ");
result.Append(" WHEN MATCHED THEN UPDATE SET ");
result.Append(string.Join(", ", updateExpressions.Select(e => $"t.{EscapeName(e.ColumnName)} = {ExpandValue(e.Value)}")));
if (updateCondition is not null)
{
result.Append(CultureInfo.InvariantCulture, $" WHERE {ExpandExpression(updateCondition)} ");
result.Append(CultureInfo.InvariantCulture, $" WHERE {ExpandExpression(updateCondition)}");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,12 @@ protected virtual string GetTableName(IEntityType entityType)
}

KnownExpression? updateConditionExpression = null;
ConstantValue[]? updateConditionConstants = null;
if (updateCondition != null)
{
var updateConditionValue = updateCondition.Body.GetValue<TEntity>(updateCondition, entityType.FindProperty, queryOptions.UseExpressionCompiler);
if (updateConditionValue is not KnownExpression updateConditionExp)
throw new InvalidOperationException(Resources.TheUpdateConditionMustBeAComparisonExpression);
updateConditionExpression = updateConditionExp;
updateConditionConstants = updateConditionExpression.GetConstantValues().Where(c => c.Value != null).ToArray();
}

var newEntities = entities
Expand All @@ -171,8 +169,16 @@ protected virtual string GetTableName(IEntityType entityType)
.ToArray() as ICollection<(string ColumnName, ConstantValue Value, string DefaultSql, bool AllowInserts)>)
.ToArray();

var constantArgumentSourceValues = updateExpressions?.Select(e => e.Value);
if (updateConditionExpression != null)
constantArgumentSourceValues = constantArgumentSourceValues?.Append(updateConditionExpression) ?? [updateConditionExpression];
var expressionConstants = constantArgumentSourceValues
?.SelectMany(v => v.GetConstantValues())
.Where(c => c.Value != null)
.ToArray();

var entitiesProcessed = 0;
var singleEntityArguments = newEntities[0].Count + (updateExpressions?.Count ?? 0) + (updateConditionConstants?.Length ?? 0);
var singleEntityArguments = newEntities[0].Count + (expressionConstants?.Length ?? 0);
while (entitiesProcessed < newEntities.Length)
{
var arguments = new List<ConstantValue>();
Expand All @@ -187,11 +193,8 @@ protected virtual string GetTableName(IEntityType entityType)
while (entitiesProcessed < newEntities.Length &&
(MaxQueryParams == null || arguments.Count + singleEntityArguments < MaxQueryParams));

if (updateExpressions != null)
arguments.AddRange(updateExpressions.SelectMany(e => e.Value.GetConstantValues()));

if (updateConditionConstants != null)
arguments.AddRange(updateConditionConstants);
if (expressionConstants != null)
arguments.AddRange(expressionConstants);

foreach (var (arg, index) in arguments.Select((a, i) => (a, i)))
arg.ArgumentIndex = index;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,58 @@ public void Upsert_ConditionalExpression_UpdateFalse()
test => test.Should().MatchModel(dbItem, num2: 0));
}

[Fact]
public void Upsert_ConditionalExpression_CoalesceCheck()
{
ResetDb();
using var dbContext = new TestDbContext(_fixture.DataContextOptions);

var newItem = new TestEntity
{
Num1 = 1,
Num2 = 7,
Text1 = "hello",
Text2 = "world",
};

dbContext.TestEntities.Upsert(newItem)
.On(j => j.Num1)
.WhenMatched((old, ins) => new TestEntity
{
Text1 = ins.Text1 ?? old.Text1,
})
.Run();

dbContext.TestEntities.OrderBy(t => t.ID).Should().SatisfyRespectively(
test => test.Should().MatchModel(newItem));
}

[Fact]
public void Upsert_ConditionalExpression_NullValueCheck()
{
ResetDb();
using var dbContext = new TestDbContext(_fixture.DataContextOptions);

var newItem = new TestEntity
{
Num1 = 1,
Num2 = 7,
Text1 = "hello",
Text2 = "world",
};

dbContext.TestEntities.Upsert(newItem)
.On(j => j.Num1)
.WhenMatched((old, ins) => new TestEntity
{
Text1 = ins.Text1 == null ? old.Text1 : ins.Text1,
})
.Run();

dbContext.TestEntities.OrderBy(t => t.ID).Should().SatisfyRespectively(
test => test.Should().MatchModel(newItem));
}

[Fact]
public void Upsert_UpdateCondition_Constant()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,80 @@ public MySqlUpsertCommandRunnerTests()
{ }

protected override string NoUpdate_Sql =>
"INSERT IGNORE INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3)";
"INSERT IGNORE " +
"INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3)";

protected override string NoUpdate_Multiple_Sql =>
"INSERT IGNORE INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3), (@p4, @p5, @p6, @p7)";
"INSERT IGNORE " +
"INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3), (@p4, @p5, @p6, @p7)";

protected override string NoUpdate_WithNullable_Sql =>
"INSERT IGNORE INTO `TestEntityWithNullableKey` (`ID`, `ID1`, `ID2`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3, @p4, @p5)";
"INSERT IGNORE " +
"INTO `TestEntityWithNullableKey` (`ID`, `ID1`, `ID2`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3, @p4, @p5)";

protected override string Update_Constant_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Name` = @p4";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Name` = @p4";

protected override string Update_Constant_Multiple_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3), (@p4, @p5, @p6, @p7) ON DUPLICATE KEY UPDATE `Name` = @p8";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3), (@p4, @p5, @p6, @p7) " +
"ON DUPLICATE KEY UPDATE `Name` = @p8";

protected override string Update_Source_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Name` = VALUES(`Name`)";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Name` = VALUES(`Name`)";

protected override string Update_BinaryAdd_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Total` = ( `Total` + @p4 )";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Total` = ( `Total` + @p4 )";

protected override string Update_Coalesce_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Status` = ( COALESCE(`Status`, @p4) )";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Status` = ( COALESCE(`Status`, @p4) )";

protected override string Update_BinaryAddMultiply_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Total` = ( ( `Total` + @p4 ) * VALUES(`Total`) )";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Total` = ( ( `Total` + @p4 ) * VALUES(`Total`) )";

protected override string Update_BinaryAddMultiplyGroup_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Total` = ( `Total` + ( @p4 * VALUES(`Total`) ) )";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Total` = ( `Total` + ( @p4 * VALUES(`Total`) ) )";

protected override string Update_Condition_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Name` = IF (`Total` > @p5, @p4, `Name`)";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Name` = IF (`Total` > @p5, @p4, `Name`)";

protected override string Update_Condition_UpdateConditionColumn_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Name` = COALESCE (IF ((@xTotal := `Total`) IS NOT NULL, NULL, NULL), IF (@xTotal > @p6, @p4, `Name`)), `Total` = IF (@xTotal > @p6, ( `Total` + @p5 ), `Total`)";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE " +
"`Name` = COALESCE (IF ((@xTotal := `Total`) IS NOT NULL, NULL, NULL), IF (@xTotal > @p6, @p4, `Name`)), " +
"`Total` = IF (@xTotal > @p6, ( `Total` + @p5 ), `Total`)";

protected override string Update_Condition_AndCondition_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Name` = IF (( `Total` > @p5 ) AND ( `Status` != VALUES(`Status`) ), @p4, `Name`)";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Name` = IF (( `Total` > @p5 ) AND ( `Status` != VALUES(`Status`) ), @p4, `Name`)";

protected override string Update_Condition_NullCheck_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) VALUES (@p0, @p1, @p2, @p3) ON DUPLICATE KEY UPDATE `Name` = IF (`Status` IS NOT NULL, @p4, `Name`)";
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Name` = IF (`Status` IS NOT NULL, @p4, `Name`)";

protected override string Update_WatchWithNullCheck_Sql =>
"INSERT INTO `TestEntity` (`ID`, `Name`, `Status`, `Total`) " +
"VALUES (@p0, @p1, @p2, @p3) " +
"ON DUPLICATE KEY UPDATE `Name` = ( CASE WHEN ( VALUES(`Name`) IS NULL ) THEN @p4 ELSE VALUES(`Name`) END )";
}
}
Loading

0 comments on commit c5191a7

Please sign in to comment.