amaye15 commited on
Commit
c43b33b
·
1 Parent(s): bf42631
Files changed (4) hide show
  1. .dockerignore +1 -1
  2. .gitignore +2 -1
  3. index.html +492 -0
  4. main.py +160 -101
.dockerignore CHANGED
@@ -12,4 +12,4 @@ venv/
12
  *.db.wal
13
  data/*.db
14
  data/*.db.wal
15
- # Add other files/directories to ignore if needed
 
12
  *.db.wal
13
  data/*.db
14
  data/*.db.wal
15
+ # Add other files/directories to ignore if needed
.gitignore CHANGED
@@ -42,4 +42,5 @@ Thumbs.db
42
 
43
  # IDE files
44
  .idea/
45
- .vscode/
 
 
42
 
43
  # IDE files
44
  .idea/
45
+ .vscode/
46
+ *.csv
index.html ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DuckDB Explorer</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 0;
12
+ display: flex;
13
+ flex-direction: column;
14
+ height: 100vh;
15
+ background-color: #f4f7f6;
16
+ color: #333;
17
+ }
18
+
19
+ header {
20
+ background-color: #4CAF50;
21
+ color: white;
22
+ padding: 10px 20px;
23
+ display: flex;
24
+ align-items: center;
25
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
26
+ }
27
+
28
+ header input[type="text"] {
29
+ flex-grow: 1;
30
+ margin-right: 10px;
31
+ padding: 8px;
32
+ border: 1px solid #ccc;
33
+ border-radius: 4px;
34
+ }
35
+
36
+ header button {
37
+ padding: 8px 15px;
38
+ background-color: #367c39;
39
+ color: white;
40
+ border: none;
41
+ border-radius: 4px;
42
+ cursor: pointer;
43
+ transition: background-color 0.2s;
44
+ }
45
+
46
+ header button:hover {
47
+ background-color: #2a622d;
48
+ }
49
+
50
+ .container {
51
+ display: flex;
52
+ flex: 1;
53
+ overflow: hidden; /* Prevent overall container scroll */
54
+ }
55
+
56
+ #sidebar {
57
+ width: 200px;
58
+ background-color: #e9ecef;
59
+ padding: 15px;
60
+ overflow-y: auto;
61
+ border-right: 1px solid #dee2e6;
62
+ }
63
+
64
+ #sidebar h3 {
65
+ margin-top: 0;
66
+ color: #495057;
67
+ }
68
+
69
+ #tableList {
70
+ list-style: none;
71
+ padding: 0;
72
+ margin: 0;
73
+ }
74
+
75
+ #tableList li {
76
+ padding: 8px 5px;
77
+ cursor: pointer;
78
+ border-radius: 4px;
79
+ margin-bottom: 5px;
80
+ transition: background-color 0.2s;
81
+ }
82
+
83
+ #tableList li:hover, #tableList li.active {
84
+ background-color: #d4dadf;
85
+ }
86
+
87
+ #mainContent {
88
+ flex: 1;
89
+ display: flex;
90
+ flex-direction: column;
91
+ padding: 20px;
92
+ overflow: hidden; /* Prevent main content scroll */
93
+ }
94
+
95
+ .content-area {
96
+ flex: 1;
97
+ display: flex;
98
+ flex-direction: column;
99
+ overflow: hidden; /* Child takes scroll */
100
+ }
101
+
102
+ #schemaDisplay, #dataDisplayContainer, #queryResultContainer {
103
+ background-color: #fff;
104
+ border: 1px solid #dee2e6;
105
+ border-radius: 4px;
106
+ padding: 15px;
107
+ margin-bottom: 20px;
108
+ overflow: auto; /* Allow scrolling within these areas */
109
+ }
110
+
111
+ #dataDisplayContainer {
112
+ flex: 1; /* Takes remaining space */
113
+ }
114
+
115
+
116
+ #queryArea {
117
+ margin-top: auto; /* Push query area to bottom if space */
118
+ padding-top: 20px;
119
+ border-top: 1px solid #dee2e6;
120
+ }
121
+
122
+ #queryArea textarea {
123
+ width: 100%;
124
+ min-height: 80px;
125
+ padding: 10px;
126
+ border: 1px solid #ccc;
127
+ border-radius: 4px;
128
+ box-sizing: border-box;
129
+ font-family: monospace;
130
+ margin-bottom: 10px;
131
+ }
132
+
133
+ #queryArea button {
134
+ padding: 10px 20px;
135
+ background-color: #007bff;
136
+ color: white;
137
+ border: none;
138
+ border-radius: 4px;
139
+ cursor: pointer;
140
+ transition: background-color 0.2s;
141
+ }
142
+ #queryArea button:hover {
143
+ background-color: #0056b3;
144
+ }
145
+
146
+ table {
147
+ width: 100%;
148
+ border-collapse: collapse;
149
+ margin-top: 10px;
150
+ }
151
+
152
+ th, td {
153
+ border: 1px solid #ddd;
154
+ padding: 8px;
155
+ text-align: left;
156
+ white-space: nowrap;
157
+ }
158
+
159
+ th {
160
+ background-color: #f2f2f2;
161
+ font-weight: bold;
162
+ }
163
+
164
+ tr:nth-child(even) {
165
+ background-color: #f9f9f9;
166
+ }
167
+
168
+ #statusMessage {
169
+ padding: 10px;
170
+ margin-top: 10px;
171
+ border-radius: 4px;
172
+ display: none; /* Hidden by default */
173
+ }
174
+
175
+ #statusMessage.success {
176
+ background-color: #d4edda;
177
+ color: #155724;
178
+ border: 1px solid #c3e6cb;
179
+ }
180
+
181
+ #statusMessage.error {
182
+ background-color: #f8d7da;
183
+ color: #721c24;
184
+ border: 1px solid #f5c6cb;
185
+ }
186
+ .loader {
187
+ border: 4px solid #f3f3f3; /* Light grey */
188
+ border-top: 4px solid #3498db; /* Blue */
189
+ border-radius: 50%;
190
+ width: 20px;
191
+ height: 20px;
192
+ animation: spin 1s linear infinite;
193
+ display: none; /* Hidden by default */
194
+ margin-left: 10px;
195
+ }
196
+
197
+ @keyframes spin {
198
+ 0% { transform: rotate(0deg); }
199
+ 100% { transform: rotate(360deg); }
200
+ }
201
+ </style>
202
+ </head>
203
+ <body>
204
+
205
+ <header>
206
+ <label for="apiUrl" style="margin-right: 10px; font-weight: bold; color: white;">API URL:</label>
207
+ <input type="text" id="apiUrl" value="http://localhost:8000" placeholder="Enter API Base URL">
208
+ <button id="connectButton">Connect</button>
209
+ <div class="loader" id="loadingIndicator"></div>
210
+ </header>
211
+
212
+ <div class="container">
213
+ <aside id="sidebar">
214
+ <h3>Tables</h3>
215
+ <ul id="tableList">
216
+ <!-- Table names will be loaded here -->
217
+ </ul>
218
+ </aside>
219
+
220
+ <main id="mainContent">
221
+ <div class="content-area">
222
+ <div id="schemaDisplay">
223
+ <h4>Schema</h4>
224
+ <p>Select a table from the list to view its schema.</p>
225
+ <table id="schemaTable"></table>
226
+ </div>
227
+ <div id="dataDisplayContainer">
228
+ <h4>Data <span id="tableDataHeader"></span></h4>
229
+ <p>Select a table from the list to view its data (limited rows).</p>
230
+ <div id="dataDisplay" style="max-height: 300px; overflow-y: auto;">
231
+ <table id="dataTable"></table>
232
+ </div>
233
+ </div>
234
+ <div id="queryResultContainer" style="display: none;">
235
+ <h4>Query Result</h4>
236
+ <div id="queryResultDisplay" style="max-height: 300px; overflow-y: auto;">
237
+ <table id="queryResultTable"></table>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ <div id="queryArea">
243
+ <h4>Custom SQL Query (SELECT only)</h4>
244
+ <textarea id="sqlInput" placeholder="Enter your SELECT query here..."></textarea>
245
+ <button id="runSqlButton">Run SQL</button>
246
+ </div>
247
+
248
+ <div id="statusMessage"></div>
249
+ </main>
250
+ </div>
251
+
252
+ <script>
253
+ const apiUrlInput = document.getElementById('apiUrl');
254
+ const connectButton = document.getElementById('connectButton');
255
+ const tableList = document.getElementById('tableList');
256
+ const schemaDisplay = document.getElementById('schemaDisplay');
257
+ const schemaTable = document.getElementById('schemaTable');
258
+ const dataDisplayContainer = document.getElementById('dataDisplayContainer');
259
+ const dataDisplay = document.getElementById('dataDisplay');
260
+ const dataTable = document.getElementById('dataTable');
261
+ const tableDataHeader = document.getElementById('tableDataHeader');
262
+ const sqlInput = document.getElementById('sqlInput');
263
+ const runSqlButton = document.getElementById('runSqlButton');
264
+ const queryResultContainer = document.getElementById('queryResultContainer');
265
+ const queryResultDisplay = document.getElementById('queryResultDisplay');
266
+ const queryResultTable = document.getElementById('queryResultTable');
267
+ const statusMessage = document.getElementById('statusMessage');
268
+ const loadingIndicator = document.getElementById('loadingIndicator');
269
+
270
+ let API_BASE_URL = '';
271
+ let currentTables = [];
272
+ let selectedTable = null;
273
+
274
+ // --- Utility Functions ---
275
+
276
+ function showLoader(show) {
277
+ loadingIndicator.style.display = show ? 'inline-block' : 'none';
278
+ }
279
+
280
+ function showStatus(message, isError = false) {
281
+ statusMessage.textContent = message;
282
+ statusMessage.className = isError ? 'error' : 'success';
283
+ statusMessage.style.display = 'block';
284
+ // Automatically hide after a few seconds
285
+ setTimeout(() => { statusMessage.style.display = 'none'; }, 5000);
286
+ }
287
+
288
+ function clearStatus() {
289
+ statusMessage.textContent = '';
290
+ statusMessage.style.display = 'none';
291
+ }
292
+
293
+ async function fetchAPI(endpoint, options = {}) {
294
+ showLoader(true);
295
+ clearStatus();
296
+ const url = `${API_BASE_URL}${endpoint}`;
297
+ try {
298
+ const response = await fetch(url, options);
299
+ if (!response.ok) {
300
+ let errorDetail = `HTTP error! status: ${response.status}`;
301
+ try {
302
+ const errorJson = await response.json();
303
+ errorDetail += ` - ${errorJson.detail || JSON.stringify(errorJson)}`;
304
+ } catch (e) { /* Ignore if response is not JSON */ }
305
+ throw new Error(errorDetail);
306
+ }
307
+ // Handle empty responses for non-JSON endpoints if necessary
308
+ if (response.headers.get("content-type")?.includes("application/json")) {
309
+ return await response.json();
310
+ }
311
+ return await response.text(); // Or handle other types
312
+ } catch (error) {
313
+ console.error('API Fetch Error:', error);
314
+ showStatus(`Error: ${error.message}`, true);
315
+ throw error; // Re-throw to stop further processing
316
+ } finally {
317
+ showLoader(false);
318
+ }
319
+ }
320
+
321
+ function renderTable(data, tableElement) {
322
+ tableElement.innerHTML = ''; // Clear previous content
323
+
324
+ if (!data || data.length === 0) {
325
+ tableElement.innerHTML = '<tbody><tr><td>No data available.</td></tr></tbody>';
326
+ return;
327
+ }
328
+
329
+ const headers = Object.keys(data[0]);
330
+ const thead = tableElement.createTHead();
331
+ const headerRow = thead.insertRow();
332
+ headers.forEach(headerText => {
333
+ const th = document.createElement('th');
334
+ th.textContent = headerText;
335
+ headerRow.appendChild(th);
336
+ });
337
+
338
+ const tbody = tableElement.createTBody();
339
+ data.forEach(rowData => {
340
+ const row = tbody.insertRow();
341
+ headers.forEach(header => {
342
+ const cell = row.insertCell();
343
+ // Handle null or undefined gracefully
344
+ cell.textContent = rowData[header] === null || rowData[header] === undefined ? 'NULL' : String(rowData[header]);
345
+ });
346
+ });
347
+ }
348
+
349
+ function renderSchema(schemaData) {
350
+ const tableElement = schemaTable;
351
+ tableElement.innerHTML = ''; // Clear previous
352
+
353
+ if (!schemaData || !schemaData.columns || schemaData.columns.length === 0) {
354
+ schemaDisplay.innerHTML = '<h4>Schema</h4><p>No schema information available.</p>';
355
+ return;
356
+ }
357
+
358
+ schemaDisplay.innerHTML = '<h4>Schema</h4>'; // Reset header
359
+
360
+ const thead = tableElement.createTHead();
361
+ const headerRow = thead.insertRow();
362
+ ['Name', 'Type'].forEach(headerText => {
363
+ const th = document.createElement('th');
364
+ th.textContent = headerText;
365
+ headerRow.appendChild(th);
366
+ });
367
+
368
+ const tbody = tableElement.createTBody();
369
+ schemaData.columns.forEach(column => {
370
+ const row = tbody.insertRow();
371
+ row.insertCell().textContent = column.name;
372
+ row.insertCell().textContent = column.type;
373
+ });
374
+ }
375
+
376
+
377
+ // --- Event Handlers ---
378
+
379
+ async function loadTables() {
380
+ API_BASE_URL = apiUrlInput.value.trim().replace(/\/$/, ''); // Remove trailing slash
381
+ if (!API_BASE_URL) {
382
+ showStatus("API URL cannot be empty.", true);
383
+ return;
384
+ }
385
+ try {
386
+ // Optional: Ping root or health endpoint first
387
+ // await fetchAPI('/');
388
+ currentTables = await fetchAPI('/tables');
389
+ displayTables(currentTables);
390
+ showStatus("Connected. Tables loaded.", false);
391
+ // Clear previous displays
392
+ schemaDisplay.innerHTML = '<h4>Schema</h4><p>Select a table from the list.</p>';
393
+ dataTable.innerHTML = '';
394
+ tableDataHeader.textContent = '';
395
+ queryResultContainer.style.display = 'none';
396
+ } catch (error) {
397
+ tableList.innerHTML = '<li>Error loading tables.</li>';
398
+ }
399
+ }
400
+
401
+ function displayTables(tables) {
402
+ tableList.innerHTML = ''; // Clear list
403
+ if (tables.length === 0) {
404
+ tableList.innerHTML = '<li>No tables found.</li>';
405
+ return;
406
+ }
407
+ tables.sort().forEach(tableName => {
408
+ const li = document.createElement('li');
409
+ li.textContent = tableName;
410
+ li.dataset.tableName = tableName; // Store table name
411
+ li.onclick = () => handleTableSelection(li);
412
+ tableList.appendChild(li);
413
+ });
414
+ }
415
+
416
+ async function handleTableSelection(listItem) {
417
+ // Remove active class from previously selected
418
+ const currentActive = tableList.querySelector('.active');
419
+ if (currentActive) {
420
+ currentActive.classList.remove('active');
421
+ }
422
+ // Add active class to newly selected
423
+ listItem.classList.add('active');
424
+
425
+ selectedTable = listItem.dataset.tableName;
426
+ if (!selectedTable) return;
427
+
428
+ queryResultContainer.style.display = 'none'; // Hide query results
429
+ dataDisplayContainer.style.display = 'flex'; // Show table data area
430
+
431
+ tableDataHeader.textContent = `for table "${selectedTable}"`;
432
+ schemaTable.innerHTML = '<tbody><tr><td>Loading schema...</td></tr></tbody>';
433
+ dataTable.innerHTML = '<tbody><tr><td>Loading data...</td></tr></tbody>';
434
+
435
+ try {
436
+ const [schemaData, tableData] = await Promise.all([
437
+ fetchAPI(`/tables/${selectedTable}/schema`),
438
+ fetchAPI(`/tables/${selectedTable}?limit=100`) // Load first 100 rows
439
+ ]);
440
+ renderSchema(schemaData);
441
+ renderTable(tableData, dataTable);
442
+ } catch (error) {
443
+ schemaTable.innerHTML = '<tbody><tr><td>Error loading schema.</td></tr></tbody>';
444
+ dataTable.innerHTML = '<tbody><tr><td>Error loading data.</td></tr></tbody>';
445
+ }
446
+ }
447
+
448
+ async function runCustomQuery() {
449
+ const sql = sqlInput.value.trim();
450
+ if (!sql) {
451
+ showStatus("SQL query cannot be empty.", true);
452
+ return;
453
+ }
454
+ if (!API_BASE_URL) {
455
+ showStatus("Connect to the API first (enter URL and press Connect).", true);
456
+ return;
457
+ }
458
+
459
+ dataDisplayContainer.style.display = 'none'; // Hide table data
460
+ queryResultContainer.style.display = 'block'; // Show query results area
461
+ queryResultTable.innerHTML = '<tbody><tr><td>Running query...</td></tr></tbody>';
462
+
463
+
464
+ try {
465
+ const resultData = await fetchAPI('/query', {
466
+ method: 'POST',
467
+ headers: {
468
+ 'Content-Type': 'application/json',
469
+ },
470
+ body: JSON.stringify({ sql: sql }),
471
+ });
472
+ renderTable(resultData, queryResultTable);
473
+ showStatus("Query executed successfully.", false);
474
+ } catch (error) {
475
+ queryResultTable.innerHTML = '<tbody><tr><td>Error executing query.</td></tr></tbody>';
476
+ // fetchAPI already shows the status
477
+ }
478
+ }
479
+
480
+ // --- Initial Setup ---
481
+ connectButton.onclick = loadTables;
482
+ runSqlButton.onclick = runCustomQuery;
483
+
484
+ // Optional: Load tables on page load if API URL is preset
485
+ // if (apiUrlInput.value) {
486
+ // loadTables();
487
+ // }
488
+
489
+ </script>
490
+
491
+ </body>
492
+ </html>
main.py CHANGED
@@ -1,12 +1,14 @@
 
