Skip to content

Commit 2f5e4c2

Browse files
committed
Parentheses in expressions are now handled directly.
1 parent ea1271c commit 2f5e4c2

4 files changed

Lines changed: 92 additions & 79 deletions

File tree

SqlServerSimulator.Tests.EFCore/EFCoreBasics.cs

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,29 @@ public static async Task HotPath(TestContext context)
1313

1414
// Triggers JIT compilation of the most common path among all tests, improving the accuracy of their timings.
1515
// Also functions as a sanity check against the simulator being completely broken.
16-
using var dbContext = new TestDbContext();
17-
18-
Assert.IsEmpty(dbContext.Rows.Select(x => x.Id).AsEnumerable());
19-
20-
Assert.AreEqual(0, await dbContext.SaveChangesAsync(context.CancellationToken));
16+
using var dbContext = new TestDbContext(1);
17+
_ = await dbContext.Rows.Select(x => x.Id).FirstOrDefaultAsync(context.CancellationToken);
2118
}
2219

23-
public static Simulation CreateDefaultSimulation()
20+
public static Simulation CreateDefaultSimulation(params ReadOnlySpan<int> values)
2421
{
2522
var simulation = new Simulation();
2623
_ = simulation
2724
.CreateOpenConnection()
2825
.CreateCommand("create table Rows ( Id int )")
2926
.ExecuteNonQuery();
3027

28+
if (values.Length != 0)
29+
{
30+
using var context = new TestDbContext(simulation);
31+
foreach (var value in values)
32+
{
33+
var row = new TestRow { Id = value };
34+
_ = context.Rows.Add(row);
35+
_ = context.SaveChanges();
36+
}
37+
}
38+
3139
return simulation;
3240
}
3341

@@ -40,8 +48,8 @@ class TestDbContext(Simulation simulation) : DbContext
4048
{
4149
public Simulation Simulation { get; set; } = simulation;
4250

43-
public TestDbContext()
44-
: this(CreateDefaultSimulation())
51+
public TestDbContext(params ReadOnlySpan<int> values)
52+
: this(CreateDefaultSimulation(values))
4553
{
4654
}
4755

@@ -56,13 +64,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
5664
[TestMethod]
5765
public void InsertRowSync()
5866
{
59-
using var context = new TestDbContext();
60-
61-
var row = new TestRow { Id = 1 };
62-
63-
_ = context.Rows.Add(row);
64-
65-
_ = context.SaveChanges();
67+
using var context = new TestDbContext(1);
6668
}
6769

6870
/// <summary>
@@ -84,69 +86,48 @@ public async Task InsertRowAsync()
8486
[TestMethod]
8587
public void RoundTrip()
8688
{
87-
var simulation = CreateDefaultSimulation();
8889
const int storedValue = 3;
90+
using var context = new TestDbContext(storedValue);
91+
var receivedValue = context.Rows.Select(x => x.Id).AsEnumerable();
8992

90-
using (var context = new TestDbContext(simulation))
91-
{
92-
var row = new TestRow { Id = storedValue };
93-
94-
_ = context.Rows.Add(row);
95-
96-
_ = context.SaveChanges();
97-
}
98-
99-
using (var context = new TestDbContext(simulation))
100-
{
101-
var receivedValue = context.Rows.Select(x => x.Id).AsEnumerable();
102-
103-
Assert.AreEqual(storedValue, receivedValue.FirstOrDefault());
104-
}
93+
Assert.AreEqual(storedValue, receivedValue.FirstOrDefault());
10594
}
10695

10796
[TestMethod]
10897
public void SeparateInserts()
10998
{
110-
var simulation = CreateDefaultSimulation();
11199
int[] storedValues = [2, 3];
112-
113-
using (var context = new TestDbContext(simulation))
114-
{
115-
foreach (var value in storedValues)
116-
{
117-
var row = new TestRow { Id = value };
118-
_ = context.Rows.Add(row);
119-
_ = context.SaveChanges();
120-
}
121-
}
122-
123-
using (var context = new TestDbContext(simulation))
124-
{
125-
CollectionAssert.AreEquivalent(storedValues, context.Rows.Select(x => x.Id).ToArray());
126-
}
100+
using var context = new TestDbContext(storedValues);
101+
CollectionAssert.AreEquivalent(storedValues, context.Rows.Select(x => x.Id).ToArray());
127102
}
128103

129104
[TestMethod]
130105
public void FirstOrDefault()
131106
{
132-
var simulation = CreateDefaultSimulation();
133107
int[] storedValues = [4, 5];
108+
using var context = new TestDbContext(storedValues);
109+
var receivedValue = context.Rows.Select(x => x.Id);
110+
// Without an OrderBy, we can't guarantee which of the two possibilities is returned.
111+
CollectionAssert.Contains(storedValues, receivedValue.FirstOrDefault());
112+
}
134113

135-
using (var context = new TestDbContext(simulation))
136-
{
137-
foreach (var value in storedValues)
138-
{
139-
var row = new TestRow { Id = value };
140-
_ = context.Rows.Add(row);
141-
_ = context.SaveChanges();
142-
}
143-
}
114+
[TestMethod]
115+
public void SingleOrDefault()
116+
{
117+
const int storedValue = 6;
118+
using var context = new TestDbContext(storedValue);
119+
var receivedValue = context.Rows.Select(x => x.Id);
120+
// Without an OrderBy, we can't guarantee which of the two possibilities is returned.
121+
Assert.AreEqual(storedValue, receivedValue.SingleOrDefault());
122+
}
144123

145-
using (var context = new TestDbContext(simulation))
146-
{
147-
var receivedValue = context.Rows.Select(x => x.Id);
148-
// Without an OrderBy, we can't guarantee which of the two possibilities is returned.
149-
CollectionAssert.Contains(storedValues, receivedValue.FirstOrDefault());
150-
}
124+
[TestMethod]
125+
public void Take()
126+
{
127+
int[] storedValues = [4, 5];
128+
using var context = new TestDbContext(storedValues);
129+
var receivedValue = context.Rows.Select(x => x.Id);
130+
// Without an OrderBy, we can't guarantee which of the two possibilities is returned.
131+
CollectionAssert.Contains(storedValues, receivedValue.Take(1).AsEnumerable().First());
151132
}
152133
}

