Skip to content

Commit f8db52d

Browse files
committed
Wired CREATE/INSERT/SELECT through the page-storage backend end-to-end.
1 parent 381454e commit f8db52d

10 files changed

Lines changed: 484 additions & 39 deletions

File tree

SqlServerSimulator.Tests/PagesBackendIntegrationTests.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,122 @@ public void TypedNullParameter_ReturnsDBNull(DbType dbType)
170170

171171
AreEqual(DBNull.Value, command.ExecuteScalar());
172172
}
173+
174+
[TestMethod]
175+
public void CreateInsertSelect_IntegerConstant_RoundTripsThroughHeapTable()
176+
{
177+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
178+
179+
AreEqual(-1, connection.CreateCommand("create table t ( v int )").ExecuteNonQuery());
180+
AreEqual(1, connection.CreateCommand("insert t values ( 42 )").ExecuteNonQuery());
181+
182+
using var reader = connection.CreateCommand("select v from t").ExecuteReader();
183+
IsTrue(reader.Read());
184+
AreEqual("v", reader.GetName(0));
185+
AreEqual(42, reader[0]);
186+
IsFalse(reader.Read());
187+
}
188+
189+
[TestMethod]
190+
public void CreateInsertSelect_MultipleRowsAndColumns()
191+
{
192+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
193+
194+
_ = connection.CreateCommand("create table t ( a int, b int )").ExecuteNonQuery();
195+
AreEqual(3, connection.CreateCommand("insert t values ( 1, 10 ), ( 2, 20 ), ( 3, 30 )").ExecuteNonQuery());
196+
197+
using var reader = connection.CreateCommand("select a, b from t").ExecuteReader();
198+
199+
IsTrue(reader.Read());
200+
AreEqual(1, reader[0]);
201+
AreEqual(10, reader[1]);
202+
203+
IsTrue(reader.Read());
204+
AreEqual(2, reader[0]);
205+
AreEqual(20, reader[1]);
206+
207+
IsTrue(reader.Read());
208+
AreEqual(3, reader[0]);
209+
AreEqual(30, reader[1]);
210+
211+
IsFalse(reader.Read());
212+
}
213+
214+
[TestMethod]
215+
public void Insert_PartialColumnList_LeavesUnspecifiedColumnsNull()
216+
{
217+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
218+
219+
_ = connection.CreateCommand("create table t ( a int, b int )").ExecuteNonQuery();
220+
_ = connection.CreateCommand("insert t ( a ) values ( 7 )").ExecuteNonQuery();
221+
222+
using var reader = connection.CreateCommand("select a, b from t").ExecuteReader();
223+
IsTrue(reader.Read());
224+
AreEqual(7, reader[0]);
225+
AreEqual(DBNull.Value, reader[1]);
226+
IsFalse(reader.Read());
227+
}
228+
229+
[TestMethod]
230+
public void Select_FromEmptyTable_ReturnsNoRows()
231+
{
232+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
233+
234+
_ = connection.CreateCommand("create table t ( v int )").ExecuteNonQuery();
235+
236+
using var reader = connection.CreateCommand("select v from t").ExecuteReader();
237+
IsFalse(reader.Read());
238+
}
239+
240+
[TestMethod]
241+
public void CreateInsertSelect_AllSupportedColumnTypes_ViaParameters()
242+
{
243+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
244+
245+
_ = connection.CreateCommand("create table t ( b bit, ti tinyint, si smallint, i int )").ExecuteNonQuery();
246+
247+
using (var insert = connection.CreateCommand())
248+
{
249+
insert.CommandText = "insert t values ( @b, @ti, @si, @i )";
250+
AddTypedParameter(insert, "b", DbType.Boolean, true);
251+
AddTypedParameter(insert, "ti", DbType.Byte, (byte)200);
252+
AddTypedParameter(insert, "si", DbType.Int16, (short)-1);
253+
AddTypedParameter(insert, "i", DbType.Int32, 12345);
254+
AreEqual(1, insert.ExecuteNonQuery());
255+
}
256+
257+
using var reader = connection.CreateCommand("select b, ti, si, i from t").ExecuteReader();
258+
IsTrue(reader.Read());
259+
AreEqual(true, reader[0]);
260+
AreEqual((byte)200, reader[1]);
261+
AreEqual((short)-1, reader[2]);
262+
AreEqual(12345, reader[3]);
263+
IsFalse(reader.Read());
264+
}
265+
266+
[TestMethod]
267+
public void SelectFromTable_ProjectionReorderingAndSubsetting()
268+
{
269+
using var connection = new Simulation(StorageBackend.Pages).CreateOpenConnection();
270+
271+
_ = connection.CreateCommand("create table t ( a int, b int, c int )").ExecuteNonQuery();
272+
_ = connection.CreateCommand("insert t values ( 1, 2, 3 )").ExecuteNonQuery();
273+
274+
using var reader = connection.CreateCommand("select c, a from t").ExecuteReader();
275+
IsTrue(reader.Read());
276+
AreEqual(2, reader.FieldCount);
277+
AreEqual("c", reader.GetName(0));
278+
AreEqual("a", reader.GetName(1));
279+
AreEqual(3, reader[0]);
280+
AreEqual(1, reader[1]);
281+
}
282+
283+
private static void AddTypedParameter(System.Data.Common.DbCommand command, string name, DbType dbType, object value)
284+
{
285+
var parameter = command.CreateParameter();
286+
parameter.ParameterName = name;
287+
parameter.DbType = dbType;
288+
parameter.Value = value;
289+
_ = command.Parameters.Add(parameter);
290+
}
173291
}

