Skip to content
AI360XpertBeta

Data Versioning

Code pins do not reproduce runs. Hash the rows, store the bytes once, and keep the small pointer beside the commit that used them.

Git holds a small pointer hash while cloud storage holds the bytes, so checking out a commit restores the exact 3 rows and a 1 row change makes a new version.
Git holds a small pointer hash while cloud storage holds the bytes, so checking out a commit restores the exact 3 rows and a 1 row change makes a new version.

Why Does This Exist?

Code alone does not reproduce a model. A model is code plus rows, and rows move. Warehouse tables accept late events, corrections, and backfills by design, so the same query next week returns different bytes.

A team that versions code perfectly but overwrites training files in place loses the past. A score drop cannot be blamed on code or data because both changed at once. Audits fail for the same reason: nobody can name the exact rows behind a shipped model. This page covers row identity and restore. Run identity lives in experiment tracking, run repeatability lives in reproducibility, and field meaning lives in ML metadata.

Basics first: a hash is a short fingerprint of bytes where any byte change remakes the fingerprint. A pointer file is a tiny text file with that fingerprint plus a path. Git stores the pointer well and stores multi-gigabyte tables badly, so the split is the whole design.

Think of It Like This

A card catalog beside heavy shelves

A library holds heavy books on shelves and small cards in a catalog drawer. Each card names one edition and its shelf spot. A revised book keeps the old edition on its shelf and gains a new card for the new edition.

Checking out means copying the card text, walking to the shelf, and pulling the named edition. A ten-year-old card still pulls the ten-year-old book because the shelf never overwrites.

That maps directly. Cards are pointer files in Git, shelves are cloud buckets keyed by hash, and editions are immutable snapshots. Three rows under hash 04cb and four rows under hash 8c40 sit side by side like two editions.

Where it stops: a library card never rots, while a bucket object can be deleted by a lifecycle rule. The pointer stays valid only when retention protects the bytes it names.

How It Actually Works

Split the job: Git tracks history, remote storage tracks bytes, and the hash joins them.

Hash the bytes

Read each file, fingerprint the content, and combine file prints in sorted order so export order cannot change the result. One changed pixel, one corrected cell, or one added row remakes the top hash. Store objects under the hash as the name, so equal bytes share one object and different bytes never collide in practice.

Store once remotely

Upload the bytes to object storage under the hash name. Never overwrite. A 3-row snapshot stays at 04cb forever. A 4-row snapshot lands beside it at 8c40. Old models keep reading old bytes while new models read new ones.

Commit the pointer with code

Write the hash, row count, and source query into a small pointer file and commit it with the model code. Cloning gives history without weight. A checkout command reads the pointer at that commit, fetches the matching bytes, and places them locally. Code state and data state move in lockstep through one link.

Worked example: 3 rows become 4

Start with 3 rows: 1,42.0, 2,17.5, and 3,88.0. Hash each row, sort the prints, and hash the join, keeping 4 hex chars as the short pointer. That pointer reads 04cb in the diagram.

Add one late row: 4,9.5. The set is now 4 rows. The same recipe now reads 8c40. The pointer changed completely from one added row:

4−3=14 - 3 = 1

One row added, one new object stored, zero old bytes touched. Restoring the v1 commit fetches 04cb with 3 rows. Restoring v2 fetches 8c40 with 4 rows. Order does not matter because leaves sort first, so a reshuffled export still matches 04cb while any content change breaks it.

Lineage is the other half

Identity says which rows. Lineage says where they came from: source tables, transform code, and job run. Without it a bad source cannot be traced to the models it fed, so the retrain list cannot be built on a bad day.

Code

The point is the fingerprint: hash rows in sorted leaf order so content decides the pointer, with the same 3 versus 4 rows as the worked example.

import hashlib

def short_hash(rows: list[str]) -> str:    leaves = sorted(hashlib.sha256(r.encode()).hexdigest() for r in rows)    return hashlib.sha256("".join(leaves).encode()).hexdigest()[:4]

rows_v1: list[str] = ["1,42.0", "2,17.5", "3,88.0"]rows_v2: list[str] = ["1,42.0", "2,17.5", "3,88.0", "4,9.5"]
print(len(rows_v1))  # -> 3print(len(rows_v2))  # -> 4print(short_hash(rows_v1))  # -> 04cbprint(short_hash(rows_v2))  # -> 8c40print(short_hash(rows_v1) == short_hash(rows_v1[::-1]))  # -> Trueprint(short_hash(rows_v1) == short_hash(rows_v2))  # -> False

Three rows against four, order flipped still matches, content changed never matches. The prints 04cb and 8c40 are the real outputs for these rows. Log the 4-char pointer beside every metric and same-data questions become string compares. Swap the slice to 12 chars for real use so collisions stay out of reach.

Watch Out For

Committing heavy data straight to Git

Raw CSVs and binaries go in with git add. Diffs bloat, clones slow to a crawl, and history becomes unusable.

Symptom: a repo that grows hundreds of megabytes per dataset update with ever-slower fetches. Fix: keep bytes in object storage under content hashes and commit only pointer files. Treat a growing .git folder as the alarm, and log the dataset hash in experiment tracking so runs stay comparable.

Versioning raw inputs but not the built table

Raw dumps are pinned while the cleaned training table is rebuilt on the fly. A later preprocessing fix silently remakes every past model.

Symptom: reruns with the same raw hash and code still drift because the middle step moved. Fix: materialize and hash the built table too, or pin the transform code plus its inputs and log both hashes. Intermediate artifacts are data and need pointers like any other.

The Quick Version

  • A model is code plus rows, so pinning code alone reproduces nothing when tables move.
  • Hash bytes in sorted leaf order so the pointer depends on content, not on export order.
  • Three rows point at 04cb while four point at 8c40, with one added row making a whole new object.
  • Store bytes under the hash remotely and never overwrite, so old commits keep reading old rows.
  • Commit the small pointer with code so checkout restores the exact rows that trained the model.
  • Lineage names the sources and jobs, so a bad upstream table maps to the models that need retraining.