SqlServerSimulator.Tests/SelectTests.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public void Select1ViaExecuteReaderGetInt32()
3333
[TestMethod]
3434
[DataRow("select @p0", "p0", 5)]
3535
[DataRow("select @p0", "@p0", 6)]
36+
[DataRow("select (@p0)", "p0", 7)]
3637
public void SelectParameterValue(string commandText, string name, object value)
3738
{
3839
var result = new Simulation()
@@ -44,6 +45,10 @@ public void SelectParameterValue(string commandText, string name, object value)
4445
}
4546

4647
[TestMethod]
48+
[DataRow("1", 1)]
49+
[DataRow("(1)", 1)]
50+
[DataRow("(1) + 1", 2)]
51+
[DataRow("(1) + (1)", 2)]
4752
[DataRow("1 + 1", 2)]
4853
[DataRow("1 - 1", 0)]
4954
[DataRow("-1", -1)]
@@ -73,7 +78,7 @@ public void BareAs()
7378
[DataRow("select 1 as [c]]d]", "c]d", 1)]
7479
[DataRow("select 1 as [e f]", "e f", 1)]
7580
[DataRow("select 1 + 1 as c", "c", 2)]
76-
public void Expression(string commandText, string name, object value)
81+
public void NamedExpression(string commandText, string name, object value)
7782
{
7883
using var reader = new Simulation().ExecuteReader(commandText);
7984

@@ -229,15 +234,29 @@ public void DerivedTable(string commandText, string name, object value)
229234
}
230235

