De BIFF PivotCache substream bewaart de gecachte dataset van een PivotTable los van de view die hem toont, en HotXLS leest en schrijft die substream door recordbodies te inspecteren in plaats van op recordnummers te vertrouwen. Dat onderscheid is het hele verhaal: hetzelfde recordnummer draagt twee onverenigbare bodylayouts afhankelijk van welke writer de file produceerde, dus de reader beslist de framing op basis van de eerste recordbody die hij ziet
U komt deze laag tegen op het moment dat een PivotTable een round trip moet overleven. Een pivot view zonder zijn cache is een lege schil, en Excel bouwt de cache opnieuw uit het source range zodra hij de file opent, wat prima gaat totdat het source range weg is, de data uit een query is geplakt, of de workbook een gearchiveerde afsluiting is die niet mag veranderen zodra iemand hem opent
Twee structuren, twee plekken in de file
De gecachte data en de cachedefinitie wonen in verschillende delen van de workbook, en ze door elkaar halen is het eerste wat goed moet zitten. De gecachte records vormen hun eigen substream, in [MS-XLS] §2.1.7.12 gegeven als PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Merk op wat ontbreekt: er staat geen BOF aan het hoofd van die productie
De definitie zit in plaats daarvan in de workbook globals, als PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), gepositioneerd na de opmaakrecords en vóór de BoundSheet- en Country-records. Eén enkele cache wordt dus op twee plekken beschreven die honderden records uit elkaar liggen, en de schakel tussen hen is een stream-identificator die op drie plekken tegelijk moet kloppen
Elke cache hoort in een stream onder _SX_DB_CUR waarvan de naam de viercijferige hoofdletter-hexadecimale spelling van zijn identificator is. SXStreamID.idStm, het idstm-veld dat in de SXDB-header herhaald wordt, en die streamnaam moeten alle drie matchen. Wijs u een nieuwe identificator toe, reserveer dan eerst elk nummer dat al uit de file is gelezen, anders kan een nieuwe cache een nummer claimen dat toebehoort aan een oudere cache waar de reader nog niet is geweest
Nog één identificator vangt mensen. De iCache-waarde in een pivot view is de nulgebaseerde positie van de bijbehorende SXStreamID in de globale reeks, geen cache-identificator die u zelf mag kiezen. Bij het schrijven moet hij van het cache-object naar zijn werkelijke outputpositie worden gemapped, en bestaande views moeten mee worden hernummerd, anders wijst het upgraden van één cache stilletjes een view naar een andere
var
Book: TXLSWorkbook;
Cache: TXLSPivotCache;
Field: TXLSPivotCacheField;
V: TXLSPivotCacheValue;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('sales.xls');
Cache := Book.PivotCaches.Add;
Cache.SourceRangeSheet := 'Data';
Cache.SourceFirstRow := 1; Cache.SourceFirstCol := 1;
Cache.SourceLastRow := 500; Cache.SourceLastCol := 6;
Cache.SourceDataType := 1; // SXVS SHEET, MS-XLS 2.4.317
Cache.RefreshOnLoad := False; // vertrouw op de gecachte records
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // wissen, daarna de recordgrid vullen
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
De dubbele SetRecordCount is geen bijgeloof. RecordCount is een platte property-write die niets alloceert, en het interne groeipad initialiseert alleen de nieuw toegevoegde rijen, dus een cache waarvan de telling via het headerpad is gezet kan stranden met een indexgrid van lengte nul. Schrijfacties naar RecordIndices worden dan foutloos weggegooid. De telling op nul zetten en terug herstelt het grid, en dat moet gebeuren nadat elk veld is toegevoegd, want de rijbreedte komt uit het veldaantal
Waarom kan een recordnummer u niet de bodylayout vertellen?
Omdat recordnummers en bodylayouts op verschillende momenten veranderden, is de mapping tussen hen geen functie. Eén nummer in de legacy-set verschijnt uitsluitend in files van oudere writers, wat hem een betrouwbaar signaal in één richting maakt. Een ander nummer is echt dubbelzinnig: hij verschijnt zowel in correcte files als in een reeks tussenliggende versies die het nieuwe nummer met de oude bodylayout gebruikten
De framing moet daarom uit de body worden besloten, en één keer per cache substream in plaats van per record. HotXLS vergrendelt het dialect aan de lengte van de eerste SXDBB-record in elke substream. In de specificatieframing bevat één SXDBB precies één cache record, dus zijn lengte is gelijk aan één rijbreedte. In de oudere gepakte framing bevat de eerste record zoveel rijen als passen, dus voor elke cache met meer dan één rij is hij ten minste twee rijbreedtes. De vergelijking is doorslaggevend telkens wanneer de twee voorspellingen verschillen
Wanneer ze niet verschillen, neemt de reader de specificatielezing, op het principe dat files geschreven door Excel talrijker zijn dan files geschreven door een tussenliggende build. Dat blinde vlekje is van constructie smal en als het zich voordoet, speelt de file zelf nog steeds byte voor byte af. Alleen de getypeerde indices die aan aanroepers worden blootgesteld, zijn getroffen
De indexbreedte woont in een andere record
SXDBB (§2.4.276) draagt één index per cacheveld waarvan de distinct-value-flag is gezet, in veldvolgorde, en de breedte van elke index wordt elders beslist: de bijbehorende SXFDB-veldrecord (§2.4.283) declareert een short-items-flag, en die flag zegt of de index twee bytes of één beslaat. Twee records, één impliciet contract, en één enkele zin in de specificatie die ze verbindt
Die koppeling is precies waar een zelfgebouwde codering de fout in gaat. Een eerdere HotXLS-writer pakte elk veld in het minimale aantal bits, aangevuld tot een bytegrens tussen rijen, wat op zichzelf te verdedigen is en direct ingaat tegen de breedte die dezelfde writer net daarvoor in SXFDB had gedeclareerd. Een veld met drie verschillende waarden werd in de ene record als één byte breed beschreven en besloeg in de andere twee bits. De fix was niet de rekenkunde corrigeren, maar de breedtebeslissing extraheren naar één functie die beide emitters aanroepen, zodat de twee records niet meer uit elkaar kunnen drijven. Dat is dezelfde defectklasse als beschreven in BIFF record length declaration drift, waar een gedeclareerde grootte en een werkelijke body uit elkaar gaan
Het gevolg van deze records helemaal niet lezen is het uitspreken waard, want het is makkelijk te onderschatten. Toen de reader de recordindices oversloeg, meldde elke cache uit een file index nul voor elk veld van elke rij, wat betekent dat elke rij naar de eerste waarde van elk veld wees. Dat is niet slechts verminderde introspectie: de pivot-evaluatieroute en de cache-naar-cel-vulroute verbruiken beide dat grid. En een round-trip-test kan het niet detecteren, want een cache die nog op rauwe replay staat wordt teruggeschreven vanuit zijn originele bytes
// Provenance-flags vertellen u wat u in handen heeft en wat herschreven mag worden
if Cache.FromRawBlobs then
begin
Writeln('stream id : ', IntToHex(Cache.StreamId, 4));
Writeln('legacy framing : ', Cache.RawFramingIsLegacy);
Writeln('own storage : ', Cache.RawHasStorageStream);
Writeln('model complete : ', Cache.RawModelIsComplete);
// Opnieuw emitteren is alleen verliesvrij als elke record hier een model heeft
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
Wanneer is het herschrijven van een cache verliesvrij?
Alleen als drie voorwaarden tegelijk gelden, en CanUpgradeFraming is de enige property die de vraag beantwoordt. De cache moet nog op rauwe replay staan, de substream moet in een van de framings zitten die deze library vroeger foutief schreef, en de reader moet een volledig getypeerd model van elke record erin hebben gebouwd. Een cache die Excel schreef komt nooit in aanmerking, want zijn substream bevat records waar HotXLS geen model voor heeft, en opnieuw emitteren vanuit het model zou ze laten vallen
De volledigheidstest is strenger dan hij eerst lijkt. Een record die de reader alleen als opake bytes bewaarde, markeert het model als incompleet. Een gedeclareerde telling van formula records die de emitter niet kan reproduceren evenzo, want opnieuw emitteren zou een declaratie van meerdere formula records herschrijven als een declaratie van nul, en een waarde in de file die niet reproduceerbaar is komt aan een record gelijk die niet reproduceerbaar is
Bewust conservatisme loopt ook door de writer heen. Indices worden afgeklemd tot het legale bereik in plaats van als out-of-band sentinel gecodeerd, want de specificatie definieert een index in de distinct-value-reeks en niets anders, en een lege cel is zelf een waarde in die reeks. Een cache record body die het BIFF-recordplafond overschrijdt wordt helemaal niet geschreven, wat duizenden cachevelden zou vereisen en binnen de BIFF8-kolomlimiet sowieso onbereikbaar is; de fallback is dat Excel uit het source range ververst, wat gedefinieerd gedrag is in plaats van een corrupte file
Data dragen de laatste cross-record-afhankelijkheid. De serial-naar-datum-conversie hangt af van het workbook-datumsysteem, en de record-emitter kan de workbook niet zien, dus de basisdatumkeuze wordt als parameter meegegeven die standaard op het 1900-systeem staat en door het savepad op workbookniveau wordt geleverd. Onder het 1900-systeem is het serienummer direct de waarde; het 1904-systeem verschilt 1462 dagen. De bredere behandeling van datumserials staat in datumserials, het 1904-systeem en getalnotaties
Werkt u op de viewlaag in plaats van op de cachelaag, dan worden de records die de zichtbare pivot beschrijven behandeld in de BIFF8 PivotTable record set, en het gedrag aan rekenkant in calculated fields, calculated items en refresh. Alle drie de lagen worden geleverd in de HotXLS Delphi spreadsheet component, en dat is wat het mogelijk maakt om een legacy workbook te laden, te inspecteren wat zijn cache werkelijk bevat en te besluiten of herschrijven veilig is voordat u het doet