مقاله فنی

قاب‌بندی رکوردهای PivotCache یک BIFF با HotXLS

زیرجریان PivotCache یک BIFF دیتاست کش‌شده یک PivotTable را جدا از نماییشی که آن را نمایش می‌دهد ذخیره می‌کند، و HotXLS آن زیرجریان را با بازرسی بدنه رکوردها می‌خواند و می‌نویسد نه با اعتماد به شماره رکوردها. همین تمایز کل ماجراست: یک شماره رکورد واحد بسته به اینکه کدام writer فایل را تولید کرده دو چیدمان بدنه ناسازگار حمل می‌کند، پس reader قاب‌بندی را از اولین بدنه رکوردی که می‌بیند تصمیم می‌گیرد

با این لایه همان لحظه‌ای روبه‌رو می‌شوید که یک PivotTable باید از یک رفت‌وبرگشت سالم بیرون بیاید. یک نمای pivot بدون کش خودش فقط یک پوسته است، و Excel کش را از بازه مبدأ بازسازی می‌کند وقتی فایل را باز می‌کند، که تا جایی خوب است که بازه مبدأ ناپدید شده باشد، داده از یک query پیست شده باشد، یا workbook یک close بایگانی‌شده باشد که نباید وقتی کسی بازش می‌کند تغییر کند

دو ساختار، دو جای فایل

