기술 문서

HotXLS로 BIFF PivotCache 레코드 프레이밍하기

BIFF PivotCache 서브스트림은 PivotTable의 캐시된 데이터셋을 그것을 표시하는 뷰와 별도로 저장하며, HotXLS는 레코드 번호를 믿는 대신 레코드 본문을 들여다보며 그 서브스트림을 읽고 씁니다. 이 구분이 이야기의 전부입니다. 같은 레코드 번호가 어떤 라이터가 파일을 만들었는지에 따라 호환되지 않는 두 본문 레이아웃을 실기 때문에, 리더는 처음 보는 레코드 본문에서 프레이밍을 결정합니다

이 계층을 만나는 순간은 PivotTable이 왕복을 넘겨야 할 때입니다. 캐시 없는 피벗 뷰는 껍데기이고, Excel은 파일을 열 때 원본 범위에서 캐시를 재구축하는데, 원본 범위가 사라졌거나 데이터가 쿼리에서 붙여넣어졌거나 워크북이 누군가 열 때 변하면 안 되는 아카이브된 결산이라면 그때까지는 괜찮은 얘기가 아닙니다

두 구조, 파일 안의 두 자리

캐시된 데이터와 캐시 정의는 워크북의 다른 부분에 살고, 둘을 뭉개는 것이 첫 번째로 바로잡아야 할 일입니다. 캐시된 레코드들은 자기 서브스트림을 이루며 [MS-XLS] §2.1.7.12는 이를 PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF로 줍니다. 없는 것에 주목하세요. 그 프로덕션 머리에 BOF가 없습니다

정의는 대신 워크북 globals에 있으며, PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE](§2.1.7.20.3)로, 서식 레코드들 뒤 BoundSheet와 Country 레코드 앞에 자리합니다. 즉 단일 캐시가 수백 개 레코드 떨어진 두 곳에서 기술되고, 둘 사이의 연결은 세 위치에서 동시에 일치해야 하는 스트림 식별자입니다

BIFF8에서 HotXLS PivotCache 프레이밍: SXStreamID를 실은 PIVOTCACHEDEFINITION은 워크북 globals에서 서식 뒤 BoundSheet 앞에 자리하고, 캐시된 레코드들은 _SX_DB_CUR 스토리지 아래 네 자리 대문자 헥스로 이름 붙은 스트림에서 BOF 없이 SXDB, SXDBEx, SXFORMULA, FDB, DBB 레코드들을 실으며, SXStreamID.idStm과 SXDB의 idstm 필드와 스트림 이름이 일치해야 한다
단일 피벗 캐시는 수백 개 레코드 떨어진 두 곳에서 기술되며, globals와 SXDB 헤더와 서브스트림 이름에서 동시에 일치해야 하는 스트림 식별자로 연결됩니다

각 캐시는 이름이 자기 식별자의 네 자리 대문자 헥스 표기인 _SX_DB_CUR 아래 스트림에 속합니다. SXStreamID.idStm, SXDB 헤더에 반복되는 idstm 필드, 그리고 그 스트림 이름이 모두 일치해야 합니다. 새 식별자를 할당할 때는 파일에서 이미 읽은 모든 번호를 먼저 예약하세요. 그렇지 않으면 새 캐시가 리더가 아직 걸어 가지 못한 오래된 캐시의 번호를 차지할 수 있습니다

사람들을 한 번 더 잡는 식별자가 있습니다. 피벗 뷰의 iCache 값은 여러분이 고를 수 있는 캐시 식별자가 아니라 글로벌 시퀀스에서 해당 SXStreamID의 0 기반 위치입니다. 쓸 때는 캐시 오브젝트에서 실제 출력 위치로 매핑해야 하고 기존 뷰들도 함께 재번호 매겨야 합니다. 그렇지 않으면 캐시 하나를 업그레이드하는 것이 조용히 어떤 뷰를 다른 캐시로 가리키게 합니다

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는 할당하지 않는 평범한 프로퍼티 쓰기이고 내부 성장 경로는 새로 추가된 행만 초기화하므로, 헤더 경로로 카운트가 설정된 캐시는 길이 0의 인덱스 그리드로 끝날 수 있습니다. RecordIndices에 대한 쓰기는 그러면 에러 없이 버려집니다. 카운트를 0으로 설정했다가 되돌리는 것이 그리드를 재건하고, 그것은 모든 필드가 추가된 뒤에 일어나야 합니다. 행 폭이 필드 수에서 나오기 때문입니다

