Skip to content

Commit 786144e

Browse files
committed
feat(seed): derive primary keys from the slug instead of a counter
The dump exposes `id`, and an autoincrement counter renumbered every row after an inserted record, so a regenerated dump rewrote pages whose data had not changed. That is what made TechAPI #180 ~1M files: GitHub could render neither its diff nor its merge (502), and the dump PR had to be merged blind. Ids are now blake2b(table:slug) truncated to 48 bits — deterministic, independent of insertion order, and inside the JSON-safe integer range. A collision rehashes with a suffix rather than falling back to a counter. Refs #1
1 parent 6a34dd0 commit 786144e

2 files changed

Lines changed: 72 additions & 11 deletions

File tree

‎app/seed.py‎

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import hashlib
1819
import json
1920
from datetime import date
2021
from pathlib import Path
@@ -56,13 +57,40 @@ def _load_dir(subdir: Path) -> list[dict[str, Any]]:
5657
return items
5758

5859

60+
# Primary keys are derived from the slug instead of an autoincrement counter.
61+
# The dump exposes `id`, so with a counter one inserted record renumbered every
62+
# row after it and the regenerated dump rewrote pages whose data never changed
63+
# (TechAPI #180: ~1M files, GitHub could not even render the diff).
64+
_ID_BITS = 48 # < 2**53, so the value survives JSON round-trips intact
65+
66+
67+
def _stable_id(table: str, slug: str, taken: set[int]) -> int:
68+
"""Deterministic id for ``table``/``slug``, avoiding ids already assigned."""
69+
attempt = 0
70+
while True:
71+
key = f"{table}:{slug}" if attempt == 0 else f"{table}:{slug}#{attempt}"
72+
digest = hashlib.blake2b(key.encode("utf-8"), digest_size=8).digest()
73+
value = int.from_bytes(digest, "big") % (1 << _ID_BITS) or 1
74+
if value not in taken:
75+
taken.add(value)
76+
return value
77+
attempt += 1 # collision: rehash rather than fall back to a counter
78+
79+
80+
def _with_id(obj: SQLModel, taken: set[int]) -> SQLModel:
81+
obj.id = _stable_id(type(obj).__tablename__, obj.slug, taken) # type: ignore[attr-defined]
82+
return obj
83+
84+
5985
def _existing_slugs(session: Session, model: type[SQLModel]) -> set[str]:
6086
rows = session.exec(select(model)).all()
6187
return {row.slug for row in rows} # type: ignore[attr-defined] # all data models have slug
6288

6389

6490
def seed(session: Session, data_dir: Path = DATA_DIR) -> dict[str, int]:
6591
"""Idempotently insert seed data. Returns counts of newly inserted rows."""
92+
# Ids assigned in this run; only used to break hash collisions.
93+
taken: set[int] = set()
6694
counts = {
6795
"brands": 0,
6896
"socs": 0,
@@ -87,7 +115,7 @@ def seed(session: Session, data_dir: Path = DATA_DIR) -> dict[str, int]:
87115
# `categories` lives in the JSON for browsing/validation only — the Brand
88116
# table model does not (yet) carry it, so drop before construction.
89117
record.pop("categories", None)
90-
session.add(Brand(**record))
118+
session.add(_with_id(Brand(**record), taken))
91119
counts["brands"] += 1
92120
session.commit()
93121

@@ -104,7 +132,7 @@ def seed(session: Session, data_dir: Path = DATA_DIR) -> dict[str, int]:
104132
raise ValueError(
105133
f"SoC '{record['slug']}' references unknown brand '{manufacturer}'"
106134
)
107-
session.add(SoC(manufacturer_id=manufacturer_id, **record))
135+
session.add(_with_id(SoC(manufacturer_id=manufacturer_id, **record), taken))
108136
counts["socs"] += 1
109137
session.commit()
110138

@@ -127,7 +155,8 @@ def seed(session: Session, data_dir: Path = DATA_DIR) -> dict[str, int]:
127155
raise ValueError(
128156
f"Smartphone '{record['slug']}' references unknown SoC '{soc_slug}'"
129157
)
130-
session.add(Smartphone(brand_id=brand_id, soc_id=soc_id, **record))
158+
phone = Smartphone(brand_id=brand_id, soc_id=soc_id, **record)
159+
session.add(_with_id(phone, taken))
131160
counts["smartphones"] += 1
132161
session.commit()
133162

@@ -150,7 +179,8 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
150179
f"{subdir.rstrip('s').title()} '{record['slug']}' "
151180
f"references unknown SoC '{soc_slug}'"
152181
)
153-
session.add(model(brand_id=brand_id, soc_id=soc_id, **record))
182+
device = model(brand_id=brand_id, soc_id=soc_id, **record)
183+
session.add(_with_id(device, taken))
154184
counts[count_key] += 1
155185
session.commit()
156186

