Skip to content
About
Main content

Reader-side default rewrites history

Measure your machineNeeds Python

Problem

When a field is added with a default, the default can live in the reader (old records without the field are read as if they had it) or in the writer (the value is stored with each new record). A is applied every time the data is read, so changing it changes the meaning of every old record, without touching the data. A records what was true when the record was written, and changing it affects only new records.

Create a table of 1,000 events with no delivery_priority field. Apply the reader schema {"delivery_priority": "STANDARD"} and count by value. Change the reader default to "EXPRESS" without touching the data and count again. Then do it with a writer-side default (store the value in the data). Write down which of the three states (, null, defaulted) each version can distinguish.

The question. Does changing the default change the counts of existing data, and which states can each approach tell apart?


Predict

Changing a reader-side default from STANDARD to EXPRESS: how many of 1,000 existing events change category?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    reader-side-default-rewrites-history.py43 linesDownload
    View the code
    reader-side-default-rewrites-history.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """A reader-side default rewrites history.
    
    1,000 stored events with no delivery_priority field, counted by value under a
    reader schema whose default is STANDARD, then EXPRESS, without touching the
    data. Then the same with the default written into the data at write time.
    Also checks which of the three states (absent, null, defaulted) each approach
    can still tell apart.
    """
    import argparse, json, random
    from collections import Counter
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--events", type=int, default=1000, help="Events.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    stored_old = [{"id": i} for i in range(args.events)]                     # written before the field existed
    def read(stored, default):
        return Counter(e.get("delivery_priority", default) for e in stored)  # the reader fills in the default
    def write(default): return [{"id": i, "delivery_priority": default} for i in range(args.events)]   # the writer stores it
    rows = []
    for d in ("STANDARD", "EXPRESS"):
        c = read(stored_old, d); rows.append({"approach": "reader-side default", "default_now": d, "STANDARD": c.get("STANDARD", 0), "EXPRESS": c.get("EXPRESS", 0)})
    stored_new = write("STANDARD")                                           # the default as it was at write time
    for d in ("STANDARD", "EXPRESS"):
        c = read(stored_new, d); rows.append({"approach": "writer-side default", "default_now": d, "STANDARD": c.get("STANDARD", 0), "EXPRESS": c.get("EXPRESS", 0)})
    mixed = [{"id": 1}, {"id": 2, "delivery_priority": None}, {"id": 3, "delivery_priority": "STANDARD"}]
    states = [{"approach": "reader-side default", "absent_vs_defaulted": "no", "null_vs_defaulted": "yes",
               "seen": ", ".join(str(e.get("delivery_priority", "STANDARD")) for e in mixed)},
              {"approach": "writer-side default", "absent_vs_defaulted": "only if absent rows predate the field", "null_vs_defaulted": "yes",
               "seen": ", ".join(str(e.get("delivery_priority", "<absent>")) for e in mixed)}]
    for r in rows: print(f"{r['approach']:<20} default {r['default_now']:<9} STANDARD {r['STANDARD']:>5}  EXPRESS {r['EXPRESS']:>5}")
    for s in states: print(f"{s['approach']:<20} absent / null / defaulted reads as: {s['seen']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"counts": rows, "states": states}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run reader-side-default-rewrites-history.py
    uv run reader-side-default-rewrites-history.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--events1000 Events.

Our run

1,000 existing events

1,000 existing events
DefaultDefault value nowSTANDARDEXPRESS
reader-side defaultSTANDARD1,0000
reader-side defaultEXPRESS01,000
writer-side defaultSTANDARD1,0000
writer-side defaultEXPRESS1,0000

Absent, null and defaulted records, as read

Absent, null and defaulted records, as read
DefaultRead as
reader-side defaultSTANDARD, None, STANDARD
writer-side default<absent>, None, STANDARD
Reference runApple M2 · 8 cores · 8 GB
Output · uv run reader-side-default-rewrites-history.py
reader-side default  default STANDARD  STANDARD  1000  EXPRESS     0
reader-side default  default EXPRESS   STANDARD     0  EXPRESS  1000
writer-side default  default STANDARD  STANDARD  1000  EXPRESS     0
writer-side default  default EXPRESS   STANDARD  1000  EXPRESS     0
reader-side default  absent / null / defaulted reads as: STANDARD, None, STANDARD
writer-side default  absent / null / defaulted reads as: <absent>, None, STANDARD
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

With the reader-side default, changing it moved all 1,000 events from STANDARD to EXPRESS without any data changing. With the writer-side default, the counts stayed at 1,000 STANDARD. Read through the reader-side default, an absent, a null and a defaulted record came out as STANDARD, None, STANDARD: absent and defaulted are indistinguishable. With writer-side defaults: <absent>, None, STANDARD.


Read it

A reader-side default is a statement about the past made in the present. That is acceptable for a value that genuinely applies to all history and dangerous for a business assumption that can change. If the default might ever change, write it into the data, so that history records what was assumed when it was written.