Tehnički članak

Okviranje BIFF PivotCache zapisa uz HotXLS u Delphi-ju

BIFF PivotCache substream čuva keširani skup podataka PivotTable-a odvojeno od prikaza koji ga prikazuje, i HotXLS taj substream čita i piše ispitivanjem tela zapisa, a ne verujući brojevima zapisa. Ta razlika je cela priča: isti broj zapisa nosi dva neskladna rasporeda tela zavisno od toga koji ga je pisac proizveo, pa čitač odlučuje okvir iz prvog tela zapisa koje vidi

Ovaj sloj sretnete u trenutku kad PivotTable mora da preživi povratni krug. Pivot prikaz bez svog keša je ljuska, i Excel će keš ponovo izgraditi iz izvornog opsega kad otvori datoteku, što je sasvim u redu sve do trenutka kad izvorni opseg nestane, podaci su nalepljeni iz upita, ili je radna sveska arhiviran zatvor koji ne sme da se promeni kad ga neko otvori

Dve strukture, dva mesta u datoteci

Keširani podaci i definicija keša žive u različitim delovima radne sveske, i njihovo pomešanje je prvo što treba ispravno razumeti. Keširani zapisi formiraju sopstveni substream, dat u [MS-XLS] §2.1.7.12 kao PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Primetite šta izostaje: nema BOF-a na čelu te produkcije

Definicija sedi u workbook globals umesto toga, kao PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), pozicionirana posle zapisa formatiranja a pre BoundSheet i Country zapisa. Dakle, jedan keš je opisan na dva mesta udaljena stotinama zapisa, i veza između njih je identifikator toka koji se mora poklapati na tri mesta istovremeno

HotXLS PivotCache okviranje u BIFF8: PIVOTCACHEDEFINITION sa svojim SXStreamID sedi u workbook globals posle formatiranja a pre BoundSheet, dok keširani zapisi žive u toku pod _SX_DB_CUR skladištem imenovanom četvorocifrenim velikim heksadekadskim brojem koje drži SXDB, SXDBEx, SXFORMULA, FDB i DBB zapise bez BOF-a, i SXStreamID.idStm, SXDB idstm polje i ime toka moraju da se poklapaju
Jedan pivot keš je opisan na dva mesta udaljena stotinama zapisa, spojena identifikatorom toka koji se mora poklapati u globals-u, SXDB zaglavlju i imenu substream-a istovremeno

Svaki keš pripada toku pod _SX_DB_CUR čije je ime četvorocifreno veliko heksadekadsko spelovanje njegovog identifikatora. SXStreamID.idStm, idstm polje ponovljeno u SXDB zaglavlju, i to ime toka moraju se sva poklapati. Kad dodeljujete novi identifikator, prvo rezervišite svaki broj već pročitan iz datoteke, ili novi keš može da zatraži broj koji pripada starijem kešu do koga čitač još nije došao

Još jedan identifikator ljude uhvati. Vrednost iCache u pivot prikazu je pozicija odgovarajućeg SXStreamID u globalnom nizu sa bazom nula, a ne identifikator keša koji birate. Pri upisu mora se mapirati iz objekta keša na njegovu stvarnu izlaznu poziciju, i postojeći prikazi moraju se renumerisati uz njega, ili nadogradnja jednog keša tiho usmeri prikaz na drugi

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;     // veruj keširanim zapisima
    Cache.SaveData := True;

    Field := Cache.AddField('Region', xlpcftString);
    V.ValueType := xlpcftString;
    V.StrValue := 'North';
    Field.FindOrAddItem(V);

    Cache.SetRecordCount(0);          // očisti, pa dimenzioniši mrežu zapisa
    Cache.SetRecordCount(500);
    Book.StorePivotCaches;
  finally
    Book.Free;
  end;
end;

Dvostruki SetRecordCount nije praznoverje. RecordCount je običan upis svojstva koji ne alocira, i unutrašnja putanja rasta inicijalizuje samo novododate redove, pa keš čiji je broj postavljen kroz putanju zaglavlja može završiti sa mrežom indeksa nulte dužine. Upisi u RecordIndices se tada odbacuju bez greške. Postavljanje broja na nulu i nazad uspostavlja mrežu, i to se mora desiti posle dodavanja svakog polja, jer širina reda dolazi iz broja polja

Zašto broj zapisa ne može da vam kaže raspored tela?

Zato što su se brojevi zapisa i rasporedi tela menjali u različito vreme, pa mapiranje između njih nije funkcija. Jedan broj u legacy skupu pojavljuje se samo u datotekama starijih pisaca, što ga čini pouzdanim signalom u jednom smeru. Drugi broj je stvarno dvosmislen: pojavljuje se i u ispravnim datotekama i u opsegu međuverzija koje su koristile novi broj sa starim rasporedom tela

Okviranje se zato mora odlučiti iz tela, i jednom po cache substream-u, a ne po zapisu. HotXLS učvršćuje dijalekt iz dužine prvog SXDBB zapisa u svakom substream-u. U specifikacijskom okviranju jedan SXDBB drži tačno jedan zapis keša, pa mu je dužina jednaka jednoj širini reda. U starijem pakovanom okviranju prvi zapis drži koliko redova stane, pa je za svaki keš sa više od jednog reda najmanje dve širine reda. Poređenje je presudno kad god se ta dva predviđanja razlikuju

HotXLS SXDBB okviranje kvačalo: jedan broj zapisa nosi dva neskladna rasporeda tela, pa čitač poredi dužinu prvog SXDBB zapisa sa širinom reda, jedna širina reda učvršćuje specifikacijski dijalekt dok dve ili više širina reda učvršćuje legacy pakovano okviranje, izjednačeni rezultat uzima specifikacijsko tumačenje, i dijalekt se učvršćuje jednom po cache substream-u, ne po zapisu
Brojevi zapisa ne mogu da odluče raspored tela jer su se dva menjala u različito vreme, pa HotXLS učvršćuje dijalekt jednom po substream-u iz dužine prvog SXDBB i uzima specifikacijsko tumačenje pri izjednačenju