داده کش‌شده و تعریف کش در بخش‌های متفاوتی از workbook زندگی می‌کنند، و قاطی‌نکردنشان اولین چیزی است که باید درست بگیرید. رکوردهای کش‌شده زیرجریان خودشان را می‌سازند، داده‌شده در [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 است که باید هم‌زمان در سه محل موافق باشد

قاب‌بندی PivotCache در HotXLS در BIFF8: تعریف PIVOTCACHEDEFINITION با SXStreamID خودش در workbook globals بعد از formatting و قبل از BoundSheet می‌نشیند، در حالی که رکوردهای کش‌شده در استریمی زیر storage با نام _SX_DB_CUR زندگی می‌کنند که با hex چهاررقعی حروف بزرگ نام‌گذاری شده و رکوردهای SXDB، SXDBEx، SXFORMULA، FDB و DBB را بدون BOF حمل می‌کند، و SXStreamID.idStm، فیلد idstm در SXDB و نام stream باید موافق باشند
یک pivot cache واحد در دو جای صدها رکورد فاصله‌دار توصیف می‌شود، با پیوندی از جنس شناسه stream که باید هم‌زمان در globals، هدر SXDB و نام زیرجریان موافق باشد

هر کش در استریمی زیر _SX_DB_CUR جا می‌گیرد که نامش املای هگزادسیمال چهاررقعی حروف بزرگ شناسه‌اش است. SXStreamID.idStm، فیلد idstm تکرارشده در هدر SXDB، و آن نام stream همه باید بخوانند. وقتی شناسه جدید رزرو می‌کنید اول هر عددی که از قبل از فایل خوانده شده را کنار بگذارید، وگرنه یک کش جدید می‌تواند عددی را تصاحب کند که متعلق به کش قدیمی‌تری است که reader هنوز به آن نرسیده

یک شناسه دیگر هم آدم‌ها را می‌گیرد. مقدار iCache در یک نمای pivot موقعیت صفر-مبنای SXStreamID متناظر در توالی global است، نه شناسه کشی که بتوانید انتخابش کنید. موقع نوشتن باید از آبجکت کش به موقعیت خروجی واقعی‌اش مپ شود، و نمای‌های موجود هم باید همراهش شماره‌گذاری دوباره شوند، وگرنه ارتقای یک کش بی‌سروصدا یک نما را به کش دیگری اشاره می‌دهد

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 یک نوشتن پراپرتی ساده است که چیزی allocate نمی‌کند، و مسیر رشد داخلی فقط ردیف‌های تازه‌اضافه‌شده را مقداردهی اولیه می‌کند، پس کشی که شمارشش از مسیر هدر ست شده می‌تواند با گرید ایندکس طول-صفر تمام شود. نوشتن‌ها روی RecordIndices بعد بدون خطا دور ریخته می‌شوند. ست‌کردن شمارش به صفر و برگرداندنش گرید را دوباره برپا می‌کند، و باید بعد از اضافه‌شدن تک‌تک فیلدها اتفاق بیفتد، چون عرض ردیف از تعداد فیلدها می‌آید

چرا یک شماره رکورد نمی‌تواند چیدمان بدنه را به شما بگوید؟

چون شماره‌های رکورد و چیدمان‌های بدنه در زمان‌های متفاوتی عوض شده‌اند، پس مپینگ بینشان یک تابع نیست. یک عدد از مجموعه قدیمی فقط در فایل‌های writerهای قدیمی‌تر ظاهر می‌شود، که آن را در یک جهت به سیگنال قابل‌اعتماد تبدیل می‌کند. عدد دیگری واقعاً مبهم است: هم در فایل‌های درست ظاهر می‌شود هم در بازه‌ای از نسخه‌های میانی که عدد جدید را با چیدمان بدنه قدیمی به کار می‌بردند

پس قاب‌بندی باید از بدنه تصمیم گرفته شود، و یک بار به‌ازای هر زیرجریان کش نه به‌ازای هر رکورد. HotXLS گویش را از طول اولین رکورد SXDBB در هر زیرجریان قفل می‌کند. در قاب‌بندی مشخصات، یک SXDBB دقیقاً یک رکورد کش را حمل می‌کند، پس طولش برابر یک عرض ردیف است. در قاب‌بندی پک‌شده قدیمی‌تر، رکورد اول هر تعداد ردیفی که جا شود را حمل می‌کند، پس برای هر کشی با بیش از یک ردیف دست‌کم دو عرض ردیف است. مقایسه هر وقت دو پیش‌بینی فرق کنند تعیین‌کننده است

قفل گویش قاب‌بندی SXDBB در HotXLS: یک شماره رکورد دو چیدمان بدنه ناسازگار را حمل می‌کند، پس reader طول اولین رکورد SXDBB را با عرض ردیف مقایسه می‌کند، یک عرض ردیف گویش مشخصات را قفل می‌کند در حالی که دو عرض ردیف یا بیشتر قاب‌بندی پک‌شده قدیمی را قفل می‌کند، مساوی‌ها برداشت مشخصات را می‌گیرند، و گویش یک بار به‌ازای هر زیرجریان کش قفل می‌شود نه به‌ازای هر رکورد
شماره‌های رکورد نمی‌توانند چیدمان بدنه را تصمیم بگیرند چون این دو در زمان‌های متفاوتی عوض شده‌اند، پس HotXLS گویش را یک بار به‌ازای هر زیرجریان از طول اولین SXDBB قفل می‌کند و روی مساوی برداشت مشخصات را برمی‌دارد

وقتی فرق نمی‌کنند، reader برداشت مشخصات را برمی‌دارد، با این اصل که فایل‌های نوشته‌شده با Excel از فایل‌های نوشته‌شده با یک بیلد میانی بیشترند. این نقطه کور از نظر ساختاری باریک است و وقتی واقعاً پیش می‌آید خود فایل همچنان بایت‌به‌بایت بازپخش می‌شود. فقط ایندکس‌های تایپ‌شده‌ای که به callerها داده می‌شوند تحت تأثیرند

عرض ایندکس در یک رکورد دیگر زندگی می‌کند

SXDBB (§2.4.276) به‌ازای هر فیلد کشی که پرچم distinct-value‌اش ست شده یک ایندکس حمل می‌کند، به ترتیب فیلدها، و عرض هر ایندکس جای دیگر تصمیم گرفته می‌شود: رکورد فیلد SXFDB متناظر (§2.4.283) یک پرچم short-items اعلان می‌کند، و آن پرچم می‌گوید ایندکس دو بایت اشغال می‌کند یا یک بایت. دو رکورد، یک قرارداد ضمنی، و یک جمله در مشخصات که آنها را به هم وصل می‌کند

همین جفت‌شدگی دقیقاً جایی است که یک encoding خانگی اشتباه می‌شود. یک writer قدیمی‌تر HotXLS هر فیلد را در حداقل تعداد بیت پک می‌کرد، با پد کردن تا مرز بایت بین ردیف‌ها، که در انزوا قابل دفاع است و مستقیماً با عرضی که همان writer همین الان در SXFDB اعلان کرده بود در تضاد است. فیلدی با سه مقدار متمایز در یک رکورد به‌عنوان یک بایت عرض توصیف می‌شد و در رکورد دیگر دو بیت اشغال می‌کرد. درست‌کردن، اصلاح محاسبات نبود؛ بیرون‌کشیدن تصمیم عرض به یک تابع بود که هر دو emit‌کننده صدا می‌زنند، تا دو رکورد دیگر نتوانند از هم فاصله بگیرند. این همان رده نقصی است که در انحراف اعلان طول رکورد BIFF توصیف شده، جایی که یک سایز اعلان‌شده و بدنه واقعی از هم جدا می‌شوند

پیامد نخواندن اصلاً این رکوردها ارزش روایت‌کردن دارد، چون دست‌کم‌گرفتنش آسان است. وقتی reader ایندکس‌های رکورد را جا می‌انداخت، هر کشی که از فایل لود می‌شد برای هر فیلد هر ردیف ایندکس صفر گزارش می‌کرد، یعنی هر ردیف به اولین مقدار هر فیلد اشاره می‌کرد. این صرفاً کاهش بینش نیست: مسیر ارزیابی 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 تک پراپرتی‌ای است که به این سؤال جواب می‌دهد. کش باید هنوز روی بازپخش خام باشد، زیرجریان باید در یکی از قاب‌بندی‌هایی باشد که این کتابخانه قبلاً غلط نوشته، و reader باید یک مدل تایپ‌شده کامل از هر رکورد درونش ساخته باشد. کشی که Excel نوشته هیچ‌وقت واجد شرایط نیست، چون زیرجریانش رکوردهایی را حمل می‌کند که HotXLS برایشان مدلی ندارد، و بازسری از روی مدل آنها را دور می‌انداخت

تست کامل‌بودن از ظاهر اولش سخت‌گیرتر است. رکوردی که reader فقط به‌شکل بایت‌های مات نگه داشته مدل را ناقص علامت می‌زند. تعداد اعلان‌شده رکوردهای فرمولی هم که emit‌کننده نتواند بازتولیدش کند همین‌طور، چون بازسری یک اعلانِ چند رکورد فرمولی را به اعلان هیچ بازنویسی می‌کرد، و مقداری در فایل که نتوان بازتولیدش کرد معادل رکوردی است که نتوان بازتولیدش کرد

محافظه‌کاری عمدی در سراسر writer هم جاری است. ایندکس‌ها به‌جای اینکه به‌شکل sentinel برون-از-باند encode شوند به بازه قانونی clamp می‌شوند، چون مشخصات یک ایندکس به توالی مقادیر متمایز تعریف می‌کند و هیچ چیز دیگر، و سلول خالی خودش یک مقدار در آن توالی است. بدنه رکورد کشی که از سقف رکورد BIFF بگذرد اصلاً نوشته نمی‌شود، که به هزاران فیلد کش نیاز دارد و درون محدودیت ستون BIFF8 به هر حال دست‌نیافتنی است؛ fallback این است که Excel از بازه مبدأ refresh می‌کند، که رفتار تعریف‌شده است نه یک فایل خراب

تاریخ‌ها آخرین وابستگی بین-رکوردی را حمل می‌کنند. تبدیل سریال-به-تاریخ به سیستم تاریخ workbook وابسته است، و emit‌کننده رکورد نمی‌تواند workbook را ببیند، پس انتخاب تاریخ مبدأ به‌شکل پارامتری پاس داده می‌شود که پیش‌فرضش سیستم 1900 است و مسیر ذخیره سطح-workbook آن را می‌دهد. زیر سیستم 1900 عدد سریال مستقیماً مقدار است؛ سیستم 1904 به‌اندازه 1462 روز فرق دارد. بررسی وسیع‌تر سریال‌های تاریخ در سریال‌های تاریخ، سیستم 1904 و فرمت‌های عددی است

اگر در لایه نما کار می‌کنید نه لایه کش، رکوردهایی که pivot قابل‌مشاهده را توصیف می‌کنند در مجموعه رکوردهای PivotTable یک BIFF8 پوشش داده شده‌اند، و رفتار سمت محاسبه در فیلدهای محاسبه‌شده، آیتم‌های محاسبه‌شده و refresh. هر سه لایه در HotXLS Delphi spreadsheet component عرضه می‌شوند، که همان چیزی است که ممکن می‌کند یک workbook قدیمی را لود کنید، ببینید کشش واقعاً چه دارد، و قبل از بازنویسی تصمیم بگیرید که آیا امن است