Skip to content

Commit 3a92c92

Browse files
committed
WIP binary back-end.
1 parent c62beca commit 3a92c92

25 files changed

Lines changed: 1591 additions & 34 deletions
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator.Storage;
4+
5+
[TestClass]
6+
public class MixedTypeRowTests
7+
{
8+
[TestMethod]
9+
[DataRow(0L)]
10+
[DataRow(1L)]
11+
[DataRow(-1L)]
12+
[DataRow(long.MaxValue)]
13+
[DataRow(long.MinValue)]
14+
public void BigInt_RoundTrips(long value)
15+
{
16+
var decoded = RowDecoder.DecodeRow([SqlType.BigInt], RowEncoder.EncodeRow([SqlType.BigInt], [SqlValue.FromInt64(value)]));
17+
AreEqual(SqlValue.FromInt64(value), decoded[0]);
18+
}
19+
20+
[TestMethod]
21+
[DataRow((short)0)]
22+
[DataRow((short)1)]
23+
[DataRow((short)-1)]
24+
[DataRow(short.MaxValue)]
25+
[DataRow(short.MinValue)]
26+
public void SmallInt_RoundTrips(short value)
27+
{
28+
var decoded = RowDecoder.DecodeRow([SqlType.SmallInt], RowEncoder.EncodeRow([SqlType.SmallInt], [SqlValue.FromInt16(value)]));
29+
AreEqual(SqlValue.FromInt16(value), decoded[0]);
30+
}
31+
32+
[TestMethod]
33+
[DataRow((byte)0)]
34+
[DataRow((byte)1)]
35+
[DataRow((byte)127)]
36+
[DataRow(byte.MaxValue)]
37+
public void TinyInt_RoundTrips(byte value)
38+
{
39+
var decoded = RowDecoder.DecodeRow([SqlType.TinyInt], RowEncoder.EncodeRow([SqlType.TinyInt], [SqlValue.FromByte(value)]));
40+
AreEqual(SqlValue.FromByte(value), decoded[0]);
41+
}
42+
43+
[TestMethod]
44+
[DataRow(false)]
45+
[DataRow(true)]
46+
public void Bit_RoundTrips(bool value)
47+
{
48+
var decoded = RowDecoder.DecodeRow([SqlType.Bit], RowEncoder.EncodeRow([SqlType.Bit], [SqlValue.FromBoolean(value)]));
49+
AreEqual(SqlValue.FromBoolean(value), decoded[0]);
50+
}
51+
52+
[TestMethod]
53+
public void EachType_NullRoundTrips()
54+
{
55+
foreach (var type in new[] { SqlType.Int32, SqlType.BigInt, SqlType.SmallInt, SqlType.TinyInt, SqlType.Bit })
56+
{
57+
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [SqlValue.Null(type)]));
58+
IsTrue(decoded[0].IsNull);
59+
AreSame(type, decoded[0].Type);
60+
}
61+
}
62+
63+
[TestMethod]
64+
public void MixedRow_Int32BigIntBit_RoundTrips()
65+
{
66+
// Exercises offset arithmetic across a 4-byte, 8-byte, and 1-byte column.
67+
SqlType[] schema = [SqlType.Int32, SqlType.BigInt, SqlType.Bit];
68+
SqlValue[] values = [42, SqlValue.FromInt64(0x0102030405060708L), SqlValue.FromBoolean(true)];
69+
var bytes = RowEncoder.EncodeRow(schema, values);
70+
var decoded = RowDecoder.DecodeRow(schema, bytes);
71+
CollectionAssert.AreEqual(values, decoded);
72+
}
73+
74+
[TestMethod]
75+
public void MixedRow_AllFixedWidths_RoundTrips()
76+
{
77+
// Every fixed-width type in one row, with a NULL in the middle.
78+
SqlType[] schema = [SqlType.TinyInt, SqlType.SmallInt, SqlType.Int32, SqlType.BigInt, SqlType.Bit];
79+
SqlValue[] values =
80+
[
81+
SqlValue.FromByte(255),
82+
SqlValue.Null(SqlType.SmallInt),
83+
int.MinValue,
84+
SqlValue.FromInt64(long.MaxValue),
85+
SqlValue.FromBoolean(false),
86+
];
87+
var bytes = RowEncoder.EncodeRow(schema, values);
88+
var decoded = RowDecoder.DecodeRow(schema, bytes);
89+
CollectionAssert.AreEqual(values, decoded);
90+
}
91+
92+
[TestMethod]
93+
public void MixedRow_Layout_FixedSectionLengthMatchesSum()
94+
{
95+
SqlType[] schema = [SqlType.TinyInt, SqlType.SmallInt, SqlType.Int32, SqlType.BigInt, SqlType.Bit];
96+
// 1 + 2 + 4 + 8 + 1 = 16 bytes of fixed data; plus 4 header + 2 column count + 1 NULL bitmap = 23.
97+
var bytes = RowEncoder.EncodeRow(schema, [SqlValue.FromByte(0), SqlValue.FromInt16(0), 0, SqlValue.FromInt64(0L), SqlValue.FromBoolean(false)]);
98+
AreEqual(23, bytes.Length);
99+
AreEqual(20, BitConverter.ToUInt16(bytes, 2)); // fixed-end = 4 + 16
100+
AreEqual(5, BitConverter.ToUInt16(bytes, 20)); // column count
101+
}
102+
103+
[TestMethod]
104+
public void Bit_Decoder_RejectsInvalidByte()
105+
{
106+
var bytes = RowEncoder.EncodeRow([SqlType.Bit], [SqlValue.FromBoolean(true)]);
107+
bytes[4] = 0x02;
108+
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Bit], bytes));
109+
}
110+
111+
[TestMethod]
112+
public void Encoder_RejectsValueTypeMismatch() =>
113+
Throws<ArgumentException>(() => RowEncoder.EncodeRow([SqlType.BigInt], [42])); // 42 is Int32, schema is BigInt
114+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator.Storage;
4+
5+
[TestClass]
6+
public class RowRoundTripTests
7+
{
8+
[TestMethod]
9+
[DataRow(1)]
10+
[DataRow(0)]
11+
[DataRow(-1)]
12+
[DataRow(int.MaxValue)]
13+
[DataRow(int.MinValue)]
14+
public void SingleColumn_Int32_RoundTrips(int value)
15+
{
16+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [value]);
17+
var decoded = RowDecoder.DecodeRow([SqlType.Int32], bytes);
18+
AreEqual(1, decoded.Length);
19+
AreEqual(SqlValue.FromInt32(value), decoded[0]);
20+
}
21+
22+
[TestMethod]
23+
public void SingleColumn_Null_RoundTrips()
24+
{
25+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [SqlValue.Null(SqlType.Int32)]);
26+
var decoded = RowDecoder.DecodeRow([SqlType.Int32], bytes);
27+
AreEqual(1, decoded.Length);
28+
IsTrue(decoded[0].IsNull);
29+
AreEqual(SqlType.Int32, decoded[0].Type);
30+
}
31+
32+
[TestMethod]
33+
public void TwoColumns_BothPresent_RoundTrips()
34+
{
35+
SqlType[] schema = [SqlType.Int32, SqlType.Int32];
36+
var decoded = RowDecoder.DecodeRow(schema, RowEncoder.EncodeRow(schema, [1, 2]));
37+
CollectionAssert.AreEqual(new SqlValue[] { 1, 2 }, decoded);
38+
}
39+
40+
[TestMethod]
41+
public void TwoColumns_FirstNull_RoundTrips()
42+
{
43+
SqlType[] schema = [SqlType.Int32, SqlType.Int32];
44+
SqlValue[] values = [SqlValue.Null(SqlType.Int32), 1];
45+
var decoded = RowDecoder.DecodeRow(schema, RowEncoder.EncodeRow(schema, values));
46+
CollectionAssert.AreEqual(values, decoded);
47+
}
48+
49+
[TestMethod]
50+
public void TwoColumns_SecondNull_RoundTrips()
51+
{
52+
SqlType[] schema = [SqlType.Int32, SqlType.Int32];
53+
SqlValue[] values = [1, SqlValue.Null(SqlType.Int32)];
54+
var decoded = RowDecoder.DecodeRow(schema, RowEncoder.EncodeRow(schema, values));
55+
CollectionAssert.AreEqual(values, decoded);
56+
}
57+
58+
[TestMethod]
59+
public void ThreeColumns_AllNull_RoundTrips()
60+
{
61+
SqlType[] schema = [SqlType.Int32, SqlType.Int32, SqlType.Int32];
62+
SqlValue[] values = [SqlValue.Null(SqlType.Int32), SqlValue.Null(SqlType.Int32), SqlValue.Null(SqlType.Int32)];
63+
var decoded = RowDecoder.DecodeRow(schema, RowEncoder.EncodeRow(schema, values));
64+
CollectionAssert.AreEqual(values, decoded);
65+
}
66+
67+
[TestMethod]
68+
public void EightColumns_BitmapByteBoundary_RoundTrips()
69+
{
70+
// Exactly 8 columns => 1 byte of NULL bitmap. Alternating null/value tests
71+
// the bit ordering within the byte (column i ↔ bit i mod 8).
72+
SqlType[] schema = [SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32];
73+
var n = SqlValue.Null(SqlType.Int32);
74+
SqlValue[] values = [1, n, 2, n, 3, n, 4, n];
75+
var decoded = RowDecoder.DecodeRow(schema, RowEncoder.EncodeRow(schema, values));
76+
CollectionAssert.AreEqual(values, decoded);
77+
}
78+
79+
[TestMethod]
80+
public void NineColumns_MultiByteBitmap_RoundTrips()
81+
{
82+
// 9 columns => 2 bytes of NULL bitmap; the 9th column lives in the second byte.
83+
SqlType[] schema = [SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32, SqlType.Int32];
84+
SqlValue[] values = [10, 20, 30, 40, 50, 60, 70, 80, SqlValue.Null(SqlType.Int32)];
85+
var decoded = RowDecoder.DecodeRow(schema, RowEncoder.EncodeRow(schema, values));
86+
CollectionAssert.AreEqual(values, decoded);
87+
}
88+
89+
[TestMethod]
90+
public void Encoder_RejectsEmptySchema() =>
91+
Throws<ArgumentException>(() => RowEncoder.EncodeRow([], []));
92+
93+
[TestMethod]
94+
public void Encoder_RejectsSchemaValueLengthMismatch() =>
95+
Throws<ArgumentException>(() => RowEncoder.EncodeRow([SqlType.Int32, SqlType.Int32], [1]));
96+
97+
[TestMethod]
98+
public void EncodedSingleColumnRow_HasLength11()
99+
{
100+
// 4 (header) + 4 (one Int32) + 2 (column count) + 1 (NULL bitmap byte) = 11.
101+
AreEqual(11, RowEncoder.EncodeRow([SqlType.Int32], [42]).Length);
102+
AreEqual(11, RowEncoder.EncodeRow([SqlType.Int32], [SqlValue.Null(SqlType.Int32)]).Length);
103+
}
104+
105+
[TestMethod]
106+
public void EncodedTwoColumnRow_HasLength15()
107+
{
108+
// 4 (header) + 8 (two Int32s) + 2 (column count) + 1 (NULL bitmap byte) = 15.
109+
AreEqual(15, RowEncoder.EncodeRow([SqlType.Int32, SqlType.Int32], [1, 2]).Length);
110+
}
111+
112+
[TestMethod]
113+
public void EncodedSingleColumnRow_MatchesDocumentedLayout()
114+
{
115+
// Guards the byte-level layout described on RowEncoder.EncodeRow.
116+
// See RowEncoder's <remarks> for the public references that informed the
117+
// structural shape; the specific bit values are simulator-defined.
118+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [0x04030201]);
119+
AreEqual(0x10, bytes[0]); // TagA
120+
AreEqual(0x00, bytes[1]); // TagB
121+
AreEqual(8, BitConverter.ToUInt16(bytes, 2)); // fixed-length end offset
122+
CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03, 0x04 }, bytes[4..8]); // int LE
123+
AreEqual(1, BitConverter.ToUInt16(bytes, 8)); // column count
124+
AreEqual(0x00, bytes[10]); // NULL bitmap (not null)
125+
}
126+
127+
[TestMethod]
128+
public void EncodedTwoColumnRow_MatchesDocumentedLayout()
129+
{
130+
var bytes = RowEncoder.EncodeRow([SqlType.Int32, SqlType.Int32], [0x04030201, 0x08070605]);
131+
AreEqual(0x10, bytes[0]); // TagA
132+
AreEqual(0x00, bytes[1]); // TagB
133+
AreEqual(12, BitConverter.ToUInt16(bytes, 2)); // fixed-length end offset (4 + 2*4)
134+
CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03, 0x04 }, bytes[4..8]); // first int LE
135+
CollectionAssert.AreEqual(new byte[] { 0x05, 0x06, 0x07, 0x08 }, bytes[8..12]); // second int LE
136+
AreEqual(2, BitConverter.ToUInt16(bytes, 12)); // column count
137+
AreEqual(0x00, bytes[14]); // NULL bitmap
138+
}
139+
140+
[TestMethod]
141+
public void EncodedNullRow_SetsNullBitmapBit0()
142+
{
143+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [SqlValue.Null(SqlType.Int32)]);
144+
AreEqual(0x01, bytes[10]);
145+
}
146+
147+
[TestMethod]
148+
public void EncodedTwoColumnRow_SecondNullSetsBit1()
149+
{
150+
var bytes = RowEncoder.EncodeRow([SqlType.Int32, SqlType.Int32], [1, SqlValue.Null(SqlType.Int32)]);
151+
AreEqual(0x02, bytes[14]);
152+
}
153+
154+
[TestMethod]
155+
public void Decoder_RejectsTruncatedRow()
156+
{
157+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [1]).AsSpan(0, 10).ToArray();
158+
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Int32], bytes));
159+
}
160+
161+
[TestMethod]
162+
public void Decoder_RejectsBadTagA()
163+
{
164+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [1]);
165+
bytes[0] = 0x00;
166+
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Int32], bytes));
167+
}
168+
169+
[TestMethod]
170+
public void Decoder_RejectsColumnCountMismatch()
171+
{
172+
// The decoder cross-checks the declared column count against the schema length.
173+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [1]);
174+
bytes[8] = 0x02;
175+
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Int32], bytes));
176+
}
177+
178+
[TestMethod]
179+
public void Decoder_RejectsFixedEndMismatch()
180+
{
181+
// Fixed-length end offset is derived from the schema; tampering should be rejected.
182+
var bytes = RowEncoder.EncodeRow([SqlType.Int32], [1]);
183+
bytes[2] = 0x09;
184+
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Int32], bytes));
185+
}
186+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
<DebugType>embedded</DebugType>
9+
<DebugSymbols>true</DebugSymbols>
10+
<NoWarn>1591</NoWarn>
11+
<IsPackable>false</IsPackable>
12+
<RootNamespace>SqlServerSimulator.Storage</RootNamespace>
13+
<ImplicitUsings>enable</ImplicitUsings>
14+
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
19+
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
20+
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\SqlServerSimulator\SqlServerSimulator.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<None Include="..\.editorconfig" Link=".editorconfig" />
29+
</ItemGroup>
30+
31+
</Project>

0 commit comments

Comments
 (0)