Kad se ne razlikuju, čitač uzima specifikacijsko tumačenje, po principu da datoteke napisane Excel-om brojčano nadmašuju datoteke napisane međuverzijom. Ta slepa mrlja je uska po konstrukciji i, kad se ipak pojavi, sama datoteka se i dalje ponovo reprodukuje bajt po bajt. Pogođeni su samo tipizirani indeksi izloženi pozivaocima

Širina indeksa živi u drugom zapisu

SXDBB (§2.4.276) nosi po jedan indeks za svako cache polje čija je distinct-value zastavica postavljena, u redosledu polja, a širina svakog indeksa odlučuje se drugde: odgovarajući SXFDB zapis polja (§2.4.283) deklariše short-items zastavicu, i ta zastavica kaže da li indeks zauzima dva bajta ili jedan. Dva zapisa, jedan podrazumevani ugovor, i jedna rečenica u specifikaciji koja ih povezuje

To sprezanje je baš mesto gde domaće enkodiranje pogreši. Raniji HotXLS pisac pakovao je svako polje u minimalan broj bitova, popunjavajući do bajt granice između redova, što je branjivo izolovano i direktno protivreči širini koju je isti pisac upravo deklarisao u SXFDB. Polje sa tri različite vrednosti opisano je kao široko jedan bajt u jednom zapisu i zauzimalo je dva bita u drugom. Popravka nije bila da se aritmetika ispravi nego da se odluka o širini izdvoji u jednu funkciju koju oba emittera zovu, pa ta dva zapisa više ne mogu da razbeže. To je ista klasa defekta opisana u članku o driftu deklaracije dužine BIFF zapisa, gde se deklarisana veličina i stvarno telo razidu

Posledica toga da se ovi zapisi uopšte ne čitaju vredi ispisati, jer se lako podceni. Kad je čitač preskakao indekse zapisa, svaki keš učitan iz datoteke prijavljivao je indeks nulu za svako polje svakog reda, što znači da je svaki red pokazivao na prvu vrednost svakog polja. To nije samo umanjena introspekcija: i pivot putanja vrednovanja i putanja punjenja ćelija iz keša troše tu mrežu. I round-trip test to ne može otkriti, jer se keš još na sirovoj reprodukciji upisuje nazad iz svojih originalnih bajtova

// Provenance zastavice vam kažu šta držite u rukama i šta sme da se prepiše
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);
  // Ponovno emitovanje je bez gubitka tek kad svaki zapis ima model ovde
  if Cache.CanUpgradeFraming then
    Writeln('safe to rewrite with the current emitters');
end;

Kada je prepisivanje keša bez gubitka?

Tek kad tri uslova stoje zajedno, i CanUpgradeFraming je jedino svojstvo koje na pitanje odgovara. Keš mora i dalje biti na sirovoj reprodukciji, substream mora biti u jednom od okviranja koje je biblioteka ranije pogrešno pisala, i čitač je morao sagraditi kompletan tipiziran model svakog zapisa u njemu. Keš koji je napisao Excel nikada ne kvalifikuje, jer njegov substream nosi zapise za koje HotXLS nema model, a ponovno emitovanje iz modela bi ih odbacilo

Test potpunosti je stroži nego što deluje na prvi pogled. Zapis koji je čitač čuvao samo kao neprozirne bajtove označava model nepotpunim. Isto čini i deklarisano brojanje zapisa formula koje emitter ne može reprodukovati, jer bi ponovno emitovanje deklaraciju nekoliko zapisa formula prepisalo u deklaraciju nijednog, a vrednost u datoteci koja se ne može reprodukovati jednaka je zapisu koji se ne može reprodukovati

Namerni konzervativizam prolazi i kroz pisca. Indeksi se stežu u legalni opseg umesto da se kodiraju kao out-of-band sentinela, jer specifikacija definiše indeks u niz različitih vrednosti i ništa drugo, i prazna ćelija je sama vrednost u tom nizu. Telo zapisa keša koje premašuje BIFF plafon zapisa ne upisuje se uopšte, što bi tražilo hiljade cache polja i ionako je nedostižno unutar BIFF8 ograničenja kolona; fallback je da Excel osveži iz izvornog opsega, što je definisano ponašanje, a ne pokvarena datoteka

Datumi nose poslednju zavisnost između zapisa. Konverzija serijskog broja u datum zavisi od datumskog sistema radne sveske, i emitter zapisa ne može videti svesku, pa se izbor baznog datuma prosleđuje kao parametar koji po defaultu uzima 1900 sistem i snabdeva ga putanja čuvanja na nivou sveske. Pod 1900 sistemom serijski broj je vrednost direktno; 1904 sistem odstupa 1462 dana. Šire rukovanje datumskim serijskim brojevima je u članku o datumskim serijskim brojevima, 1904 sistemu i formatima brojeva

Ako radite na sloju prikaza, a ne na sloju keša, zapisi koji opisuju vidljivi pivot pokriveni su u članku o BIFF8 PivotTable skupu zapisa, a ponašanje na strani računanja u članku o izračunatim poljima, izračunatim stavkama i osvežavanju. Sva tri sloja stižu u HotXLS Delphi spreadsheet komponenti, što je ono što omogućava da učitate legacy radnu svesku, pregledate šta njen keš stvarno sadrži i odlučite da li je bezbedno njeno prepisivanje pre nego što to uradite