1
  import duckdb
2
  import os
3
- from fastapi import FastAPI, HTTPException, Request, Path as FastPath
4
  from fastapi.responses import FileResponse, StreamingResponse
5
  from pydantic import BaseModel, Field
6
  from typing import List, Dict, Any, Optional
7
  import logging
8
  import io
9
  import asyncio
 
10
 
11
  # --- Configuration ---
12
  DATABASE_PATH = os.environ.get("DUCKDB_PATH", "data/mydatabase.db")
@@ -26,24 +28,24 @@ app = FastAPI(
26
  version="0.1.0"
27
  )
28
 
29
- # --- Database Connection ---
30
- # For simplicity in this example, we connect within each request.
31
- # For production, consider dependency injection or connection pooling.
32
- def get_db():
33
  try:
34
  # Check if the database file needs initialization
35
  initialize = not os.path.exists(DATABASE_PATH) or os.path.getsize(DATABASE_PATH) == 0
36
- conn = duckdb.connect(DATABASE_PATH, read_only=False)
37
  if initialize:
38
  logger.info(f"Database file not found or empty at {DATABASE_PATH}. Initializing.")
39
- # You could add initial schema setup here if needed
40
- # conn.execute("CREATE TABLE IF NOT EXISTS initial_table (id INTEGER, name VARCHAR);")
41
  yield conn
