Skip to content

Commit 0e8640b

Browse files
committed
Five new EFCoreUpdateDelete tests for ExecuteUpdate / ExecuteDelete shapes the existing coverage skipped: multi-SetProperty chain (single UPDATE with multiple SET clauses), .OrderBy(...).Take(n).ExecuteUpdate / .ExecuteDelete() (TOP-N subquery filter), column-derived arithmetic setter (c.Value * 3 + 1) via a dedicated Score / ScoreContext pair, and Any-subquery WHERE (EXISTS-translated bulk DML). All shapes were probe-confirmed against the simulator before locking in — no implementation changes needed.
1 parent b689301 commit 0e8640b

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

SqlServerSimulator.Tests.EFCore/EFCoreUpdateDelete.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,92 @@ public void ExecuteDelete_BulkDelete_EmitsMultiTableSyntax()
176176
CollectionAssert.AreEqual(new[] { "alice", "carol" }, names);
177177
}
178178

179+
[TestMethod]
180+
public void ExecuteUpdate_MultipleProperties_BothApplied()
181+
{
182+
// Chained SetProperty calls compile to a single UPDATE with multiple SET clauses.
183+
using var context = SeededContext();
184+
var rows = context.People.Where(p => p.Id == 1)
185+
.ExecuteUpdate(setters => setters
186+
.SetProperty(p => p.Name, "renamed")
187+
.SetProperty(p => p.Code, "X"));
188+
Assert.AreEqual(1, rows);
189+
190+
using var fresh = new TestDbContext(context.Simulation);
191+
var reloaded = fresh.People.Single(p => p.Id == 1);
192+
Assert.AreEqual("renamed", reloaded.Name);
193+
Assert.AreEqual("X", reloaded.Code);
194+
}
195+
196+
[TestMethod]
197+
public void ExecuteUpdate_TopNSubqueryFilter_OrderByTake()
198+
{
199+
// .OrderBy(...).Take(n).ExecuteUpdate(...) emits the
200+
// UPDATE [c] ... WHERE [c].[Id] IN (SELECT TOP(@p) [c0].[Id] FROM ... ORDER BY ...)
201+
// shape — different from the flat WHERE form. Confirms the subquery
202+
// filter through bulk-DML works against the TOP-N pattern.
203+
using var context = SeededContext();
204+
var rows = context.People.OrderBy(p => p.Id).Take(2)
205+
.ExecuteUpdate(setters => setters.SetProperty(p => p.Code, "TOP"));
206+
Assert.AreEqual(2, rows);
207+
208+
using var fresh = new TestDbContext(context.Simulation);
209+
var codes = fresh.People.OrderBy(p => p.Id).Select(p => p.Code).ToArray();
210+
CollectionAssert.AreEqual(new[] { "TOP", "TOP", "C", "D" }, codes);
211+
}
212+
213+
[TestMethod]
214+
public void ExecuteDelete_TopNSubqueryFilter_OrderByTake()
215+
{
216+
// .OrderBy(...).Take(n).ExecuteDelete() emits the
217+
// DELETE [c] ... WHERE [c].[Id] IN (SELECT TOP(@p) [c0].[Id] FROM ... ORDER BY ...) shape.
218+
using var context = SeededContext();
219+
var rows = context.People.OrderBy(p => p.Id).Take(2).ExecuteDelete();
220+
Assert.AreEqual(2, rows);
221+
222+
using var fresh = new TestDbContext(context.Simulation);
223+
var ids = fresh.People.OrderBy(p => p.Id).Select(p => p.Id).ToArray();
224+
CollectionAssert.AreEqual(new[] { 3, 4 }, ids);
225+
}
226+
227+
[TestMethod]
228+
public void ExecuteUpdate_ColumnDerivedArithmetic_AppliesPerRow()
229+
{
230+
// SetProperty(col, c => c.OtherCol + literal) compiles to a setter
231+
// that references the row's other columns — a different shape from
232+
// Name.ToUpper() because the right-hand expression is a binary op.
233+
var simulation = new Simulation();
234+
_ = simulation.ExecuteNonQuery("create table Scores (Id int primary key, Value int)");
235+
using var context = new ScoreContext(simulation);
236+
context.Scores.AddRange(
237+
new Score { Id = 1, Value = 10 },
238+
new Score { Id = 2, Value = 20 });
239+
_ = context.SaveChanges();
240+
241+
var rows = context.Scores.ExecuteUpdate(s => s.SetProperty(x => x.Value, x => (x.Value * 3) + 1));
242+
Assert.AreEqual(2, rows);
243+
244+
using var fresh = new ScoreContext(simulation);
245+
var values = fresh.Scores.OrderBy(x => x.Id).Select(x => x.Value).ToArray();
246+
CollectionAssert.AreEqual(new[] { 31, 61 }, values);
247+
}
248+
249+
[TestMethod]
250+
public void ExecuteUpdate_AnySubqueryFilter_ScopesAffectedRows()
251+
{
252+
// WHERE EXISTS / WHERE col IN (SELECT ...) reach bulk DML through
253+
// EF Core's standard subquery translation. This case translates Any
254+
// into EXISTS — confirms ExecuteUpdate's WHERE accepts subquery shapes.
255+
using var context = SeededContext();
256+
var rows = context.People.Where(p => context.People.Any(o => o.Id == p.Id - 1))
257+
.ExecuteUpdate(setters => setters.SetProperty(p => p.Code, "NEXT"));
258+
Assert.AreEqual(3, rows);
259+
260+
using var fresh = new TestDbContext(context.Simulation);
261+
var codes = fresh.People.OrderBy(p => p.Id).Select(p => p.Code).ToArray();
262+
CollectionAssert.AreEqual(new[] { "A", "NEXT", "NEXT", "NEXT" }, codes);
263+
}
264+
179265
[TestMethod]
180266
public void SaveChanges_TimestampEntity_ReadsBackAutoBumpedRowVersion()
181267
{
@@ -199,6 +285,25 @@ public void SaveChanges_TimestampEntity_ReadsBackAutoBumpedRowVersion()
199285
}
200286
}
201287

288+
internal class Score
289+
{
290+
public int Id { get; set; }
291+
292+
public int Value { get; set; }
293+
}
294+
295+
internal class ScoreContext(Simulation simulation) : DbContext
296+
{
297+
public Simulation Simulation { get; set; } = simulation;
298+
299+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
300+
{
301+
_ = optionsBuilder.UseSqlServer(this.Simulation.CreateDbConnection());
302+
}
303+
304+
public DbSet<Score> Scores => Set<Score>();
305+
}
306+
202307
internal class TimestampedItem
203308
{
204309
public int Id { get; set; }

0 commit comments

Comments
 (0)