From 5bf180c4c60a2b87e90d6a29cf4711fbc2f195b8 Mon Sep 17 00:00:00 2001 From: Torres Date: Tue, 10 Nov 2026 18:57:17 -0400 Subject: [PATCH] app: add SQLite schema and db helper (WAL, busy_timeout, four tables + indexes) --- app/__init__.py | 1 + app/db.py | 29 ++++++++++++++++++++++++++ app/init.py | 20 ++++++++++++++++++ app/schema.sql | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/db.py create mode 100644 app/init.py create mode 100644 app/schema.sql diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..532a53f --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""Bike-Tinder application package (Phase 1: data spine).""" diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..37d0918 --- /dev/null +++ b/app/db.py @@ -0,0 +1,29 @@ +"""SQLite connection + schema init for Bike-Tinder. + +Stdlib only. The sqlite3 CLI is not installed on this host, so everything +goes through the sqlite3 module. +""" +import os +import sqlite3 + +SCHEMA_PATH = os.path.join(os.path.dirname(__file__), 'schema.sql') + + +def connect(db_path): + """Open a connection with WAL mode and a busy_timeout, then ensure schema.""" + parent = os.path.dirname(os.path.abspath(db_path)) + if parent: + os.makedirs(parent, exist_ok=True) + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + conn.execute('PRAGMA journal_mode=WAL') + conn.execute('PRAGMA busy_timeout=5000') + conn.execute('PRAGMA foreign_keys=ON') + return conn + + +def init_db(conn): + """Apply schema.sql (idempotent).""" + with open(SCHEMA_PATH, encoding='utf-8') as fh: + conn.executescript(fh.read()) + conn.commit() diff --git a/app/init.py b/app/init.py new file mode 100644 index 0000000..0ace2b0 --- /dev/null +++ b/app/init.py @@ -0,0 +1,20 @@ +"""Explicit DB init path: python3 -m app.init --db .""" +import argparse +import sys + +from . import db + + +def main(argv=None): + ap = argparse.ArgumentParser(description='Initialize the Bike-Tinder DB') + ap.add_argument('--db', default='data/bike-tinder.sqlite3') + args = ap.parse_args(argv) + conn = db.connect(args.db) + db.init_db(conn) + conn.close() + print('init: schema applied to %s' % args.db) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/app/schema.sql b/app/schema.sql new file mode 100644 index 0000000..4619fe1 --- /dev/null +++ b/app/schema.sql @@ -0,0 +1,54 @@ +-- Bike-Tinder v1 schema (SQLite). Locked per docs/DATA-MODEL.md. +-- Applied by app.db.init_db(); WAL mode and busy_timeout are set there. + +CREATE TABLE IF NOT EXISTS listings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + region TEXT NOT NULL CHECK (region IN ('ns','nb')), + src_id TEXT NOT NULL, -- source ad id (Kijiji id in v1) + title TEXT NOT NULL, + price_display TEXT, -- "$5,000"; "Please Contact" + price_num INTEGER, -- NULL when unpackable + km INTEGER, -- NULL when not stated + location TEXT, + url TEXT NOT NULL, -- source ad page + img_url TEXT, -- source CDN card image + desc TEXT, -- card description snippet + family TEXT, -- top rubric family label (taste signal) + score INTEGER NOT NULL DEFAULT 0, -- rubric score, recomputed each import + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','gone','dup')), + dup_of INTEGER, -- canonical row id when status='dup' + first_seen TEXT, + last_seen TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT, + UNIQUE (region, src_id) +); + +CREATE INDEX IF NOT EXISTS idx_listings_deck ON listings (status, score DESC); +CREATE INDEX IF NOT EXISTS idx_listings_dup ON listings (dup_of) WHERE status = 'dup'; + +CREATE TABLE IF NOT EXISTS swipes ( + listing_id INTEGER PRIMARY KEY REFERENCES listings(id) ON DELETE CASCADE, + action TEXT NOT NULL CHECK (action IN ('like','nope','maybe')), + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT +); + +CREATE TABLE IF NOT EXISTS import_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + started_at TEXT NOT NULL, + ok INTEGER NOT NULL, + partial INTEGER NOT NULL DEFAULT 0, + src_fetched_ns INTEGER, + src_fetched_nb INTEGER, + total INTEGER, + new INTEGER, + gone INTEGER, + dup INTEGER, + error TEXT +); + +CREATE TABLE IF NOT EXISTS prefs ( + key TEXT PRIMARY KEY, + value TEXT +);