42
  except duckdb.Error as e:
43
  logger.error(f"Database connection error: {e}")
44
  raise HTTPException(status_code=500, detail=f"Database connection error: {e}")
45
  finally:
46
- if 'conn' in locals() and conn:
47
  conn.close()
48
 
49
  # --- Pydantic Models ---
@@ -51,19 +53,24 @@ class ColumnDefinition(BaseModel):
51
  name: str
52
  type: str
53
 
 
 
 
54
  class CreateTableRequest(BaseModel):
55
  columns: List[ColumnDefinition]
56
 
57
  class CreateRowRequest(BaseModel):
58
- # List of rows, where each row is a dict of column_name: value
59
  rows: List[Dict[str, Any]]
60
 
61
  class UpdateRowRequest(BaseModel):
62
- updates: Dict[str, Any] # Column value pairs to set
63
- condition: str # SQL WHERE clause string to identify rows
64
 
65
  class DeleteRowRequest(BaseModel):
66
- condition: str # SQL WHERE clause string to identify rows
 
 
 
67
 
68
  class ApiResponse(BaseModel):
69
  message: str
@@ -71,35 +78,53 @@ class ApiResponse(BaseModel):
71
 
72
  # --- Helper Functions ---
73
  def safe_identifier(name: str) -> str:
74
- """Quotes an identifier safely."""
75
- if not name.isidentifier():
76
- # Basic check, consider more robust validation/sanitization if needed
77
- # Use DuckDB's quoting
78
- try:
79
- conn = duckdb.connect(':memory:')
80
- quoted = conn.execute(f"SELECT '{name}'::IDENTIFIER").fetchone()[0]
81
- conn.close()
82
- return quoted
83
- except duckdb.Error:
84
- raise HTTPException(status_code=400, detail=f"Invalid identifier: {name}")
85
- # Also quote standard identifiers to be safe
86
- return f'"{name}"'
 
 
 
 
 
87
 
88
  def generate_column_sql(columns: List[ColumnDefinition]) -> str:
89
  """Generates the column definition part of a CREATE TABLE statement."""
90
  defs = []
91
  for col in columns:
92
  col_name_safe = safe_identifier(col.name)
93
- # Basic type validation (can be expanded)
94
- allowed_types = ['INTEGER', 'VARCHAR', 'TEXT', 'BOOLEAN', 'FLOAT', 'DOUBLE', 'DATE', 'TIMESTAMP', 'BLOB', 'BIGINT', 'DECIMAL']
95
  type_upper = col.type.strip().upper()
96
- # Allow DECIMAL(p,s) syntax
97
- if not (type_upper.startswith('DECIMAL(') and type_upper.endswith(')')) and \
98
- not any(base_type in type_upper for base_type in allowed_types):
99
- raise HTTPException(status_code=400, detail=f"Unsupported or invalid data type: {col.type}")
100
- defs.append(f"{col_name_safe} {col.type}")
 
 
 
 
 
 
 
 
101
  return ", ".join(defs)
102
 
 
 
 
 
 
103
  # --- API Endpoints ---
104
 
105
  @app.get("/", summary="API Root", response_model=ApiResponse)
@@ -107,6 +132,74 @@ async def read_root():
107
  """Provides a welcome message for the API."""
