Техническа статия

PivotCache substream и SXDB записи в HotXLS (BIFF8)

BIFF PivotCache substream съхранява кеширания dataset на PivotTable отделно от изгледа, който го показва, а HotXLS чете и пише този substream като оглежда телата на записите, вместо да вярва на номерата им. Това разграничение е цялата история: един и същ номер на запис носи два несъвместими оформления на тялото в зависимост от writer-а, произвел файла, така че читателят отсъжда framing-а от първото тяло на запис, което види

С този слой се сблъсквате в момента, в който PivotTable трябва да оцелее на кръг-обратно. Pivot изглед без кеша си е черупка, а Excel ще преизгради кеша от source диапазона, когато отвори файла – добре чак до момента, в който source диапазонът го няма, данните са поставени от query, или workbook-ът е архивирано тримесечно затваряне, което не бива да се мени, когато някой го отвори

Две структури, две места във файла

Кешираните данни и дефиницията на кеша живеят в различни части на workbook-а, а смесването им е първото нещо, което трябва да се изясни. Кешираните записи образуват собствен substream, даден в [MS-XLS] §2.1.7.12 като PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Забележете какво липсва: няма BOF в главата на тази продукция

Дефиницията седи вместо това в workbook globals, като PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), позиционирана след форматните записи и преди BoundSheet и Country записите. Така един кеш е описан на две места, раздалечени със стотици записи, а връзката между тях е stream идентификатор, който трябва да съгласува на три места наведнъж

HotXLS PivotCache framing в BIFF8: PIVOTCACHEDEFINITION със своя SXStreamID седи в workbook globals след форматите и преди BoundSheet, докато кешираните записи живеят в stream под storage-а _SX_DB_CUR, кръстен с четирицифрен главен hex, държащ SXDB, SXDBEx, SXFORMULA, FDB и DBB записи без BOF, а SXStreamID.idStm, полето idstm на SXDB и името на stream-а трябва да съгласуват
Един pivot кеш е описан на две места, раздалечени със стотици записи, съединени от stream идентификатор, който трябва да съгласува в globals, в SXDB главата и в името на substream-а наведнъж

Всеки кеш принадлежи в stream под _SX_DB_CUR, чието име е четирицифреното главно шестнадесетично изписване на идентификатора му. SXStreamID.idStm, полето idstm, повторено в главата на SXDB, и това име на stream трябва всички да съвпадат. Когато разпределяте нов идентификатор, резервирайте първо всеки номер, вече прочетен от файла, иначе нов кеш може да претендира номер, принадлежащ на по-стар кеш, до който читателят още не е стигнал

Още един идентификатор хваща хората. Стойността iCache в pivot изглед е нула-базираната позиция на съответния SXStreamID в глобалната последователност, не идентификатор на кеш, който вие избирате. При запис трябва да бъде съпоставен от cache обекта към неговата реална изходна позиция, а съществуващите изгледи да бъдат преномерирани заедно с него, иначе ъпгрейд на един кеш тихо насочва изглед към друг

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;     // вярвай на кешираните записи
    Cache.SaveData := True;

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

    Cache.SetRecordCount(0);          // изчисти, после оразмери решетката от записи
    Cache.SetRecordCount(500);
    Book.StorePivotCaches;
  finally
    Book.Free;
  end;
end;

Двойният SetRecordCount не е суеверие. RecordCount е обикновено писане на свойство, което не алокира, а вътрешният път на растеж инициализира само новодобавените редове, така че кеш, чийто брой е зададен през header пътя, може да се окаже с решетка от индекси с нулева дължина. Записванията в RecordIndices тогава се изхвърлят без грешка. Задаването на броя на нула и обратно възстановява решетката, а това трябва да стане, след като всяко поле е добавено, защото ширината на ред идва от броя полета

Защо номерът на запис не може да ви каже оформлението на тялото?

Защото номерата на записите и оформленията на телата са се менени в различни моменти, така че съпоставката между тях не е функция. Един номер в легаси набора се появява само във файлове от по-стари writer-и, което го прави надежден сигнал в една посока. Друг номер е истински двусмислен: появява се и в коректни файлове, и в диапазон междинни версии, ползвали новия номер със старото оформление на тялото

Framing-ът затова трябва да бъде отсъден от тялото, и то по веднъж на cache substream, а не на запис. HotXLS закачва диалекта от дължината на първия SXDBB запис във всеки substream. В спецификационния framing един SXDBB държи точно един cache запис, така че дължината му равнява една широчина на ред. В по-стария пакетиран framing първият запис държи колкото редове поберат, така че за всеки кеш с повече от един ред е поне две широчини на ред. Сравнението е решаващо винаги, когато двете предсказания се различават

HotXLS SXDBB framing закачане: един номер на запис носи два несъвместими оформления на тялото, така че читателят сравнява дължината на първия SXDBB запис със ширината на ред, една широчина на ред закача спецификационния диалект, докато две или повече широчини закача легаси пакетирания framing, равенствата вземат спецификационното четене, а диалектът се закача по веднъж на cache substream, не на запис
Номерата на записите не могат да отсъдят оформлението на тялото, защото двете са се менени в различни моменти, затова HotXLS закача диалекта по веднъж на substream от дължината на първия SXDBB и взема спецификационното четене при равенство

