""" BAN YARO — Datenbank SQLite mit WAL-Modus (bewährt von akku-werkstatt). """ import sqlite3 import os import logging from contextlib import contextmanager logger = logging.getLogger(__name__) DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db") def get_connection() -> sqlite3.Connection: conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA busy_timeout=5000") return conn @contextmanager def db(): conn = get_connection() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def init_db(): """Erstellt alle Tabellen falls nicht vorhanden.""" logger.info(f"Initialisiere Datenbank: {DB_PATH}") os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) with db() as conn: conn.executescript(""" -- USERS CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, pw_hash TEXT NOT NULL, name TEXT NOT NULL, rolle TEXT NOT NULL DEFAULT 'user', is_premium INTEGER NOT NULL DEFAULT 0, push_sub TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_login TEXT ); -- HUNDE CREATE TABLE IF NOT EXISTS dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, rasse TEXT, geburtstag TEXT, geschlecht TEXT, gewicht_kg REAL, chip_nr TEXT, foto_url TEXT, bio TEXT, is_public INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- TAGEBUCH CREATE TABLE IF NOT EXISTS diary ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, datum TEXT NOT NULL DEFAULT (date('now')), typ TEXT NOT NULL DEFAULT 'eintrag', titel TEXT, text TEXT, media_url TEXT, tags TEXT, -- JSON Array gps_lat REAL, gps_lon REAL, is_milestone INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_diary_dog ON diary(dog_id, datum DESC); -- TAGEBUCH ↔ HUNDE (n:m — ein Eintrag kann mehrere Hunde betreffen) CREATE TABLE IF NOT EXISTS diary_dogs ( diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, PRIMARY KEY (diary_id, dog_id) ); CREATE INDEX IF NOT EXISTS idx_diary_dogs_dog ON diary_dogs(dog_id); -- GESUNDHEIT CREATE TABLE IF NOT EXISTS health ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, typ TEXT NOT NULL, -- impfung | entwurmung | tierarzt | medikament | gewicht bezeichnung TEXT NOT NULL, datum TEXT NOT NULL, naechstes TEXT, notiz TEXT, wert REAL, -- für Gewicht o.ä. einheit TEXT, erinnerung INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_health_dog ON health(dog_id, datum DESC); -- GIFTKÖDER-ALARM CREATE TABLE IF NOT EXISTS poison ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), lat REAL NOT NULL, lon REAL NOT NULL, beschreibung TEXT, typ TEXT DEFAULT 'unbekannt', foto_url TEXT, bestaetigt INTEGER NOT NULL DEFAULT 0, bestaetigt_von INTEGER, geloest INTEGER NOT NULL DEFAULT 0, expires_at TEXT NOT NULL, -- auto-expire nach 7 Tagen created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_poison_location ON poison(lat, lon); CREATE INDEX IF NOT EXISTS idx_poison_active ON poison(geloest, expires_at); -- ORTE (hundefreundliche Orte, Kotbeutelspender, etc.) CREATE TABLE IF NOT EXISTS places ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), name TEXT NOT NULL, typ TEXT NOT NULL, -- restaurant | shop | freilauf | kotbeutel | tierarzt | hundeschule lat REAL NOT NULL, lon REAL NOT NULL, adresse TEXT, website TEXT, hund_rein INTEGER, leine_pflicht INTEGER, wasser_fuer_hunde INTEGER, foto_url TEXT, bewertung REAL DEFAULT 0, anz_bewertungen INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_places_location ON places(lat, lon); CREATE INDEX IF NOT EXISTS idx_places_typ ON places(typ); -- ROUTEN CREATE TABLE IF NOT EXISTS routes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), name TEXT NOT NULL, beschreibung TEXT, gps_track TEXT NOT NULL, -- JSON Array von {lat, lon} distanz_km REAL, dauer_min INTEGER, schwierigkeit TEXT DEFAULT 'leicht', untergrund TEXT, -- wald | asphalt | wiese | mix schatten INTEGER, leine_empfohlen INTEGER, bewertung REAL DEFAULT 0, anz_bewertungen INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- GASSI-TREFFEN CREATE TABLE IF NOT EXISTS walks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), titel TEXT NOT NULL, datum TEXT NOT NULL, uhrzeit TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL, ort_name TEXT, max_teilnehmer INTEGER DEFAULT 10, beschreibung TEXT, status TEXT NOT NULL DEFAULT 'offen', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- GASSI-TREFFEN TEILNEHMER CREATE TABLE IF NOT EXISTS walk_participants ( walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id), joined_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (walk_id, user_id) ); -- GASSI-TREFFEN ↔ HUNDE (n:m — Teilnehmer kann mehrere Hunde mitbringen) CREATE TABLE IF NOT EXISTS walk_participant_dogs ( walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, PRIMARY KEY (walk_id, user_id, dog_id) ); CREATE INDEX IF NOT EXISTS idx_wpd_dog ON walk_participant_dogs(dog_id); -- FORUM CREATE TABLE IF NOT EXISTS forum_threads ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), dog_id INTEGER REFERENCES dogs(id), titel TEXT NOT NULL, kategorie TEXT NOT NULL DEFAULT 'allgemein', rasse_tag TEXT, plz TEXT, views INTEGER NOT NULL DEFAULT 0, pinned INTEGER NOT NULL DEFAULT 0, locked INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_threads_kat ON forum_threads(kategorie, created_at DESC); CREATE TABLE IF NOT EXISTS forum_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER NOT NULL REFERENCES forum_threads(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), edited_at TEXT ); -- PUSH SUBSCRIPTIONS (alternativ zu users.push_sub für mehrere Geräte) CREATE TABLE IF NOT EXISTS push_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, endpoint TEXT NOT NULL UNIQUE, p256dh TEXT NOT NULL, auth TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- PREMIUM-TRANSAKTIONEN (später für Zahlungsabwicklung) CREATE TABLE IF NOT EXISTS premium_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), status TEXT NOT NULL DEFAULT 'pending', betrag_cent INTEGER NOT NULL, zahlungsart TEXT, valid_until TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- EVENTS (Hundeveranstaltungen) CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), titel TEXT NOT NULL, datum TEXT NOT NULL, -- YYYY-MM-DD uhrzeit TEXT, lat REAL, lon REAL, ort_name TEXT, typ TEXT NOT NULL DEFAULT 'sonstiges', beschreibung TEXT, link TEXT, status TEXT NOT NULL DEFAULT 'aktiv', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC); -- SITTING — Sitter-Profile CREATE TABLE IF NOT EXISTS sitters ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, beschreibung TEXT, preis_pro_tag REAL DEFAULT 0, max_hunde INTEGER DEFAULT 1, lat REAL, lon REAL, radius_km INTEGER DEFAULT 20, services TEXT DEFAULT '[]', -- JSON: ['tagesbetreuung','uebernachtung','gassi','hausbesuch'] aktiv INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- SITTING — Anfragen CREATE TABLE IF NOT EXISTS sitting_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), -- Anfragender sitter_id INTEGER NOT NULL REFERENCES sitters(id), dog_ids TEXT DEFAULT '[]', -- JSON Array von TEXT NOT NULL, -- YYYY-MM-DD bis TEXT NOT NULL, nachricht TEXT, status TEXT NOT NULL DEFAULT 'offen', -- offen|angenommen|abgelehnt|abgebrochen created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- OSM POI-Cache (Mülleimer, Hundewiesen, Wasserstellen aus OpenStreetMap) CREATE TABLE IF NOT EXISTS osm_pois ( osm_id INTEGER NOT NULL, type TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL, name TEXT, cached_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (osm_id, type) ); CREATE INDEX IF NOT EXISTS idx_osm_pois_loc ON osm_pois(type, lat, lon); -- VERLORENE HUNDE CREATE TABLE IF NOT EXISTS lost_dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, name TEXT NOT NULL, rasse TEXT, beschreibung TEXT NOT NULL, foto_url TEXT, lat REAL NOT NULL, lon REAL NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, gefunden_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_lost_active ON lost_dogs(is_active, created_at DESC); -- OSM Tile-Cache: welche Kacheln wurden schon geladen? CREATE TABLE IF NOT EXISTS osm_tiles ( type TEXT NOT NULL, tile_key TEXT NOT NULL, -- "zoom_x_y" cached_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (type, tile_key) ); -- Community-Pins: von Nutzern gesetzte Karten-Marker CREATE TABLE IF NOT EXISTS user_map_pois ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, type TEXT NOT NULL, -- waste_basket | drinking_water | dog_park | sonstiges lat REAL NOT NULL, lon REAL NOT NULL, name TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_user_pois_loc ON user_map_pois(type, lat, lon); -- Meldungen: ungültige OSM- oder Community-Marker CREATE TABLE IF NOT EXISTS osm_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, osm_id INTEGER, -- gesetzt wenn OSM-Marker gemeldet user_poi_id INTEGER, -- gesetzt wenn Community-Marker gemeldet type TEXT, grund TEXT NOT NULL, -- existiert_nicht | falsche_position | spam | sonstiges created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- TIERÄRZTE (user-level, nie löschen — Historien-Erhalt bei Umzug) CREATE TABLE IF NOT EXISTS tieraerzte ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, strasse TEXT, plz TEXT, ort TEXT, telefon TEXT, notfall_telefon TEXT, email TEXT, website TEXT, notizen TEXT, ist_notfallpraxis INTEGER NOT NULL DEFAULT 0, aktiv INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_tieraerzte_user ON tieraerzte(user_id, aktiv); """) # Migrations: neue Spalten zu bestehenden Tabellen hinzufügen (idempotent) _migrate(conn_factory=db) logger.info("Datenbank initialisiert.") def _migrate(conn_factory): """Fügt fehlende Spalten hinzu und führt Datenmigration durch (idempotent).""" migrations = [ # Giftköder: Auflösungs-Details für spätere KI-Analyse ("poison", "geloest_von", "INTEGER"), ("poison", "geloest_at", "TEXT"), ("poison", "geloest_grund", "TEXT"), # Gesundheit: erweiterte Felder je nach Eintragstyp ("health", "charge_nr", "TEXT"), ("health", "tierarzt_name", "TEXT"), ("health", "kosten", "REAL"), ("health", "diagnose", "TEXT"), ("health", "dosierung", "TEXT"), ("health", "haeufigkeit", "TEXT"), ("health", "aktiv", "INTEGER NOT NULL DEFAULT 1"), ("health", "bis_datum", "TEXT"), ("health", "schweregrad", "TEXT"), ("health", "reaktion", "TEXT"), ("health", "datei_url", "TEXT"), ("health", "datei_typ", "TEXT"), ("health", "tierarzt_id", "INTEGER"), # Tierärzte: Adresse aufgeteilt in Strasse/PLZ/Ort ("tieraerzte", "strasse", "TEXT"), ("tieraerzte", "plz", "TEXT"), ("tieraerzte", "ort", "TEXT"), # Gesundheit: Erinnerungsintervall für wiederkehrende Einträge ("health", "intervall_tage", "INTEGER"), # Routen: neue Felder ("routes", "is_public", "INTEGER NOT NULL DEFAULT 1"), ("routes", "hunde_tauglichkeit", "TEXT"), ("routes", "foto_urls", "TEXT NOT NULL DEFAULT '[]'"), # OSM POIs: Kontaktdaten ("osm_pois", "opening_hours", "TEXT"), ("osm_pois", "phone", "TEXT"), ("osm_pois", "website", "TEXT"), # Forum: Threads brauchen text + antworten-Zähler ("forum_threads", "text", "TEXT NOT NULL DEFAULT ''"), ("forum_threads", "antworten", "INTEGER NOT NULL DEFAULT 0"), # Forum Sprint 11: erweiterte Thread-Felder ("forum_threads", "foto_urls", "TEXT"), ("forum_threads", "is_pinned", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "is_locked", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "is_deleted", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "likes", "INTEGER NOT NULL DEFAULT 0"), # Forum Sprint 11: erweiterte Post-Felder ("forum_posts", "foto_urls", "TEXT"), ("forum_posts", "is_deleted", "INTEGER NOT NULL DEFAULT 0"), ("forum_posts", "likes", "INTEGER NOT NULL DEFAULT 0"), # Users: Moderator-Flag + Forum-Standort ("users", "is_moderator", "INTEGER NOT NULL DEFAULT 0"), ("users", "forum_lat", "REAL"), ("users", "forum_lon", "REAL"), ("users", "forum_show_location", "INTEGER NOT NULL DEFAULT 0"), # Events: Quelle + externe ID für gescrapte Events ("events", "quelle", "TEXT NOT NULL DEFAULT 'nutzer'"), ("events", "external_id", "TEXT"), # Admin: User-Sperre ("users", "is_banned", "INTEGER NOT NULL DEFAULT 0"), ("users", "ban_reason", "TEXT"), # WebCal: Kalender-Abo-Token ("users", "calendar_token", "TEXT"), # User-Profil-Felder ("users", "email_verified", "INTEGER NOT NULL DEFAULT 0"), ("users", "bio", "TEXT"), ("users", "wohnort", "TEXT"), ("users", "erfahrung", "TEXT"), ("users", "social_link", "TEXT"), ("users", "profil_sichtbarkeit", "TEXT NOT NULL DEFAULT 'public'"), ("users", "avatar_url", "TEXT"), ("places", "telefon", "TEXT"), ] with conn_factory() as conn: for table, column, col_type in migrations: existing = [ row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall() ] if column not in existing: conn.execute( f"ALTER TABLE {table} ADD COLUMN {column} {col_type}" ) logger.info(f"Migration: {table}.{column} hinzugefügt.") # Knigge: Community-Votes conn.executescript(""" CREATE TABLE IF NOT EXISTS knigge_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, szenario_id TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, answer TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(szenario_id, user_id) ); """) # Forum Sprint 11: neue Tabellen conn.executescript(""" CREATE TABLE IF NOT EXISTS forum_likes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, target_type TEXT NOT NULL, target_id INTEGER NOT NULL, UNIQUE(user_id, target_type, target_id) ); CREATE TABLE IF NOT EXISTS forum_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, target_type TEXT NOT NULL, target_id INTEGER NOT NULL, grund TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), resolved INTEGER NOT NULL DEFAULT 0 ); """) # Wiki: Community-Berichte conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_berichte ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rasse TEXT NOT NULL, titel TEXT NOT NULL, text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_wiki_berichte_rasse ON wiki_berichte(rasse, created_at DESC); """) # Hunde-Filme: Bewertungen + Hund des Monats conn.executescript(""" CREATE TABLE IF NOT EXISTS movie_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, film_id TEXT NOT NULL, bewertung INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, film_id) ); CREATE TABLE IF NOT EXISTS hund_des_monats_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, monat TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, monat) ); """) # Events: Unique-Index für externe IDs (idempotent) conn.executescript(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external ON events(external_id) WHERE external_id IS NOT NULL; """) # Users: Eindeutiger Benutzername (case-insensitive via COLLATE NOCASE) conn.executescript(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_users_name_unique ON users(name COLLATE NOCASE); """) # Wiki: User-Foto-Einreichungen conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_foto_submissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rasse_id INTEGER NOT NULL REFERENCES wiki_rassen(id) ON DELETE CASCADE, foto_url TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL DEFAULT (datetime('now')), reviewed_by INTEGER REFERENCES users(id), reviewed_at TEXT, reject_reason TEXT ); CREATE INDEX IF NOT EXISTS idx_wfs_status ON wiki_foto_submissions(status, created_at DESC); """) # Freundschaften + Direktnachrichten conn.executescript(""" CREATE TABLE IF NOT EXISTS friendships ( id INTEGER PRIMARY KEY AUTOINCREMENT, requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, addressee_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT, UNIQUE(requester_id, addressee_id) ); CREATE INDEX IF NOT EXISTS idx_friendships_addressee ON friendships(addressee_id, status); CREATE INDEX IF NOT EXISTS idx_friendships_requester ON friendships(requester_id, status); CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_a_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_b_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, last_msg_at TEXT, a_read_at TEXT, b_read_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_a_id, user_b_id) ); CREATE TABLE IF NOT EXISTS direct_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, text TEXT NOT NULL, is_deleted INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dm_conv ON direct_messages(conversation_id, created_at ASC); """) # Wiki: Rassen-Datenbank (TheDogAPI) conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_rassen ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id INTEGER UNIQUE, name TEXT NOT NULL, name_de TEXT, gruppe TEXT, herkunft TEXT, temperament TEXT, gewicht_min_kg REAL, gewicht_max_kg REAL, groesse TEXT, lebensdauer TEXT, foto_url TEXT, bred_for TEXT, aktivitaet TEXT, wohnung_geeignet INTEGER DEFAULT 0, kinder_geeignet INTEGER DEFAULT 1, erfahrung TEXT DEFAULT 'anfaenger', slug TEXT UNIQUE, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_wiki_rassen_slug ON wiki_rassen(slug); CREATE INDEX IF NOT EXISTS idx_wiki_rassen_gruppe ON wiki_rassen(gruppe); """) # Datenmigration: diary_dogs für bestehende Einträge befüllen conn.execute(""" INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) SELECT id, dog_id FROM diary """) logger.info("Migration: diary_dogs Backfill abgeschlossen.") # Hund-Teilen: Einladungssystem conn.executescript(""" CREATE TABLE IF NOT EXISTS dog_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, shared_with_id INTEGER REFERENCES users(id) ON DELETE SET NULL, invite_token TEXT NOT NULL UNIQUE, role TEXT NOT NULL DEFAULT 'editor', accepted_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dog_shares_dog ON dog_shares(dog_id); CREATE INDEX IF NOT EXISTS idx_dog_shares_token ON dog_shares(invite_token); CREATE INDEX IF NOT EXISTS idx_dog_shares_user ON dog_shares(shared_with_id); """) logger.info("Migration: dog_shares Tabelle bereit.")