Skip to content

Commit fb72d26

Browse files
committed
BACPAC loader perf — devirtualized stream wiring, eliminated per-row allocations on the BCP hot path, fast-path decimal decode. Profile-driven via dotnet-trace CPU sampling against WideWorldImporters-Full (61 MB, 4.7M rows). Three contained micro-optimizations in BcpRowReader.cs + BacpacReader.cs plus a peer cleanup in RowEncoder.cs. No behavior change — all tests pass.
# Devirtualized BufferedStream wiring `BacpacReader.Load` wraps every BCP entry's `ZipArchiveEntry.Open()` stream in a `BufferedStream(rawStream, 64 * 1024)` before handing it down. `BufferedStream` is sealed, so typing the parameter as the concrete type lets the JIT devirtualize all per-call `ReadByte` / `Read(Span<byte>)` invocations rather than going through the polymorphic `Stream` vtable. Two signatures change to opt in: - `BacpacReader.LoadRowsFromBcp(BufferedStream bcpStream, …)` (was `Stream`) - `BcpRowReader.TryReadRow(BufferedStream stream, …)` (was `Stream`) — and its internal `PushbackStream`'s `baseStream` field type matches. The 64 KB buffer also collapses thread-safety lock acquisitions inside `DeflateStream.ReadCore` from `~1 per column` to `~1 per 64 KB chunk` — profiled as 20% → noise on the lock-contention slice. Measured wall-clock A/B over 7 iters: MemoryStream-decompress-then-read (the obvious alternative) and BufferedStream finished within noise of each other, but BufferedStream wins on peak memory (64 KB transient vs. up to 4 MB per BCP shard for the bigger tables). # Per-row allocation elimination in BCP wire decoding `BcpRowReader.PushbackStream` flipped from `private sealed class` to `private struct` and is now passed `ref` through `DecodeColumn` / `ReadFixedRaw` / `ReadLengthPrefixed1` / `ReadDecimal` / `ReadVarchar2` / `ReadVarbinary2` / `ReadEightBytePrefixed`. Removes one heap allocation per row across the entire data-load phase — for `Warehouse.ColdRoomTemperatures_Archive` (the dominant table at ~50% of wall clock, 5M+ rows across 49 BCP shards) that's millions of gen0 allocations gone. Per-column scratch buffers in the fixed-width and length-prefixed-fixed read paths flip from `new byte[width]` to `stackalloc byte[width]`: - `ReadFixedRaw` — int/bigint/smallint/tinyint/datetime/smalldatetime/date/money/smallmoney/time(N)/datetime2(N)/datetimeoffset(N) - `ReadLengthPrefixed1` — bit/uniqueidentifier - `ReadDecimal` — the mantissa buffer (now also threaded through the fast path) - `ReadVarchar2` / `ReadVarbinary2` — the 2-byte length prefix (variable-length payload still goes to heap since it owns the `SqlValue`'s string/byte[] backing) Widths are bounded (≤ 16 bytes for fixed types, ≤ 17 bytes for decimal mantissa, exactly 2 bytes for length prefixes), so stackalloc is safe without an unbounded-input fallback. # Decimal/numeric fast path — bypass BigInteger.Pow + BigInteger division `BcpRowReader.ReadDecimal` previously decoded every mantissa via `System.Numerics.BigInteger`: read N bytes into an unsigned BigInteger, divide by `BigInteger.Pow(10, scale)` (allocates per call), then compose the decimal. Profiled as **~15% of total BACPAC wall-clock time** against WWI-Full (`ReadDecimal` 4.7s + `BigInteger.Pow` 1.0s + `DecCalc.VarDecDiv` 0.9s out of ~24s of inclusive CPU). The fast path covers SQL Server's typical precision range. When `mantissaSpan.Length <= 12` (precision ≤ 28) AND `type.scale <= 28`, the mantissa fits in System.Decimal's 96-bit representation. Pad the LE mantissa to 12 bytes, slice into three int32s, and construct the decimal directly via `new decimal(lo, mid, hi, isNegative, scale)` — zero arithmetic, no `BigInteger`, no `Pow`, no `VarDecDiv`. The wide path (precision > 28, mantissa > 12 bytes — outside System.Decimal's modeled range anyway, matches the simulator's documented decimal-precision quirk) keeps the BigInteger code for diagnostic completeness. # RowEncoder.EncodeRow — cache `schema[i].Type` locally Peer cleanup in the encoder's two inner loops. Hoists `schema[i].Type` into a `var t` local so the per-iteration walk does one indexed-property read instead of 3-5. The JIT probably already CSE'd this in tier-1 codegen, but explicit caching is free and the diff is small. A separate experiment in the same encoder (defer the per-row `varByteCounts` + `chainPointers` allocations when the schema has no var columns, stackalloc them when n is small) measured at the noise floor across 15-iter runs — the GC pressure those arrays generate is gen0-amortized to nearly zero already. Reverted; not in this bundle. # Aggregate impact Wall-clock (Release, WWI-Full, median over 15 iters): baseline → post-bundle measured noisily but consistently ~10% faster, with `ReadDecimal`-heavy tables (Sales.Invoices, Sales.OrderLines, Sales.CustomerTransactions) benefiting the most. The CPU profile after this bundle shows the remaining cost concentrated outside our control: `DeflateStream.ReadCore` (~36% — pure decompression CPU, irreducible without bypassing the zlib codec), `ZipArchiveEntry.Open` overhead at 488 entries (~7%), and `BacpacReader.LoadRowsFromBcp`'s mostly-saturated single-threaded `Heap.Insert` loop.
1 parent 76c20f0 commit fb72d26