108
  return {"message": "Welcome to the DuckDB API!"}
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  @app.post("/tables/{table_name}", summary="Create Table", response_model=ApiResponse, status_code=201)
111
  async def create_table(
112
  table_name: str = FastPath(..., description="Name of the table to create"),
@@ -119,12 +212,12 @@ async def create_table(
119
 
120
  try:
121
  columns_sql = generate_column_sql(schema.columns)
122
- sql = f"CREATE TABLE {table_name_safe} ({columns_sql});"
123
  logger.info(f"Executing SQL: {sql}")
124
- for conn in get_db():
125
  conn.execute(sql)
126
- return {"message": f"Table '{table_name}' created successfully."}
127
- except HTTPException as e: # Re-raise validation errors
128
  raise e
129
  except duckdb.Error as e:
130
  logger.error(f"Error creating table '{table_name}': {e}")
@@ -136,28 +229,27 @@ async def create_table(
136
  @app.get("/tables/{table_name}", summary="Read Table Data")
137
  async def read_table(
138
  table_name: str = FastPath(..., description="Name of the table to read from"),
139
- limit: Optional[int] = None,
140
- offset: Optional[int] = None
141
  ):
142
- """Reads and returns all rows from a specified table. Supports limit and offset."""
143
  table_name_safe = safe_identifier(table_name)
144
  sql = f"SELECT * FROM {table_name_safe}"
145
  params = []
146
- if limit is not None:
147
  sql += " LIMIT ?"
148
  params.append(limit)
149
- if offset is not None:
150
  sql += " OFFSET ?"
151
  params.append(offset)
152
  sql += ";"
153
 
154
  try:
155
  logger.info(f"Executing SQL: {sql} with params: {params}")
156
- for conn in get_db():
157
- result = conn.execute(sql, params).fetchall()
158
- # Convert rows to dictionaries for JSON serialization
159
- column_names = [desc[0] for desc in conn.description]
160
- data = [dict(zip(column_names, row)) for row in result]
161
  return data
162
  except duckdb.CatalogException as e:
163
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
@@ -196,16 +288,14 @@ async def create_rows(
196
 
197
  try:
198
  logger.info(f"Executing SQL: {sql} for {len(params_list)} rows")
199
- for conn in get_db():
200
  conn.executemany(sql, params_list)
201
- conn.commit() # Explicit commit after potential bulk insert
202
  return {"message": f"Successfully inserted {len(params_list)} rows into '{table_name}'."}
203
  except duckdb.CatalogException as e:
204
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
205
  except duckdb.Error as e:
206
  logger.error(f"Error inserting rows into '{table_name}': {e}")
207
- # Rollback on error might be needed depending on transaction behavior
208
- # For get_db creating connection per request, this is less critical
209
  raise HTTPException(status_code=400, detail=f"Error inserting rows: {e}")
210
  except Exception as e:
211
  logger.error(f"Unexpected error inserting rows into '{table_name}': {e}")
@@ -232,15 +322,14 @@ async def update_rows(
232
 
233
  set_sql = ", ".join(set_clauses)
234
  # WARNING: Injecting request.condition directly is a security risk.
235
- # In a real app, use query parameters or a safer way to build the WHERE clause.
236
- sql = f"UPDATE {table_name_safe} SET {set_sql} WHERE {request.condition};"
237
 
238
  try:
239
  logger.info(f"Executing SQL: {sql} with params: {params}")
240
- for conn in get_db():
241
- # Use execute for safety with parameters
242
  conn.execute(sql, params)
243
- conn.commit()
244
  return {"message": f"Rows in '{table_name}' updated successfully based on condition."}
245
  except duckdb.CatalogException as e:
246
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
@@ -262,15 +351,13 @@ async def delete_rows(
262
  raise HTTPException(status_code=400, detail="Delete condition (WHERE clause) is required.")
263
 
264
  # WARNING: Injecting request.condition directly is a security risk.
265
- # In a real app, use query parameters or a safer way to build the WHERE clause.
266
- sql = f"DELETE FROM {table_name_safe} WHERE {request.condition};"
267
 
268
  try:
269
  logger.info(f"Executing SQL: {sql}")
270
- for conn in get_db():
271
- # Execute does not directly support parameters for WHERE in DELETE like this easily
272
  conn.execute(sql)
273
- conn.commit()
274
  return {"message": f"Rows from '{table_name}' deleted successfully based on condition."}
275
  except duckdb.CatalogException as e:
276
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
@@ -282,49 +369,38 @@ async def delete_rows(
282
  raise HTTPException(status_code=500, detail="An unexpected error occurred.")
283
 
284
  # --- Download Endpoints ---
285
-
286
  @app.get("/download/table/{table_name}", summary="Download Table as CSV")
287
  async def download_table_csv(
288
  table_name: str = FastPath(..., description="Name of the table to download")
289
  ):
290
  """Downloads the entire content of a table as a CSV file."""
291
  table_name_safe = safe_identifier(table_name)
292
- # Use COPY TO STDOUT for efficient streaming
293
  sql = f"COPY (SELECT * FROM {table_name_safe}) TO STDOUT (FORMAT CSV, HEADER)"
294
 
295
  async def stream_csv_data():
296
- # We need a non-blocking way to stream data from DuckDB.
297
- # DuckDB's Python API is blocking. A simple approach for this demo
298
- # is to fetch all data first, then stream it.
299
- # A more advanced approach would involve running the DuckDB query
300
- # in a separate thread or process pool managed by asyncio.
301
-
302
  try:
 
 
 
 
 
 
303
  all_data_io = io.StringIO()
304
- # This COPY TO variant isn't directly available in Python API for streaming to a buffer easily.
305
- # Let's fetch data and format as CSV manually or use Pandas.
306
- for conn in get_db():
307
- df = conn.execute(f"SELECT * FROM {table_name_safe}").df() # Use pandas for CSV conversion
308
-
309
- # Use an in-memory text buffer
310
  df.to_csv(all_data_io, index=False)
311
  all_data_io.seek(0)
312
-
313
- # Stream the content chunk by chunk
314
  chunk_size = 8192
315
  while True:
316
  chunk = all_data_io.read(chunk_size)
317
  if not chunk:
318
  break
319
- yield chunk
320
- # Allow other tasks to run
321
  await asyncio.sleep(0)
322
  all_data_io.close()
323
 
324
  except duckdb.CatalogException as e:
325
- # Stream an error message if the table doesn't exist
326
  yield f"Error: Table '{table_name}' not found.".encode('utf-8')
327
- logger.error(f"Error downloading table '{table_name}': {e}")
328
  except duckdb.Error as e:
329
  yield f"Error: Could not export table '{table_name}'. {e}".encode('utf-8')
330
  logger.error(f"Error downloading table '{table_name}': {e}")
@@ -332,7 +408,6 @@ async def download_table_csv(
332
  yield f"Error: An unexpected error occurred.".encode('utf-8')
333
  logger.error(f"Unexpected error downloading table '{table_name}': {e}")
334
 
335
-
336
  return StreamingResponse(
337
  stream_csv_data(),
338
  media_type="text/csv",
@@ -345,16 +420,11 @@ async def download_database_file():
345
  """Downloads the entire DuckDB database file."""
346
  if not os.path.exists(DATABASE_PATH):
347
  raise HTTPException(status_code=404, detail="Database file not found.")
348
-
349
- # Ensure connections are closed before downloading to avoid partial writes/locking issues.
350
- # This is tricky with the current get_db pattern. A proper app stop/start or
351
- # dedicated maintenance mode would be better. For this demo, we hope for the best.
352
  logger.warning("Attempting to download database file. Ensure no active writes are occurring.")
353
-
354
  return FileResponse(
355
  path=DATABASE_PATH,
356
  filename=os.path.basename(DATABASE_PATH),
357
- media_type="application/octet-stream" # Generic binary file type
358
  )
359
 
360
 
@@ -363,20 +433,9 @@ async def download_database_file():
363
  async def health_check():
364
  """Checks if the API and database connection are working."""
365
  try:
366
- for conn in get_db():
367
  conn.execute("SELECT 1")
368
  return {"message": "API is healthy and database connection is successful."}
369
  except Exception as e:
370
  logger.error(f"Health check failed: {e}")
371
- raise HTTPException(status_code=503, detail=f"Health check failed: {e}")
372
-
373
- # --- Optional: Add Startup/Shutdown events if needed ---
374
- # @app.on_event("startup")
375
- # async def startup_event():
376
- # # Initialize database connection pool, etc.
377
- # logger.info("Application startup.")
378
-
379
- # @app.on_event("shutdown")
380
- # async def shutdown_event():
381
- # # Clean up resources, close connections, etc.
382
- # logger.info("Application shutdown.")
 
1
+ # ... (keep existing imports and setup) ...
2
  import duckdb
3
  import os
4
+ from fastapi import FastAPI, HTTPException, Request, Path as FastPath, Body
5
  from fastapi.responses import FileResponse, StreamingResponse
6
  from pydantic import BaseModel, Field
7
  from typing import List, Dict, Any, Optional
8
  import logging
9
  import io
10
  import asyncio
11
+ from contextlib import contextmanager # <--- Add contextlib
12
 
13
  # --- Configuration ---
14
  DATABASE_PATH = os.environ.get("DUCKDB_PATH", "data/mydatabase.db")
 
28
  version="0.1.0"
29
  )
30
 
31
+ # --- Database Connection (using context manager for safety) ---
32
+ @contextmanager
33
+ def get_db_context():
34
+ conn = None
35
  try:
36
  # Check if the database file needs initialization
37
  initialize = not os.path.exists(DATABASE_PATH) or os.path.getsize(DATABASE_PATH) == 0
38
+ conn = duckdb.connect(DATABASE_PATH, read_only=False) # Allow writes for setup
39
  if initialize:
40
  logger.info(f"Database file not found or empty at {DATABASE_PATH}. Initializing.")
41
+ # Optionally create a default table if the DB is new
42
+ # conn.execute("CREATE TABLE IF NOT EXISTS example (id INTEGER, name VARCHAR);")
43
  yield conn
44
  except duckdb.Error as e:
45
  logger.error(f"Database connection error: {e}")
46
  raise HTTPException(status_code=500, detail=f"Database connection error: {e}")
47
  finally:
48
+ if conn:
49
  conn.close()
50
 
51
  # --- Pydantic Models ---
 
53
  name: str
54
  type: str
55
 
56
+ class TableSchemaResponse(BaseModel):
57
+ columns: List[ColumnDefinition]
58
+
59
  class CreateTableRequest(BaseModel):
60
  columns: List[ColumnDefinition]
61
 
62
  class CreateRowRequest(BaseModel):
 
63
  rows: List[Dict[str, Any]]
64
 
65
  class UpdateRowRequest(BaseModel):
66
+ updates: Dict[str, Any]
67
+ condition: str
68
 
69
  class DeleteRowRequest(BaseModel):
70
+ condition: str
71
+
72
+ class SQLQueryRequest(BaseModel):
73
+ sql: str
74
 
75
  class ApiResponse(BaseModel):
76
  message: str
 
78
 
79
  # --- Helper Functions ---
80
  def safe_identifier(name: str) -> str:
81
+ """Quotes an identifier safely using DuckDB."""
82
+ # Basic check
83
+ if not name or not isinstance(name, str):
84
+ raise HTTPException(status_code=400, detail=f"Invalid identifier provided: {name}")
85
+ # Use DuckDB's quoting mechanism
86
+ try:
87
+ # Use a temporary in-memory connection for quoting safely
88
+ with duckdb.connect(':memory:') as temp_conn:
89
+ # Use sql() which returns a relation, then fetch the result
90
+ quoted = temp_conn.sql(f"SELECT '{name}'::IDENTIFIER").fetchone()
91
+ if quoted:
92
+ return quoted[0]
93
+ else:
94
+ raise HTTPException(status_code=500, detail="Failed to quote identifier")
95
+ except duckdb.Error as e:
96
+ logger.error(f"Error quoting identifier '{name}': {e}")
97
+ # Fallback or re-raise depending on policy, here we raise
98
+ raise HTTPException(status_code=400, detail=f"Invalid identifier '{name}': {e}")
99
 
100
  def generate_column_sql(columns: List[ColumnDefinition]) -> str:
101
  """Generates the column definition part of a CREATE TABLE statement."""
102
  defs = []
103
  for col in columns:
104
  col_name_safe = safe_identifier(col.name)
105
+ # More robust type validation needed for production
106
+ allowed_types_prefix = ['INTEGER', 'VARCHAR', 'TEXT', 'BOOLEAN', 'FLOAT', 'DOUBLE', 'DATE', 'TIMESTAMP', 'BLOB', 'BIGINT', 'DECIMAL', 'LIST', 'STRUCT', 'MAP', 'UNION']
107
  type_upper = col.type.strip().upper()
108
+
109
+ is_allowed = False
110
+ for prefix in allowed_types_prefix:
111
+ # Allow types like VARCHAR(255), DECIMAL(10,2), LIST<INT>, STRUCT<a INT> etc.
112
+ if type_upper.startswith(prefix):
113
+ is_allowed = True
114
+ break
115
+
116
+ if not is_allowed:
117
+ # Very basic check, expand as needed
118
+ raise HTTPException(status_code=400, detail=f"Unsupported or potentially invalid data type: {col.type}")
119
+
120
+ defs.append(f"{col_name_safe} {col.type}") # Pass type string directly
121
  return ", ".join(defs)
122
 
123
+ def result_to_dict(cursor_description, rows):
124
+ """Converts cursor results (description + rows) to a list of dictionaries."""
125
+ column_names = [desc[0] for desc in cursor_description]
126
+ return [dict(zip(column_names, row)) for row in rows]
127
+
128
  # --- API Endpoints ---
129
 
130
  @app.get("/", summary="API Root", response_model=ApiResponse)
 
132
  """Provides a welcome message for the API."""
133
  return {"message": "Welcome to the DuckDB API!"}
134
 
135
+ # --- NEW ENDPOINT ---
136
+ @app.get("/tables", summary="List Tables", response_model=List[str])
137
+ async def list_tables():
138
+ """Lists all tables in the default schema."""
139
+ try:
140
+ with get_db_context() as conn:
141
+ # Show user tables (excluding system tables)
142
+ tables = conn.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'").fetchall()
143
+ return [table[0] for table in tables]
144
+ except duckdb.Error as e:
145
+ logger.error(f"Error listing tables: {e}")
146
+ raise HTTPException(status_code=500, detail=f"Error listing tables: {e}")
147
+
148
+ # --- NEW ENDPOINT ---
149
+ @app.get("/tables/{table_name}/schema", summary="Get Table Schema", response_model=TableSchemaResponse)
150
+ async def get_table_schema(
151
+ table_name: str = FastPath(..., description="Name of the table")
152
+ ):
153
+ """Gets the schema (column names and types) for a specific table."""
154
+ table_name_safe = safe_identifier(table_name)
155
+ # Use PRAGMA for schema info
156
+ sql = f"PRAGMA table_info({table_name_safe});"
157
+ try:
158
+ with get_db_context() as conn:
159
+ result = conn.execute(sql).fetchall()
160
+ if not result:
161
+ raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found or has no columns.")
162
+ # PRAGMA table_info columns: cid, name, type, notnull, dflt_value, pk
163
+ columns = [ColumnDefinition(name=row[1], type=row[2]) for row in result]
164
+ return TableSchemaResponse(columns=columns)
165
+ except duckdb.CatalogException as e:
166
+ raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
167
+ except duckdb.Error as e:
168
+ logger.error(f"Error getting schema for table '{table_name}': {e}")
169
+ raise HTTPException(status_code=400, detail=f"Error getting table schema: {e}")
170
+
171
+ # --- NEW ENDPOINT ---
172
+ @app.post("/query", summary="Execute Read-Only SQL Query")
173
+ async def execute_query(query_request: SQLQueryRequest):
174
+ """Executes a provided SQL query (read-only enforced)."""
175
+ sql = query_request.sql.strip()
176
+
177
+ # **Security:** Basic check to prevent modification queries.
178
+ # This is NOT foolproof. A robust solution needs proper SQL parsing or
179
+ # database roles/permissions restricting the API user.
180
+ forbidden_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'ATTACH', 'DETACH', 'COPY', 'EXPORT', 'IMPORT']
181
+ sql_upper = sql.upper()
182
+ if any(keyword in sql_upper for keyword in forbidden_keywords):
183
+ raise HTTPException(status_code=403, detail="Only SELECT queries are allowed.")
184
+ if not sql_upper.startswith('SELECT') and not sql_upper.startswith('WITH'):
185
+ raise HTTPException(status_code=400, detail="Query must start with SELECT or WITH.")
186
+
187
+ try:
188
+ logger.info(f"Executing user SQL: {sql}")
189
+ with get_db_context() as conn:
190
+ description = conn.execute(sql).description
191
+ result = conn.fetchall()
192
+ # Convert rows to dictionaries for JSON serialization
193
+ data = result_to_dict(description, result)
194
+ return data
195
+ except duckdb.Error as e:
196
+ logger.error(f"Error executing user query: {e}")
197
+ raise HTTPException(status_code=400, detail=f"Error executing query: {e}")
198
+ except Exception as e:
199
+ logger.error(f"Unexpected error executing user query: {e}")
200
+ raise HTTPException(status_code=500, detail="An unexpected error occurred during query execution.")
201
+
202
+ # --- Existing Endpoints (Keep or adapt as needed) ---
203
  @app.post("/tables/{table_name}", summary="Create Table", response_model=ApiResponse, status_code=201)
204
  async def create_table(
205
  table_name: str = FastPath(..., description="Name of the table to create"),
 
212
 
213
  try:
214
  columns_sql = generate_column_sql(schema.columns)
215
+ sql = f"CREATE OR REPLACE TABLE {table_name_safe} ({columns_sql});" # Use CREATE OR REPLACE for simplicity
216
  logger.info(f"Executing SQL: {sql}")
217
+ with get_db_context() as conn:
218
  conn.execute(sql)
219
+ return {"message": f"Table '{table_name}' created or replaced successfully."}
220
+ except HTTPException as e:
221
  raise e
222
  except duckdb.Error as e:
223
  logger.error(f"Error creating table '{table_name}': {e}")
 
229
  @app.get("/tables/{table_name}", summary="Read Table Data")
230
  async def read_table(
231
  table_name: str = FastPath(..., description="Name of the table to read from"),
232
+ limit: Optional[int] = 100, # Default limit
233
+ offset: Optional[int] = 0 # Default offset
234
  ):
235
+ """Reads and returns rows from a specified table. Supports limit and offset."""
236
  table_name_safe = safe_identifier(table_name)
237
  sql = f"SELECT * FROM {table_name_safe}"
238
  params = []
239
+ if limit is not None and limit >= 0:
240
  sql += " LIMIT ?"
241
  params.append(limit)
242
+ if offset is not None and offset >= 0:
243
  sql += " OFFSET ?"
244
  params.append(offset)
245
  sql += ";"
246
 
247
  try:
248
  logger.info(f"Executing SQL: {sql} with params: {params}")
249
+ with get_db_context() as conn:
250
+ description = conn.execute(sql, params).description
251
+ result = conn.fetchall()
252
+ data = result_to_dict(description, result)
 
253
  return data
254
  except duckdb.CatalogException as e:
255
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
 
288
 
289
  try:
290
  logger.info(f"Executing SQL: {sql} for {len(params_list)} rows")
291
+ with get_db_context() as conn:
292
  conn.executemany(sql, params_list)
293
+ # Removed commit - context manager handles it
294
  return {"message": f"Successfully inserted {len(params_list)} rows into '{table_name}'."}
295
  except duckdb.CatalogException as e:
296
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
297
  except duckdb.Error as e:
298
  logger.error(f"Error inserting rows into '{table_name}': {e}")
 
 
299
  raise HTTPException(status_code=400, detail=f"Error inserting rows: {e}")
300
  except Exception as e:
301
  logger.error(f"Unexpected error inserting rows into '{table_name}': {e}")
 
322
 
323
  set_sql = ", ".join(set_clauses)
324
  # WARNING: Injecting request.condition directly is a security risk.
325
+ # Use parameters for values, but condition structure still needs care.
326
+ sql = f"UPDATE {table_name_safe} SET {set_sql} WHERE {request.condition};" # Condition not parameterized here
327
 
328
  try:
329
  logger.info(f"Executing SQL: {sql} with params: {params}")
330
+ with get_db_context() as conn:
 
331
  conn.execute(sql, params)
332
+ # Removed commit
333
  return {"message": f"Rows in '{table_name}' updated successfully based on condition."}
334
  except duckdb.CatalogException as e:
335
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
 
351
  raise HTTPException(status_code=400, detail="Delete condition (WHERE clause) is required.")
352
 
353
  # WARNING: Injecting request.condition directly is a security risk.
354
+ sql = f"DELETE FROM {table_name_safe} WHERE {request.condition};" # Condition not parameterized here
 
355
 
356
  try:
357
  logger.info(f"Executing SQL: {sql}")
358
+ with get_db_context() as conn:
 
359
  conn.execute(sql)
360
+ # Removed commit
361
  return {"message": f"Rows from '{table_name}' deleted successfully based on condition."}
362
  except duckdb.CatalogException as e:
363
  raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found.")
 
369
  raise HTTPException(status_code=500, detail="An unexpected error occurred.")
370
 
371
  # --- Download Endpoints ---
 
372
  @app.get("/download/table/{table_name}", summary="Download Table as CSV")
373
  async def download_table_csv(
374
  table_name: str = FastPath(..., description="Name of the table to download")
375
  ):
376
  """Downloads the entire content of a table as a CSV file."""
377
  table_name_safe = safe_identifier(table_name)
 
378
  sql = f"COPY (SELECT * FROM {table_name_safe}) TO STDOUT (FORMAT CSV, HEADER)"
379
 
380
  async def stream_csv_data():
 
 
 
 
 
 
381
  try:
382
+ # Use pandas for CSV conversion in-memory
383
+ with get_db_context() as conn:
384
+ # Check if table exists before fetching
385
+ conn.execute(f"SELECT 1 FROM {table_name_safe} LIMIT 0")
386
+ df = conn.execute(f"SELECT * FROM {table_name_safe}").df()
387
+
388
  all_data_io = io.StringIO()
 
 
 
 
 
 
389
  df.to_csv(all_data_io, index=False)
390
  all_data_io.seek(0)
391
+
 
392
  chunk_size = 8192
393
  while True:
394
  chunk = all_data_io.read(chunk_size)
395
  if not chunk:
396
  break
397
+ yield chunk.encode('utf-8') # Encode to bytes for streaming response
 
398
  await asyncio.sleep(0)
399
  all_data_io.close()
400
 
401
  except duckdb.CatalogException as e:
 
402
  yield f"Error: Table '{table_name}' not found.".encode('utf-8')
403
+ logger.error(f"Error downloading table '{table_name}': Table not found.")
404
  except duckdb.Error as e:
405
  yield f"Error: Could not export table '{table_name}'. {e}".encode('utf-8')
406
  logger.error(f"Error downloading table '{table_name}': {e}")
 
408
  yield f"Error: An unexpected error occurred.".encode('utf-8')
409
  logger.error(f"Unexpected error downloading table '{table_name}': {e}")
410
 
 
411
  return StreamingResponse(
412
  stream_csv_data(),
413
  media_type="text/csv",
 
420
  """Downloads the entire DuckDB database file."""
421
  if not os.path.exists(DATABASE_PATH):
422
  raise HTTPException(status_code=404, detail="Database file not found.")
 
 
 
 
423
  logger.warning("Attempting to download database file. Ensure no active writes are occurring.")
 
424
  return FileResponse(
425
  path=DATABASE_PATH,
426
  filename=os.path.basename(DATABASE_PATH),
427
+ media_type="application/vnd.duckdb.database" # More specific media type
428
  )
429
 
430
 
 
433
  async def health_check():
434
  """Checks if the API and database connection are working."""
435
  try:
436
+ with get_db_context() as conn:
437
  conn.execute("SELECT 1")
438
  return {"message": "API is healthy and database connection is successful."}
439
  except Exception as e:
440
  logger.error(f"Health check failed: {e}")
441
+ raise HTTPException(status_code=503, detail=f"Health check failed: {e}")