SqlServerSimulator.Tests/PagesBackendTests.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,6 @@ public void Reader_RoundsTripsRowAndExposesScalar()
3636
IsFalse(reader.Read());
3737
}
3838

39-
[TestMethod]
40-
public void ColumnReference_NotYetSupported_OnPages()
41-
{
42-
// Reference.RunSql isn't implemented yet, so a tableless `select x`
43-
// surfaces NotSupportedException via the base Expression.RunSql.
44-
// Demonstrates that the parallel evaluator surface is honest about its
45-
// coverage; this lifts when Reference.RunSql arrives alongside Pages-
46-
// backed tables.
47-
var simulation = new Simulation(StorageBackend.Pages);
48-
_ = Throws<NotSupportedException>(() => simulation.ExecuteScalar("select x"));
49-
}
50-
5139
[TestMethod]
5240
public void InMemoryBackend_StillHandlesArithmetic()
5341
{
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator.Parser;
4+
5+
/// <summary>
6+
/// Transitional translation from <see cref="DataValue"/> to <see cref="SqlValue"/>,
7+
/// applied at parser boundaries where parser tokens still produce
8+
/// <see cref="DataValue"/>s but the page-storage execution path requires
9+
/// <see cref="SqlValue"/>s. Goes away when parser tokens are rewritten to
10+
/// produce <see cref="SqlValue"/> directly.
11+
/// </summary>
12+
/// <remarks>
13+
/// <c>BuiltInDbSystemName</c> isn't mapped: it's only used for system-table
14+
/// column definitions, never as a literal or parameter value that flows
15+
/// through this shim.
16+
/// </remarks>
17+
internal static class DataValueShim
18+
{
19+
public static SqlValue ToSqlValue(DataValue dv) => dv.Type switch
20+
{
21+
var t when t == DataType.BuiltInDbInt32 => dv.Value is null ? SqlValue.Null(SqlType.Int32) : SqlValue.FromInt32((int)dv.Value),
22+
var t when t == DataType.BuiltInDbInt16 => dv.Value is null ? SqlValue.Null(SqlType.SmallInt) : SqlValue.FromInt16((short)dv.Value),
23+
var t when t == DataType.BuiltInDbByte => dv.Value is null ? SqlValue.Null(SqlType.TinyInt) : SqlValue.FromByte((byte)dv.Value),
24+
var t when t == DataType.BuiltInDbBoolean => dv.Value is null ? SqlValue.Null(SqlType.Bit) : SqlValue.FromBoolean((bool)dv.Value),
25+
var t when t == DataType.BuiltInDbAnsiString => dv.Value is null ? SqlValue.Null(SqlType.Varchar) : SqlValue.FromVarchar((string)dv.Value),
26+
var t when t == DataType.BuiltInDbString => dv.Value is null ? SqlValue.Null(SqlType.NVarchar) : SqlValue.FromNVarchar((string)dv.Value),
27+
_ => throw new NotSupportedException($"No SqlType mapping for value of type {dv.Type}."),
28+
};
29+
}

SqlServerSimulator/Parser/Expressions/Reference.cs

Lines changed: 3 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.Expressions;
45

@@ -12,6 +13,8 @@ internal sealed class Reference(Name name) : Expression
1213

1314
public override DataValue Run(Func<List<string>, DataValue> getColumnValue) => getColumnValue(this.name);
1415

16+
public override SqlValue RunSql(Func<List<string>, SqlValue> getColumnValue) => getColumnValue(this.name);
17+
1518
#if DEBUG
1619
public override string ToString() => string.Join('.', name);
1720
#endif

SqlServerSimulator/Parser/Expressions/Value.cs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,7 @@ public Value(DoubleAtPrefixedString doubleAtPrefixedString)
3232

3333
public override DataValue Run(Func<List<string>, DataValue> getColumnValue) => value;
3434

35-
public override SqlValue RunSql(Func<List<string>, SqlValue> getColumnValue) => ToSqlValue(this.value);
36-
37-
/// <remarks>
38-
/// Transitional: parser tokens still produce <see cref="DataValue"/>, so a
39-
/// literal <see cref="Value"/> expression has to translate at this seam.
40-
/// When parser tokens are rewritten to produce <see cref="SqlValue"/>
41-
/// directly, this helper goes away. <c>BuiltInDbSystemName</c> isn't
42-
/// mapped: it's only used for system-table column definitions, never as a
43-
/// literal that flows through this shim.
44-
/// </remarks>
45-
private static SqlValue ToSqlValue(DataValue dv) => dv.Type switch
46-
{
47-
var t when t == DataType.BuiltInDbInt32 => dv.Value is null ? SqlValue.Null(SqlType.Int32) : SqlValue.FromInt32((int)dv.Value),
48-
var t when t == DataType.BuiltInDbInt16 => dv.Value is null ? SqlValue.Null(SqlType.SmallInt) : SqlValue.FromInt16((short)dv.Value),
49-
var t when t == DataType.BuiltInDbByte => dv.Value is null ? SqlValue.Null(SqlType.TinyInt) : SqlValue.FromByte((byte)dv.Value),
50-
var t when t == DataType.BuiltInDbBoolean => dv.Value is null ? SqlValue.Null(SqlType.Bit) : SqlValue.FromBoolean((bool)dv.Value),
51-
var t when t == DataType.BuiltInDbAnsiString => dv.Value is null ? SqlValue.Null(SqlType.Varchar) : SqlValue.FromVarchar((string)dv.Value),
52-
var t when t == DataType.BuiltInDbString => dv.Value is null ? SqlValue.Null(SqlType.NVarchar) : SqlValue.FromNVarchar((string)dv.Value),
53-
_ => throw new NotSupportedException($"No SqlType mapping for literal of type {dv.Type}."),
54-
};
35+
public override SqlValue RunSql(Func<List<string>, SqlValue> getColumnValue) => DataValueShim.ToSqlValue(this.value);
5536

5637
#if DEBUG
5738
public override string ToString() => value.Value?.ToString() ?? "null";

SqlServerSimulator/Parser/Selection.cs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using SqlServerSimulator.Parser.Tokens;
1+
using SqlServerSimulator.Parser.Expressions;
2+
using SqlServerSimulator.Parser.Tokens;
23
using SqlServerSimulator.Storage;
34

45
namespace SqlServerSimulator.Parser;
@@ -100,6 +101,29 @@ [.. expressions.Select(x => x.Run(columnName => getColumnValueFromRow(row, colum
100101
switch (context.GetNextRequired())
101102
{
102103
case Name tableName:
104+
if (context.Simulation.Backend == StorageBackend.Pages)
105+
{
106+
if (!context.Simulation.HeapTables.TryGetValue(tableName.Value, out var heapTable))
107+
throw SimulatedSqlException.InvalidObjectName(tableName);
108+
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+
}
121+
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));
125+
}
126+
103127
if (!context.Simulation.Tables.TryGetValue(tableName.Value, out var table) && !context.Simulation.SystemTables.Value.TryGetValue(tableName.Value, out table))
104128
throw SimulatedSqlException.InvalidObjectName(tableName);
105129

@@ -195,4 +219,69 @@ private static SimulatedSqlResultSet BuildSynthesizedSqlRow(List<Expression> exp
195219

196220
return new SimulatedSqlResultSet(schema, columnNames, [RowEncoder.EncodeRow(schema, values)]);
197221
}
222+
223+
/// <summary>
224+
/// Builds the page-backend result for a SELECT-FROM-table query. Each input
225+
/// row is decoded column-by-column on demand via
226+
/// <see cref="RowDecoder.DecodeColumn"/>; each projection expression is
227+
/// evaluated against that row through <see cref="Expression.RunSql"/>; the
228+
/// resulting values are re-encoded into a fresh output row.
229+
/// </summary>
230+
/// <remarks>
231+
/// Output schema is determined statically from the projection list. Today
232+
/// only bare <see cref="Reference"/> projections are supported (the output
233+
/// type is the source column's type). Lifting this to arbitrary expressions
234+
/// requires a static type-of resolver on <see cref="Expression"/> and is
235+
/// deferred.
236+
/// </remarks>
237+
private static SimulatedSqlResultSet BuildHeapProjection(HeapTable heapTable, List<Expression> expressions)
238+
{
239+
var outputSchema = new SqlType[expressions.Count];
240+
var outputColumnNames = new string[expressions.Count];
241+
242+
for (var i = 0; i < expressions.Count; i++)
243+
{
244+
if (expressions[i] is not Reference)
245+
throw new NotSupportedException("Page-backend SELECT FROM table currently supports only column-reference projections.");
246+
247+
var sourceColumnName = expressions[i].Name;
248+
var sourceColumn = heapTable.Columns.FirstOrDefault(c => Collation.Default.Equals(c.Name, sourceColumnName))
249+
?? throw SimulatedSqlException.InvalidColumnName([sourceColumnName]);
250+
251+
outputSchema[i] = sourceColumn.Type;
252+
outputColumnNames[i] = expressions[i].Name;
253+
}
254+
255+
return new SimulatedSqlResultSet(outputSchema, outputColumnNames, ProjectHeapRows(heapTable, expressions, outputSchema));
256+
}
257+
258+
private static IEnumerable<byte[]> ProjectHeapRows(HeapTable heapTable, List<Expression> expressions, SqlType[] outputSchema)
259+
{
260+
foreach (var rowBytes in heapTable.Rows)
261+
{
262+
var bytes = rowBytes;
263+
var projected = new SqlValue[expressions.Count];
264+
for (var i = 0; i < expressions.Count; i++)
265+
{
266+
projected[i] = expressions[i].RunSql(name =>
267+
{
268+
var columnIndex = -1;
269+
for (var j = 0; j < heapTable.Columns.Count; j++)
270+
{
271+
if (Collation.Default.Equals(heapTable.Columns[j].Name, name[^1]))
272+
{
273+
columnIndex = j;
274+
break;
275+
}
276+
}
277+
278+
return columnIndex == -1
279+
? throw SimulatedSqlException.InvalidColumnName(name)
280+
: RowDecoder.DecodeColumn(heapTable.Schema, bytes, columnIndex);
281+
});
282+
}
283+
284+
yield return RowEncoder.EncodeRow(outputSchema, projected);
285+
}
286+
}
198287
}

0 commit comments

Comments
 (0)