goctests0 commited on
Commit
2020fa0
·
verified ·
1 Parent(s): 1d69108

Upload database.py

Browse files
Files changed (1) hide show
  1. database.py +102 -166
database.py CHANGED
@@ -1,218 +1,154 @@
1
  """
2
- Database access layer for TalkToDoc.
3
- Handles both SQLite (local development) and PostgreSQL (production) through
4
- a single shared query interface. The only difference between the two is how
5
- get_connection() builds the connection object; every function above that is
6
- identical regardless of which database is running underneath.
7
-
8
- Connection management: every public function in this module uses the
9
- _connection() context manager (not get_connection() directly). That
10
- context manager commits on success, rolls back on exception, and always
11
- closes the connection before returning control. The bare
12
- `with get_connection() as conn:` pattern that psycopg2 and sqlite3 both
13
- support commits/rolls back correctly but never closes the connection,
14
- which leaks file handles against SQLite and exhausts the Postgres
15
- connection pool in production over time.
16
  """
17
 
18
  import os
19
- import contextlib
20
 
21
- from dotenv import load_dotenv
22
 
23
- load_dotenv("env")
 
 
24
 
25
  DATABASE_URL = os.environ.get("DATABASE_URL")
26
 
 
 
 
 
27
 
28
  def get_connection():
29
- """
30
- Returns a raw database connection. Callers should use _connection()
31
- instead, which wraps this and guarantees the connection is closed.
32
- """
33
  if DATABASE_URL:
34
- import psycopg2
35
- import psycopg2.extras
36
- conn = psycopg2.connect(DATABASE_URL, cursor_factory=psycopg2.extras.RealDictCursor)
37
- return conn
38
- else:
39
- import sqlite3
40
- conn = sqlite3.connect(
41
- os.path.join(os.path.dirname(os.path.abspath(__file__)), "talktodoc.db")
42
- )
43
- conn.row_factory = sqlite3.Row
44
- return conn
45
-
46
-
47
- @contextlib.contextmanager
48
- def _connection():
49
- """
50
- Context manager that opens a connection, yields it, commits on clean
51
- exit, rolls back on exception, and always closes. Use this everywhere
52
- instead of `with get_connection() as conn:`.
53
- """
54
- conn = get_connection()
55
- try:
56
- yield conn
57
- conn.commit()
58
- except Exception:
59
- conn.rollback()
60
- raise
61
- finally:
62
- conn.close()
63
-
64
-
65
- def _run(connection, sql, params=()):
66
  cursor = connection.cursor()
67
- cursor.execute(sql, params)
68
  return cursor
69
 
70
 
71
- def _placeholder():
72
- """
73
- SQLite uses ? for bind parameters; psycopg2 uses %s.
74
- Returns the right one for whichever database is active.
75
- """
76
- return "%s" if DATABASE_URL else "?"
77
 
78
 
79
  def init_db():
80
- schema_file = "schema_postgres.sql" if DATABASE_URL else "schema.sql"
81
- schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), schema_file)
82
- with open(schema_path) as f:
83
- schema = f.read()
84
- with _connection() as conn:
85
  if DATABASE_URL:
86
- for statement in schema.split(";"):
87
- stmt = statement.strip()
88
- if stmt:
89
- _run(conn, stmt)
90
  else:
91
- conn.executescript(schema)
92
 
93
 
94
  def add_user(name, preferred_language, role):
95
- p = _placeholder()
96
- with _connection() as conn:
97
- if DATABASE_URL:
98
- cursor = _run(
99
- conn,
100
- f"INSERT INTO app_user (name, preferred_language, role) VALUES ({p}, {p}, {p}) RETURNING id",
101
- (name, preferred_language, role),
102
- )
103
- return cursor.fetchone()["id"]
104
- else:
105
- cursor = _run(
106
- conn,
107
- f"INSERT INTO app_user (name, preferred_language, role) VALUES ({p}, {p}, {p})",
108
- (name, preferred_language, role),
109
- )
110
- return cursor.lastrowid
111
-
112
-
113
- def add_session(user_id, start_time):
114
- p = _placeholder()
115
- with _connection() as conn:
116
- if DATABASE_URL:
117
- cursor = _run(
118
- conn,
119
- f"INSERT INTO session (user_id, start_time) VALUES ({p}, {p}) RETURNING id",
120
- (user_id, start_time),
121
- )
122
- return cursor.fetchone()["id"]
123
- else:
124
- cursor = _run(
125
- conn,
126
- f"INSERT INTO session (user_id, start_time) VALUES ({p}, {p})",
127
- (user_id, start_time),
128
- )
129
- return cursor.lastrowid
130
-
131
-
132
- def end_session(session_id, end_time):
133
- p = _placeholder()
134
- with _connection() as conn:
135
- _run(
136
- conn,
137
- f"UPDATE session SET end_time = {p} WHERE id = {p}",
138
- (end_time, session_id),
139
  )
140
 
141
 
142
- def add_interaction(user_id, input_text, detected_language, translated_text,
143
- nlu_summary, timestamp):
144
- p = _placeholder()
145
-
146
- sql = (
147
- f"INSERT INTO interaction "
148
- f"(user_id, input_text, detected_language, translated_text, nlu_summary, timestamp) "
149
- f"VALUES ({p}, {p}, {p}, {p}, {p}, {p})"
150
- )
151
-
152
- params = (
153
- user_id,
154
- input_text,
155
- detected_language,
156
- translated_text,
157
- nlu_summary,
158
- timestamp
159
- )
160
-
161
- with _connection() as conn:
162
- if DATABASE_URL:
163
- cursor = _run(conn, sql + " RETURNING id", params)
164
- return cursor.fetchone()["id"]
165
- else:
166
- cursor = _run(conn, sql, params)
167
- return cursor.lastrowid
168
-
169
-
170
- def get_interaction(interaction_id):
171
- p = _placeholder()
172
- with _connection() as conn:
173
- cursor = _run(
174
- conn,
175
- f"SELECT * FROM interaction WHERE id = {p}",
176
- (interaction_id,),
177
  )
178
- row = cursor.fetchone()
179
- return dict(row) if row else None
180
 
181
 
182
- def update_interaction_response(interaction_id, provider_response, translated_response):
183
- p = _placeholder()
184
- with _connection() as conn:
185
- _run(
186
- conn,
187
- f"UPDATE interaction SET provider_response = {p}, translated_response = {p} WHERE id = {p}",
188
- (provider_response, translated_response, interaction_id),
 
 
 
 
 
 
189
  )
190
 
191
 
192
  def get_interactions_for_user(user_id):
193
- p = _placeholder()
194
- with _connection() as conn:
195
  cursor = _run(
196
- conn,
197
- f"SELECT * FROM interaction WHERE user_id = {p} ORDER BY timestamp",
198
  (user_id,),
199
  )
200
  return [dict(row) for row in cursor.fetchall()]
201
 
202
 
203
  def get_pending_interactions():
204
- with _connection() as conn:
205
  cursor = _run(
206
- conn,
207
  "SELECT * FROM interaction WHERE provider_response IS NULL ORDER BY timestamp",
208
  )
209
  return [dict(row) for row in cursor.fetchall()]
210
 
211
 
212
  def get_completed_interactions():
213
- with _connection() as conn:
214
  cursor = _run(
215
- conn,
216
  "SELECT * FROM interaction WHERE provider_response IS NOT NULL ORDER BY timestamp DESC",
217
  )
218
  return [dict(row) for row in cursor.fetchall()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Database layer for TalkToDoc.
3
+ Local development and testing use SQLite (no setup needed). When deployed,
4
+ DATABASE_URL is set by Render and the app uses Postgres instead, since
5
+ Render's free tier wipes local files like a SQLite database on every
6
+ restart.
7
+
8
+ Queries are written once using SQLite-style '?' placeholders and adapted
9
+ automatically for Postgres, so there's a single query per function rather
10
+ than two versions of everything.
 
 
 
 
 
11
  """
12
 
13
  import os
14
+ from pathlib import Path
15
 
16
+ import sqlite3
17
 
18
+ DB_PATH = Path(__file__).parent / "talktodoc.db"
19
+ SCHEMA_PATH = Path(__file__).parent / "schema.sql"
20
+ SCHEMA_PATH_POSTGRES = Path(__file__).parent / "schema_postgres.sql"
21
 
22
  DATABASE_URL = os.environ.get("DATABASE_URL")
23
 
24
+ if DATABASE_URL:
25
+ import psycopg2
26
+ import psycopg2.extras
27
+
28
 
29
  def get_connection():
 
 
 
 
30
  if DATABASE_URL:
31
+ connection = psycopg2.connect(DATABASE_URL, cursor_factory=psycopg2.extras.RealDictCursor)
32
+ # psycopg2 does not auto-commit by default. Without this, every
33
+ # INSERT and UPDATE is silently rolled back when the connection
34
+ # closes, so nothing is ever actually written to Postgres.
35
+ # SQLite's context manager commits on clean exit, so this brings
36
+ # the Postgres path in line with SQLite's behaviour.
37
+ connection.autocommit = True
38
+ return connection
39
+ connection = sqlite3.connect(DB_PATH)
40
+ connection.row_factory = sqlite3.Row
41
+ connection.execute("PRAGMA foreign_keys = ON")
42
+ return connection
43
+
44
+
45
+ def _run(connection, query, params=()):
46
+ if DATABASE_URL:
47
+ query = query.replace("?", "%s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  cursor = connection.cursor()
49
+ cursor.execute(query, params)
50
  return cursor
51
 
52
 
53
+ def _insert_and_get_id(connection, query, params):
54
+ if DATABASE_URL:
55
+ cursor = _run(connection, query + " RETURNING id", params)
56
+ return cursor.fetchone()["id"]
57
+ cursor = _run(connection, query, params)
58
+ return cursor.lastrowid
59
 
60
 
61
  def init_db():
62
+ schema_path = SCHEMA_PATH_POSTGRES if DATABASE_URL else SCHEMA_PATH
63
+ schema_sql = schema_path.read_text()
64
+ with get_connection() as connection:
 
 
65
  if DATABASE_URL:
66
+ connection.cursor().execute(schema_sql)
 
 
 
67
  else:
68
+ connection.executescript(schema_sql)
69
 
70
 
71
  def add_user(name, preferred_language, role):
72
+ with get_connection() as connection:
73
+ return _insert_and_get_id(
74
+ connection,
75
+ "INSERT INTO app_user (name, preferred_language, role) VALUES (?, ?, ?)",
76
+ (name, preferred_language, role),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  )
78
 
79
 
80
+ def add_session(user_id, start_time):
81
+ with get_connection() as connection:
82
+ return _insert_and_get_id(
83
+ connection,
84
+ "INSERT INTO session (user_id, start_time) VALUES (?, ?)",
85
+ (user_id, start_time),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  )
 
 
87
 
88
 
89
+ def end_session(session_id, end_time):
90
+ with get_connection() as connection:
91
+ _run(connection, "UPDATE session SET end_time = ? WHERE id = ?", (end_time, session_id))
92
+
93
+
94
+ def add_interaction(user_id, input_text, detected_language, translated_text, nlu_summary, timestamp):
95
+ with get_connection() as connection:
96
+ return _insert_and_get_id(
97
+ connection,
98
+ """INSERT INTO interaction
99
+ (user_id, input_text, detected_language, translated_text, nlu_summary, timestamp)
100
+ VALUES (?, ?, ?, ?, ?, ?)""",
101
+ (user_id, input_text, detected_language, translated_text, nlu_summary, timestamp),
102
  )
103
 
104
 
105
  def get_interactions_for_user(user_id):
106
+ with get_connection() as connection:
 
107
  cursor = _run(
108
+ connection,
109
+ "SELECT * FROM interaction WHERE user_id = ? ORDER BY timestamp",
110
  (user_id,),
111
  )
112
  return [dict(row) for row in cursor.fetchall()]
113
 
114
 
115
  def get_pending_interactions():
116
+ with get_connection() as connection:
117
  cursor = _run(
118
+ connection,
119
  "SELECT * FROM interaction WHERE provider_response IS NULL ORDER BY timestamp",
120
  )
121
  return [dict(row) for row in cursor.fetchall()]
122
 
123
 
124
  def get_completed_interactions():
125
+ with get_connection() as connection:
126
  cursor = _run(
127
+ connection,
128
  "SELECT * FROM interaction WHERE provider_response IS NOT NULL ORDER BY timestamp DESC",
129
  )
130
  return [dict(row) for row in cursor.fetchall()]
131
+
132
+
133
+ def get_interaction(interaction_id):
134
+ with get_connection() as connection:
135
+ cursor = _run(connection, "SELECT * FROM interaction WHERE id = ?", (interaction_id,))
136
+ row = cursor.fetchone()
137
+ return dict(row) if row else None
138
+
139
+
140
+ def update_interaction_response(interaction_id, provider_response, translated_response):
141
+ with get_connection() as connection:
142
+ _run(
143
+ connection,
144
+ "UPDATE interaction SET provider_response = ?, translated_response = ? WHERE id = ?",
145
+ (provider_response, translated_response, interaction_id),
146
+ )
147
+
148
+
149
+ if __name__ == "__main__":
150
+ init_db()
151
+ if DATABASE_URL:
152
+ print("Database initialized (Postgres)")
153
+ else:
154
+ print("Database created at:", DB_PATH)