substream PivotCache ของ BIFF เก็บชุดข้อมูลที่แคชไว้ของ PivotTable แยกออกจาก view ที่ใช้แสดงผล และ HotXLS อ่านกับเขียน substream นี้ด้วยการส่องเนื้อในระเบียน ไม่ใช่เชื่อเลขระเบียนตัวเลขเดียว ตรงนี้คือประเด็นทั้งหมดของเรื่อง: เลขระเบียนเดียวกันพาเนื้อหาในเลย์เอาต์ที่เข้ากันไม่ได้สองแบบ ขึ้นอยู่กับว่าตัวเขียนตัวไหนผลิตไฟล์นั้นขึ้น ผู้อ่านจึงตัดสินการจัดกรอบจากเนื้อระเบียนแรกที่เจอ
คุณจะเจอชั้นนี้ทันทีที่ PivotTable ต้องรอดจากการวิ่งไปกลับ view แบบ pivot ที่ไม่มีแคชติดมาคือเปลือกเปล่า และ Excel จะสร้างแคชใหม่จาก source range เวลาเปิดไฟล์ ซึ่งไม่มีปัญหา จวบจนวันที่ source range หายไปแล้ว ข้อมูลถูกวางมาจาก query หรือเวิร์กบุ๊กเป็นของปิดงบที่เก็บถาวรซึ่งห้ามเปลี่ยนแปลงเด็ดขาดเมื่อใครสักคนเปิดมัน
โครงสร้างสองชุด อยู่สองที่ในไฟล์
ข้อมูลที่แคชไว้กับนิยามของแคชอาศัยอยู่คนละส่วนของเวิร์กบุ๊ก และการแยกสองอย่างนี้ออกจากกันคือสิ่งแรกที่ต้องทำถูก ระเบียนที่แคชไว้รวมกันเป็น substream ของตัวเอง ตามที่ [MS-XLS] §2.1.7.12 ให้ไว้ว่า PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF ลองสังเกตว่าอะไรหายไป: ไม่มี BOF นำหน้า production นี้เลย
ส่วนนิยามไปจอดอยู่ใน workbook globals แทน ในรูป PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3) วางไว้หลังระเบียนการจัดรูปแบบและก่อนระเบียน BoundSheet กับ Country แคชเดียวจึงถูกบรรยายไว้สองที่ที่ห่างกันหลายร้อยระเบียน และเส้นเชื่อมระหว่างสองที่คือ stream identifier ที่ต้องตรงกันในสามจุดพร้อมกัน
แคชแต่ละชุดต้องอยู่ในสตรีมใต้ _SX_DB_CUR ที่ชื่อสตรีมสะกดด้วยเลขฐานสิบหกตัวพิมพ์ใหญ่สี่หลักของ identifier ของมัน SXStreamID.idStm, ฟิลด์ idstm ที่ซ้ำอยู่ในหัว SXDB และชื่อสตรีมนั้นต้องตรงกันหมด เวลาจัดสรร identifier ใหม่ ให้กันเลขทุกตัวที่อ่านมาแล้วจากไฟล์ไว้ก่อน ไม่อย่างนั้นแคชใหม่อาจไปอ้างเลขที่เป็นของแคชเก่าที่ผู้อ่านยังเดินไปไม่ถึง
มี identifier อีกตัวที่คอยเล่นงานคน ค่า iCache ใน view แบบ pivot คือตำแหน่งเริ่มนับศูนย์ของ SXStreamID ที่สัมพันธ์กันในลำดับ global ไม่ใช่ cache identifier ที่คุณจะเลือกเองได้ ตอนเขียนต้อง map จากออบเจกต์แคชไปยังตำแหน่งเอาต์พุตจริง และ view เดิมทั้งหลายต้องถูกเรียกเลขใหม่ตามไปด้วย ไม่อย่างนั้นการอัปเกรดแคชชุดเดียวจะเงียบ ๆ ชี้ view ไปที่แคชอีกชุดหนึ่งแทน
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 เป็นแค่การเขียน property ธรรมดาที่ไม่จัดสรรอะไร และเส้นทางการขยายภายในจะ initialize เฉพาะแถวที่เพิ่มใหม่ แคชที่ตัวนับถูกตั้งผ่านเส้นทางหัวไฟล์จึงอาจจบด้วยกริดดัชนีความยาวศูนย์ การเขียนลง RecordIndices ก็จะถูกทิ้งไปเงียบ ๆ โดยไม่มี error การตั้งตัวนับเป็นศูนย์แล้วตั้งกลับจะสร้างกริดขึ้นมาใหม่ และต้องทำหลังเพิ่มฟิลด์ครบทุกฟิลด์แล้ว เพราะความกว้างแถวมาจากจำนวนฟิลด์
ทำไมเลขระเบียนถึงบอกเลย์เอาต์เนื้อหาให้คุณไม่ได้?
เพราะเลขระเบียนกับเลย์เอาต์เนื้อหาเปลี่ยนไม่พร้อมกัน การ map ระหว่างสองสิ่งจึงไม่ใช่ฟังก์ชัน เลขหนึ่งตัวในชุดเลกาซีมีแค่โผล่ในไฟล์จากตัวเขียนรุ่นเก่าเท่านั้น ทำให้มันเป็นสัญญาณที่เชื่อถือได้ทางเดียว อีกเลขหนึ่งคือความกำกวมของจริง: มันโผล่ทั้งในไฟล์ที่ถูกต้องและในช่วงเวอร์ชันระหว่างทางหลายรุ่นที่ใช้เลขใหม่แต่ยังเป็นเลย์เอาต์เนื้อหาแบบเก่า
การจัดกรอบจึงต้องตัดสินจากเนื้อหา และตัดสินรอบเดียวต่อหนึ่ง substream ของแคช ไม่ใช่รายระเบียน HotXLS ล็อกแบบภาษาจากความยาวของระเบียน SXDBB ตัวแรกในแต่ละ substream ในการจัดกรอบตามสเปก SXDBB หนึ่งตัวบรรจุแคชระเบียนเดียวพอดี ความยาวของมันจึงเท่ากับความกว้างหนึ่งแถว ส่วนการจัดกรอบแบบ packed รุ่นเก่า ระเบียนแรกจะบรรจุแถวมากเท่าที่ใส่ได้ แคชไหนที่มีมากกว่าหนึ่งแถวมันจึงยาวอย่างน้อยสองความกว้างแถว การเทียบนี้ชี้ขาดทุกครั้งที่คำทำนายสองฝั่งไม่เท่ากัน
เมื่อคำทำนายทั้งสองเท่ากัน ผู้อ่านจะเลือกการอ่านตามสเปก ด้วยหลักการง่าย ๆ ว่าไฟล์ที่ Excel เขียนมีมากกว่าไฟล์จากบิลด์ระหว่างทางอยู่หลายเท่า จุดอับนี้แคบตั้งแต่การออกแบบ และแม้มันจะเกิดขึ้นจริง ตัวไฟล์ก็ยังเล่นซ้ำได้เหมือนเดิมทุกไบต์ สิ่งที่ได้รับผลกระทบมีแค่ดัชนีแบบมีไทป์ที่เปิดให้ผู้เรียกใช้เห็นเท่านั้น
ความกว้างดัชนีอยู่ในอีกระเบียนหนึ่ง
SXDBB (§2.4.276) พาดัชนีหนึ่งตัวต่อฟิลด์แคชที่ติดแฟล็ก distinct-value เรียงตามลำดับฟิลด์ ส่วนความกว้างของแต่ละดัชนีถูกตัดสินที่อื่น: ระเบียนฟิลด์ SXFDB ที่สัมพันธ์กัน (§2.4.283) ประกาศแฟล็ก short-items และแฟล็กนั้นบอกว่าดัชนีกินพื้นที่สองไบต์หรือหนึ่งไบต์ สองระเบียน สัญญาโดยนัยหนึ่งฉบับ และประโยคเดียวในสเปกที่เชื่อมทั้งคู่เข้าด้วยกัน
การยึดโยงตรงนี้แหละคือจุดที่การเข้ารหัสแบบคิดเองเพี้ยน ตัวเขียน HotXLS รุ่นก่อนหน้าเคยอัดแต่ละฟิลด์ลงบิตให้น้อยที่สุด โดยเติมเต็มให้จบที่ขอบไบต์ระหว่างแถว ซึ่งฟังขึ้นในตัวเองก็ป้องกันตัวได้ แต่ขัดแย้งโดยตรงกับความกว้างที่ตัวเขียนตัวเดิมเพิ่งประกาศไว้ใน SXFDB ฟิลด์ที่มีค่าไม่ซ้ำสามค่าถูกบรรยายในระเบียนหนึ่งว่ากว้างหนึ่งไบต์ แล้วกลับกินแค่สองบิตในอีกระเบียนหนึ่ง ทางแก้ไม่ใช่การไปแก้เลขคณิต แต่เป็นการดึงการตัดสินความกว้างออกมารวมไว้ในฟังก์ชันเดียวที่ตัวปล่อยทั้งสองเรียกใช้ สองระเบียนจึงไม่มีทางคลาดเคลื่อนแยกจากกันได้อีก นี่คือ defect ตระกูลเดียวกับที่อธิบายไว้ในบทความ BIFF record length declaration drift ที่ขนาดที่ประกาศไว้กับเนื้อหาจริงแยกทางกัน
ผลที่ตามมาจากการไม่อ่านระเบียนพวกนี้เลยคุ้มค่าที่จะพูดให้ชัด เพราะประเมินต่ำกันได้ง่าย เมื่อผู้อ่านข้ามดัชนีระเบียนไป แคชทุกชุดที่โหลดจากไฟล์จะรายงานดัชนีศูนย์สำหรับทุกฟิลด์ของทุกแถว แปลว่าทุกแถวชี้ไปที่ค่าแรกของทุกฟิลด์ มันไม่ใช่แค่ความสามารถส่องข้างในที่ลดลง: เส้นทางประเมิน pivot และเส้นทางเทแคชลงเซลล์ต่างกินกริดนี้ทั้งคู่ และเทสต์แบบวิ่งไปกลับก็จับไม่ได้ เพราะแคชที่ยังอยู่บน raw replay จะถูกเขียนกลับจากไบต์เดิมของมัน
// แฟล็ก 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 คือ property เดียวที่ตอบคำถามนี้ แคชต้องยังอยู่บน raw replay, substream ต้องอยู่ในการจัดกรอบแบบที่ไลบรารีนี้เคยเขียนผิดมาก่อน และผู้อ่านต้องสร้างโมเดลแบบมีไทป์ครบทุกระเบียนในนั้นแล้ว แคชที่ Excel เขียนจะไม่ผ่านเกณฑ์เด็ดขาด เพราะ substream ของมันพาระเบียนที่ HotXLS ไม่มีโมเดลให้ และการปล่อยออกมาใหม่จากโมเดลจะทำระเบียนพวกนั้นหล่นหาย
การทดสอบความครบถ้วนเข้มกว่าที่คิดตอนแรก ระเบียนที่ผู้อ่านเก็บไว้แค่ในรูปไบต์ที่อ่านไม่ออกจะทำเครื่องหมายว่าโมเดลไม่สมบูรณ์ทันที จำนวนระเบียนสูตรที่ประกาศไว้ซึ่งตัวปล่อยทำซ้ำไม่ได้ก็เช่นกัน เพราะการปล่อยใหม่จะเขียนคำประกาศว่ามีระเบียนสูตรหลายตัวทับด้วยคำประกาศว่าไม่มีเลย และค่าในไฟล์ที่ทำซ้ำไม่ได้มีความหมายเท่ากับระเบียนที่ทำซ้ำไม่ได้
ความอนุรักษ์นิยมแบบตั้งใจไหลผ่านตัวเขียนไปจนถึงปลาย ดัชนีถูก clamp ให้อยู่ในช่วงที่อนุญาต แทนการเข้ารหัสเป็น sentinel นอกแถว เพราะสเปกนิยามดัชนีไว้ว่าชี้เข้าลำดับค่าไม่ซ้ำแค่นั้น และเซลล์ว่างเองก็เป็นค่าหนึ่งในลำดับนั้น เนื้อระเบียนแคชที่ล้นเพดานระเบียน BIFF จะไม่ถูกเขียนออกไปเลย ซึ่งต้องใช้ฟิลด์แคชเป็นพันฟิลด์และเข้าไม่ถึงอยู่ดีภายใต้ขีดจำกัดคอลัมน์ของ BIFF8 เส้นทาง fallback คือ Excel จะรีเฟรชจาก source range ซึ่งเป็นพฤติกรรมที่นิยามไว้ ไม่ใช่ไฟล์พัง
วันที่พาดีเพนเดนซีข้ามระเบียนตัวสุดท้ายมาให้ การแปลง serial เป็นวันที่ขึ้นกับระบบวันที่ของเวิร์กบุ๊ก และตัวปล่อยระเบียนมองไม่เห็นเวิร์กบุ๊ก ตัวเลือกวันฐานจึงถูกส่งเข้ามาเป็นพารามิเตอร์ที่ค่าเริ่มต้นเป็นระบบ 1900 และถูกป้อนจากเส้นทางบันทึกระดับเวิร์กบุ๊ก ภายใต้ระบบ 1900 เลข serial คือค่านั้นตรง ๆ ระบบ 1904 ต่างออกไป 1462 วัน ส่วนการจัดการเลข serial วันที่แบบกว้างกว่านี้มีอยู่ในบทความเลข serial วันที่ ระบบ 1904 และ number format
ถ้าคุณทำงานอยู่ที่ชั้น view มากกว่าชั้นแคช ระเบียนที่บรรยาย pivot ที่มองเห็นได้มีเล่าไว้ในบทความชุดระเบียน PivotTable ของ BIFF8 และพฤติกรรมฝั่งการคำนวณอยู่ในบทความ calculated fields, calculated items และ refresh ทั้งสามชั้นมาพร้อมกันในHotXLS Delphi spreadsheet component ซึ่งเป็นสิ่งที่ทำให้โหลดเวิร์กบุ๊กรุ่นเก่า ส่องดูว่าแคชข้างในมีอะไรกันแน่ และตัดสินใจได้ก่อนลงมือว่าการเขียนมันใหม่ปลอดภัยหรือไม่