Когато не се различават, читателят взема спецификационното четене, по принципа, че файловете, писани от Excel, числено надвишават файловете от междинен build. Това слепо петно е тясно по конструкция и, когато все пак се случи, файлът сам продължава да се възпроизвежда байт по байт. Засегнати са само типизираните индекси, изложени на викащите

Ширината на индекса живее в друг запис

SXDBB (§2.4.276) носи по един индекс на cache поле, чийто distinct-value флаг е зададен, в реда на полетата, а широчината на всеки индекс се решава другаде: съответният SXFDB полеви запис (§2.4.283) обявява флаг за кратки елементи, а този флаг казва дали индексът заема два байта или един. Два записа, един подразбиран договор и едно-единствено изречение в спецификацията, свързващо ги

Тази свързаност е точно мястото, където домашно израснало кодиране греши. По-ранен HotXLS writer пакетиране всяко поле в минималния брой битове, подплатяйки до байтова граница между редовете – защитимо в изолация и директно противоречащо на широчината, която същият writer току-що беше обявил в SXFDB. Поле с три различни стойности бе описано като еднобайтова широчина в един запис и заемаше два бита в другия. Поправката не беше да се поправи аритметиката, а да се извади решението за широчина в една функция, която и двата emitter-а викат, така че двата записа вече не могат да се разминат. Това е същият клас дефекти, описан в BIFF drift на декларации за дължина на записи, където обявен размер и реално тяло се разделят

Последицата от изобщо нечетене на тези записи си заслужава да бъде изречена, защото е лесно да бъде подценена. Когато читателят прескачаше полевите индекси, всеки кеш, зареден от файл, докладваше индекс нула за всяко поле на всеки ред, тоест всеки ред сочеше първата стойност на всяко поле. Това не е само съкратена интроспекция: пътят на pivot оценяване и пътят на пълнене от кеш към клетки и двата поглъщат тази решетка. А кръг-обратен тест не може да го засече, защото кеш, още на сурово възпроизвеждане, се записва обратно от оригиналните си байтове

// Provenance флаговете ви казват какво държите и какво може да бъде пренаписано
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);
  // Преизлъчването е беззагубно само когато всеки запис има модел тук
  if Cache.CanUpgradeFraming then
    Writeln('safe to rewrite with the current emitters');
end;

Кога пренаписването на кеш е беззагубно?

Само когато три условия важат заедно, а CanUpgradeFraming е единственото свойство, отговарящо на въпроса. Кешът трябва още да е на сурово възпроизвеждане, substream-ът трябва да е в едно от оформленията, които тази библиотека е писала погрешно преди, а читателят трябва да е построил пълен типизиран модел на всеки запис в него. Кеш, писан от Excel, никога не квалифицира, защото неговият substream носи записи, за които HotXLS няма модел, а преизлъчването от модела би ги изпуснало

Тестът за пълнота е по-строг, отколкото изглежда първо. Запис, който читателят е пазил само като непрозрачни байтове, маркира модела непълен. Също и обявен брой formula записи, който emitter-ът не може да възпроизведе, защото преизлъчването би пренаписало декларация за няколко formula записа като декларация за нула, а стойност във файла, която не може да бъде възпроизведена, е еквивалент на запис, който не може

Нарочен консерватизъм минава и през writer-а. Индексите се захващат в легалния диапазон, вместо да се кодират като out-of-band сентинел, защото спецификацията дефинира индекс в последователността на различните стойности и нищо друго, а празна клетка сама е стойност в тази последователност. Тяло на cache запис, надхвърлящо BIFF тавана за запис, изобщо не се записва, което би искало хиляди cache полета и е недостижимо в рамките на BIFF8 лимита за колони все пак; fallback-ът е, че Excel refresh-ва от source диапазона, което е дефинирано поведение, а не повреден файл

Датите носят последната между-записна зависимост. Конверсията серал-към-дата зависи от системата за дати на workbook-а, а полевият emitter не може да види workbook-а, така че изборът на базова дата се подава като параметър, подразбиращ се до 1900 системата и доставян от save пътя на ниво workbook. При 1900 системата сералният номер е стойността директно; 1904 системата се различава с 1462 дни. По-широкото третиране на датни серали е в датни серали, 1904 системата и числови формати

Ако работите на изгледния слой, а не на кешовия, записите, описващи видимия pivot, са обхванати в BIFF8 PivotTable набор от записи, а поведението от изчислителната страна – в изчислени полета, изчислени елементи и refresh. И трите слоя ship-ват с HotXLS Delphi spreadsheet компонента, което прави възможно да заредите легаси workbook, да огледате какво реално съдържа кешът ѝ и да решите дали пренаписването е безопасно, преди да го свършите