RussellSpitzer commented on PR #16425:
URL: https://github.com/apache/iceberg/pull/16425#issuecomment-6779153428

   From the sync today, Ryan wanted to loosen the rules from the ones I posted 
earlier. Please take a look, I spent a lot of time on this and Promise my human 
eyes and brain thought a lot about this although I did use LLM assistance on 
the wording and images. 
   
   The goal is to allow more general column replacements by making writers 
avoid ambiguous situations. Unlike my earlier rules, this requires writers to 
be more aware of exactly which values they are overwriting, but it allows more 
generic replacement.
   
   The key rule we are trying to respect is:
   
   "Listing a nested field does not change whether its parent struct is null. 
To change that, the column file must list the parent struct."
   
   Going through the rules, they change a bit (see 3):
   
   1. A column file must not list a field nested in a list or map.
   
   2. A column file stores the values of the fields listed in its `field_ids`. 
Readers must ignore fields in a column file that are not listed in its 
`field_ids`.
   
   3. An added column file must list:
      - each field whose value changes.
      - each struct that changes between null and non-null.
      - each field directly nested in a struct that has changed from null to 
non-null.
   
      A field nested in a struct that is null after the update does not need to 
be listed.
   
   4. When a writer adds a column file, the writer must remove each listed 
field from the column files of the previous entry. The writer must remove a 
column file that has no remaining fields.
   
   5. The reader must read a field from the column file that lists it. If no 
column file lists the field, the reader must read it from the base file, using 
Column Projection for a data file.
   
   6. If a struct is null in a row, each field nested in that struct is null in 
that row.
   
   **Why the rules are required**
   
   ```
   2: customer  optional struct
       3: name     string
       4: address  optional struct
           5: street  string
           6: city    string
   8: tags      list<9: string>
   ```
   
   Column files are written as CF-# [field IDs listed].
   
   **Rule 1 - No fields inside lists or maps**
   
   <img width="1648" height="524" alt="image" 
src="https://github.com/user-attachments/assets/80234361-5aa5-4de9-bd7c-d06efd4e186d";
 />
   
   
   `SET tags[0] = 'x'` - a writer cannot write CF-1 [9] with only element 0.
   
   We don't want any rules about how to combine lists or maps across files, so 
the writer must write CF-1 [8] with the entire contents of the list. The same 
applies to maps
   
   **Rule 2 - Ignore unlisted fields**
   
   This lets us replace fields without rewriting the column files we already 
wrote. 
   
   For example:
   
   If we have CF-1 [3, 4, 5, 6] and only want to update field 3, we can write 
CF-2 [3] and change the entry for CF-1 to [4, 5, 6].
    CF-1 still physically contains field 3, and readers must ignore it.
    
    
   <img width="1648" height="904" alt="image" 
src="https://github.com/user-attachments/assets/414828e2-c8ab-44dd-a929-4114ffab5b4a";
 />
   
   
   It also covers fields a file format needs in order to store a listed field. 
For example, CF-1 [5] has to store `customer` and `address` on the path to 
`street`. Readers must not take the null state of `customer` or `address` from 
CF-1.
   
   **Rule 3 - No implicit change to a parent struct's null state**
   
   Note that this rule is explicitly *row based* if no rows violate the rule 
then nothing has to be done other than listing the field being modified. 
   
   Start with `customer` null in row 0.
   
   User calls `SET name = 'andrew'`
   
   Creating CF-1 [3] is *not allowed*.
   
   <img width="1648" height="482" alt="image" 
src="https://github.com/user-attachments/assets/1360446a-e18b-4d11-be31-36aa3dd27e2a";
 />
   
   The reader takes `customer` (field 2) from the base file, where it is null, 
so rule 6 hides the new `name`. The update changed whether `customer` is null, 
so `customer` must be listed.
   
   Instead the writer must create:
   
   CF-1 [2, 3, 4]
   
   <img width="1648" height="776" alt="image" 
src="https://github.com/user-attachments/assets/4de33962-605c-43b3-99c5-dea25b766aee";
 />
   
   
   `name` and `address` are listed because they are directly nested in a struct 
that changed from null to non-null. Here is why that is needed:
   
   Start with `address = {street: '1 Elm', city: 'Austin'}` in the base file.
   
   `SET address = null` - CF-1 [4]. `street` and `city` do not need to be 
listed, because their parent is null after the update. Rule 6 hides their old 
values.
   
   Then `SET street = '2 Oak'` - `address` becomes non-null again. CF-2 [4, 5] 
is *not allowed*: `city` is no longer hidden, so the reader would return 
`'Austin'` from the base file instead of null. 
   
   <img width="1648" height="904" alt="image" 
src="https://github.com/user-attachments/assets/3b2a64a6-579c-47aa-845b-00d7fdfd1d30";
 />
   
   The writer must create CF-2 [4, 5, 6] with `city = null`.
   
   <img width="1648" height="776" alt="image" 
src="https://github.com/user-attachments/assets/2bcc1ca6-e793-4dc8-8392-4f8258b930fc";
 />
   
   
   **Rule 4 - Remove listed fields from older column files**
   
   Continuing the example, after CF-2 [4, 5, 6] is written, field 4 is removed 
from CF-1. CF-1 has no fields left, so it is removed from the entry.
   
   <img width="1648" height="658" alt="image" 
src="https://github.com/user-attachments/assets/79831e33-7524-4516-8f2a-67b73596226b";
 />
   
   Without this, `address` would be listed in both CF-1 and CF-2, and the 
reader would have no way to pick one.
   
   <img width="1648" height="648" alt="image" 
src="https://github.com/user-attachments/assets/e59c3275-c810-4ead-9f13-f466f0a801a9";
 />
   
   **Rule 5 - Read each field from the column file that lists it**
   
   With CF-1 [3], the reader takes `name` from CF-1 and everything else from 
the base file, including the null state of `customer`. The reader never needs 
to look at history. Each field has exactly one source.
   
   **Rule 6 - A null struct makes every field nested in it null**
   
   Start with `address` null in row 0 in the base file. Then `SET street = '1 
Elm'` for row 1 writes CF-1 [5]. For row 0, CF-1 may store any value for 
`street` (the spec only says writers *should* store null). The reader must 
still return null, because `address` is null in that row.
   
   <img width="1648" height="714" alt="image" 
src="https://github.com/user-attachments/assets/76ae4788-3015-4896-bd78-8e2e6b9e5dda";
 />
   
   Writers cannot cover every case on their own. If `zip` is added to `address` 
with `initial-default = '00000'` while `address` is null in some rows, no file 
has `zip`, and Column Projection returns `'00000'`. Only the reader's null 
check returns the correct null.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to