"""Restaurant DB operations — save extracted data, link to videos.""" from __future__ import annotations import json import oracledb from core.db import conn def find_by_name(name: str) -> dict | None: """Find a restaurant by exact name match.""" sql = "SELECT id, name, address, region, latitude, longitude FROM restaurants WHERE name = :n" with conn() as c: cur = c.cursor() cur.execute(sql, {"n": name}) r = cur.fetchone() if r: return { "id": r[0], "name": r[1], "address": r[2], "region": r[3], "latitude": r[4], "longitude": r[5], } return None def upsert( name: str, address: str | None = None, region: str | None = None, latitude: float | None = None, longitude: float | None = None, cuisine_type: str | None = None, price_range: str | None = None, google_place_id: str | None = None, ) -> str: """Insert or update a restaurant. Returns row id.""" existing = find_by_name(name) if existing: sql = """ UPDATE restaurants SET address = COALESCE(:addr, address), region = COALESCE(:reg, region), latitude = COALESCE(:lat, latitude), longitude = COALESCE(:lng, longitude), cuisine_type = COALESCE(:cuisine, cuisine_type), price_range = COALESCE(:price, price_range), google_place_id = COALESCE(:gid, google_place_id), updated_at = SYSTIMESTAMP WHERE id = :id """ with conn() as c: c.cursor().execute(sql, { "addr": address, "reg": region, "lat": latitude, "lng": longitude, "cuisine": cuisine_type, "price": price_range, "gid": google_place_id, "id": existing["id"], }) return existing["id"] sql = """ INSERT INTO restaurants (name, address, region, latitude, longitude, cuisine_type, price_range, google_place_id) VALUES (:name, :addr, :reg, :lat, :lng, :cuisine, :price, :gid) RETURNING id INTO :out_id """ with conn() as c: cur = c.cursor() out_id = cur.var(oracledb.STRING) cur.execute(sql, { "name": name, "addr": address, "reg": region, "lat": latitude, "lng": longitude, "cuisine": cuisine_type, "price": price_range, "gid": google_place_id, "out_id": out_id, }) return out_id.getvalue()[0] def link_video_restaurant( video_db_id: str, restaurant_id: str, foods: list[str] | None = None, evaluation: str | None = None, guests: list[str] | None = None, citation: str | None = None, ) -> str | None: """Create video-restaurant mapping. Returns row id or None if duplicate.""" sql = """ INSERT INTO video_restaurants (video_id, restaurant_id, foods_mentioned, evaluation, guests, citation_text) VALUES (:vid, :rid, :foods, :eval, :guests, :cite) RETURNING id INTO :out_id """ with conn() as c: cur = c.cursor() out_id = cur.var(oracledb.STRING) try: cur.execute(sql, { "vid": video_db_id, "rid": restaurant_id, "foods": json.dumps(foods or [], ensure_ascii=False), "eval": json.dumps({"text": evaluation} if evaluation else {}, ensure_ascii=False), "guests": json.dumps(guests or [], ensure_ascii=False), "cite": citation, "out_id": out_id, }) return out_id.getvalue()[0] except Exception as e: if "UQ_VR_VIDEO_REST" in str(e).upper(): return None raise def get_all( limit: int = 100, offset: int = 0, cuisine: str | None = None, region: str | None = None, ) -> list[dict]: """List restaurants with optional filters.""" conditions = ["latitude IS NOT NULL"] params: dict = {"lim": limit, "off": offset} if cuisine: conditions.append("cuisine_type = :cuisine") params["cuisine"] = cuisine if region: conditions.append("region LIKE :region") params["region"] = f"%{region}%" where = " AND ".join(conditions) sql = f""" SELECT id, name, address, region, latitude, longitude, cuisine_type, price_range, google_place_id FROM restaurants WHERE {where} ORDER BY updated_at DESC OFFSET :off ROWS FETCH NEXT :lim ROWS ONLY """ with conn() as c: cur = c.cursor() cur.execute(sql, params) cols = [d[0].lower() for d in cur.description] return [dict(zip(cols, row)) for row in cur.fetchall()] def get_by_id(restaurant_id: str) -> dict | None: sql = """ SELECT r.id, r.name, r.address, r.region, r.latitude, r.longitude, r.cuisine_type, r.price_range, r.phone, r.website, r.google_place_id FROM restaurants r WHERE r.id = :id """ with conn() as c: cur = c.cursor() cur.execute(sql, {"id": restaurant_id}) row = cur.fetchone() if not row: return None cols = [d[0].lower() for d in cur.description] return dict(zip(cols, row)) def get_video_links(restaurant_id: str) -> list[dict]: """Get all video appearances for a restaurant.""" sql = """ SELECT v.video_id, v.title, v.url, v.published_at, vr.foods_mentioned, vr.evaluation, vr.guests FROM video_restaurants vr JOIN videos v ON v.id = vr.video_id WHERE vr.restaurant_id = :rid ORDER BY v.published_at DESC """ with conn() as c: cur = c.cursor() cur.execute(sql, {"rid": restaurant_id}) results = [] for r in cur.fetchall(): foods_raw = r[4].read() if hasattr(r[4], "read") else r[4] eval_raw = r[5].read() if hasattr(r[5], "read") else r[5] guests_raw = r[6].read() if hasattr(r[6], "read") else r[6] results.append({ "video_id": r[0], "title": r[1], "url": r[2], "published_at": r[3].isoformat() if r[3] else None, "foods_mentioned": _parse_json_field(foods_raw, []), "evaluation": _parse_json_field(eval_raw, {}), "guests": _parse_json_field(guests_raw, []), }) return results def _parse_json_field(val, default): """Parse a JSON field that may be a string, already-parsed object, or None.""" if val is None: return default if isinstance(val, (list, dict)): return val if isinstance(val, str): try: return json.loads(val) except (json.JSONDecodeError, ValueError): return default return default