@@ -169,7 +199,7 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
169199
raise ValueError(
170200
f"GPU '{record['slug']}' references unknown brand '{manufacturer}'"
171201
)
172-
session.add(DiscreteGPU(manufacturer_id=manufacturer_id, **record))
202+
session.add(_with_id(DiscreteGPU(manufacturer_id=manufacturer_id, **record), taken))
173203
counts["gpus"] += 1
174204
session.commit()
175205

@@ -184,7 +214,7 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
184214
raise ValueError(
185215
f"CPU '{record['slug']}' references unknown brand '{manufacturer}'"
186216
)
187-
session.add(CPU(manufacturer_id=manufacturer_id, **record))
217+
session.add(_with_id(CPU(manufacturer_id=manufacturer_id, **record), taken))
188218
counts["cpus"] += 1
189219
session.commit()
190220

@@ -213,7 +243,8 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
213243
raise ValueError(
214244
f"Laptop '{record['slug']}' references unknown GPU '{gpu_slug}'"
215245
)
216-
session.add(Laptop(brand_id=brand_id, cpu_id=cpu_id, gpu_id=gpu_id, **record))
246+
laptop = Laptop(brand_id=brand_id, cpu_id=cpu_id, gpu_id=gpu_id, **record)
247+
session.add(_with_id(laptop, taken))
217248
counts["laptops"] += 1
218249
session.commit()
219250

@@ -228,7 +259,7 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
228259
raise ValueError(
229260
f"Monitor '{record['slug']}' references unknown brand '{brand_slug}'"
230261
)
231-
session.add(Monitor(brand_id=brand_id, **record))
262+
session.add(_with_id(Monitor(brand_id=brand_id, **record), taken))
232263
counts["monitors"] += 1
233264
session.commit()
234265

@@ -237,7 +268,7 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
237268
for record in _load_dir(data_dir / "game"):
238269
if record["slug"] in game_slugs:
239270
continue
240-
session.add(Game(**record))
271+
session.add(_with_id(Game(**record), taken))
241272
counts["games"] += 1
242273
session.commit()
243274

@@ -246,7 +277,7 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
246277
for record in _load_dir(data_dir / "software"):
247278
if record["slug"] in software_slugs:
248279
continue
249-
session.add(Software(**record))
280+
session.add(_with_id(Software(**record), taken))
250281
counts["software"] += 1
251282
session.commit()
252283

@@ -255,7 +286,7 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N
255286
for record in _load_dir(data_dir / "website"):
256287
if record["slug"] in website_slugs:
257288
continue
258-
session.add(Website(**record))
289+
session.add(_with_id(Website(**record), taken))
259290
counts["websites"] += 1
260291
session.commit()
261292

‎tests/unit/test_stable_ids.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Ids must depend only on table+slug, so a dump only changes what the data did."""
2+
3+
from __future__ import annotations
4+
5+
from app.seed import _stable_id
6+
7+
8+
def test_same_slug_same_id_regardless_of_insertion_order():
9+
assert _stable_id("brands", "samsung", set()) == _stable_id("brands", "samsung", set())
10+
11+
12+
def test_inserting_a_record_does_not_renumber_the_others():
13+
first_pass = {slug: _stable_id("cpus", slug, set()) for slug in ("a", "b", "c")}
14+
taken: set[int] = set()
15+
second_pass = {slug: _stable_id("cpus", slug, taken) for slug in ("a", "new", "b", "c")}
16+
assert all(second_pass[slug] == first_pass[slug] for slug in first_pass)
17+
18+
19+
def test_same_slug_in_two_tables_gets_two_ids():
20+
assert _stable_id("cpus", "a1", set()) != _stable_id("gpus", "a1", set())
21+
22+
23+
def test_collision_falls_back_to_a_rehash_not_a_duplicate():
24+
first = _stable_id("brands", "samsung", set())
25+
taken = {first}
26+
assert _stable_id("brands", "samsung", taken) != first
27+
28+
29+
def test_id_fits_in_a_json_safe_integer():
30+
assert 0 < _stable_id("games", "doom", set()) < 2**53

0 commit comments

Comments
 (0)