Articolo tecnico

Framing dei record PivotCache BIFF con HotXLS in Delphi

Il substream PivotCache BIFF memorizza il dataset in cache di una PivotTable separatamente dalla vista che lo visualizza, e HotXLS legge e scrive quel substream ispezionando i corpi dei record invece di fidarsi dei numeri di record. Quella distinzione è tutta la storia: lo stesso numero di record porta due layout di corpo incompatibili a seconda di quale writer ha prodotto il file, così il reader decide il framing dal primo corpo di record che vede

Incontri questo layer nel momento in cui una PivotTable deve sopravvivere a un round-trip. Una vista pivot senza la sua cache è un guscio, ed Excel ricostruirà la cache dall'intervallo sorgente quando apre il file, il che va bene fino al punto in cui l'intervallo sorgente non c'è più, i dati erano stati incollati da una query, o il workbook è una chiusura archiviata che non deve cambiare quando qualcuno la apre

Due strutture, due posti nel file

I dati in cache e la definizione della cache vivono in parti diverse del workbook, e confonderle è la prima cosa da evitare. I record in cache formano un substream proprio, dato in [MS-XLS] §2.1.7.12 come PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Nota cosa manca: non c'è nessun BOF in testa a quella produzione

La definizione sta invece nei workbook globals, come PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), posizionata dopo i record di formattazione e prima dei record BoundSheet e Country. Quindi una singola cache è descritta in due posti distanti centinaia di record, e il collegamento tra loro è un identificatore di stream che deve combaciare in tre punti contemporaneamente

Framing PivotCache di HotXLS in BIFF8: la PIVOTCACHEDEFINITION con il suo SXStreamID sta nei workbook globals dopo la formattazione e prima di BoundSheet, mentre i record in cache vivono in uno stream sotto lo storage _SX_DB_CUR nominato con hex maiuscolo a quattro cifre che contiene record SXDB, SXDBEx, SXFORMULA, FDB e DBB senza BOF, e SXStreamID.idStm, il campo idstm di SXDB e il nome dello stream devono combaciare
Una singola cache pivot è descritta in due posti distanti centinaia di record, uniti da un identificatore di stream che deve combaciare contemporaneamente nei globals, nell'intestazione SXDB e nel nome del substream

Ogni cache sta in uno stream sotto _SX_DB_CUR il cui nome è la scrittura esadecimale maiuscola a quattro cifre del suo identificatore. SXStreamID.idStm, il campo idstm ripetuto nell'intestazione SXDB, e quel nome di stream devono tutti combaciare. Quando allochi un nuovo identificatore, riserva prima ogni numero già letto dal file, o una nuova cache può rivendicare un numero che appartiene a una cache più vecchia verso cui il reader non è ancora arrivato

Un altro identificatore becca la gente. Il valore iCache in una vista pivot è la posizione a base zero del corrispondente SXStreamID nella sequenza globale, non un identificatore di cache che puoi scegliere. In scrittura va mappato dall'oggetto cache alla sua posizione di output effettiva, e le viste esistenti vanno rinumerate insieme, o aggiornare una cache punta in silenzio una vista verso una diversa

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;     // fida dei record in cache
    Cache.SaveData := True;

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

    Cache.SetRecordCount(0);          // azzera, poi dimensiona la griglia dei record
    Cache.SetRecordCount(500);
    Book.StorePivotCaches;
  finally
    Book.Free;
  end;
end;

Il doppio SetRecordCount non è superstizione. RecordCount è una semplice scrittura di proprietà che non alloca, e il percorso di crescita interno inizializza solo le righe appena aggiunte, quindi una cache il cui conteggio è stato impostato tramite il percorso header può ritrovarsi con una griglia di indici a lunghezza zero. Le scritture su RecordIndices vengono allora scartate senza errore. Impostare il conteggio a zero e ritorno ristabilisce la griglia, e deve avvenire dopo che ogni campo è stato aggiunto, perché la larghezza di riga viene dal conteggio dei campi

Perché un numero di record non può dirti il layout del corpo?

Perché i numeri di record e i layout di corpo sono cambiati in momenti diversi, quindi la mappatura tra loro non è una funzione. Un numero dell'insieme legacy appare solo in file di writer più vecchi, il che lo rende un segnale affidabile in una direzione. Un altro numero è genuinamente ambiguo: appare sia in file corretti sia in una gamma di versioni intermedie che usavano il numero nuovo col layout di corpo vecchio

Il framing quindi va deciso dal corpo, e una volta per substream di cache invece che per record. HotXLS aggancia il dialetto dalla lunghezza del primo record SXDBB di ogni substream. Nel framing della specifica, un SXDBB contiene esattamente un record di cache, quindi la sua lunghezza eguaglia una larghezza di riga. Nel framing packed più vecchio, il primo record contiene quante righe ci stanno, quindi per ogni cache con più di una riga vale almeno due larghezze di riga. Il confronto è decisivo ogni volta che le due predizioni differiscono

