chowdary1 commited on
Commit
6022edc
Β·
1 Parent(s): 972d42b

Smart DB connection with pooler support and SQLite fallback

Browse files
Files changed (1) hide show
  1. backend/app/db/session.py +243 -62
backend/app/db/session.py CHANGED
@@ -1,85 +1,255 @@
1
  """
2
- Database Session
3
 
4
- Database engine creation and session management.
5
- Uses Supabase transaction pooler (port 6543) for HF Spaces compatibility.
 
 
 
 
 
 
6
  """
7
 
8
  from sqlalchemy import create_engine, text
9
  from sqlalchemy.orm import sessionmaker, Session
10
- from typing import Generator
11
  import os
 
 
12
 
13
  from app.core.config import settings
14
  from app.db.base import Base
15
 
16
 
 
 
 
 
 
 
 
 
17
  # ===========================================
18
- # DATABASE CONNECTION - SUPABASE
19
- # Use port 6543 (transaction pooler) - works on HF Spaces
20
  # ===========================================
21
 
22
- def _get_pooler_url(direct_url: str) -> str:
23
  """
24
- Convert direct Supabase URL (port 5432) to transaction pooler URL (port 6543).
25
-
26
- Direct: postgresql://postgres:password@db.PROJECT.supabase.co:5432/postgres
27
- Pooler: postgresql://postgres:password@db.PROJECT.supabase.co:6543/postgres
28
  """
29
- if ':5432/' in direct_url:
30
- return direct_url.replace(':5432/', ':6543/')
31
- return direct_url
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
 
34
- def _create_engine_with_supabase():
35
  """
36
- Create database engine with Supabase connection.
37
- Uses transaction pooler (port 6543) which works on HF Spaces.
 
 
 
 
 
 
38
  """
39
- db_url = settings.DATABASE_URL
40
 
41
- # If already SQLite, use it directly
42
  if db_url.startswith("sqlite"):
43
- print("πŸ”— Using SQLite database")
44
- return create_engine(
45
- db_url,
46
- connect_args={"check_same_thread": False},
47
- echo=False,
48
- ), True
49
 
50
- # Convert to pooler URL (port 6543)
51
- pooler_url = _get_pooler_url(db_url)
 
 
 
52
 
53
- print(f"πŸ”— Connecting to Supabase (transaction pooler port 6543)...")
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  try:
56
- pg_engine = create_engine(
57
- pooler_url,
 
 
 
 
58
  pool_pre_ping=True,
59
- pool_size=5,
60
- max_overflow=10,
61
- pool_timeout=30,
62
  connect_args={
63
- "connect_timeout": 15,
64
  },
65
  echo=False,
66
  )
67
 
68
  # Test the connection
69
- with pg_engine.connect() as conn:
70
  result = conn.execute(text("SELECT 1"))
71
- print(f"βœ… Supabase connected successfully!")
72
 
73
- return pg_engine, False
74
 
75
  except Exception as e:
76
- print(f"❌ Supabase connection failed: {str(e)[:100]}")
77
- print("⚠️ Check DATABASE_URL environment variable")
78
- raise RuntimeError(f"Cannot connect to Supabase: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
- # Create engine
82
- engine, is_sqlite = _create_engine_with_supabase()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  # Create session factory
85
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@@ -91,11 +261,6 @@ def get_db() -> Generator[Session, None, None]:
91
 
92
  Yields a database session and ensures it's closed after use.
93
  Use with FastAPI's Depends().
94
-
95
- Example:
96
- @app.get("/items")
97
- def get_items(db: Session = Depends(get_db)):
98
- return db.query(Item).all()
99
  """
100
  db = SessionLocal()
101
  try:
@@ -104,30 +269,46 @@ def get_db() -> Generator[Session, None, None]:
104
  db.close()
105
 
106
 
107
- def init_db() -> None:
108
  """
109
  Initialize database tables.
110
 
111
- Creates all tables defined in models if they don't exist.
112
  Should be called on application startup.
113
  """
114
- # Import all models to ensure they're registered with Base
115
- from app.users.models import User, UserSession
116
- from app.interviews.models import InterviewSession, InterviewQuestion, InterviewResponse
117
  from app.interviews.plan_models import InterviewPlan
118
- from app.interviews.live_models import LiveInterviewSession
119
- from app.evaluations.models import Evaluation
 
 
 
 
 
 
 
120
 
121
- # Create tables
122
  Base.metadata.create_all(bind=engine)
123
- print("βœ… Database tables initialized")
124
 
125
 
126
- def get_engine():
127
- """Get the database engine."""
128
- return engine
 
 
 
 
 
129
 
130
 
131
- def is_using_sqlite() -> bool:
132
- """Check if using SQLite (vs PostgreSQL)."""
133
- return is_sqlite
 
 
 
 
 
 
 
1
  """
2
+ Database Session - FINAL BOSS EDITION
3
 
4
+ Handles Supabase connection with multiple fallback strategies:
5
+ 1. Supabase Pooler hostname (pooler.supabase.com) - for HF Spaces (IPv4)
6
+ 2. Direct connection with port 6543 (transaction pooler)
7
+ 3. Direct connection with port 5432
8
+ 4. SQLite fallback - absolute last resort
9
+
10
+ Key insight: HF Spaces blocks IPv6. Supabase direct hostname returns IPv6.
11
+ Solution: Use pooler.supabase.com which supports IPv4.
12
  """
13
 
14
  from sqlalchemy import create_engine, text
15
  from sqlalchemy.orm import sessionmaker, Session
16
+ from typing import Generator, Tuple, Optional
17
  import os
18
+ import re
19
+ import socket
20
 
21
  from app.core.config import settings
22
  from app.db.base import Base
23
 
24
 
25
+ # Force IPv4 for DNS resolution (workaround for HF Spaces IPv6 blocking)
26
+ _original_getaddrinfo = socket.getaddrinfo
27
+
28
+ def _ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
29
+ """Force IPv4 resolution by setting family to AF_INET"""
30
+ return _original_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
31
+
32
+
33
  # ===========================================
34
+ # DATABASE CONNECTION STRATEGIES
 
35
  # ===========================================
36
 
37
+ def _extract_supabase_info(url: str) -> Optional[dict]:
38
  """
39
+ Extract Supabase connection info from DATABASE_URL.
 
 
 
40
  """
41
+ # Match: postgresql://user:password@db.PROJECT.supabase.co:PORT/database
42
+ match = re.match(
43
+ r'postgresql://([^:]+):([^@]+)@db\.([^.]+)\.supabase\.co:(\d+)/(.+)',
44
+ url
45
+ )
46
+
47
+ if match:
48
+ return {
49
+ 'user': match.group(1),
50
+ 'password': match.group(2),
51
+ 'project_ref': match.group(3),
52
+ 'port': match.group(4),
53
+ 'database': match.group(5),
54
+ }
55
+ return None
56
 
57
 
58
+ def _build_connection_urls(db_url: str) -> list:
59
  """
60
+ Build list of connection URLs to try, in order of preference.
61
+
62
+ Priority order:
63
+ 1. SUPABASE_POOLER_URL if set (user-provided pooler URL)
64
+ 2. Session pooler (port 5432 on pooler.supabase.com) - IPv4 friendly
65
+ 3. Transaction pooler (port 6543 on pooler.supabase.com) - serverless optimized
66
+ 4. Direct port 6543 - transaction mode on direct host
67
+ 5. Direct port 5432 - original connection
68
  """
69
+ urls = []
70
 
71
+ # Check if it's already SQLite
72
  if db_url.startswith("sqlite"):
73
+ return [("sqlite", db_url)]
 
 
 
 
 
74
 
75
+ # PRIORITY 0: Check for user-provided pooler URL
76
+ pooler_url = os.environ.get("SUPABASE_POOLER_URL")
77
+ if pooler_url:
78
+ print(f" πŸ“ Found SUPABASE_POOLER_URL environment variable")
79
+ urls.append(("env-pooler", pooler_url))
80
 
81
+ # Extract Supabase info
82
+ info = _extract_supabase_info(db_url)
83
 
84
+ if info:
85
+ project = info['project_ref']
86
+ password = info['password']
87
+ database = info['database']
88
+
89
+ # Common AWS regions for Supabase
90
+ regions = [
91
+ 'ap-south-1', # India (likely region based on project)
92
+ 'us-east-1', # US East (most common)
93
+ 'ap-southeast-1', # Singapore
94
+ 'eu-west-1', # Europe
95
+ 'us-west-1', # US West
96
+ 'eu-central-1', # Frankfurt
97
+ ]
98
+
99
+ # Strategy 1: Session Mode Pooler (port 5432) - IPv4 compatible
100
+ # Format: postgres://postgres.PROJECT:PASSWORD@aws-0-REGION.pooler.supabase.com:5432/postgres
101
+ for region in regions:
102
+ session_url = f"postgresql://postgres.{project}:{password}@aws-0-{region}.pooler.supabase.com:5432/{database}"
103
+ urls.append((f"session-{region}", session_url))
104
+
105
+ # Strategy 2: Transaction Mode Pooler (port 6543) - serverless optimized
106
+ for region in regions:
107
+ txn_url = f"postgresql://postgres.{project}:{password}@aws-0-{region}.pooler.supabase.com:6543/{database}"
108
+ urls.append((f"txn-{region}", txn_url))
109
+
110
+ # Strategy 3: Direct connection with port 6543 (transaction mode on direct host)
111
+ direct_6543 = db_url.replace(':5432/', ':6543/')
112
+ urls.append(("direct-6543", direct_6543))
113
+
114
+ # Strategy 4: Direct connection with port 5432 (original)
115
+ urls.append(("direct-5432", db_url))
116
+ else:
117
+ # Not a standard Supabase URL, try as-is
118
+ urls.append(("custom", db_url))
119
+
120
+ return urls
121
+
122
+
123
+ def _test_connection(url: str, name: str, timeout: int = 10, force_ipv4: bool = False) -> Optional[object]:
124
+ """
125
+ Test a database connection and return engine if successful.
126
+ """
127
  try:
128
+ # Optionally force IPv4 resolution
129
+ if force_ipv4:
130
+ socket.getaddrinfo = _ipv4_getaddrinfo
131
+
132
+ engine = create_engine(
133
+ url,
134
  pool_pre_ping=True,
135
+ pool_size=3,
136
+ max_overflow=5,
137
+ pool_timeout=timeout,
138
  connect_args={
139
+ "connect_timeout": timeout,
140
  },
141
  echo=False,
142
  )
143
 
144
  # Test the connection
145
+ with engine.connect() as conn:
146
  result = conn.execute(text("SELECT 1"))
147
+ result.fetchone()
148
 
149
+ return engine
150
 
151
  except Exception as e:
152
+ error_msg = str(e)[:80]
153
+ print(f" ❌ {name}: {error_msg}")
154
+ return None
155
+ finally:
156
+ # Restore original getaddrinfo
157
+ if force_ipv4:
158
+ socket.getaddrinfo = _original_getaddrinfo
159
+
160
+
161
+ def _create_sqlite_fallback():
162
+ """
163
+ Create SQLite database as absolute last resort.
164
+ """
165
+ sqlite_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "data", "fallback.db")
166
+ os.makedirs(os.path.dirname(sqlite_path), exist_ok=True)
167
+
168
+ sqlite_url = f"sqlite:///{sqlite_path}"
169
+
170
+ engine = create_engine(
171
+ sqlite_url,
172
+ connect_args={"check_same_thread": False},
173
+ echo=False,
174
+ )
175
+
176
+ return engine, True
177
 
178
 
179
+ def _create_engine_smart():
180
+ """
181
+ Create database engine using smart connection strategy.
182
+ Tries multiple connection methods until one works.
183
+ """
184
+ db_url = settings.DATABASE_URL
185
+
186
+ print("πŸ”— Connecting to database...")
187
+ print(f"πŸ“¦ Environment: {os.environ.get('ENVIRONMENT', 'development')}")
188
+
189
+ # If already SQLite, use it directly
190
+ if db_url.startswith("sqlite"):
191
+ print("πŸ“ Using SQLite database (configured)")
192
+ return create_engine(
193
+ db_url,
194
+ connect_args={"check_same_thread": False},
195
+ echo=False,
196
+ ), True
197
+
198
+ # Get all connection URLs to try
199
+ connection_urls = _build_connection_urls(db_url)
200
+
201
+ print(f"🎯 Trying {len(connection_urls)} connection strategies...")
202
+
203
+ # Try each connection strategy
204
+ for name, url in connection_urls:
205
+ print(f" πŸ”„ Trying {name}...")
206
+
207
+ # Force IPv4 for direct connections (HF Spaces blocks IPv6)
208
+ force_ipv4 = "direct-" in name or name == "custom"
209
+ engine = _test_connection(url, name, force_ipv4=force_ipv4)
210
+
211
+ if engine:
212
+ print(f"βœ… Connected via {name}!")
213
+
214
+ # Verify we can query
215
+ try:
216
+ with engine.connect() as conn:
217
+ result = conn.execute(text("SELECT COUNT(*) FROM users"))
218
+ count = result.fetchone()[0]
219
+ print(f" πŸ“Š Users in database: {count}")
220
+ except Exception:
221
+ print(f" πŸ“Š Database connected (tables may not exist yet)")
222
+
223
+ return engine, False
224
+
225
+ # All strategies failed - use SQLite fallback
226
+ print("")
227
+ print("=" * 60)
228
+ print("⚠️ ALL DATABASE CONNECTIONS FAILED!")
229
+ print("=" * 60)
230
+ print("")
231
+ print("Could not connect to Supabase. This usually happens on HF Spaces")
232
+ print("because the direct database hostname only supports IPv6.")
233
+ print("")
234
+ print("πŸ”§ TO FIX: Set SUPABASE_POOLER_URL in HF Spaces Secrets:")
235
+ print(" 1. Go to Supabase Dashboard β†’ Your Project β†’ Connect")
236
+ print(" 2. Select 'Transaction pooler' tab")
237
+ print(" 3. Copy the connection string")
238
+ print(" 4. Add as HF Spaces Secret: SUPABASE_POOLER_URL")
239
+ print("")
240
+ print("πŸ”„ Using SQLite fallback to keep app running...")
241
+ print(" ⚠️ Data will NOT sync to Supabase!")
242
+ print("")
243
+
244
+ return _create_sqlite_fallback()
245
+
246
+
247
+ # ===========================================
248
+ # INITIALIZE DATABASE
249
+ # ===========================================
250
+
251
+ # Create engine using smart connection strategy
252
+ engine, is_sqlite = _create_engine_smart()
253
 
254
  # Create session factory
255
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
261
 
262
  Yields a database session and ensures it's closed after use.
263
  Use with FastAPI's Depends().
 
 
 
 
 
264
  """
265
  db = SessionLocal()
266
  try:
 
269
  db.close()
270
 
271
 
272
+ def init_db():
273
  """
274
  Initialize database tables.
275
 
276
+ Creates all tables defined in the models.
277
  Should be called on application startup.
278
  """
279
+ from app.users.models import User
280
+ from app.interviews.models import Interview
 
281
  from app.interviews.plan_models import InterviewPlan
282
+ from app.interviews.live_models import LiveInterview
283
+ from app.evaluations.models import InterviewEvaluation
284
+ from app.ats.models import ATSConfiguration, CandidateProfile
285
+ from app.admin.models import AdminSettings
286
+ from app.roadmap.models import RoadmapProgress
287
+ from app.personalities.models import InterviewerPersonality
288
+
289
+ db_type = "SQLite" if is_sqlite else "PostgreSQL (Supabase)"
290
+ print(f"πŸ—„οΈ Initializing {db_type} tables...")
291
 
 
292
  Base.metadata.create_all(bind=engine)
293
+ print(f"βœ… Database tables ready!")
294
 
295
 
296
+ def close_db():
297
+ """
298
+ Close database connections.
299
+
300
+ Should be called on application shutdown.
301
+ """
302
+ engine.dispose()
303
+ print("πŸ”Œ Database connections closed")
304
 
305
 
306
+ # Export for use in other modules
307
+ __all__ = [
308
+ 'engine',
309
+ 'SessionLocal',
310
+ 'get_db',
311
+ 'init_db',
312
+ 'close_db',
313
+ 'is_sqlite'
314
+ ]