레코드 번호가 본문 레이아웃을 말해 줄 수 없는 이유는?

레코드 번호와 본문 레이아웃이 서로 다른 시점에 바뀌었으므로 둘 사이의 매핑이 함수가 아니기 때문입니다. 레거시 집합의 한 번호는 오래된 라이터의 파일에만 등장하므로 한 방향으로는 믿을 만한 신호입니다. 다른 번호는 진짜로 모호합니다. 올바른 파일에도, 새 번호에 옛 본문 레이아웃을 쓴 중간 버전 범위의 파일에도 등장합니다

그래서 프레이밍은 본문에서, 그리고 레코드마다가 아니라 캐시 서브스트림당 한 번 결정되어야 합니다. HotXLS는 각 서브스트림의 첫 번째 SXDBB 레코드의 길이에서 방언을 래치합니다. 스펙 프레이밍에서는 하나의 SXDBB가 정확히 하나의 캐시 레코드를 담으므로 길이가 한 행 폭과 같습니다. 더 오래된 패킹 프레이밍에서는 첫 레코드가 들어가는 만큼의 행을 담으므로, 행이 둘 이상인 캐시에서는 최소 두 행 폭입니다. 두 예측이 다를 때마다 비교는 결정적입니다

HotXLS SXDBB 프레이밍 래치: 하나의 레코드 번호가 호환되지 않는 두 본문 레이아웃을 실으므로, 리더는 첫 SXDBB 레코드 길이를 행 폭과 비교한다. 한 행 폭이면 스펙 방언을, 두 행 폭 이상이면 레거시 패킹 프레이밍을 래치하고, 비기면 스펙 해석을 택하며, 방언은 레코드마다가 아니라 캐시 서브스트림당 한 번 래치된다
레코드 번호는 둘이 서로 다른 시점에 바뀌었기에 본문 레이아웃을 결정할 수 없습니다. HotXLS는 첫 SXDBB 길이에서 서브스트림당 한 번 방언을 래치하고 비기에는 스펙 해석을 택합니다

다르지 않을 때는 Excel이 쓴 파일이 중간 빌드가 쓴 파일보다 많다는 원칙에 따라 리더가 스펙 해석을 택합니다. 그 맹점은 구조적으로 좁고, 발생하더라도 파일 자체는 여전히 바이트 그대로 재생됩니다. 호출자에게 노출되는 타입화된 인덱스만 영향을 받습니다

인덱스 폭은 다른 레코드에 삽니다

SXDBB(§2.4.276)는 고유값 플래그가 설정된 캐시 필드마다 필드 순서대로 인덱스를 하나 실으며, 각 인덱스의 폭은 다른 곳에서 결정됩니다. 대응하는 SXFDB 필드 레코드(§2.4.283)가 short-items 플래그를 선언하고, 그 플래그가 인덱스가 2바이트를 차지하는지 1바이트를 차지하는지를 말합니다. 레코드 둘, 암묵적 계약 하나, 그것을 잇는 스펙의 한 문장입니다

이 커플링이 바로 직접 만든 인코딩이 틀어지는 자리입니다. 이전 HotXLS 라이터는 각 필드를 최소 비트 수로 패킹하고 행 사이를 바이트 경계로 패딩했는데, 그 자체로는 방어 가능하고 같은 라이터가 방금 SXFDB에서 선언한 폭과 정면으로 모순됩니다. 고유값 셋을 가진 필드가 한 레코드에서는 1바이트 폭으로 기술되고 다른 레코드에서는 2비트를 차지한 것이죠. 수정은 산술을 고치는 것이 아니라 폭 결정을 두 이미터가 모두 호출하는 하나의 함수로 추출하는 것이었고, 이제 두 레코드는 더 이상 어긋날 수 없습니다. 이것은 선언된 크기와 실제 본문이 헤어지는 BIFF 레코드 길이 선언 드리프트에서 기술하는 같은 부류의 결함입니다

