Skip to content

Instantly share code, notes, and snippets.

@cjus
Created September 27, 2026 23:32
Show Gist options
  • Select an option

  • Save cjus/5704e25a222a59c86f4bd596a249fd39 to your computer and use it in GitHub Desktop.

Select an option

Save cjus/5704e25a222a59c86f4bd596a249fd39 to your computer and use it in GitHub Desktop.
build_birds.py
"""Build birds.db, the practice database for the AIE250 SQLite guide.
Run it from the folder you want the database in:
python build_birds.py
It deletes any birds.db already there and builds a fresh one, so you can
always get back to a clean copy by running it again. It uses only the Python
standard library.
The data is invented. The species names are real birds; the sites, the
observers and every sighting are made up. The numbers come from a small
arithmetic generator rather than the random module, so every copy of
birds.db is identical, on any computer and any Python version.
"""
import os
import sqlite3
DB_PATH = "birds.db"
SCHEMA = """
CREATE TABLE species (
species_id INTEGER PRIMARY KEY,
common_name TEXT NOT NULL UNIQUE,
family TEXT NOT NULL
);
CREATE TABLE sites (
site_code TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
habitat TEXT NOT NULL,
elevation_m INTEGER NOT NULL
);
CREATE TABLE observers (
observer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
joined_on TEXT NOT NULL
);
CREATE TABLE sightings (
sighting_id INTEGER PRIMARY KEY,
species_id INTEGER NOT NULL REFERENCES species(species_id),
site_code TEXT NOT NULL REFERENCES sites(site_code),
observer_id INTEGER NOT NULL REFERENCES observers(observer_id),
seen_on TEXT NOT NULL,
bird_count INTEGER NOT NULL CHECK (bird_count > 0)
);
"""
SPECIES = [
("American Robin", "Turdidae"),
("Mountain Bluebird", "Turdidae"),
("Black-capped Chickadee", "Paridae"),
("Mountain Chickadee", "Paridae"),
("Steller's Jay", "Corvidae"),
("Black-billed Magpie", "Corvidae"),
("Common Raven", "Corvidae"),
("California Gull", "Laridae"),
("Red-tailed Hawk", "Accipitridae"),
("Golden Eagle", "Accipitridae"),
("Bald Eagle", "Accipitridae"),
("Great Blue Heron", "Ardeidae"),
("Mallard", "Anatidae"),
("Canada Goose", "Anatidae"),
("American Avocet", "Recurvirostridae"),
("Black-necked Stilt", "Recurvirostridae"),
("House Finch", "Fringillidae"),
("Pine Siskin", "Fringillidae"),
("Western Meadowlark", "Icteridae"),
("Red-winged Blackbird", "Icteridae"),
("Downy Woodpecker", "Picidae"),
("Northern Flicker", "Picidae"),
("Broad-tailed Hummingbird", "Trochilidae"),
("Black-chinned Hummingbird", "Trochilidae"),
("Dark-eyed Junco", "Passerellidae"),
("Song Sparrow", "Passerellidae"),
("Spotted Towhee", "Passerellidae"),
("Western Tanager", "Cardinalidae"),
("Lazuli Bunting", "Cardinalidae"),
("Cedar Waxwing", "Bombycillidae"),
]
SITES = [
("NMR", "North Marsh", "wetland", 1283),
("SMR", "South Marsh", "wetland", 1286),
("CTR", "Cottonwood Trail", "riparian", 1402),
("OAK", "Oak Hollow", "woodland", 1610),
("MDW", "Long Meadow", "grassland", 1455),
("RDG", "Ridge Overlook", "cliff", 2210),
("ASP", "Aspen Bowl", "forest", 2640),
("TRN", "Tarn Basin", "alpine", 2915),
("PRK", "City Park Pond", "urban", 1311),
("ORC", "Old Orchard", "urban", 1340),
]
OBSERVERS = [
("Ava Lindqvist", "2024-02-11"),
("Mateo Ruiz", "2024-03-02"),
("Priya Natarajan", "2024-03-19"),
("Jonah Whitfield", "2024-04-07"),
("Keiko Tanabe", "2024-05-23"),
("Samuel Okafor", "2024-06-14"),
("Lena Fischer", "2024-07-01"),
("Diego Castillo", "2024-08-30"),
("Hannah Brooks", "2024-09-12"),
("Tariq Haddad", "2024-10-05"),
("Mei Chen", "2024-11-18"),
("Owen Gallagher", "2025-01-09"),
("Rosa Delgado", "2025-02-27"),
("Ivan Petrov", "2025-04-15"),
("Nadia Rahman", "2025-06-03"),
("Caleb Morgan", "2025-08-21"),
]
SIGHTING_COUNT = 100_000
# Days in each month of 2025, then of 2026 up to August. Sightings fall
# between 2025-01-01 and 2026-08-31.
MONTHS = [(2025, m, d) for m, d in enumerate([31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], 1)]
MONTHS += [(2026, m, d) for m, d in enumerate([31, 28, 31, 30, 31, 30, 31, 31], 1)]
DATES = [f"{y}-{m:02d}-{day:02d}" for y, m, days in MONTHS for day in range(1, days + 1)]
class Generator:
"""A linear congruential generator: the same numbers on every machine."""
def __init__(self, seed):
self.state = seed
def below(self, n):
"""Return a whole number from 0 up to, but not including, n."""
self.state = (1664525 * self.state + 1013904223) % 2**32
return (self.state * n) >> 32
def make_sightings(gen):
rows = []
for _ in range(SIGHTING_COUNT):
# Squaring a fraction makes low species ids more common than high
# ones, so some birds are seen far more often than others.
fraction = gen.below(10_000) / 10_000
species_id = 1 + int(fraction * fraction * len(SPECIES))
site_code = SITES[gen.below(len(SITES))][0]
observer_id = 1 + gen.below(len(OBSERVERS))
seen_on = DATES[gen.below(len(DATES))]
bird_count = 1 + gen.below(4) * gen.below(6)
rows.append((species_id, site_code, observer_id, seen_on, bird_count))
return rows
def main():
if os.path.exists(DB_PATH):
os.remove(DB_PATH)
con = sqlite3.connect(DB_PATH)
con.execute("PRAGMA foreign_keys = ON")
con.executescript(SCHEMA)
with con:
con.executemany(
"INSERT INTO species (common_name, family) VALUES (?, ?)", SPECIES
)
con.executemany(
"INSERT INTO sites (site_code, name, habitat, elevation_m) VALUES (?, ?, ?, ?)",
SITES,
)
con.executemany(
"INSERT INTO observers (name, joined_on) VALUES (?, ?)", OBSERVERS
)
con.executemany(
"INSERT INTO sightings (species_id, site_code, observer_id, seen_on, bird_count)"
" VALUES (?, ?, ?, ?, ?)",
make_sightings(Generator(seed=250)),
)
for table in ("species", "sites", "observers", "sightings"):
count = con.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
print(f"{table:<10} {count:>7} rows")
con.close()
print(f"Built {DB_PATH} with SQLite {sqlite3.sqlite_version}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment