Spaces:
Sleeping
Sleeping
Upload backend/database.py
Browse files- backend/database.py +67 -4
backend/database.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
| 1 |
"""
|
| 2 |
Bharat Tech Atlas β Database layer using SQLite with R-Tree spatial indexing.
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
import sqlite3
|
| 5 |
import os
|
| 6 |
-
import json
|
| 7 |
import math
|
|
|
|
| 8 |
|
| 9 |
DB_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "bharattechatlas.db")
|
|
|
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
conn.row_factory = sqlite3.Row
|
| 15 |
conn.execute("PRAGMA journal_mode=WAL")
|
| 16 |
conn.execute("PRAGMA foreign_keys=ON")
|
|
@@ -18,11 +26,34 @@ def get_db():
|
|
| 18 |
conn.execute("PRAGMA mmap_size=67108864")
|
| 19 |
conn.execute("PRAGMA synchronous=NORMAL")
|
| 20 |
conn.execute("PRAGMA temp_store=MEMORY")
|
|
|
|
| 21 |
return conn
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def init_db():
|
| 25 |
-
|
|
|
|
| 26 |
|
| 27 |
conn.executescript("""
|
| 28 |
CREATE TABLE IF NOT EXISTS entities (
|
|
@@ -148,10 +179,42 @@ def init_db():
|
|
| 148 |
('wealthtech', 'WealthTech', 'π', '#7C3AED', 'dpiit_category');
|
| 149 |
""")
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
conn.close()
|
| 153 |
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def haversine_distance(lat1, lon1, lat2, lon2):
|
| 156 |
R = 6371
|
| 157 |
dlat = math.radians(lat2 - lat1)
|
|
|
|
| 1 |
"""
|
| 2 |
Bharat Tech Atlas β Database layer using SQLite with R-Tree spatial indexing.
|
| 3 |
+
v3.2: Added connection pooling, schema_version table for migrations, and
|
| 4 |
+
proper cleanup on shutdown.
|
| 5 |
"""
|
| 6 |
import sqlite3
|
| 7 |
import os
|
|
|
|
| 8 |
import math
|
| 9 |
+
import threading
|
| 10 |
|
| 11 |
DB_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "bharattechatlas.db")
|
| 12 |
+
SCHEMA_VERSION = 2 # Bump when schema changes
|
| 13 |
|
| 14 |
+
# βββ Connection pool ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
_pool_lock = threading.Lock()
|
| 16 |
+
_pool = {}
|
| 17 |
|
| 18 |
+
|
| 19 |
+
def _create_connection():
|
| 20 |
+
"""Create a new SQLite connection with optimized PRAGMAs."""
|
| 21 |
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
| 22 |
conn.row_factory = sqlite3.Row
|
| 23 |
conn.execute("PRAGMA journal_mode=WAL")
|
| 24 |
conn.execute("PRAGMA foreign_keys=ON")
|
|
|
|
| 26 |
conn.execute("PRAGMA mmap_size=67108864")
|
| 27 |
conn.execute("PRAGMA synchronous=NORMAL")
|
| 28 |
conn.execute("PRAGMA temp_store=MEMORY")
|
| 29 |
+
conn.execute("PRAGMA busy_timeout=5000")
|
| 30 |
return conn
|
| 31 |
|
| 32 |
|
| 33 |
+
def get_db():
|
| 34 |
+
"""Get a connection from the pool (one per thread)."""
|
| 35 |
+
tid = threading.current_thread().ident
|
| 36 |
+
with _pool_lock:
|
| 37 |
+
if tid not in _pool or _pool[tid] is None:
|
| 38 |
+
_pool[tid] = _create_connection()
|
| 39 |
+
return _pool[tid]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def close_all_connections():
|
| 43 |
+
"""Close all pooled connections. Call on shutdown."""
|
| 44 |
+
with _pool_lock:
|
| 45 |
+
for tid, conn in list(_pool.items()):
|
| 46 |
+
try:
|
| 47 |
+
conn.close()
|
| 48 |
+
except Exception:
|
| 49 |
+
pass
|
| 50 |
+
_pool[tid] = None
|
| 51 |
+
_pool.clear()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
def init_db():
|
| 55 |
+
"""Initialize database schema with migration tracking."""
|
| 56 |
+
conn = _create_connection()
|
| 57 |
|
| 58 |
conn.executescript("""
|
| 59 |
CREATE TABLE IF NOT EXISTS entities (
|
|
|
|
| 179 |
('wealthtech', 'WealthTech', 'π', '#7C3AED', 'dpiit_category');
|
| 180 |
""")
|
| 181 |
|
| 182 |
+
# βββ Schema versioning / migrations βββββββββββββββββββββββββββββββββββββββ
|
| 183 |
+
conn.execute("""
|
| 184 |
+
CREATE TABLE IF NOT EXISTS schema_version (
|
| 185 |
+
version INTEGER PRIMARY KEY,
|
| 186 |
+
applied_at TEXT DEFAULT (datetime('now'))
|
| 187 |
+
)
|
| 188 |
+
""")
|
| 189 |
+
conn.execute("INSERT OR IGNORE INTO schema_version (version) VALUES (0)")
|
| 190 |
conn.commit()
|
| 191 |
+
|
| 192 |
+
# Run migrations
|
| 193 |
+
current = conn.execute("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").fetchone()[0]
|
| 194 |
+
if current < 1:
|
| 195 |
+
conn.execute("UPDATE schema_version SET version = 1 WHERE version = 0")
|
| 196 |
+
conn.commit()
|
| 197 |
+
if current < 2:
|
| 198 |
+
try:
|
| 199 |
+
conn.execute("ALTER TABLE entities ADD COLUMN github_stars INTEGER DEFAULT 0")
|
| 200 |
+
except sqlite3.OperationalError:
|
| 201 |
+
pass # Column already exists
|
| 202 |
+
conn.execute("UPDATE schema_version SET version = 2 WHERE version = 1")
|
| 203 |
+
conn.commit()
|
| 204 |
+
|
| 205 |
conn.close()
|
| 206 |
|
| 207 |
|
| 208 |
+
def is_seeded() -> bool:
|
| 209 |
+
"""Check if database already has seed data."""
|
| 210 |
+
try:
|
| 211 |
+
conn = get_db()
|
| 212 |
+
count = conn.execute("SELECT COUNT(*) FROM entities WHERE is_active = 1").fetchone()[0]
|
| 213 |
+
return count > 100
|
| 214 |
+
except Exception:
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
|
| 218 |
def haversine_distance(lat1, lon1, lat2, lon2):
|
| 219 |
R = 6371
|
| 220 |
dlat = math.radians(lat2 - lat1)
|