3 files changed

Lines changed: 80 additions & 63 deletions

File tree

SqlServerSimulator/Storage/Bacpac/BacpacReader.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public static void Load(Stream stream, Simulation simulation, BacpacLoadResult r
6666

6767
try
6868
{
69-
using var bcpStream = entry.Open();
69+
using var rawStream = entry.Open();
70+
using var bcpStream = new BufferedStream(rawStream, 64 * 1024);
7071
LoadRowsFromBcp(bcpStream, table, schemaName, tableName, result);
7172
}
7273
catch (Exception ex) when (ex is InvalidDataException or NotSupportedException
@@ -91,7 +92,7 @@ or ArgumentException or OverflowException or FormatException
9192
/// values), no FK enforcement, no triggers (data is already consistent
9293
/// from the source DB).
9394
/// </summary>
94-
private static void LoadRowsFromBcp(Stream bcpStream, HeapTable table, string schemaName, string tableName, BacpacLoadResult result)
95+
private static void LoadRowsFromBcp(BufferedStream bcpStream, HeapTable table, string schemaName, string tableName, BacpacLoadResult result)
9596
{
9697
// Look up the per-column alias map populated during model.xml table
9798
// emission. Key uses bracketed [schema].[table] form to match the

SqlServerSimulator/Storage/Bacpac/BcpRowReader.cs

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal static class BcpRowReader
4747
/// wire format even for fixed-raw types like int/bit/etc.
4848
/// Returns null when the stream is at EOF (no more rows).
4949
/// </summary>
50-
public static SqlValue[]? TryReadRow(Stream stream, ReadOnlySpan<HeapColumn> columns, ReadOnlySpan<bool> columnIsAlias)
50+
public static SqlValue[]? TryReadRow(BufferedStream stream, ReadOnlySpan<HeapColumn> columns, ReadOnlySpan<bool> columnIsAlias)
5151
{
5252
// Peek one byte to detect EOF without throwing.
5353
var firstByte = stream.ReadByte();
@@ -57,11 +57,11 @@ internal static class BcpRowReader
5757

5858
var values = new SqlValue[columns.Length];
5959
for (var i = 0; i < columns.Length; i++)
60-
values[i] = DecodeColumn(pushback, columns[i], i < columnIsAlias.Length && columnIsAlias[i]);
60+
values[i] = DecodeColumn(ref pushback, columns[i], i < columnIsAlias.Length && columnIsAlias[i]);
6161
return values;
6262
}
6363

64-
private static SqlValue DecodeColumn(PushbackStream stream, HeapColumn column, bool isAliasTyped)
64+
private static SqlValue DecodeColumn(ref PushbackStream stream, HeapColumn column, bool isAliasTyped)
6565
{
6666
var type = column.Type;
6767
// UDDT-aliased columns use 1-byte-prefix wire format regardless of
@@ -75,59 +75,59 @@ private static SqlValue DecodeColumn(PushbackStream stream, HeapColumn column, b
7575
// despite the prereqs-doc matrix's "length-prefixed fixed" claim
7676
// (probe-confirmed against AW's SpecialOffer.DiscountPct on
7777
// 2026-05-15: 4 raw bytes with value 0 for the first row, no prefix).
78-
if (type == SqlType.Int32) return ReadFixedRaw(stream, nullable, 4, type, b => SqlValue.FromInt32(BinaryPrimitives.ReadInt32LittleEndian(b)));
79-
if (type == SqlType.BigInt) return ReadFixedRaw(stream, nullable, 8, type, b => SqlValue.FromInt64(BinaryPrimitives.ReadInt64LittleEndian(b)));
80-
if (type == SqlType.SmallInt) return ReadFixedRaw(stream, nullable, 2, type, b => SqlValue.FromInt16(BinaryPrimitives.ReadInt16LittleEndian(b)));
81-
if (type == SqlType.TinyInt) return ReadFixedRaw(stream, nullable, 1, type, b => SqlValue.FromByte(b[0]));
78+
if (type == SqlType.Int32) return ReadFixedRaw(ref stream, nullable, 4, type, b => SqlValue.FromInt32(BinaryPrimitives.ReadInt32LittleEndian(b)));
79+
if (type == SqlType.BigInt) return ReadFixedRaw(ref stream, nullable, 8, type, b => SqlValue.FromInt64(BinaryPrimitives.ReadInt64LittleEndian(b)));
80+
if (type == SqlType.SmallInt) return ReadFixedRaw(ref stream, nullable, 2, type, b => SqlValue.FromInt16(BinaryPrimitives.ReadInt16LittleEndian(b)));
81+
if (type == SqlType.TinyInt) return ReadFixedRaw(ref stream, nullable, 1, type, b => SqlValue.FromByte(b[0]));
8282
// bit is always 1-byte length-prefixed (1 byte prefix == 1, then 1
8383
// raw byte for the value). Probe-confirmed via AW's plain-bit
8484
// Production.Document.FolderFlag on 2026-05-15.
85-
if (type == SqlType.Bit) return ReadLengthPrefixed1(stream, 1, type, b => SqlValue.FromBoolean(b[0] != 0));
86-
if (type == SqlType.DateTime) return ReadFixedRaw(stream, nullable, 8, type, DecodeDateTime);
87-
if (type == SqlType.SmallDateTime) return ReadFixedRaw(stream, nullable, 4, type, DecodeSmallDateTime);
88-
if (type == SqlType.Date) return ReadFixedRaw(stream, nullable, 3, type, DecodeDate);
89-
if (type == SqlType.Money) return ReadFixedRaw(stream, nullable, 8, type, b => DecodeMoney(b, type));
90-
if (type == SqlType.SmallMoney) return ReadFixedRaw(stream, nullable, 4, type, b => DecodeSmallMoney(b, type));
85+
if (type == SqlType.Bit) return ReadLengthPrefixed1(ref stream, 1, type, b => SqlValue.FromBoolean(b[0] != 0));
86+
if (type == SqlType.DateTime) return ReadFixedRaw(ref stream, nullable, 8, type, DecodeDateTime);
87+
if (type == SqlType.SmallDateTime) return ReadFixedRaw(ref stream, nullable, 4, type, DecodeSmallDateTime);
88+
if (type == SqlType.Date) return ReadFixedRaw(ref stream, nullable, 3, type, DecodeDate);
89+
if (type == SqlType.Money) return ReadFixedRaw(ref stream, nullable, 8, type, b => DecodeMoney(b, type));
90+
if (type == SqlType.SmallMoney) return ReadFixedRaw(ref stream, nullable, 4, type, b => DecodeSmallMoney(b, type));
9191

9292
// Length-prefixed fixed — always 1-byte length prefix even when
9393
// NOT NULL. uniqueidentifier has fixed 16-byte payload but always
9494
// emits its 0x10 prefix per probe.
95-
if (type == SqlType.UniqueIdentifier) return ReadLengthPrefixed1(stream, 16, type, b => SqlValue.FromGuid(new Guid(b)));
95+
if (type == SqlType.UniqueIdentifier) return ReadLengthPrefixed1(ref stream, 16, type, b => SqlValue.FromGuid(new Guid(b)));
9696

9797
// Decimal/numeric — 1-byte length prefix + sign byte + LE mantissa.
9898
// Mantissa bytes depend on precision: 1-9 = 4 bytes, 10-19 = 8 bytes,
9999
// 20-28 = 12 bytes, 29-38 = 16 bytes. Prefix = sign(1) + mantissa.
100-
if (type is DecimalSqlType decimalType) return ReadDecimal(stream, decimalType);
100+
if (type is DecimalSqlType decimalType) return ReadDecimal(ref stream, decimalType);
101101

102102
// datetime2 / time / datetimeoffset — precision-dependent fixed width
103103
// (probe-confirmed via AW HumanResources.Shift on 2026-05-15: time(7)
104104
// NOT NULL = 5 raw bytes, no prefix). Width comes from the
105105
// precision-specific singleton fields.
106-
if (type is TimeSqlType tt) return ReadFixedRaw(stream, nullable, tt.timeBytes, type, b => DecodeTime(b, type));
107-
if (type is DateTime2SqlType dt2t) return ReadFixedRaw(stream, nullable, dt2t.timeBytes + 3, type, b => DecodeDateTime2(b, type));
108-
if (type is DateTimeOffsetSqlType dtot) return ReadFixedRaw(stream, nullable, dtot.timeBytes + 5, type, b => DecodeDateTimeOffset(b, type));
106+
if (type is TimeSqlType tt) return ReadFixedRaw(ref stream, nullable, tt.timeBytes, type, b => DecodeTime(b, type));
107+
if (type is DateTime2SqlType dt2t) return ReadFixedRaw(ref stream, nullable, dt2t.timeBytes + 3, type, b => DecodeDateTime2(b, type));
108+
if (type is DateTimeOffsetSqlType dtot) return ReadFixedRaw(ref stream, nullable, dtot.timeBytes + 5, type, b => DecodeDateTimeOffset(b, type));
109109

110110
// 8-byte LE length-prefix types (MAX text/binary, xml, CLR-UDT family).
111111
// 0xFFFFFFFFFFFFFFFF = NULL; otherwise N bytes inline (no TDS-PLP
112112
// chunk markers — probe-confirmed against AW on 2026-05-15).
113-
if (type is XmlSqlType) return ReadEightBytePrefixed(stream, type, EightBytePayload.Xml);
114-
if (type is GeographySqlType) return ReadEightBytePrefixed(stream, type, EightBytePayload.Geography);
115-
if (type is GeometrySqlType) return ReadEightBytePrefixed(stream, type, EightBytePayload.Geometry);
116-
if (type is HierarchyIdSqlType) return ReadEightBytePrefixed(stream, type, EightBytePayload.HierarchyId);
117-
if (type is VarcharSqlType vc && vc.length == -1) return ReadEightBytePrefixed(stream, type, EightBytePayload.VarcharMax);
118-
if (type is NVarcharSqlType nv && nv.length == -1) return ReadEightBytePrefixed(stream, type, EightBytePayload.NVarcharMax);
119-
if (type is VarbinarySqlType vb && vb.length == -1) return ReadEightBytePrefixed(stream, type, EightBytePayload.VarbinaryMax);
113+
if (type is XmlSqlType) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.Xml);
114+
if (type is GeographySqlType) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.Geography);
115+
if (type is GeometrySqlType) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.Geometry);
116+
if (type is HierarchyIdSqlType) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.HierarchyId);
117+
if (type is VarcharSqlType vc && vc.length == -1) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.VarcharMax);
118+
if (type is NVarcharSqlType nv && nv.length == -1) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.NVarcharMax);
119+
if (type is VarbinarySqlType vb && vb.length == -1) return ReadEightBytePrefixed(ref stream, type, EightBytePayload.VarbinaryMax);
120120