Aggancio del framing SXDBB di HotXLS: un numero di record porta due layout di corpo incompatibili, quindi il reader confronta la lunghezza del primo record SXDBB con la larghezza di riga, una larghezza di riga aggancia il dialetto della specifica mentre due o più larghezze di riga agganciano il framing legacy packed, i pareggi prendono la lettura della specifica, e il dialetto si aggancia una volta per substream di cache, non per record
I numeri di record non possono decidere il layout del corpo perché i due sono cambiati in momenti diversi, quindi HotXLS aggancia il dialetto una volta per substream dalla prima lunghezza SXDBB e prende la lettura della specifica in caso di pareggio

Quando non differiscono, il reader prende la lettura della specifica, sul principio che i file scritti da Excel superano in numero i file scritti da una build intermedia. Quello punto cieco è stretto per costruzione e, quando si verifica, il file in sé viene comunque riprodotto byte per byte. Solo gli indici tipizzati esposti ai chiamanti ne risentono

La larghezza dell'indice sta in un record diverso

SXDBB (§2.4.276) porta un indice per campo di cache il cui flag di valori distinti è impostato, in ordine di campo, e la larghezza di ogni indice è decisa altrove: il corrispondente record di campo SXFDB (§2.4.283) dichiara un flag di short-items, e quel flag dice se l'indice occupa due byte o uno. Due record, un contratto implicito, e una singola frase nella specifica che li collega

Quel accoppiamento è esattamente dove un encoding fatto in casa va storto. Un writer HotXLS precedente impacchettava ogni campo nel numero minimo di bit, riempiendo fino al confine di byte tra le righe, il che è difendibile in isolamento e contraddice direttamente la larghezza che lo stesso writer aveva appena dichiarato in SXFDB. Un campo con tre valori distinti era descritto largo un byte in un record e occupava due bit nell'altro. La correzione non è stata correggere l'aritmetica ma estrarre la decisione di larghezza in una funzione che entrambi gli emettitori chiamano, così i due record non possono più divergere. È la stessa classe di difetto descritta in deriva delle dichiarazioni di lunghezza dei record BIFF, dove una dimensione dichiarata e un corpo effettivo si separano

La conseguenza di non leggere affatto questi record vale la pena esplicitarla, perché è facile sottovalutarla. Quando il reader saltava gli indici dei record, ogni cache caricata da un file riportava indice zero per ogni campo di ogni riga, il che significa che ogni riga puntava al primo valore di ogni campo. Non è solo introspezione ridotta: il percorso di valutazione pivot e il percorso di riempimento da cache a celle consumano entrambi quella griglia. E un test di round-trip non può rilevarlo, perché una cache ancora su replay grezzo viene riscritta dai suoi byte originali

// I flag di provenienza dicono cosa stai tenendo in mano e cosa si può riscrivere
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);
  // Il re-emit è senza perdita solo quando ogni record ha un modello qui
  if Cache.CanUpgradeFraming then
    Writeln('safe to rewrite with the current emitters');
end;

Quando la riscrittura di una cache è senza perdita?

Solo quando tre condizioni reggono insieme, e CanUpgradeFraming è la singola proprietà che risponde alla domanda. La cache deve essere ancora su replay grezzo, il substream deve essere in uno dei framing che questa libreria in passato scriveva in modo errato, e il reader deve aver costruito un modello tipizzato completo di ogni record al suo interno. Una cache scritta da Excel non si qualifica mai, perché il suo substream porta record per i quali HotXLS non ha modello, e riemettere dal modello li farebbe cadere

Il test di completezza è più severo di quanto appaia all'inizio. Un record che il reader ha conservato solo come byte opachi marca il modello incompleto. Lo fa anche un conteggio dichiarato di record di formula che l'emettitore non sa riprodurre, perché riemettere riscriverebbe una dichiarazione di più record di formula come dichiarazione di nessuno, e un valore nel file che non si può riprodurre equivale a un record che non si può riprodurre

Il conservatorismo deliberato attraversa anche il writer. Gli indici vengono serrati nell'intervallo legale invece che codificati come un sentinel fuori banda, perché la specifica definisce un indice nella sequenza dei valori distinti e nient'altro, e una cella vuota è essa stessa un valore in quella sequenza. Un corpo di record di cache che supera il tetto dei record BIFF non viene scritto affatto, il che richiederebbe migliaia di campi cache ed è irraggiungibile comunque dentro il limite di colonna BIFF8; il fallback è che Excel rinfresca dall'intervallo sorgente, che è comportamento definito e non un file corrotto

Le date portano l'ultima dipendenza tra record. La conversione da seriale a data dipende dal sistema di date del workbook, e l'emettitore di record non può vedere il workbook, quindi la scelta della data base viene passata come parametro che ha un default sul sistema 1900 e viene fornita dal percorso di salvataggio a livello workbook. Sotto il sistema 1900 il numero seriale è direttamente il valore; il sistema 1904 differisce di 1462 giorni. Il trattamento più ampio dei seriali di data è in seriali di data, sistema 1904 e formati numerici

Se lavori al layer della vista invece che al layer della cache, i record che descrivono la pivot visibile sono trattati in l'insieme di record PivotTable BIFF8, e il comportamento lato calcolo in campi calcolati, elementi calcolati e refresh. Tutti e tre i layer vengono spediti nel HotXLS Delphi spreadsheet component, che è ciò che rende possibile caricare un workbook legacy, ispezionare cosa contiene davvero la sua cache, e decidere se riscriverla è sicuro prima di farlo