-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase_migrations.py
More file actions
844 lines (791 loc) · 31.6 KB
/
Copy pathdatabase_migrations.py
File metadata and controls
844 lines (791 loc) · 31.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
"""Versioned, additive database migrations and backup helpers."""
from __future__ import annotations
import shutil
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
SCHEMA_VERSION = 11
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def create_database_backup(db_path: str | Path, destination: str | Path | None = None) -> Path:
"""Create a consistent SQLite backup without modifying the source database."""
source_path = Path(db_path)
if not source_path.exists():
raise FileNotFoundError(source_path)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
target = Path(destination) if destination else source_path.with_suffix(f".{stamp}.bak")
if source_path.resolve() == target.resolve():
raise ValueError("Backup destination must differ from the active database")
target.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(source_path) as source, sqlite3.connect(target) as output:
source.backup(output)
return target
def restore_database_backup(backup_path: str | Path, db_path: str | Path) -> Path:
"""Restore a user-selected backup after validating that it is SQLite."""
source = Path(backup_path)
target = Path(db_path)
if source.resolve() == target.resolve():
raise ValueError("Restore source must differ from the active database")
with sqlite3.connect(source) as connection:
connection.execute("PRAGMA schema_version").fetchone()
target.parent.mkdir(parents=True, exist_ok=True)
temporary = target.with_suffix(target.suffix + ".restore")
shutil.copy2(source, temporary)
temporary.replace(target)
return target
def ensure_application_schema(connection: sqlite3.Connection) -> int:
"""Apply every additive UnityScraper schema migration."""
connection.execute(
"""
CREATE TABLE IF NOT EXISTS app_schema_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL
)
"""
)
applied = {
int(row[0])
for row in connection.execute("SELECT version FROM app_schema_migrations").fetchall()
}
migrations = (
(1, "collection intelligence", _migration_collection),
(2, "preservation records", _migration_preservation),
(3, "console synchronization", _migration_console_sync),
(4, "user overrides and recovery", _migration_reliability),
(5, "XboxUnity title catalog", _migration_xboxunity_catalog),
(6, "profile and save management", _migration_profiles_and_saves),
(7, "profile intelligence and knowledge controls", _migration_roadmap),
(8, "community roadmap workspaces", _migration_community_roadmap),
(9, "hardening and plugin runtime", _migration_hardening),
(10, "release readiness workspaces", _migration_release_readiness),
(11, "offline knowledge archive", _migration_offline_knowledge),
)
for version, name, migration in migrations:
if version in applied:
continue
migration(connection)
connection.execute(
"INSERT INTO app_schema_migrations(version, name, applied_at) VALUES (?, ?, ?)",
(version, name, _now()),
)
connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
return SCHEMA_VERSION
def _migration_collection(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS collection_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_kind TEXT NOT NULL,
source_location TEXT NOT NULL,
label TEXT,
started_at TEXT NOT NULL,
completed_at TEXT,
item_count INTEGER NOT NULL DEFAULT 0,
total_size INTEGER NOT NULL DEFAULT 0,
health_score INTEGER,
status TEXT NOT NULL DEFAULT 'running',
warnings_json TEXT
);
CREATE TABLE IF NOT EXISTS collection_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER NOT NULL,
titleid TEXT,
media_id TEXT,
name TEXT NOT NULL,
format TEXT NOT NULL,
content_type TEXT,
path TEXT NOT NULL,
size INTEGER NOT NULL DEFAULT 0,
disc_number INTEGER,
disc_count INTEGER,
status TEXT NOT NULL,
compatibility TEXT,
notes_json TEXT,
FOREIGN KEY(snapshot_id) REFERENCES collection_snapshots(id)
);
CREATE INDEX IF NOT EXISTS idx_collection_items_titleid
ON collection_items(titleid, media_id);
CREATE INDEX IF NOT EXISTS idx_collection_items_snapshot
ON collection_items(snapshot_id);
"""
)
def _migration_preservation(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS local_file_hashes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
size INTEGER NOT NULL,
modified_ns INTEGER NOT NULL,
crc32 TEXT,
md5 TEXT,
sha1 TEXT,
sha256 TEXT,
calculated_at TEXT NOT NULL,
UNIQUE(path, size, modified_ns)
);
CREATE INDEX IF NOT EXISTS idx_local_hash_sha256 ON local_file_hashes(sha256);
CREATE INDEX IF NOT EXISTS idx_local_hash_sha1 ON local_file_hashes(sha1);
CREATE TABLE IF NOT EXISTS preservation_matches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_hash_id INTEGER NOT NULL,
entity_id INTEGER NOT NULL,
identifier_type TEXT NOT NULL,
identifier_value TEXT NOT NULL,
matched_at TEXT NOT NULL,
UNIQUE(file_hash_id, entity_id, identifier_type),
FOREIGN KEY(file_hash_id) REFERENCES local_file_hashes(id),
FOREIGN KEY(entity_id) REFERENCES knowledge_entities(id)
);
CREATE TABLE IF NOT EXISTS repair_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'preview',
summary_json TEXT,
FOREIGN KEY(snapshot_id) REFERENCES collection_snapshots(id)
);
CREATE TABLE IF NOT EXISTS repair_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plan_id INTEGER NOT NULL,
action_type TEXT NOT NULL,
target TEXT NOT NULL,
reason TEXT NOT NULL,
destructive INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'proposed',
details_json TEXT,
FOREIGN KEY(plan_id) REFERENCES repair_plans(id)
);
"""
)
def _migration_console_sync(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS console_inventory_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target_id INTEGER,
label TEXT,
root TEXT NOT NULL,
captured_at TEXT NOT NULL,
item_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'running',
error_message TEXT,
FOREIGN KEY(target_id) REFERENCES backup_targets(id)
);
CREATE TABLE IF NOT EXISTS console_inventory_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER NOT NULL,
remote_path TEXT NOT NULL,
size INTEGER,
modified_at TEXT,
is_directory INTEGER NOT NULL DEFAULT 0,
titleid TEXT,
media_id TEXT,
UNIQUE(snapshot_id, remote_path),
FOREIGN KEY(snapshot_id) REFERENCES console_inventory_snapshots(id)
);
CREATE TABLE IF NOT EXISTS console_transfer_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target_id INTEGER,
direction TEXT NOT NULL CHECK(direction IN ('upload', 'download')),
local_path TEXT NOT NULL,
remote_path TEXT NOT NULL,
total_bytes INTEGER NOT NULL DEFAULT 0,
transferred_bytes INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'queued',
priority INTEGER NOT NULL DEFAULT 100,
bandwidth_limit INTEGER NOT NULL DEFAULT 0,
expected_sha256 TEXT,
error_message TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(target_id) REFERENCES backup_targets(id)
);
CREATE INDEX IF NOT EXISTS idx_console_jobs_status
ON console_transfer_jobs(status, priority, created_at);
"""
)
def _migration_reliability(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS metadata_overrides (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL,
identifier_type TEXT NOT NULL,
identifier_value TEXT NOT NULL,
property TEXT NOT NULL,
value TEXT NOT NULL,
notes TEXT,
updated_at TEXT NOT NULL,
UNIQUE(entity_type, identifier_type, identifier_value, property)
);
CREATE TABLE IF NOT EXISTS recovery_state (
key TEXT PRIMARY KEY,
value_json TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""
)
def _migration_xboxunity_catalog(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS xboxunity_title_catalog (
titleid TEXT PRIMARY KEY,
name TEXT NOT NULL,
hb_titleid TEXT,
title_type TEXT,
link_enabled INTEGER NOT NULL DEFAULT 0,
covers_count INTEGER NOT NULL DEFAULT 0,
updates_count INTEGER NOT NULL DEFAULT 0,
media_id_count INTEGER NOT NULL DEFAULT 0,
user_count INTEGER NOT NULL DEFAULT 0,
newest_content TEXT,
source_url TEXT NOT NULL,
raw_json TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_xboxunity_catalog_name
ON xboxunity_title_catalog(name COLLATE NOCASE);
CREATE INDEX IF NOT EXISTS idx_xboxunity_catalog_type
ON xboxunity_title_catalog(title_type);
CREATE TABLE IF NOT EXISTS xboxunity_catalog_sync_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL,
completed_at TEXT,
status TEXT NOT NULL,
pages_expected INTEGER NOT NULL DEFAULT 0,
pages_fetched INTEGER NOT NULL DEFAULT 0,
items_upserted INTEGER NOT NULL DEFAULT 0,
error_message TEXT
);
"""
)
def _migration_profiles_and_saves(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS profile_scan_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_root TEXT NOT NULL,
started_at TEXT NOT NULL,
completed_at TEXT,
status TEXT NOT NULL DEFAULT 'running',
profile_count INTEGER NOT NULL DEFAULT 0,
save_count INTEGER NOT NULL DEFAULT 0,
warning_count INTEGER NOT NULL DEFAULT 0,
warnings_json TEXT,
error_message TEXT
);
CREATE TABLE IF NOT EXISTS xbox_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id TEXT NOT NULL,
gamertag TEXT,
source_path TEXT NOT NULL,
package_path TEXT,
package_sha256 TEXT,
console_id TEXT,
device_id TEXT,
profile_kind TEXT NOT NULL DEFAULT 'unknown',
package_status TEXT NOT NULL DEFAULT 'unverified',
first_seen_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
metadata_json TEXT,
UNIQUE(profile_id, source_path)
);
CREATE INDEX IF NOT EXISTS idx_xbox_profiles_profile_id
ON xbox_profiles(profile_id);
CREATE TABLE IF NOT EXISTS profile_saves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id TEXT NOT NULL,
titleid TEXT NOT NULL,
name TEXT NOT NULL,
source_path TEXT NOT NULL UNIQUE,
package_magic TEXT,
content_type INTEGER,
save_game_id TEXT,
embedded_profile_id TEXT,
console_id TEXT,
device_id TEXT,
size INTEGER NOT NULL DEFAULT 0,
modified_at TEXT,
sha256 TEXT,
status TEXT NOT NULL DEFAULT 'unverified',
first_seen_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
metadata_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_profile_saves_owner
ON profile_saves(profile_id, titleid);
CREATE INDEX IF NOT EXISTS idx_profile_saves_sha256
ON profile_saves(sha256);
CREATE TABLE IF NOT EXISTS save_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id TEXT,
label TEXT,
source_root TEXT NOT NULL,
snapshot_path TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
file_count INTEGER NOT NULL DEFAULT 0,
total_size INTEGER NOT NULL DEFAULT 0,
manifest_sha256 TEXT,
status TEXT NOT NULL DEFAULT 'creating',
notes TEXT
);
CREATE TABLE IF NOT EXISTS save_snapshot_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER NOT NULL,
source_path TEXT NOT NULL,
relative_path TEXT NOT NULL,
sha256 TEXT NOT NULL,
size INTEGER NOT NULL,
modified_at TEXT,
item_kind TEXT NOT NULL,
titleid TEXT,
restore_status TEXT,
UNIQUE(snapshot_id, relative_path),
FOREIGN KEY(snapshot_id) REFERENCES save_snapshots(id)
);
CREATE INDEX IF NOT EXISTS idx_save_snapshot_files_snapshot
ON save_snapshot_files(snapshot_id);
CREATE TABLE IF NOT EXISTS profile_save_operations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
operation_type TEXT NOT NULL,
target_path TEXT,
snapshot_id INTEGER,
status TEXT NOT NULL,
started_at TEXT NOT NULL,
completed_at TEXT,
details_json TEXT,
error_message TEXT,
FOREIGN KEY(snapshot_id) REFERENCES save_snapshots(id)
);
"""
)
def _migration_roadmap(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS profile_gpd_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id TEXT,
titleid TEXT,
source_path TEXT NOT NULL UNIQUE,
sha256 TEXT NOT NULL,
size INTEGER NOT NULL,
version INTEGER NOT NULL,
entry_count INTEGER NOT NULL,
achievement_count INTEGER NOT NULL DEFAULT 0,
unlocked_count INTEGER NOT NULL DEFAULT 0,
gamerscore_earned INTEGER NOT NULL DEFAULT 0,
gamerscore_possible INTEGER NOT NULL DEFAULT 0,
parsed_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'parsed',
warnings_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_profile_gpd_owner
ON profile_gpd_files(profile_id, titleid);
CREATE TABLE IF NOT EXISTS profile_achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gpd_file_id INTEGER NOT NULL,
achievement_id INTEGER NOT NULL,
title TEXT,
locked_description TEXT,
unlocked_description TEXT,
gamerscore INTEGER NOT NULL DEFAULT 0,
unlock_state TEXT NOT NULL,
unlocked_at TEXT,
image_id INTEGER,
entry_id INTEGER,
UNIQUE(gpd_file_id, achievement_id),
FOREIGN KEY(gpd_file_id) REFERENCES profile_gpd_files(id)
);
CREATE INDEX IF NOT EXISTS idx_profile_achievements_state
ON profile_achievements(gpd_file_id, unlock_state);
CREATE TABLE IF NOT EXISTS profile_comparisons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
left_profile_id TEXT NOT NULL,
right_profile_id TEXT NOT NULL,
created_at TEXT NOT NULL,
summary_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS xenia_migration_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_profile_id TEXT NOT NULL,
target_profile_id TEXT NOT NULL,
destination_root TEXT NOT NULL,
snapshot_id INTEGER,
created_at TEXT NOT NULL,
completed_at TEXT,
status TEXT NOT NULL,
copied_count INTEGER NOT NULL DEFAULT 0,
skipped_count INTEGER NOT NULL DEFAULT 0,
conflict_count INTEGER NOT NULL DEFAULT 0,
plan_json TEXT NOT NULL,
error_message TEXT,
FOREIGN KEY(snapshot_id) REFERENCES save_snapshots(id)
);
CREATE TABLE IF NOT EXISTS knowledge_source_priorities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
property TEXT NOT NULL,
source_id INTEGER NOT NULL,
priority INTEGER NOT NULL DEFAULT 100,
updated_at TEXT NOT NULL,
UNIQUE(property, source_id),
FOREIGN KEY(source_id) REFERENCES knowledge_sources(id)
);
CREATE TABLE IF NOT EXISTS knowledge_conflict_resolutions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conflict_id INTEGER NOT NULL,
resolution TEXT NOT NULL,
preferred_value TEXT,
preferred_source_id INTEGER,
notes TEXT,
resolved_at TEXT NOT NULL,
FOREIGN KEY(conflict_id) REFERENCES knowledge_conflicts(id),
FOREIGN KEY(preferred_source_id) REFERENCES knowledge_sources(id)
);
CREATE TABLE IF NOT EXISTS scheduled_sync_state (
task_name TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 0,
interval_hours INTEGER NOT NULL DEFAULT 168,
last_started_at TEXT,
last_completed_at TEXT,
last_status TEXT,
last_error TEXT,
updated_at TEXT NOT NULL
);
"""
)
transfer_columns = {
row[1] for row in connection.execute("PRAGMA table_info(console_transfer_jobs)")
}
if "verify_remote_hash" not in transfer_columns:
connection.execute(
"""
ALTER TABLE console_transfer_jobs
ADD COLUMN verify_remote_hash INTEGER NOT NULL DEFAULT 0
"""
)
def _migration_community_roadmap(connection: sqlite3.Connection) -> None:
"""Add durable records for the community-facing roadmap workspaces."""
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS structured_knowledge_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
source_id INTEGER NOT NULL,
record_type TEXT NOT NULL,
canonical_name TEXT NOT NULL,
normalized_name TEXT NOT NULL,
properties_json TEXT NOT NULL,
confidence REAL NOT NULL DEFAULT 0.75,
extracted_at TEXT NOT NULL,
UNIQUE(document_id, record_type, normalized_name),
FOREIGN KEY(document_id) REFERENCES source_documents(id),
FOREIGN KEY(source_id) REFERENCES knowledge_sources(id)
);
CREATE INDEX IF NOT EXISTS idx_structured_knowledge_lookup
ON structured_knowledge_records(record_type, normalized_name);
CREATE TABLE IF NOT EXISTS console_sync_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dashboard_slug TEXT NOT NULL,
local_root TEXT NOT NULL,
remote_root TEXT NOT NULL,
snapshot_id INTEGER,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'preview',
summary_json TEXT NOT NULL,
FOREIGN KEY(snapshot_id) REFERENCES console_inventory_snapshots(id)
);
CREATE TABLE IF NOT EXISTS console_sync_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plan_id INTEGER NOT NULL,
action TEXT NOT NULL,
local_path TEXT,
remote_path TEXT,
size INTEGER NOT NULL DEFAULT 0,
reason TEXT NOT NULL,
selected INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'preview',
FOREIGN KEY(plan_id) REFERENCES console_sync_plans(id)
);
CREATE TABLE IF NOT EXISTS profile_migration_previews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id TEXT NOT NULL,
source_path TEXT NOT NULL,
target_profile_id TEXT,
target_device_id TEXT,
target_console_id TEXT,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'preview',
warnings_json TEXT NOT NULL,
changes_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS profile_gpd_titles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gpd_file_id INTEGER NOT NULL,
entry_id INTEGER NOT NULL,
titleid TEXT NOT NULL,
title TEXT,
achievements_earned INTEGER NOT NULL DEFAULT 0,
achievements_possible INTEGER NOT NULL DEFAULT 0,
gamerscore_earned INTEGER NOT NULL DEFAULT 0,
gamerscore_possible INTEGER NOT NULL DEFAULT 0,
last_played_at TEXT,
UNIQUE(gpd_file_id, entry_id),
FOREIGN KEY(gpd_file_id) REFERENCES profile_gpd_files(id)
);
CREATE TABLE IF NOT EXISTS profile_gpd_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gpd_file_id INTEGER NOT NULL,
entry_id INTEGER NOT NULL,
image_format TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
UNIQUE(gpd_file_id, entry_id),
FOREIGN KEY(gpd_file_id) REFERENCES profile_gpd_files(id)
);
CREATE TABLE IF NOT EXISTS save_comparison_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
left_path TEXT NOT NULL,
right_path TEXT NOT NULL,
created_at TEXT NOT NULL,
identical INTEGER NOT NULL,
summary_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS artwork_preferences (
titleid TEXT PRIMARY KEY,
source_path TEXT NOT NULL,
artwork_type TEXT NOT NULL DEFAULT 'cover',
region TEXT,
language TEXT,
width INTEGER,
height INTEGER,
sha256 TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS artwork_export_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
destination TEXT NOT NULL,
preset TEXT NOT NULL,
created_at TEXT NOT NULL,
exported_count INTEGER NOT NULL DEFAULT 0,
skipped_count INTEGER NOT NULL DEFAULT 0,
manifest_path TEXT,
status TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS disc_set_audits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER,
titleid TEXT NOT NULL,
media_id TEXT,
expected_count INTEGER NOT NULL,
present_json TEXT NOT NULL,
missing_json TEXT NOT NULL,
status TEXT NOT NULL,
audited_at TEXT NOT NULL,
FOREIGN KEY(snapshot_id) REFERENCES collection_snapshots(id)
);
CREATE TABLE IF NOT EXISTS dedup_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
root TEXT NOT NULL,
created_at TEXT NOT NULL,
duplicate_groups INTEGER NOT NULL DEFAULT 0,
reclaimable_bytes INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'preview'
);
CREATE TABLE IF NOT EXISTS dedup_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plan_id INTEGER NOT NULL,
sha256 TEXT NOT NULL,
keeper_path TEXT NOT NULL,
duplicate_path TEXT NOT NULL,
size INTEGER NOT NULL,
action TEXT NOT NULL DEFAULT 'review',
status TEXT NOT NULL DEFAULT 'preview',
FOREIGN KEY(plan_id) REFERENCES dedup_plans(id)
);
CREATE TABLE IF NOT EXISTS storage_source_audits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_path TEXT NOT NULL,
source_kind TEXT NOT NULL,
filesystem TEXT,
access_mode TEXT NOT NULL DEFAULT 'read-only',
detected_at TEXT NOT NULL,
status TEXT NOT NULL,
details_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS original_xbox_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titleid TEXT,
title_name TEXT NOT NULL,
xbe_path TEXT NOT NULL UNIQUE,
region_flags TEXT,
version TEXT,
compatibility TEXT,
metadata_json TEXT NOT NULL,
scanned_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS plugin_states (
plugin_id TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 0,
trusted_sha256 TEXT,
permissions_json TEXT NOT NULL DEFAULT '[]',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS recovery_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
source TEXT NOT NULL,
status TEXT NOT NULL,
recoverable INTEGER NOT NULL DEFAULT 1,
details_json TEXT NOT NULL,
detected_at TEXT NOT NULL,
resolved_at TEXT
);
CREATE TABLE IF NOT EXISTS dashboard_compatibility_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dashboard_slug TEXT NOT NULL,
host_label TEXT,
tested_at TEXT NOT NULL,
feature TEXT NOT NULL,
supported INTEGER NOT NULL,
details TEXT,
UNIQUE(dashboard_slug, host_label, feature)
);
CREATE TABLE IF NOT EXISTS accessibility_preferences (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""
)
def _migration_hardening(connection: sqlite3.Connection) -> None:
"""Add runtime audits and reversible-action metadata."""
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS plugin_collection_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plugin_id TEXT NOT NULL,
titleid TEXT NOT NULL,
status TEXT NOT NULL,
started_at TEXT NOT NULL,
completed_at TEXT,
result_json TEXT,
error_message TEXT,
FOREIGN KEY(plugin_id) REFERENCES plugin_states(plugin_id)
);
CREATE INDEX IF NOT EXISTS idx_plugin_collection_runs_lookup
ON plugin_collection_runs(plugin_id, titleid, started_at);
CREATE TABLE IF NOT EXISTS dedup_recovery_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action_id INTEGER NOT NULL UNIQUE,
original_path TEXT NOT NULL,
quarantine_path TEXT NOT NULL,
keeper_path TEXT NOT NULL,
mode TEXT NOT NULL,
sha256 TEXT NOT NULL,
created_at TEXT NOT NULL,
restored_at TEXT,
status TEXT NOT NULL DEFAULT 'quarantined',
FOREIGN KEY(action_id) REFERENCES dedup_actions(id)
);
"""
)
def _migration_release_readiness(connection: sqlite3.Connection) -> None:
"""Add audit history for portable metadata, reports, and hardware records."""
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS metadata_snapshot_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
operation TEXT NOT NULL,
snapshot_path TEXT NOT NULL,
created_at TEXT NOT NULL,
completed_at TEXT,
catalog_count INTEGER NOT NULL DEFAULT 0,
fact_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL,
sha256 TEXT,
error_message TEXT
);
CREATE TABLE IF NOT EXISTS library_intelligence_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
title_count INTEGER NOT NULL DEFAULT 0,
issue_count INTEGER NOT NULL DEFAULT 0,
summary_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS preservation_report_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
destination TEXT NOT NULL,
created_at TEXT NOT NULL,
report_format TEXT NOT NULL,
status TEXT NOT NULL,
sha256 TEXT
);
CREATE TABLE IF NOT EXISTS correction_packages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
operation TEXT NOT NULL,
package_path TEXT NOT NULL,
created_at TEXT NOT NULL,
correction_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL,
sha256 TEXT
);
CREATE TABLE IF NOT EXISTS hardware_inventory_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
motherboard TEXT,
dvd_drive TEXT,
nand_type TEXT,
dashboard_version TEXT,
console_type TEXT,
notes TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS package_extraction_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_path TEXT NOT NULL,
destination TEXT NOT NULL,
created_at TEXT NOT NULL,
extracted_count INTEGER NOT NULL DEFAULT 0,
skipped_count INTEGER NOT NULL DEFAULT 0,
manifest_path TEXT,
status TEXT NOT NULL
);
"""
)
def _migration_offline_knowledge(connection: sqlite3.Connection) -> None:
"""Track generated offline pages and user-provided wiki imports."""
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS offline_archive_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL,
finished_at TEXT,
status TEXT NOT NULL,
documents_written INTEGER NOT NULL DEFAULT 0,
index_path TEXT,
errors TEXT
);
CREATE TABLE IF NOT EXISTS offline_archive_documents (
document_id INTEGER PRIMARY KEY,
archive_path TEXT NOT NULL,
rendered_at TEXT NOT NULL,
content_sha256 TEXT NOT NULL,
stale INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(document_id) REFERENCES source_documents(id)
);
CREATE TABLE IF NOT EXISTS offline_page_import_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_slug TEXT NOT NULL,
source_path TEXT NOT NULL,
started_at TEXT NOT NULL,
finished_at TEXT,
status TEXT NOT NULL,
files_seen INTEGER NOT NULL DEFAULT 0,
files_imported INTEGER NOT NULL DEFAULT 0,
errors TEXT
);
"""
)