231236
[TestMethod]
232-
public void TopParenthesizedConstantUnsorted()
237+
[DataRow("1", new[] { 1 })]
238+
[DataRow("0", new int[] { })]
239+
[DataRow("(1)", new[] { 1 })]
240+
[DataRow("(0)", new int[] { })]
241+
public void TopConstantUnsorted(string topExpression, int[] expectedValues)
233242
{
234-
CollectionAssert.AreEquivalent([1], [.. new Simulation()
235-
.ExecuteReader("select top (1) 1")
243+
CollectionAssert.AreEquivalent(expectedValues, [.. new Simulation()
244+
.ExecuteReader($"select top {topExpression} 1")
236245
.EnumerateRecords()
237246
.Select(reader => (int)reader[0])], EqualityComparer<int>.Default);
247+
}
238248

239-
CollectionAssert.AreEquivalent([], [.. new Simulation()
240-
.ExecuteReader("select top (0) 1")
249+
[TestMethod]
250+
[DataRow("@p0", 1, new[] { 1 })]
251+
[DataRow("(@p0)", 1, new[] { 1 })]
252+
[DataRow("@p0", 0, new int[] { })]
253+
[DataRow("(@p0)", 0, new int[] { })]
254+
public void TopParameterizedUnsorted(string parameterExpression, int parameterValue, int[] expectedValues)
255+
{
256+
CollectionAssert.AreEquivalent(expectedValues, [.. new Simulation()
257+
.CreateOpenConnection()
258+
.CreateCommand($"select top {parameterExpression} 1", ("p0", parameterValue))
259+
.ExecuteReader()
241260
.EnumerateRecords()
242261
.Select(reader => (int)reader[0])], EqualityComparer<int>.Default);
243262
}

SqlServerSimulator/Parser/Expression.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ public static Expression Parse(ParserContext context)
5454
expression = Parse(context);
5555
expression = new Subtract(new Value(0), expression);
5656
break;
57+
case Operator { Character: '(' }:
58+
context.MoveNextRequired();
59+
expression = new Parenthesized(Parse(context));
60+
break;
5761
default:
5862
throw SimulatedSqlException.SyntaxErrorNear(context.Token);
5963
}
@@ -84,7 +88,11 @@ public static Expression Parse(ParserContext context)
8488
case Operator { Character: '(' }:
8589
{
8690
if (expression is not Reference reference)
87-
throw SimulatedSqlException.SyntaxErrorNear(context.Token);
91+
{
92+
expression = new Parenthesized(Parse(context));
93+
break;
94+
}
95+
8896
context.MoveNextRequired(); // Move past (
8997
expression = ResolveBuiltIn(reference.Name, context);
9098
context.MoveNextOptional(); // Move past )
@@ -115,6 +123,20 @@ public static Expression Parse(ParserContext context)
115123
public abstract override string ToString();
116124
#endif
117125

126+
/// <summary>
127+
/// An expression that's wrapped in parentheses, potentially affecting the order of operations.
128+
/// </summary>
129+
private sealed class Parenthesized(Expression wrapped) : Expression
130+
{
131+
private readonly Expression wrapped = wrapped;
132+
133+
public override object? Run(Func<List<string>, object?> getColumnValue) => wrapped.Run(getColumnValue);
134+
135+
#if DEBUG
136+
public override string ToString() => $"( {wrapped} )";
137+
#endif
138+
}
139+
118140
private static Expression ResolveBuiltIn(string name, ParserContext context)
119141
{
120142
Span<char> uppercaseName = stackalloc char[name.Length];

SqlServerSimulator/Parser/Selection.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,10 @@ public static Selection Parse(ParserContext context, uint depth)
2727

2828
if (context.Token is ReservedKeyword { Keyword: Keyword.Top })
2929
{
30-
// SQL Server doesn't require outer parentheses.
31-
// When Expression.Parse supports them, the checks for them here should be removed.
32-
if (context.GetNextRequired<Operator>() is not { Character: '(' })
33-
throw SimulatedSqlException.SyntaxErrorNear(context.Token);
3430
context.MoveNextRequired();
3531

3632
var resolvedExpression = Expression.Parse(context).Run(name => throw SimulatedSqlException.ColumnReferenceNotAllowed(name));
3733
topCount = resolvedExpression is int unboxed ? unboxed : throw SimulatedSqlException.TopFetchRequiresInteger();
38-
39-
if (context.Token is not null and not Operator { Character: ')' })
40-
throw SimulatedSqlException.SyntaxErrorNear(context.Token);
41-
42-
context.MoveNextRequired();
4334
}
4435

4536
List<Expression> expressions = [];

0 commit comments

Comments
 (0)