Skip to content

Commit db814f8

Browse files
committed
WHERE on page-storage backend: BooleanExpression.RunSql + SqlValue.CompareTo + filter pipeline.
1 parent f8db52d commit db814f8

4 files changed

Lines changed: 192 additions & 36 deletions

File tree

SqlServerSimulator.Tests/PagesBackendIntegrationTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,88 @@ public void SelectFromTable_ProjectionReorderingAndSubsetting()
280280
AreEqual(1, reader[1]);
281281
}
282282

283+
[TestMethod]
284+
[DataRow("1 = 1", 1)]
285+
[DataRow("1 = 0", 0)]
286+
[DataRow("1 <> 0", 1)]
287+
[DataRow("1 <> 1", 0)]
288+
[DataRow("1 > 0", 1)]
289+
[DataRow("1 > 1", 0)]
290+
[DataRow("1 >= 1", 1)]
291+
[DataRow("1 >= 2", 0)]
292+
[DataRow("0 < 1", 1)]
293+
[DataRow("1 < 1", 0)]
294+
[DataRow("1 <= 1", 1)]
295+
[DataRow("2 <= 1", 0)]
296+
public void TablelessWhere_FiltersConstantExpression(string whereExpression, int expectedRowCount)
297+
=> AreEqual(expectedRowCount, new Simulation(StorageBackend.Pages).ExecuteReader($"select 1 where {whereExpression}").EnumerateRecords().Count());
298+
299+
[TestMethod]
300+
public void TablelessWhere_NullOperand_ReturnsZeroRows()
301+
=> AreEqual(0, new Simulation(StorageBackend.Pages).ExecuteReader("select 1 where null = 1").EnumerateRecords().Count());
302+
303+
[TestMethod]
304+
public void FromTableWhere_FiltersByEqualityToLiteral()
305+
{
306+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
307+
308+
_ = connection.CreateCommand("create table t ( id int, v int )").ExecuteNonQuery();
309+
_ = connection.CreateCommand("insert t values ( 1, 100 ), ( 2, 200 ), ( 3, 300 )").ExecuteNonQuery();
310+
311+
using var reader = connection.CreateCommand("select id, v from t where id = 2").ExecuteReader();
312+
IsTrue(reader.Read());
313+
AreEqual(2, reader[0]);
314+
AreEqual(200, reader[1]);
315+
IsFalse(reader.Read());
316+
}
317+
318+
[TestMethod]
319+
public void FromTableWhere_FiltersByGreaterThan()
320+
{
321+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
322+
323+
_ = connection.CreateCommand("create table t ( v int )").ExecuteNonQuery();
324+
_ = connection.CreateCommand("insert t values ( 1 ), ( 2 ), ( 3 ), ( 4 )").ExecuteNonQuery();
325+
326+
using var reader = connection.CreateCommand("select v from t where v > 2").ExecuteReader();
327+
IsTrue(reader.Read()); AreEqual(3, reader[0]);
328+
IsTrue(reader.Read()); AreEqual(4, reader[0]);
329+
IsFalse(reader.Read());
330+
}
331+
332+
[TestMethod]
333+
public void FromTableWhere_NullColumnNeverMatches()
334+
{
335+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
336+
337+
_ = connection.CreateCommand("create table t ( id int, v int )").ExecuteNonQuery();
338+
_ = connection.CreateCommand("insert t ( id ) values ( 1 )").ExecuteNonQuery();
339+
_ = connection.CreateCommand("insert t values ( 2, 99 )").ExecuteNonQuery();
340+
341+
using var reader = connection.CreateCommand("select id from t where v = 99").ExecuteReader();
342+
IsTrue(reader.Read());
343+
AreEqual(2, reader[0]);
344+
IsFalse(reader.Read());
345+
}
346+
347+
[TestMethod]
348+
public void FromTableWhere_FiltersByParameter()
349+
{
350+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
351+
352+
_ = connection.CreateCommand("create table t ( id int )").ExecuteNonQuery();
353+
_ = connection.CreateCommand("insert t values ( 1 ), ( 2 ), ( 3 )").ExecuteNonQuery();
354+
355+
using var select = connection.CreateCommand();
356+
select.CommandText = "select id from t where id = @id";
357+
AddTypedParameter(select, "id", DbType.Int32, 2);
358+
359+
using var reader = select.ExecuteReader();
360+
IsTrue(reader.Read());
361+
AreEqual(2, reader[0]);
362+
IsFalse(reader.Read());
363+
}
364+
283365
private static void AddTypedParameter(System.Data.Common.DbCommand command, string name, DbType dbType, object value)
284366
{
285367
var parameter = command.CreateParameter();

SqlServerSimulator/Parser/BooleanExpression.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using SqlServerSimulator.Parser.Tokens;
2+
using SqlServerSimulator.Storage;
23

34
namespace SqlServerSimulator.Parser;
45

@@ -51,6 +52,13 @@ private protected BooleanExpression(Expression left, ParserContext context)
5152
/// <returns>The result of the expression.</returns>
5253
public abstract bool Run(Func<List<string>, DataValue> getColumnValue);
5354

55+
/// <summary>
56+
/// Page-storage parallel of <see cref="Run"/>; evaluates against the
57+
/// <see cref="SqlValue"/> world. Any NULL operand yields <c>false</c>
58+
/// (SQL UNKNOWN-in-WHERE semantics).
59+
/// </summary>
60+
public abstract bool RunSql(Func<List<string>, SqlValue> getColumnValue);
61+
5462
#if DEBUG
5563
public abstract override string ToString();
5664
#endif
@@ -60,6 +68,13 @@ private sealed class EqualityExpression(Expression left, ParserContext context)
6068
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
6169
(left.Run(getColumnValue).Value?.Equals(right.Run(getColumnValue).Value)).GetValueOrDefault();
6270

71+
public override bool RunSql(Func<List<string>, SqlValue> getColumnValue)
72+
{
73+
var l = left.RunSql(getColumnValue);
74+
var r = right.RunSql(getColumnValue);
75+
return !l.IsNull && !r.IsNull && l.Equals(r);
76+
}
77+
6378
#if DEBUG
6479
public override string ToString() => $"{left} = {right}";
6580
#endif
@@ -70,6 +85,13 @@ private sealed class InequalityExpression(Expression left, ParserContext context
7085
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
7186
!(left.Run(getColumnValue).Value?.Equals(right.Run(getColumnValue).Value)).GetValueOrDefault();
7287

88+
public override bool RunSql(Func<List<string>, SqlValue> getColumnValue)
89+
{
90+
var l = left.RunSql(getColumnValue);
91+
var r = right.RunSql(getColumnValue);
92+
return !l.IsNull && !r.IsNull && !l.Equals(r);
93+
}
94+
7395
#if DEBUG
7496
public override string ToString() => $"{left} <> {right}";
7597
#endif
@@ -80,6 +102,13 @@ private sealed class GreaterThanExpression(Expression left, Expression right) :
80102
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
81103
left.Run(getColumnValue).CompareTo(right.Run(getColumnValue)) > 0;
82104

105+
public override bool RunSql(Func<List<string>, SqlValue> getColumnValue)
106+
{
107+
var l = left.RunSql(getColumnValue);
108+
var r = right.RunSql(getColumnValue);
109+
return !l.IsNull && !r.IsNull && l.CompareTo(r) > 0;
110+
}
111+
83112
#if DEBUG
84113
public override string ToString() => $"{left} > {right}";
85114
#endif
@@ -90,6 +119,13 @@ private sealed class GreaterThanOrEqualExpression(Expression left, ParserContext
90119
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
91120
left.Run(getColumnValue).CompareTo(right.Run(getColumnValue)) >= 0;
92121

122+
public override bool RunSql(Func<List<string>, SqlValue> getColumnValue)
123+
{
124+
var l = left.RunSql(getColumnValue);
125+
var r = right.RunSql(getColumnValue);
126+
return !l.IsNull && !r.IsNull && l.CompareTo(r) >= 0;
127+
}
128+
93129
#if DEBUG
94130
public override string ToString() => $"{left} >= {right}";
95131
#endif
@@ -100,6 +136,13 @@ private sealed class LessThanExpression(Expression left, Expression right) : Boo
100136
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
101137
left.Run(getColumnValue).CompareTo(right.Run(getColumnValue)) < 0;
102138

139+
public override bool RunSql(Func<List<string>, SqlValue> getColumnValue)
140+
{
141+
var l = left.RunSql(getColumnValue);
142+
var r = right.RunSql(getColumnValue);
143+
return !l.IsNull && !r.IsNull && l.CompareTo(r) < 0;
144+
}
145+
103146
#if DEBUG
104147
public override string ToString() => $"{left} < {right}";
105148
#endif
@@ -110,6 +153,13 @@ private sealed class LessThanOrEqualExpression(Expression left, ParserContext co
110153
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
111154
left.Run(getColumnValue).CompareTo(right.Run(getColumnValue)) <= 0;
112155

156+
public override bool RunSql(Func<List<string>, SqlValue> getColumnValue)
157+
{
158+
var l = left.RunSql(getColumnValue);
159+
var r = right.RunSql(getColumnValue);
160+
return !l.IsNull && !r.IsNull && l.CompareTo(r) <= 0;
161+
}
162+
113163
#if DEBUG
114164
public override string ToString() => $"{left} <= {right}";
115165
#endif

SqlServerSimulator/Parser/Selection.cs

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,12 @@ [.. expressions.Select(x => x.Run(columnName => getColumnValueFromRow(row, colum
106106
if (!context.Simulation.HeapTables.TryGetValue(tableName.Value, out var heapTable))
107107
throw SimulatedSqlException.InvalidObjectName(tableName);
108108

109-
if (context.GetNextOptional() is not null)
110-
{
111-
if (context.Token is ReservedKeyword { Keyword: Keyword.As })
112-
{
113-
if (context.Token is not ReservedKeyword)
114-
break;
115-
}
116-
else
117-
{
118-
break;
119-
}
120-
}
109+
while (context.GetNextOptional() is ReservedKeyword { Keyword: Keyword.Where })
110+
excluders.Add(BooleanExpression.Parse(Expression.Parse(context.MoveNextRequiredReturnSelf()), context));
121111

122-
return excluders.Count > 0 ? throw new NotSupportedException("WHERE isn't supported on the page-storage backend yet.")
123-
: topCount is not null ? throw new NotSupportedException("TOP isn't supported on the page-storage backend yet.")
124-
: new(BuildHeapProjection(heapTable, expressions));
112+
return topCount is not null
113+
? throw new NotSupportedException("TOP isn't supported on the page-storage backend yet.")
114+
: new(BuildHeapProjection(heapTable, expressions, excluders));
125115
}
126116

127117
if (!context.Simulation.Tables.TryGetValue(tableName.Value, out var table) && !context.Simulation.SystemTables.Value.TryGetValue(tableName.Value, out table))
@@ -190,7 +180,7 @@ [.. expressions.Select(x => x.Run(columnName => getColumnValueFromRow(row, colum
190180
ExitWhileTokenLoop:
191181

192182
if (context.Simulation.Backend == StorageBackend.Pages)
193-
return new(BuildSynthesizedSqlRow(expressions));
183+
return new(BuildSynthesizedSqlRow(expressions, excluders));
194184

195185
return new(new SimulatedResultSet(
196186
expressions,
@@ -202,9 +192,11 @@ [.. expressions.Select(x => x.Run(columnName => getColumnValueFromRow(row, colum
202192
/// Builds the page-backend result for a tableless SELECT (synthesized
203193
/// constant-row branch). The row is encoded into the page-row format and
204194
/// the bytes are handed to the result set; decoding happens lazily per
205-
/// column when the reader navigates it.
195+
/// column when the reader navigates it. Any <paramref name="excluders"/>
196+
/// are evaluated against the synthesized row; if any returns false the
197+
/// result is empty.
206198
/// </summary>
207-
private static SimulatedSqlResultSet BuildSynthesizedSqlRow(List<Expression> expressions)
199+
private static SimulatedSqlResultSet BuildSynthesizedSqlRow(List<Expression> expressions, List<BooleanExpression> excluders)
208200
{
209201
var values = new SqlValue[expressions.Count];
210202
var schema = new SqlType[expressions.Count];
@@ -217,6 +209,12 @@ private static SimulatedSqlResultSet BuildSynthesizedSqlRow(List<Expression> exp
217209
columnNames[i] = expressions[i].Name;
218210
}
219211

212+
foreach (var excluder in excluders)
213+
{
214+
if (!excluder.RunSql(column => throw SimulatedSqlException.InvalidColumnName(column)))
215+
return new SimulatedSqlResultSet(schema, columnNames, []);
216+
}
217+
220218
return new SimulatedSqlResultSet(schema, columnNames, [RowEncoder.EncodeRow(schema, values)]);
221219
}
222220

@@ -234,7 +232,7 @@ private static SimulatedSqlResultSet BuildSynthesizedSqlRow(List<Expression> exp
234232
/// requires a static type-of resolver on <see cref="Expression"/> and is
235233
/// deferred.
236234
/// </remarks>
237-
private static SimulatedSqlResultSet BuildHeapProjection(HeapTable heapTable, List<Expression> expressions)
235+
private static SimulatedSqlResultSet BuildHeapProjection(HeapTable heapTable, List<Expression> expressions, List<BooleanExpression> excluders)
238236
{
239237
var outputSchema = new SqlType[expressions.Count];
240238
var outputColumnNames = new string[expressions.Count];
@@ -252,34 +250,47 @@ private static SimulatedSqlResultSet BuildHeapProjection(HeapTable heapTable, Li
252250
outputColumnNames[i] = expressions[i].Name;
253251
}
254252

255-
return new SimulatedSqlResultSet(outputSchema, outputColumnNames, ProjectHeapRows(heapTable, expressions, outputSchema));
253+
return new SimulatedSqlResultSet(outputSchema, outputColumnNames, ProjectHeapRows(heapTable, expressions, excluders, outputSchema));
256254
}
257255

258-
private static IEnumerable<byte[]> ProjectHeapRows(HeapTable heapTable, List<Expression> expressions, SqlType[] outputSchema)
256+
private static IEnumerable<byte[]> ProjectHeapRows(HeapTable heapTable, List<Expression> expressions, List<BooleanExpression> excluders, SqlType[] outputSchema)
259257
{
260258
foreach (var rowBytes in heapTable.Rows)
261259
{
262260
var bytes = rowBytes;
263-
var projected = new SqlValue[expressions.Count];
264-
for (var i = 0; i < expressions.Count; i++)
261+
262+
SqlValue ResolveColumn(List<string> name)
265263
{
266-
projected[i] = expressions[i].RunSql(name =>
264+
var columnIndex = -1;
265+
for (var j = 0; j < heapTable.Columns.Count; j++)
267266
{
268-
var columnIndex = -1;
269-
for (var j = 0; j < heapTable.Columns.Count; j++)
267+
if (Collation.Default.Equals(heapTable.Columns[j].Name, name[^1]))
270268
{
271-
if (Collation.Default.Equals(heapTable.Columns[j].Name, name[^1]))
272-
{
273-
columnIndex = j;
274-
break;
275-
}
269+
columnIndex = j;
270+
break;
276271
}
272+
}
273+
274+
return columnIndex == -1
275+
? throw SimulatedSqlException.InvalidColumnName(name)
276+
: RowDecoder.DecodeColumn(heapTable.Schema, bytes, columnIndex);
277+
}
277278

278-
return columnIndex == -1
279-
? throw SimulatedSqlException.InvalidColumnName(name)
280-
: RowDecoder.DecodeColumn(heapTable.Schema, bytes, columnIndex);
281-
});
279+
var include = true;
280+
foreach (var excluder in excluders)
281+
{
282+
if (!excluder.RunSql(ResolveColumn))
283+
{
284+
include = false;
285+
break;
286+
}
282287
}
288+
if (!include)
289+
continue;
290+
291+
var projected = new SqlValue[expressions.Count];
292+
for (var i = 0; i < expressions.Count; i++)
293+
projected[i] = expressions[i].RunSql(ResolveColumn);
283294

284295
yield return RowEncoder.EncodeRow(outputSchema, projected);
285296
}

SqlServerSimulator/Storage/SqlValue.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace SqlServerSimulator.Storage;
1818
/// <see cref="SqlType"/> documents which field it uses.
1919
/// </para>
2020
/// </remarks>
21-
internal readonly struct SqlValue : IEquatable<SqlValue>
21+
internal readonly struct SqlValue : IEquatable<SqlValue>, IComparable<SqlValue>
2222
{
2323
private readonly long primitive;
2424
private readonly object? reference;
@@ -122,6 +122,19 @@ public bool Equals(SqlValue other)
122122
&& this.IsNull == other.IsNull
123123
&& (this.IsNull || (this.primitive == other.primitive && Equals(this.reference, other.reference)));
124124

125+
/// <summary>
126+
/// Orders two non-NULL same-typed values. NULL handling is the caller's
127+
/// responsibility (SQL's NULL-comparison semantics differ from .NET's
128+
/// IComparable convention, so we throw here rather than pick a side).
129+
/// </summary>
130+
/// <exception cref="InvalidOperationException">Either operand is NULL.</exception>
131+
/// <exception cref="NotSupportedException">The operands' types differ, or comparison for that type isn't implemented yet.</exception>
132+
public int CompareTo(SqlValue other) =>
133+
this.IsNull || other.IsNull ? throw new InvalidOperationException("CompareTo on NULL is undefined; check IsNull before calling.")
134+
: this.Type != other.Type ? throw new NotSupportedException($"Cross-type comparison isn't implemented: {this.Type} vs {other.Type}.")
135+
: this.Type == SqlType.Int32 ? this.AsInt32.CompareTo(other.AsInt32)
136+
: throw new NotSupportedException($"Comparison for {this.Type} isn't implemented yet.");
137+
125138
public override bool Equals(object? obj) => obj is SqlValue other && this.Equals(other);
126139

127140
public override int GetHashCode() => HashCode.Combine(this.Type, this.IsNull, this.primitive, this.reference);

0 commit comments

Comments
 (0)