121121
// Variable-length bounded (text/binary) — 2-byte LE prefix, 0xFFFF = NULL.
122122
return type switch
123123
{
124-
VarcharSqlType => ReadVarchar2(stream, type, ansi: true),
125-
NVarcharSqlType => ReadVarchar2(stream, type, ansi: false),
126-
SystemNameSqlType => ReadVarchar2(stream, type, ansi: false),
127-
NCharSqlType => ReadVarchar2(stream, type, ansi: false),
128-
CharSqlType => ReadVarchar2(stream, type, ansi: true),
129-
VarbinarySqlType => ReadVarbinary2(stream, type),
130-
BinarySqlType => ReadVarbinary2(stream, type),
124+
VarcharSqlType => ReadVarchar2(ref stream, type, ansi: true),
125+
NVarcharSqlType => ReadVarchar2(ref stream, type, ansi: false),
126+
SystemNameSqlType => ReadVarchar2(ref stream, type, ansi: false),
127+
NCharSqlType => ReadVarchar2(ref stream, type, ansi: false),
128+
CharSqlType => ReadVarchar2(ref stream, type, ansi: true),
129+
VarbinarySqlType => ReadVarbinary2(ref stream, type),
130+
BinarySqlType => ReadVarbinary2(ref stream, type),
131131
_ => throw new NotSupportedException($"BCP decoder doesn't yet handle type {type}."),
132132
};
133133
}
@@ -179,7 +179,7 @@ private enum EightBytePayload
179179
/// files and is rejected as <see cref="NotSupportedException"/> so a bad
180180
/// payload surfaces clearly rather than corrupting the row stream.
181181
/// </summary>
182-
private static SqlValue ReadEightBytePrefixed(PushbackStream stream, SqlType type, EightBytePayload kind)
182+
private static SqlValue ReadEightBytePrefixed(ref PushbackStream stream, SqlType type, EightBytePayload kind)
183183
{
184184
Span<byte> lengthBuf = stackalloc byte[8];
185185
stream.ReadExact(lengthBuf);
@@ -209,7 +209,7 @@ private static SqlValue ReadEightBytePrefixed(PushbackStream stream, SqlType typ
209209

210210
private delegate SqlValue ByteSpanDecoder(ReadOnlySpan<byte> bytes);
211211

212-
private static SqlValue ReadFixedRaw(PushbackStream stream, bool nullable, int width, SqlType type, ByteSpanDecoder build)
212+
private static SqlValue ReadFixedRaw(ref PushbackStream stream, bool nullable, int width, SqlType type, ByteSpanDecoder build)
213213
{
214214
if (nullable)
215215
{
@@ -219,19 +219,19 @@ private static SqlValue ReadFixedRaw(PushbackStream stream, bool nullable, int w
219219
if (prefix != width)
220220
throw new InvalidDataException($"BCP: expected fixed-width prefix {width} or 0xFF, got 0x{prefix:X2}.");
221221
}
222-
var bytes = new byte[width];
222+
Span<byte> bytes = stackalloc byte[width];
223223
stream.ReadExact(bytes);
224224
return build(bytes);
225225
}
226226

227-
private static SqlValue ReadLengthPrefixed1(PushbackStream stream, int expectedWidth, SqlType type, ByteSpanDecoder build)
227+
private static SqlValue ReadLengthPrefixed1(ref PushbackStream stream, int expectedWidth, SqlType type, ByteSpanDecoder build)
228228
{
229229
var prefix = stream.ReadOneByte();
230230
if (prefix == 0xFF)
231231
return SqlValue.Null(type);
232232
if (prefix != expectedWidth)
233233
throw new InvalidDataException($"BCP: expected length-prefixed-fixed width {expectedWidth} or 0xFF, got 0x{prefix:X2}.");
234-
var bytes = new byte[expectedWidth];
234+
Span<byte> bytes = stackalloc byte[expectedWidth];
235235
stream.ReadExact(bytes);
236236
return build(bytes);
237237
}
@@ -242,27 +242,41 @@ private static SqlValue ReadLengthPrefixed1(PushbackStream stream, int expectedW
242242
/// mantissa. The mantissa is the value times 10^scale, parsed as an
243243
/// arbitrarily-large integer.
244244
/// </summary>
245-
private static SqlValue ReadDecimal(PushbackStream stream, DecimalSqlType type)
245+
private static SqlValue ReadDecimal(ref PushbackStream stream, DecimalSqlType type)
246246
{
247247
var prefix = stream.ReadOneByte();
248248
if (prefix == 0xFF)
249249
return SqlValue.Null(type);
250-
var bytes = new byte[prefix];
250+
Span<byte> bytes = stackalloc byte[prefix];
251251
stream.ReadExact(bytes);
252252
var positive = bytes[0] != 0;
253-
// Build the mantissa as a System.Numerics.BigInteger from the LE bytes,
254-
// then divide by 10^scale to produce the decimal value. The .NET
255-
// decimal type maxes at 28-29 significant digits — values requiring
256-
// more are out of scope (matches the simulator's documented decimal
257-
// quirk).
258-
var mantissaSpan = bytes.AsSpan(1);
253+
var mantissaSpan = bytes[1..];
254+
255+
// Fast path: mantissa fits in 12 bytes (96 bits) and scale fits in
256+
// System.Decimal's 28-digit ceiling — construct the decimal directly
257+
// from its (lo, mid, hi, sign, scale) representation with zero
258+
// arithmetic. SQL Server BCP mantissa for precision ≤ 28 always fits.
259+
// Skips BigInteger.Pow + BigInteger division, which profiled as ~15%
260+
// of total BACPAC load wall clock against WideWorldImporters-Full.
261+
if (mantissaSpan.Length <= 12 && type.scale <= 28)
262+
{
263+
Span<byte> padded = stackalloc byte[12];
264+
mantissaSpan.CopyTo(padded);
265+
var lo = BinaryPrimitives.ReadInt32LittleEndian(padded[..4]);
266+
var mid = BinaryPrimitives.ReadInt32LittleEndian(padded[4..8]);
267+
var hi = BinaryPrimitives.ReadInt32LittleEndian(padded[8..12]);
268+
return SqlValue.FromDecimal(type, new decimal(lo, mid, hi, isNegative: !positive, scale: type.scale));
269+
}
270+
271+
// Wide path (precision > 28, mantissa > 12 bytes) — use BigInteger.
272+
// The .NET decimal type itself maxes at 28-29 significant digits, so
273+
// anything in this branch is out of the simulator's modeled range
274+
// and may lose precision (matches the simulator's documented quirk).
259275
var unsigned = new System.Numerics.BigInteger(mantissaSpan, isUnsigned: true, isBigEndian: false);
260276
var signed = positive ? unsigned : -unsigned;
261277
var scaleDivisor = System.Numerics.BigInteger.Pow(10, type.scale);
262278
var quotient = signed / scaleDivisor;
263279
var remainder = signed % scaleDivisor;
264-
// Compose: integer part + (remainder / 10^scale). Use decimal arithmetic
265-
// for the fractional piece.
266280
var value = (decimal)quotient + ((decimal)remainder / (decimal)scaleDivisor);
267281
return SqlValue.FromDecimal(type, value);
268282
}
@@ -331,9 +345,9 @@ private static SqlValue DecodeDateTimeOffset(ReadOnlySpan<byte> bytes, SqlType t
331345
_ => 1,
332346
};
333347

334-
private static SqlValue ReadVarchar2(PushbackStream stream, SqlType type, bool ansi)
348+
private static SqlValue ReadVarchar2(ref PushbackStream stream, SqlType type, bool ansi)
335349
{
336-
var prefixBytes = new byte[2];
350+
Span<byte> prefixBytes = stackalloc byte[2];
337351
stream.ReadExact(prefixBytes);
338352
var byteLength = BinaryPrimitives.ReadUInt16LittleEndian(prefixBytes);
339353
if (byteLength == 0xFFFF)
@@ -352,9 +366,9 @@ private static SqlValue ReadVarchar2(PushbackStream stream, SqlType type, bool a
352366
};
353367
}
354368

355-
private static SqlValue ReadVarbinary2(PushbackStream stream, SqlType type)
369+
private static SqlValue ReadVarbinary2(ref PushbackStream stream, SqlType type)
356370
{
357-
var prefixBytes = new byte[2];
371+
Span<byte> prefixBytes = stackalloc byte[2];
358372
stream.ReadExact(prefixBytes);
359373
var byteLength = BinaryPrimitives.ReadUInt16LittleEndian(prefixBytes);
360374
if (byteLength == 0xFFFF)
@@ -429,7 +443,7 @@ private static SqlValue DecodeSmallMoney(ReadOnlySpan<byte> bytes, SqlType type)
429443
/// byte of each row to detect EOF; pushback puts the byte back so the
430444
/// first column's decoder reads it as normal.
431445
/// </summary>
432-
private sealed class PushbackStream(Stream baseStream, byte first)
446+
private struct PushbackStream(BufferedStream baseStream, byte first)
433447
{
434448
private readonly int pushed = first;
435449
private bool hasPushed = true;

SqlServerSimulator/Storage/RowEncoder.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,19 @@ static bool IsCompatibleColumnType(SqlType valueType, SqlType columnType) =>
120120

121121
for (var i = 0; i < n; i++)
122122
{
123-
if (!values[i].IsNull && !IsCompatibleColumnType(values[i].Type, schema[i].Type))
124-
throw new ArgumentException($"Value at column {i} has type {values[i].Type}, schema declares {schema[i].Type}.", nameof(values));
123+
var t = schema[i].Type;
124+
if (!values[i].IsNull && !IsCompatibleColumnType(values[i].Type, t))
125+
throw new ArgumentException($"Value at column {i} has type {values[i].Type}, schema declares {t}.", nameof(values));
125126

126-
if (schema[i].Type == SqlType.Bit)
127+
if (t == SqlType.Bit)
127128
{
128129
if (bitsInRun % 8 == 0)
129130
fixedSectionLength++;
130131
bitsInRun++;
131132
}
132-
else if (schema[i].Type.IsFixedLength)
133+
else if (t.IsFixedLength)
133134
{
134-
fixedSectionLength += schema[i].Type.FixedLength;
135+
fixedSectionLength += t.FixedLength;
135136
bitsInRun = 0;
136137
}
137138
else
@@ -207,7 +208,8 @@ static bool IsCompatibleColumnType(SqlType valueType, SqlType columnType) =>
207208
if (values[i].IsNull)
208209
bytes[bitmapStart + (i / 8)] |= (byte)(1 << (i % 8));
209210

210-
if (schema[i].Type == SqlType.Bit)
211+
var t = schema[i].Type;
212+
if (t == SqlType.Bit)
211213
{
212214
if (bitsInRun % 8 == 0)
213215
{
@@ -218,11 +220,11 @@ static bool IsCompatibleColumnType(SqlType valueType, SqlType columnType) =>
218220
bytes[bitByteOffset] |= (byte)(1 << (bitsInRun % 8));
219221
bitsInRun++;
220222
}
221-
else if (schema[i].Type.IsFixedLength)
223+
else if (t.IsFixedLength)
222224
{
223-
var width = schema[i].Type.FixedLength;
225+
var width = t.FixedLength;
224226
if (!values[i].IsNull)
225-
_ = schema[i].Type.Encode(values[i], bytes.AsSpan(fixedOffset, width));
227+
_ = t.Encode(values[i], bytes.AsSpan(fixedOffset, width));
226228
fixedOffset += width;
227229
bitsInRun = 0;
228230
}

0 commit comments

Comments
 (0)