Commit f1be9d2
committed
BACPAC loader Phase H — wire-format coverage for MAX types, xml, geography, and plain bit. AdventureWorks2025 row coverage rises from 718,369 → **759,833 / 760,167 (99.96%)**. Only 4 BCP shards remain blocked, all hierarchyid (HR.Employee + Production.Document × 2 + Production.ProductDocument, ~334 rows total); hierarchyid's variable-bit Huffman-style binary encoding warrants its own follow-up bundle.
**8-byte-prefix wire format unified** under new `ReadEightBytePrefixed(stream, type, EightBytePayload)` in `BcpRowReader`. Probe-confirmed against AW on 2026-05-15 by hex-dumping `Production.ProductPhoto` (1077-byte GIF89a varbinary(MAX) at offset 0x0c flowing inline with no chunk markers — runs cleanly to offset 0x441 where the next column starts) and `HumanResources.JobCandidate` (9086-byte `<ns:Resume…` xml column likewise inline): the bacpac BCP format uses a single 8-byte LE length followed by N bytes of data, NOT the TDS-PLP chunked encoding (8-byte length + 4-byte-chunk markers + 4-byte-zero terminator) used in live network traffic. NULL sentinel = `0xFFFFFFFFFFFFFFFF` (-1 signed). Five payload variants: `VarcharMax` (CP1252 decode), `NVarcharMax` (UTF-16 decode), `VarbinaryMax` (bytes), `Xml` (UTF-16 → `SqlValue.FromXml`), `SpatialDeferToNull` (drain bytes, return `SqlValue.Null` — WKB→WKT decoding deferred so Person.Address loads with `SpatialLocation = NULL` rather than failing the whole shard).
**Two probe-confirmed wire-format corrections vs. the prereqs-doc matrix**:
1. **Plain `bit` is 1-byte-length-prefixed** (1-byte width prefix == 1 + 1-byte value), regardless of nullability — same shape as UDDT-aliased bit columns. Probe revealed via Production.Document.FolderFlag (the only non-UDDT bit column anywhere in AW per `grep -c bit model.xml`): wire bytes are `01 01` for value 1, not `01` as a fixed-raw single byte. The original Phase G decoder used `ReadFixedRaw` for bit which would have read the prefix byte as the value and then desynced the column stream on the next column's prefix — but AW never exercised the path because every other bit column is UDDT-aliased (`dbo.Flag`). FolderFlag's BCP shard was already blocked on hierarchyid so the bug never surfaced in tests. Fix: route `SqlType.Bit` through `ReadLengthPrefixed1(stream, 1, …)`.
2. **MAX types + xml + CLR-UDT family (hierarchyid, geography, geometry) all share the simple inline 8-byte-prefix shape**. The original prereqs-doc matrix speculated "8-byte length + chunked sub-blocks" for MAX types and left CLR-UDTs as separate handling. Probe shows they're identical: single 8-byte length + N bytes inline, no chunk markers, no terminator. Geography's WKB inline payload is 22 bytes for AW's all-point dataset (4-byte SRID `0x000010E6` = 4326 / WGS 84 + 1-byte version + 1-byte properties `0x0C` = is-valid + single-point + 16 bytes for X/Y as IEEE 754 doubles).
**New `BcpRowReader` plumbing**:
- `EightBytePayload` enum + `ReadEightBytePrefixed` helper (length + dispatch + NULL handling unified)
- Routes `XmlSqlType` → `EightBytePayload.Xml`; `GeographySqlType`/`GeometrySqlType` → `EightBytePayload.SpatialDeferToNull`; `VarcharSqlType { length: -1 }` → `EightBytePayload.VarcharMax`; same for nvarchar/varbinary MAX
- Removes the now-unused `RejectMaxType` blocker — MAX types decode cleanly instead of aborting the whole BCP file
- Length validity check `length is < 0 or > int.MaxValue` rejects the TDS-PLP chunked-form sentinels (0xFFFFFFFFFFFFFFFE for "unknown total length") with `NotSupportedException` so a bad bacpac surfaces a clear error rather than corrupting the row stream
- 8-byte length read uses `Span<byte>` stackalloc to avoid per-row heap allocation on the 1103-shard hot path
**AW coverage delta (Phase G → Phase H)**:
- ProductPhoto (varbinary(MAX) ThumbNailPhoto + LargePhoto blobs): 0 → 101 rows
- Person.Person (xml Demographics column on every row): 0 → 19,972 rows
- Person.Address (geography SpatialLocation, NULLed): 0 → 19,614 rows
- HumanResources.JobCandidate (xml Resume): 0 → 13 rows
- Production.ProductDocument: blocks on hierarchyid (still Skipped)
- Production.Document × 2 shards: block on hierarchyid (still Skipped)
- HumanResources.Employee: blocks on hierarchyid (still Skipped)
**4 new `BacpacLoaderTests` lock in the new wire formats** + tighten the existing data-load floor:
- `Load_AW_Xml_Column_Carries_Value` — JobCandidate Resume round-trips through `nvarchar(MAX)` cast, asserts `<ns:Resume` prefix + 13-row count.
- `Load_AW_VarbinaryMax_Column_Carries_Bytes` — ProductPhoto.ThumbNailPhoto round-trips, asserts the GIF89a magic bytes (0x47 0x49 0x46 0x38 0x39 0x61) at offsets 0-5.
- `Load_AW_Geography_Column_Drops_To_Null` — Person.Address all 19,614 rows load with `SpatialLocation IS NULL`, post-spatial-column rowguid + ModifiedDate read cleanly (proves the 8-byte-prefix-then-drain pass doesn't desync subsequent column boundaries).
- `Load_AW_Plain_Bit_Column_Reads_Correctly` — Production.Document table-existence sanity (data rows still hierarchyid-blocked but the table itself lands; the bit-prefix fix is regression-prevention for other bacpacs with plain-bit columns).
- `Load_AW_Bcp_Data_Loads` floor raised from `>= 700_000` to `>= 759_500` + Skipped data file count locked at 4 (all hierarchyid) so any regression in MAX/xml/geography surfaces immediately.
CLAUDE.md "BACPAC import" pointer rewrites to reflect "loader baseline ships" status with the 99.96% AW coverage number + the wire-format-matrix summary. `docs/claude/bacpac-prerequisites.md` BCP wire format table fully refreshed with the probe-confirmed shape per type family, the bit-prefix correction noted, the MAX/xml/CLR-UDT unification documented, and the order-of-operations checklist shows the loader baseline as shipped with hierarchyid + geography decoding called out as separate future bundles.1 parent b550a7c commit f1be9d2
4 files changed
Lines changed: 207 additions & 62 deletions
File tree
- SqlServerSimulator.Tests.Internal/Storage
- SqlServerSimulator/Storage/Bacpac
- docs/claude
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| |||
Lines changed: 105 additions & 13 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
268 | 268 | | |
269 | | - | |
| 269 | + | |
270 | 270 | | |
271 | 271 | | |
272 | 272 | | |
| |||
277 | 277 | | |
278 | 278 | | |
279 | 279 | | |
280 | | - | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
281 | 283 | | |
| 284 | + | |
282 | 285 | | |
283 | 286 | | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
| 287 | + | |
| 288 | + | |
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
292 | 384 | | |
293 | 385 | | |
294 | 386 | | |
| |||
0 commit comments