Support moved compressed rows in SAS data files#365
Conversation
3f7d103 to
3ebcd73
Compare
Building a ReproducerI've now managed to create a generic reproducing SAS data file with a moved row for which I can post the file itself and the SAS code to produce it. The file features the compression codes The idea of the reproducer is to
With the SAS code below on a Linux x64 host with SAS 9.0401M9, I consistently see the effect that the 2000th of 3000 rows is physically moved from page 5 to 7 where the file has 8 pages in total and enumerations are starting from 1. Logically, the row remains at position 2000. The index on Hexdump InvestigationThe header and page size of the reproducer zipped in mcve_moved_rows.zip are both When trying to read the reproducer with the current version of ReadStat an error is shown for page 5 (using the enumeration starting with 1 as SAS data files use internally): The hexdump of the unsupported 24-bit subheader pointer on page 5 with compression type In the middle, we see the subheader pointer with compression type On page 7 we can see the subheader pointer with compression type Here all three pointers reference contiguous subheaders as the distance between the offets With the changes from this PR, the reproducer is being read correctly. Next stepsAdding moved rows to the write feature of ReadStat doesn't seem useful to me. So an automated test should rather rely on a SAS data file committed to the repository as test resource. The current reproducer is bit large for that with 800 KB. But I'm optimistic that a smaller reproducer can be found by tweaking parameters like the page size. The PR #366 to support soft-deleted rows also contains the suggestion to use fixed resources for tests. If we find a way to merge that PR, I can further investigate how to add a test with a reasonably sized fixed resource for this one. |
Introduction
This PR adds support for "moved" compressed rows in SAS data files.
I'm not aware of a public description of this feature but I've investigated the hex dumps of SAS files that ReadStat currently cannot read, reverse-engineered the logic, and then validated the read data against exports of the data files as produced by SAS.
I'm not aware what the exact conditions are that trigger the "moving" of rows but these conditions seem to be necessary:
The technical term "moved row" is something that I've made up on the basis of what I've observed. The naming is up to discussion but I'll drop the quotation marks in the text below.
Compression Types
The most widely known compression types that can be read in subheader pointers of SAS data files are
0x00, indicating no compression of the linked content0x01, indicating that the linked content can be skipped0x04, indicating that the linked content contains a compressed rowWith the feature of moved rows, there are three additional compression types:
0x03, indicating the logical position of a row that is actually on a different page of the data file0x06, indicating the physical position of a row that is referred to by a0x03compression type subheader pointer0x0d, indicating subheader pointers that can be skipped similarly to0x01for currently unknown reasonsA speculative interpretation of this list of compression types is that the compression type in subheader pointers is actually a bitmap with the following meanings of the bits:
0x01- any data that the subheader pointer may directly refer to shall be ignored0x02- the subheader pointer is related to rows that have been moved0x04- the subheader pointer directly refers to a compressed row0x08- unknown (but I've only observed it as part of the type0x0d, which also matches the bit0x01and can thus be ignored)Compression type
0x03The typical subheader pointer in a SAS data file contains the following pieces of information:
With compression type
0x03, the byte positions and lengths within the subheader pointer are the same but the meaning of the values is different:0x03The order of rows in a SAS data file is normally defined by the order in which a pass of the file encounters them. But when a subheader pointer with compression type
0x03is encountered, this only defines the logical position of the row in the order of encounter while the actual data is on a different (and as far as I can tell later to be encountered) page.For subheader pointers of compression type
0x03, the previous and the next pointer will usually refer to neighboring areas within the same page.Compression type
0x06The reference from a subheader pointer with compression type
0x03is always to a subheader pointer on another page that has the compression type0x06.A subheader pointer with compression type
0x06does not represent any logical position of a row in the usual order of encounter. But the compressed row that is at the phyical position that the pointer refers to can be read exactly like rows of compression type0x04.Compression type
0x0dI don't have a good explanation for this compression type. But just skipping subheaders with this type as one does with compression type
0x01leads to the correct result when comparing the exports of ReadStat with those of SAS itself.Implementation alternative
The implementation proposed in this PR respects the difference between the logical and the physical order of rows in a SAS data file, and replicates the order in which SAS itself presents the rows of a data file.
An alternative implementation that would be more efficient but would loose the faithfulness to the logical order of rows would be to
0x06the same as0x040x03and0x0djust like0x01For many use cases, this would be good enough.
Validation
The change proposed in this PR works as expected on the SAS data files that I have access to. But I can share neither the data files themselves nor the SAS code that produces them.
As stated in the introduction, I'm not aware of what the precise trigger for SAS is to move rows to another page. So I'm also unable to provide generic SAS code that would create a toy example of a SAS data file with moved rows.
I've implemented the proposed change with the goal to introduce minimal risk. It should not affect affect any SAS data file that ReadStat currently reads successfully and contains validations against all constraints that I've observed.
I'm opening this PR in the hope of either someone of the community stepping forward with supporting information on moved rows in SAS data files or a leap of faith on part of the maintainer.
If there is any kind of follow-up question on this proposed change, I'd be happy to engage.