local-deep-research 0.6.1__py3-none-any.whl → 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. local_deep_research/__init__.py +6 -0
  2. local_deep_research/__version__.py +1 -1
  3. local_deep_research/setup_data_dir.py +3 -2
  4. local_deep_research/utilities/db_utils.py +11 -9
  5. local_deep_research/utilities/log_utils.py +1 -1
  6. local_deep_research/utilities/threading_utils.py +17 -4
  7. local_deep_research/web/api.py +5 -3
  8. local_deep_research/web/app.py +0 -74
  9. local_deep_research/web/app_factory.py +1 -11
  10. local_deep_research/web/database/migrations.py +699 -28
  11. local_deep_research/web/database/uuid_migration.py +2 -247
  12. local_deep_research/web/models/database.py +0 -79
  13. local_deep_research/web/routes/settings_routes.py +70 -73
  14. local_deep_research/web/services/settings_manager.py +13 -28
  15. local_deep_research/web/services/settings_service.py +6 -40
  16. local_deep_research/web/services/socket_service.py +1 -1
  17. local_deep_research/web_search_engines/rate_limiting/tracker.py +1 -3
  18. {local_deep_research-0.6.1.dist-info → local_deep_research-0.6.4.dist-info}/METADATA +19 -4
  19. {local_deep_research-0.6.1.dist-info → local_deep_research-0.6.4.dist-info}/RECORD +22 -25
  20. local_deep_research/migrate_db.py +0 -149
  21. local_deep_research/web/database/migrate_to_ldr_db.py +0 -297
  22. local_deep_research/web/database/schema_upgrade.py +0 -519
  23. {local_deep_research-0.6.1.dist-info → local_deep_research-0.6.4.dist-info}/WHEEL +0 -0
  24. {local_deep_research-0.6.1.dist-info → local_deep_research-0.6.4.dist-info}/entry_points.txt +0 -0
  25. {local_deep_research-0.6.1.dist-info → local_deep_research-0.6.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,18 @@
1
+ import sqlite3
2
+ import uuid
3
+ from pathlib import Path
4
+
1
5
  from loguru import logger
2
- from sqlalchemy import inspect
6
+ from sqlalchemy import inspect, text, create_engine
3
7
 
4
8
  from ..services.settings_manager import SettingsManager
5
9
  from .models import (
6
10
  Base,
7
- Journal,
8
- Setting,
9
- ResearchLog,
10
- Research,
11
- ResearchHistory,
11
+ ResearchStrategy,
12
+ RateLimitAttempt,
13
+ RateLimitEstimate,
12
14
  )
15
+ from ...utilities.db_utils import DB_PATH, get_db_session
13
16
 
14
17
 
15
18
  def import_default_settings_file(db_session):
@@ -33,40 +36,708 @@ def import_default_settings_file(db_session):
33
36
 
34
37
  # Update the saved version.
35
38
  settings_mgr.update_db_version()
39
+ except Exception:
40
+ logger.exception("Error importing settings from files")
41
+
42
+
43
+ def remove_research_log_table(engine):
44
+ """
45
+ Remove the redundant research_log table if it exists
46
+
47
+ Args:
48
+ engine: SQLAlchemy engine
49
+
50
+ Returns:
51
+ bool: True if operation was successful, False otherwise
52
+ """
53
+ try:
54
+ inspector = inspect(engine)
55
+
56
+ # Check if table exists
57
+ if inspector.has_table("research_log"):
58
+ # For SQLite, we need to use raw SQL for DROP TABLE
59
+ with engine.connect() as conn:
60
+ conn.execute("DROP TABLE research_log")
61
+ conn.commit()
62
+ logger.info("Successfully removed redundant 'research_log' table")
63
+ return True
64
+ else:
65
+ logger.info("Table 'research_log' does not exist, no action needed")
66
+ return True
67
+ except Exception:
68
+ logger.exception("Error removing research_log table")
69
+ return False
70
+
71
+
72
+ def create_research_strategy_table(engine):
73
+ """
74
+ Create the research_strategies table if it doesn't exist
75
+
76
+ Args:
77
+ engine: SQLAlchemy engine
78
+
79
+ Returns:
80
+ bool: True if operation was successful, False otherwise
81
+ """
82
+ try:
83
+ inspector = inspect(engine)
84
+
85
+ # Check if table exists
86
+ if not inspector.has_table("research_strategies"):
87
+ # Create the table using ORM
88
+ Base.metadata.create_all(
89
+ engine, tables=[ResearchStrategy.__table__]
90
+ )
91
+ logger.info("Successfully created 'research_strategies' table")
92
+ return True
93
+ else:
94
+ logger.info(
95
+ "Table 'research_strategies' already exists, no action needed"
96
+ )
97
+ return True
98
+ except Exception:
99
+ logger.exception("Error creating research_strategies table")
100
+ return False
101
+
102
+
103
+ def create_rate_limiting_tables(engine):
104
+ """
105
+ Create rate limiting tables if they don't exist
106
+
107
+ Args:
108
+ engine: SQLAlchemy engine
109
+
110
+ Returns:
111
+ bool: True if operation was successful, False otherwise
112
+ """
113
+ try:
114
+ inspector = inspect(engine)
115
+
116
+ tables_to_create = []
117
+
118
+ # Check if rate_limit_attempts table exists
119
+ if not inspector.has_table("rate_limit_attempts"):
120
+ tables_to_create.append(RateLimitAttempt.__table__)
121
+ logger.info("Need to create 'rate_limit_attempts' table")
122
+
123
+ # Check if rate_limit_estimates table exists
124
+ if not inspector.has_table("rate_limit_estimates"):
125
+ tables_to_create.append(RateLimitEstimate.__table__)
126
+ logger.info("Need to create 'rate_limit_estimates' table")
127
+
128
+ if tables_to_create:
129
+ # Create the tables using ORM
130
+ Base.metadata.create_all(engine, tables=tables_to_create)
131
+ logger.info(
132
+ f"Successfully created {len(tables_to_create)} rate limiting tables"
133
+ )
134
+ else:
135
+ logger.info("Rate limiting tables already exist, no action needed")
136
+
137
+ return True
138
+ except Exception:
139
+ logger.exception("Error creating rate limiting tables")
140
+ return False
141
+
142
+
143
+ def add_research_id_to_benchmark_results(engine):
144
+ """
145
+ Add research_id column to benchmark_results table if it doesn't exist.
146
+ """
147
+ try:
148
+ import sqlite3
149
+
150
+ # Get database path from engine
151
+ db_path = engine.url.database
152
+
153
+ logger.info("Checking if benchmark_results needs research_id column...")
154
+
155
+ conn = sqlite3.connect(db_path)
156
+
157
+ try:
158
+ cursor = conn.cursor()
159
+
160
+ # Check if table exists
161
+ cursor.execute(
162
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='benchmark_results'"
163
+ )
164
+ if not cursor.fetchone():
165
+ logger.info("benchmark_results table does not exist, skipping")
166
+ return True
167
+
168
+ # Check if research_id column already exists
169
+ cursor.execute("PRAGMA table_info(benchmark_results)")
170
+ columns = cursor.fetchall()
171
+ has_research_id = any(col[1] == "research_id" for col in columns)
172
+
173
+ if has_research_id:
174
+ logger.info("benchmark_results already has research_id column")
175
+ return True
176
+
177
+ # Add research_id column
178
+ logger.info("Adding research_id column to benchmark_results table")
179
+ cursor.execute(
180
+ "ALTER TABLE benchmark_results ADD COLUMN research_id TEXT"
181
+ )
182
+
183
+ conn.commit()
184
+ logger.info(
185
+ "Successfully added research_id column to benchmark_results"
186
+ )
187
+ return True
188
+
189
+ finally:
190
+ conn.close()
191
+
192
+ except Exception:
193
+ logger.exception("Error adding research_id column to benchmark_results")
194
+ return False
195
+
196
+
197
+ def add_uuid_id_column_to_research_history(engine):
198
+ """
199
+ Adds a new `uuid_id` string column to the `research_history` table if it
200
+ does not exist already.
201
+ """
202
+ try:
203
+ import sqlite3
204
+
205
+ # Get database path from engine
206
+ db_path = engine.url.database
207
+
208
+ logger.info("Checking if research_history needs uuid_id column...")
209
+
210
+ conn = sqlite3.connect(db_path)
211
+
212
+ try:
213
+ cursor = conn.cursor()
214
+
215
+ # Check if table exists
216
+ cursor.execute(
217
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='research_history'"
218
+ )
219
+ if not cursor.fetchone():
220
+ logger.info("research_history table does not exist, skipping")
221
+ return True
222
+
223
+ # Check if uuid_id column already exists
224
+ cursor.execute("PRAGMA table_info(research_history)")
225
+ columns = cursor.fetchall()
226
+ has_uuid_id = any(col[1] == "uuid_id" for col in columns)
227
+
228
+ if has_uuid_id:
229
+ logger.info("research_history already has uuid_id column")
230
+ return True
231
+
232
+ # Add uuid_id column
233
+ logger.info("Adding uuid_id column to research_history table")
234
+ cursor.execute(
235
+ "ALTER TABLE research_history ADD COLUMN uuid_id CHAR(36)"
236
+ )
237
+
238
+ conn.commit()
239
+ logger.info("Successfully added uuid_id column to research_history")
240
+ return True
241
+
242
+ finally:
243
+ conn.close()
244
+
245
+ except Exception:
246
+ logger.exception("Error adding uuid_id column to research_history")
247
+ return False
248
+
249
+
250
+ def convert_research_id_to_string_if_needed(engine):
251
+ """
252
+ Convert research_id columns from Integer to String in all tables.
253
+ Preserves existing data by converting integer IDs to string format.
254
+ Only runs if integer research_id columns are detected.
255
+ """
256
+ try:
257
+ import sqlite3
258
+
259
+ # Get database path from engine
260
+ db_path = engine.url.database
261
+
262
+ logger.info(
263
+ "Checking if research_id columns need conversion to string..."
264
+ )
265
+
266
+ conn = sqlite3.connect(db_path)
267
+ conn.execute("PRAGMA foreign_keys = OFF")
268
+
269
+ try:
270
+ cursor = conn.cursor()
271
+
272
+ # List of tables that might have research_id columns
273
+ tables_to_check = [
274
+ "token_usage",
275
+ "model_usage",
276
+ "search_calls",
277
+ "benchmark_results",
278
+ ]
279
+
280
+ tables_needing_conversion = []
281
+
282
+ # Check which tables need conversion
283
+ for table_name in tables_to_check:
284
+ # Check if table exists
285
+ cursor.execute(
286
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
287
+ (table_name,),
288
+ )
289
+ if not cursor.fetchone():
290
+ continue
291
+
292
+ # Check if research_id column exists and is integer type
293
+ cursor.execute(f"PRAGMA table_info({table_name})")
294
+ columns = cursor.fetchall()
295
+
296
+ for col in columns:
297
+ col_name, col_type = col[1], col[2]
298
+ if col_name == "research_id" and (
299
+ "INTEGER" in col_type.upper()
300
+ or "INT" in col_type.upper()
301
+ ):
302
+ tables_needing_conversion.append(table_name)
303
+ break
304
+
305
+ if not tables_needing_conversion:
306
+ logger.info(
307
+ "All research_id columns are already string type, no conversion needed"
308
+ )
309
+ return True
310
+
311
+ logger.info(
312
+ f"Converting research_id to string in tables: {tables_needing_conversion}"
313
+ )
314
+
315
+ # Convert each table
316
+ for table_name in tables_needing_conversion:
317
+ logger.info(f"Converting {table_name} table...")
318
+
319
+ # Get the current table schema
320
+ cursor.execute(
321
+ f"SELECT sql FROM sqlite_master WHERE type='table' AND name='{table_name}'"
322
+ )
323
+ create_sql = cursor.fetchone()[0]
324
+
325
+ # Create new table name
326
+ new_table_name = f"{table_name}_new"
327
+
328
+ # Modify the CREATE TABLE statement to change research_id to TEXT
329
+ new_create_sql = create_sql.replace(
330
+ f"CREATE TABLE {table_name}",
331
+ f"CREATE TABLE {new_table_name}",
332
+ )
333
+ new_create_sql = new_create_sql.replace(
334
+ "research_id INTEGER", "research_id TEXT"
335
+ )
336
+ new_create_sql = new_create_sql.replace(
337
+ "research_id INT", "research_id TEXT"
338
+ )
339
+
340
+ # Create the new table
341
+ cursor.execute(new_create_sql)
342
+
343
+ # Copy data from old table to new table, converting research_id to string
344
+ cursor.execute(f"SELECT * FROM {table_name}")
345
+ old_rows = cursor.fetchall()
346
+
347
+ if old_rows:
348
+ # Get column names
349
+ cursor.execute(f"PRAGMA table_info({table_name})")
350
+ columns = cursor.fetchall()
351
+ column_names = [col[1] for col in columns]
352
+ research_id_index = (
353
+ column_names.index("research_id")
354
+ if "research_id" in column_names
355
+ else -1
356
+ )
357
+
358
+ # Prepare insert statement
359
+ placeholders = ",".join(["?" for _ in column_names])
360
+ insert_sql = f"INSERT INTO {new_table_name} ({','.join(column_names)}) VALUES ({placeholders})"
361
+
362
+ # Convert rows and insert
363
+ converted_rows = []
364
+ for row in old_rows:
365
+ row_list = list(row)
366
+ # Convert research_id to string if it's not None
367
+ if (
368
+ research_id_index >= 0
369
+ and row_list[research_id_index] is not None
370
+ ):
371
+ row_list[research_id_index] = str(
372
+ row_list[research_id_index]
373
+ )
374
+ converted_rows.append(tuple(row_list))
375
+
376
+ cursor.executemany(insert_sql, converted_rows)
377
+ logger.info(
378
+ f"Converted {len(converted_rows)} rows in {table_name}"
379
+ )
380
+
381
+ # Drop old table and rename new table
382
+ cursor.execute(f"DROP TABLE {table_name}")
383
+ cursor.execute(
384
+ f"ALTER TABLE {new_table_name} RENAME TO {table_name}"
385
+ )
386
+
387
+ logger.info(
388
+ f"Successfully converted {table_name} research_id to string"
389
+ )
390
+
391
+ # Commit all changes
392
+ conn.commit()
393
+ logger.info(
394
+ "All research_id columns converted to string successfully!"
395
+ )
396
+ return True
397
+
398
+ finally:
399
+ conn.execute("PRAGMA foreign_keys = ON")
400
+ conn.close()
401
+
402
+ except Exception:
403
+ logger.exception("Error converting research_id columns to string")
404
+ return False
405
+
406
+
407
+ def create_provider_models_table(engine):
408
+ """Create provider_models table for caching available models"""
409
+ with engine.connect() as conn:
410
+ result = conn.execute(
411
+ text(
412
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='provider_models'"
413
+ )
414
+ )
415
+ if result.fetchone():
416
+ logger.info(
417
+ "Table 'provider_models' already exists, no action needed"
418
+ )
419
+ return
420
+
421
+ logger.info("Creating 'provider_models' table...")
422
+
423
+ # Create the table
424
+ conn.execute(
425
+ text(
426
+ """
427
+ CREATE TABLE provider_models (
428
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
429
+ provider VARCHAR(50) NOT NULL,
430
+ model_key VARCHAR(255) NOT NULL,
431
+ model_label VARCHAR(255) NOT NULL,
432
+ model_metadata JSON,
433
+ last_updated DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
434
+ UNIQUE(provider, model_key)
435
+ )
436
+ """
437
+ )
438
+ )
439
+
440
+ # Create index on provider
441
+ conn.execute(
442
+ text(
443
+ "CREATE INDEX ix_provider_models_provider ON provider_models (provider)"
444
+ )
445
+ )
446
+
447
+ conn.commit()
448
+ logger.info("Table 'provider_models' created successfully")
449
+
450
+
451
+ def migrate_to_uuid():
452
+ """
453
+ Migrate all research_id fields from integers to UUIDs.
454
+
455
+ Strategy:
456
+ 1. Add new UUID columns alongside existing integer columns
457
+ 2. Generate UUIDs for existing data (or keep as string versions of integers)
458
+ 3. Update foreign key relationships
459
+ 4. Drop old integer columns and rename UUID columns
460
+ """
461
+ db_path = Path(DB_PATH)
462
+ if not db_path.exists():
463
+ logger.info("Database doesn't exist yet, migration not needed")
464
+ return
465
+
466
+ logger.info(f"Starting UUID migration on {db_path}")
467
+
468
+ conn = sqlite3.connect(db_path)
469
+ conn.execute(
470
+ "PRAGMA foreign_keys = OFF"
471
+ ) # Disable FK constraints during migration
472
+
473
+ try:
474
+ cursor = conn.cursor()
475
+
476
+ # 1. Migrate research_history table (main research IDs)
477
+ logger.info("Migrating research_history table...")
478
+
479
+ # Check if the table exists and has the old structure
480
+ cursor.execute("PRAGMA table_info(research_history)")
481
+ columns = cursor.fetchall()
482
+ has_uuid_id = any(col[1] == "uuid_id" for col in columns)
483
+
484
+ if not has_uuid_id:
485
+ # Add UUID column
486
+ cursor.execute(
487
+ "ALTER TABLE research_history ADD COLUMN uuid_id TEXT"
488
+ )
489
+
490
+ # Generate UUIDs for existing records (convert integer ID to UUID format)
491
+ cursor.execute("SELECT id FROM research_history")
492
+ existing_ids = cursor.fetchall()
493
+
494
+ for (old_id,) in existing_ids:
495
+ # Generate a deterministic UUID based on the old ID
496
+ new_uuid = str(
497
+ uuid.uuid5(uuid.NAMESPACE_OID, f"research_{old_id}")
498
+ )
499
+ cursor.execute(
500
+ "UPDATE research_history SET uuid_id = ? WHERE id = ?",
501
+ (new_uuid, old_id),
502
+ )
503
+
504
+ logger.info(
505
+ f"Generated UUIDs for {len(existing_ids)} research records"
506
+ )
507
+
508
+ # 2. Migrate metrics tables
509
+ logger.info("Migrating metrics tables...")
510
+
511
+ # Token usage table
512
+ cursor.execute("PRAGMA table_info(token_usage)")
513
+ columns = cursor.fetchall()
514
+ has_uuid_research_id = any(
515
+ col[1] == "uuid_research_id" for col in columns
516
+ )
517
+
518
+ if not has_uuid_research_id:
519
+ cursor.execute(
520
+ "ALTER TABLE token_usage ADD COLUMN uuid_research_id TEXT"
521
+ )
522
+
523
+ # Convert existing research_ids to UUIDs (deterministic conversion)
524
+ cursor.execute(
525
+ "SELECT DISTINCT research_id FROM token_usage WHERE research_id IS NOT NULL"
526
+ )
527
+ research_ids = cursor.fetchall()
528
+
529
+ for (research_id,) in research_ids:
530
+ if research_id:
531
+ new_uuid = str(
532
+ uuid.uuid5(
533
+ uuid.NAMESPACE_OID, f"research_{research_id}"
534
+ )
535
+ )
536
+ cursor.execute(
537
+ "UPDATE token_usage SET uuid_research_id = ? WHERE research_id = ?",
538
+ (new_uuid, research_id),
539
+ )
540
+
541
+ logger.info(
542
+ f"Migrated {len(research_ids)} research IDs in token_usage"
543
+ )
544
+
545
+ # Model usage table
546
+ cursor.execute("PRAGMA table_info(model_usage)")
547
+ columns = cursor.fetchall()
548
+ has_uuid_research_id = any(
549
+ col[1] == "uuid_research_id" for col in columns
550
+ )
551
+
552
+ if not has_uuid_research_id:
553
+ cursor.execute(
554
+ "ALTER TABLE model_usage ADD COLUMN uuid_research_id TEXT"
555
+ )
556
+
557
+ cursor.execute(
558
+ "SELECT DISTINCT research_id FROM model_usage WHERE research_id IS NOT NULL"
559
+ )
560
+ research_ids = cursor.fetchall()
561
+
562
+ for (research_id,) in research_ids:
563
+ if research_id:
564
+ new_uuid = str(
565
+ uuid.uuid5(
566
+ uuid.NAMESPACE_OID, f"research_{research_id}"
567
+ )
568
+ )
569
+ cursor.execute(
570
+ "UPDATE model_usage SET uuid_research_id = ? WHERE research_id = ?",
571
+ (new_uuid, research_id),
572
+ )
573
+
574
+ logger.info(
575
+ f"Migrated {len(research_ids)} research IDs in model_usage"
576
+ )
577
+
578
+ # Search calls table
579
+ cursor.execute("PRAGMA table_info(search_calls)")
580
+ columns = cursor.fetchall()
581
+ has_uuid_research_id = any(
582
+ col[1] == "uuid_research_id" for col in columns
583
+ )
584
+
585
+ if not has_uuid_research_id:
586
+ cursor.execute(
587
+ "ALTER TABLE search_calls ADD COLUMN uuid_research_id TEXT"
588
+ )
589
+
590
+ cursor.execute(
591
+ "SELECT DISTINCT research_id FROM search_calls WHERE research_id IS NOT NULL"
592
+ )
593
+ research_ids = cursor.fetchall()
594
+
595
+ for (research_id,) in research_ids:
596
+ if research_id:
597
+ new_uuid = str(
598
+ uuid.uuid5(
599
+ uuid.NAMESPACE_OID, f"research_{research_id}"
600
+ )
601
+ )
602
+ cursor.execute(
603
+ "UPDATE search_calls SET uuid_research_id = ? WHERE research_id = ?",
604
+ (new_uuid, research_id),
605
+ )
606
+
607
+ logger.info(
608
+ f"Migrated {len(research_ids)} research IDs in search_calls"
609
+ )
610
+
611
+ # 3. Migrate benchmark tables
612
+ logger.info("Migrating benchmark tables...")
613
+
614
+ # Check if benchmark_results table exists
615
+ cursor.execute(
616
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='benchmark_results'"
617
+ )
618
+ if cursor.fetchone():
619
+ cursor.execute("PRAGMA table_info(benchmark_results)")
620
+ columns = cursor.fetchall()
621
+ has_uuid_research_id = any(
622
+ col[1] == "uuid_research_id" for col in columns
623
+ )
624
+
625
+ if not has_uuid_research_id:
626
+ cursor.execute(
627
+ "ALTER TABLE benchmark_results ADD COLUMN uuid_research_id TEXT"
628
+ )
629
+
630
+ cursor.execute(
631
+ "SELECT DISTINCT research_id FROM benchmark_results WHERE research_id IS NOT NULL"
632
+ )
633
+ research_ids = cursor.fetchall()
634
+
635
+ for (research_id,) in research_ids:
636
+ if research_id:
637
+ new_uuid = str(
638
+ uuid.uuid5(
639
+ uuid.NAMESPACE_OID, f"research_{research_id}"
640
+ )
641
+ )
642
+ cursor.execute(
643
+ "UPDATE benchmark_results SET uuid_research_id = ? WHERE research_id = ?",
644
+ (new_uuid, research_id),
645
+ )
646
+
647
+ logger.info(
648
+ f"Migrated {len(research_ids)} research IDs in benchmark_results"
649
+ )
650
+
651
+ # Commit all changes
652
+ conn.commit()
653
+ logger.info("UUID migration completed successfully!")
654
+
655
+ # Note: We're keeping both old and new columns for now
656
+ # The application will use the new UUID columns
657
+ # Old columns can be dropped in a future migration once everything is stable
658
+
36
659
  except Exception as e:
37
- logger.error("Error importing settings from files: %s", e)
660
+ logger.error(f"Error during UUID migration: {e}")
661
+ conn.rollback()
662
+ raise
663
+ finally:
664
+ conn.execute("PRAGMA foreign_keys = ON") # Re-enable FK constraints
665
+ conn.close()
666
+
667
+
668
+ def cleanup_old_columns():
669
+ """
670
+ Cleanup migration - drops old integer columns after UUID migration is stable.
671
+ Run this only after confirming the UUID migration is working correctly.
672
+ """
673
+ logger.warning(
674
+ "This will permanently remove old integer research_id columns!"
675
+ )
676
+ logger.warning("Make sure to backup your database before running this!")
677
+
678
+ conn = sqlite3.connect(DB_PATH)
679
+ conn.execute("PRAGMA foreign_keys = OFF")
680
+
681
+ try:
682
+ # For SQLite, we need to recreate tables to drop columns
683
+ # This is complex, so we'll leave old columns for now
684
+ # They can be cleaned up manually if needed
685
+
686
+ logger.info("Cleanup deferred - old columns remain for safety")
38
687
 
688
+ finally:
689
+ conn.execute("PRAGMA foreign_keys = ON")
690
+ conn.close()
39
691
 
40
- def run_migrations(engine, db_session=None):
692
+
693
+ def ensure_database_initialized(engine=None, db_session=None):
41
694
  """
42
- Run any necessary database migrations
695
+ Initialize the database and run any necessary database migrations
43
696
 
44
697
  Args:
45
698
  engine: SQLAlchemy engine
46
699
  db_session: Optional SQLAlchemy session
47
700
  """
701
+ logger.info("Initializing database at {}.", DB_PATH)
702
+
703
+ if engine is None:
704
+ # Create SQLAlchemy engine
705
+ engine = create_engine(f"sqlite:///{DB_PATH}")
706
+ if db_session is None:
707
+ db_session = get_db_session()
708
+
48
709
  # Create all tables if they don't exist
49
- inspector = inspect(engine)
50
- if not inspector.has_table("settings"):
51
- logger.info("Creating settings table")
52
- Base.metadata.create_all(engine, tables=[Setting.__table__])
710
+ Base.metadata.create_all(engine)
711
+
712
+ # Import existing settings from files
713
+ import_default_settings_file(db_session)
53
714
 
54
- if not inspector.has_table(Journal.__tablename__):
55
- logger.info("Creating journals table.")
56
- Base.metadata.create_all(engine, tables=[Journal.__table__])
715
+ try:
716
+ # 1. Remove the redundant research_log table
717
+ remove_research_log_table(engine)
57
718
 
58
- if not inspector.has_table(ResearchLog.__tablename__):
59
- logger.info("Creating research logs table.")
60
- Base.metadata.create_all(engine, tables=[ResearchLog.__table__])
719
+ # 2. Create research_strategies table
720
+ create_research_strategy_table(engine)
61
721
 
62
- if not inspector.has_table(Research.__tablename__):
63
- logger.info("Creating research table.")
64
- Base.metadata.create_all(engine, tables=[Research.__table__])
722
+ # 4. Create rate limiting tables
723
+ create_rate_limiting_tables(engine)
65
724
 
66
- if not inspector.has_table(ResearchHistory.__tablename__):
67
- logger.info("Creating research table.")
68
- Base.metadata.create_all(engine, tables=[ResearchHistory.__table__])
725
+ # 5. Add research_id column to benchmark_results if missing
726
+ add_research_id_to_benchmark_results(engine)
69
727
 
70
- # Import existing settings from files
71
- if db_session:
72
- import_default_settings_file(db_session)
728
+ # 6. Convert research_id columns from integer to string
729
+ convert_research_id_to_string_if_needed(engine)
730
+
731
+ # 7. Add uuid_id column to research_history if missing
732
+ add_uuid_id_column_to_research_history(engine)
733
+
734
+ # 8. Create provider_models table for caching
735
+ create_provider_models_table(engine)
736
+
737
+ logger.info("Schema upgrades completed successfully")
738
+ return True
739
+ except Exception:
740
+ logger.exception("Error during schema upgrades")
741
+ return False
742
+ finally:
743
+ db_session.close()