이 레코드들을 아예 읽지 않을 때의 귀결은 과소평가하기 쉬우므로 적어 둘 가치가 있습니다. 리더가 레코드 인덱스를 건너뛰면 파일에서 로드된 모든 캐시가 모든 행의 모든 필드에 대해 인덱스 0을 보고했고, 그것은 모든 행이 모든 필드의 첫 번째 값을 가리킨다는 뜻입니다. 단지 줄어든 인트로스펙션이 아닙니다. 피벗 평가 경로와 캐시-셀 채움 경로가 둘 다 그 그리드를 소비합니다. 그리고 왕복 테스트는 이것을 감지할 수 없습니다. 아직 raw 재생 상태인 캐시는 원래 바이트에서 다시 쓰이기 때문입니다

// 출처 플래그는 무엇을 쥐고 있고 무엇이 재작성될 수 있는지 알려 줍니다
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이 그 질문에 답하는 단일 프로퍼티입니다. 캐시가 여전히 raw 재생 상태여야 하고, 서브스트림이 이 라이브러리가 과거에 잘못 썼던 프레이밍 중 하나여야 하며, 리더가 그 안의 모든 레코드에 대한 완전한 타입 모델을 만들었어야 합니다. Excel이 쓴 캐시는 절대 자격이 없습니다. 그 서브스트림은 HotXLS에 모델이 없는 레코드를 실고, 모델에서 재출력하면 그것들이 떨어져 나가기 때문입니다

완전성 검사는 처음 보이는 것보다 엄격합니다. 리더가 불투명 바이트로만 유지한 레코드는 모델을 불완전하게 표시합니다. 이미터가 재현할 수 없는 선언된 수식 레코드 개수도 그렇습니다. 재출력은 여러 수식 레코드의 선언을 없음의 선언으로 재작성하고, 재현할 수 없는 파일 속 값은 재현할 수 없는 레코드와 동등하기 때문입니다

의도된 보수주의는 라이터에도 흐릅니다. 인덱스는 out-of-band 센티널로 인코딩하는 대신 합법 범위로 클램프됩니다. 스펙이 고유값 시퀀스에 대한 인덱스만 정의하고 그 외 아무것도 정의하지 않으며, 빈 셀 자체가 그 시퀀스의 값이기 때문입니다. BIFF 레코드 상한을 넘는 캐시 레코드 본문은 아예 쓰이지 않는데, 그러려면 수천 개의 캐시 필드가 필요하고 어차피 BIFF8 열 한도 안에서는 도달할 수 없습니다. 폴백은 Excel이 원본 범위에서 새로 고침하는 것이며, 이는 손상된 파일이 아니라 정의된 동작입니다

날짜가 마지막 교차 레코드 의존성을 실고 있습니다. 시리얼-날짜 변환은 워크북 날짜 체계에 의존하는데 레코드 이미터는 워크북을 볼 수 없으므로, 기준 날짜 선택은 1900 체계를 기본으로 하는 파라미터로 전달되고 워크북 수준 저장 경로가 공급합니다. 1900 체계에서는 시리얼 번호가 그 값 그대로이고, 1904 체계는 1462일 다릅니다. 날짜 시리얼의 더 넓은 취급은 날짜 시리얼, 1904 체계, 숫자 서식에 있습니다

캐시 계층이 아니라 뷰 계층에서 작업한다면, 눈에 보이는 피벗을 기술하는 레코드들은 BIFF8 PivotTable 레코드 집합에서, 계산 쪽 동작은 계산된 필드, 계산된 항목, 새로 고침에서 다룹니다. 세 계층 모두 HotXLS Delphi 스프레드시트 컴포넌트에 실려 있으며, 이것이 레거시 워크북을 로드하고 그 캐시가 실제로 무엇을 담는지 검사하고 재작성이 안전한지 판단한 뒤에 손대는 것을 가능하게 합니다