11#
22# steam_library_manager/core/db/schema.py
3- # SQLite schema definitions and migration logic for schema versioning
3+ # Schema creation and migration (v3 through v9)
44#
55# Copyright © 2025-2026 SwitchBros
66# Licensed under the MIT License. See LICENSE for details.
2121
2222
2323class SchemaMixin :
24- """Mixin providing schema creation and migration logic.
24+ """Schema creation and migration logic.
2525
26- Requires ConnectionBase attributes: conn, SCHEMA_VERSION.
26+ Creates schema from schema.sql on first run, then applies
27+ migrations v3-v9 for existing databases. Each migration is
28+ idempotent (IF NOT EXISTS, try/except for ALTER TABLE).
2729 """
2830
29- def _ensure_schema (self ) -> None :
30- """Create or migrate database schema."""
31- current_version = self ._get_schema_version ()
32-
33- if current_version == 0 :
31+ def _ensure_schema (self ):
32+ ver = self ._get_schema_version ()
33+ if ver == 0 :
3434 self ._create_schema ()
3535 self ._set_schema_version (self .SCHEMA_VERSION )
36- elif current_version < self .SCHEMA_VERSION :
37- self ._migrate (current_version , self .SCHEMA_VERSION )
36+ elif ver < self .SCHEMA_VERSION :
37+ self ._migrate (ver , self .SCHEMA_VERSION )
3838
39- def _get_schema_version (self ) -> int :
40- """Get current database schema version."""
39+ def _get_schema_version (self ):
4140 try :
42- cursor = self .conn .execute ("SELECT MAX(version) FROM schema_version" )
43- result = cursor .fetchone ()
44- return result [0 ] if result [0 ] is not None else 0
41+ cur = self .conn .execute ("SELECT MAX(version) FROM schema_version" )
42+ r = cur .fetchone ()
43+ return r [0 ] if r [0 ] is not None else 0
4544 except sqlite3 .OperationalError :
4645 return 0
4746
48- def _set_schema_version (self , version : int ) -> None :
49- """Set database schema version."""
47+ def _set_schema_version (self , v ):
5048 self .conn .execute (
51- """
52- INSERT OR REPLACE INTO schema_version (version, applied_at, description)
53- VALUES (?, ?, ?)
54- """ ,
55- (version , int (time .time ()), t ("logs.db.schema_created" )),
49+ "INSERT OR REPLACE INTO schema_version (version, applied_at, description) VALUES (?, ?, ?)" ,
50+ (v , int (time .time ()), t ("logs.db.schema_created" )),
5651 )
5752 self .conn .commit ()
5853
59- def _create_schema (self ) -> None :
60- """Create initial database schema from SQL file."""
61- schema_path = Path (__file__ ).parent / "schema.sql"
54+ def _create_schema (self ):
55+ # load schema from sql
56+ p = Path (__file__ ).parent / "schema.sql"
6257 try :
63- with open (schema_path ) as f :
64- schema_sql = f .read ()
58+ with open (p ) as f :
59+ sql = f .read ()
6560 except FileNotFoundError :
66- logger .error (t ("logs.db.schema_not_found" , path = str (schema_path )))
61+ logger .error (t ("logs.db.schema_not_found" , path = str (p )))
6762 raise
6863
6964 try :
70- self .conn .executescript (schema_sql )
65+ self .conn .executescript (sql )
7166 self .conn .commit ()
7267 logger .info (t ("logs.db.schema_created" ))
7368 except sqlite3 .Error as e :
7469 logger .error (t ("logs.db.schema_error" , error = str (e )))
7570 raise
7671
77- def _migrate (self , from_version : int , to_version : int ) -> None :
78- """Migrate database schema.
79-
80- Args:
81- from_version: Current schema version.
82- to_version: Target schema version.
83- """
84- logger .info (
85- "Migrating database from version %d to %d" ,
86- from_version ,
87- to_version ,
88- )
72+ def _migrate (self , frm , to ):
73+ logger .info ("Migrating from %d to %d" % (frm , to ))
8974
90- if from_version < 3 :
91- self ._migrate_to_v3 ()
75+ if frm < 3 :
76+ self ._m3 ()
9277 self ._set_schema_version (3 )
93-
94- if from_version < 4 :
95- self ._migrate_to_v4 ()
78+ if frm < 4 :
79+ self ._m4 ()
9680 self ._set_schema_version (4 )
97-
98- if from_version < 5 :
99- self ._migrate_to_v5 ()
81+ if frm < 5 :
82+ self ._m5 ()
10083 self ._set_schema_version (5 )
101-
102- if from_version < 6 :
103- self ._migrate_to_v6 ()
84+ if frm < 6 :
85+ self ._m6 ()
10486 self ._set_schema_version (6 )
105-
106- if from_version < 7 :
107- self ._migrate_to_v7 ()
87+ if frm < 7 :
88+ self ._m7 ()
10889 self ._set_schema_version (7 )
109-
110- if from_version < 8 :
111- self ._migrate_to_v8 ()
90+ if frm < 8 :
91+ self ._m8 ()
11292 self ._set_schema_version (8 )
113-
114- if from_version < 9 :
115- self ._migrate_to_v9 ()
93+ if frm < 9 :
94+ self ._m9 ()
11695 self ._set_schema_version (9 )
11796
118- def _migrate_to_v3 (self ) -> None :
119- """Migrate to schema v3: tag_definitions table + tag_id column."""
97+ # migrations
98+
99+ def _m3 (self ):
100+ # tag_definitions + tag_id
120101 try :
121102 self .conn .execute ("ALTER TABLE game_tags ADD COLUMN tag_id INTEGER" )
122103 except sqlite3 .OperationalError :
123- pass # Column already exists
124-
104+ pass
125105 self .conn .execute ("""
126106 CREATE TABLE IF NOT EXISTS tag_definitions (
127107 tag_id INTEGER NOT NULL,
@@ -134,10 +114,9 @@ def _migrate_to_v3(self) -> None:
134114 self .conn .execute ("CREATE INDEX IF NOT EXISTS idx_tag_definitions_name ON tag_definitions(name)" )
135115 self .conn .execute ("CREATE INDEX IF NOT EXISTS idx_tag_definitions_lang ON tag_definitions(language)" )
136116 self .conn .commit ()
137- logger .info ("Migrated to schema v3: tag_definitions + tag_id " )
117+ logger .info ("Migrated to v3 " )
138118
139- def _migrate_to_v4 (self ) -> None :
140- """Migrate to schema v4: hltb_id_cache table."""
119+ def _m4 (self ):
141120 self .conn .execute ("""
142121 CREATE TABLE IF NOT EXISTS hltb_id_cache (
143122 steam_app_id INTEGER PRIMARY KEY,
@@ -146,10 +125,9 @@ def _migrate_to_v4(self) -> None:
146125 )
147126 """ )
148127 self .conn .commit ()
149- logger .info ("Migrated to schema v4: hltb_id_cache " )
128+ logger .info ("Migrated to v4 " )
150129
151- def _migrate_to_v5 (self ) -> None :
152- """Migrate to schema v5: protondb_ratings table."""
130+ def _m5 (self ):
153131 self .conn .execute ("""
154132 CREATE TABLE IF NOT EXISTS protondb_ratings (
155133 app_id INTEGER PRIMARY KEY,
@@ -162,19 +140,17 @@ def _migrate_to_v5(self) -> None:
162140 )
163141 """ )
164142 self .conn .commit ()
165- logger .info ("Migrated to schema v5: protondb_ratings " )
143+ logger .info ("Migrated to v5 " )
166144
167- def _migrate_to_v6 (self ) -> None :
168- """Migrate to schema v6: review_percentage column in games table."""
145+ def _m6 (self ):
169146 try :
170147 self .conn .execute ("ALTER TABLE games ADD COLUMN review_percentage INTEGER" )
171148 except sqlite3 .OperationalError :
172- pass # Column already exists
149+ pass
173150 self .conn .commit ()
174- logger .info ("Migrated to schema v6: review_percentage column " )
151+ logger .info ("Migrated to v6 " )
175152
176- def _migrate_to_v7 (self ) -> None :
177- """Migrate to schema v7: external_games table."""
153+ def _m7 (self ):
178154 self .conn .executescript ("""
179155 CREATE TABLE IF NOT EXISTS external_games (
180156 id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -192,23 +168,23 @@ def _migrate_to_v7(self) -> None:
192168 CREATE INDEX IF NOT EXISTS idx_external_name ON external_games(name);
193169 """ )
194170 self .conn .commit ()
195- logger .info ("Migrated to schema v7: external_games table " )
171+ logger .info ("Migrated to v7 " )
196172
197- def _migrate_to_v8 (self ) -> None :
198- """Migrate to schema v8: PEGI + user data normalization + future tables."""
199- new_columns = [
173+ def _m8 (self ):
174+ # big: pegi + user tables
175+ cs = [
200176 ("pegi_rating" , "TEXT DEFAULT ''" ),
201177 ("esrb_rating" , "TEXT DEFAULT ''" ),
202178 ("metacritic_score" , "INTEGER DEFAULT 0" ),
203179 ("steam_deck_status" , "TEXT DEFAULT ''" ),
204180 ("short_description" , "TEXT DEFAULT ''" ),
205181 ("content_descriptors" , "TEXT DEFAULT ''" ),
206182 ]
207- for col_name , col_def in new_columns :
183+ for n , d in cs :
208184 try :
209- self .conn .execute (f "ALTER TABLE games ADD COLUMN { col_name } { col_def } " )
185+ self .conn .execute ("ALTER TABLE games ADD COLUMN %s %s" % ( n , d ) )
210186 except sqlite3 .OperationalError :
211- pass # Column already exists
187+ pass
212188
213189 self .conn .executescript ("""
214190 CREATE INDEX IF NOT EXISTS idx_games_pegi ON games(pegi_rating);
@@ -295,10 +271,9 @@ def _migrate_to_v8(self) -> None:
295271 );
296272 """ )
297273 self .conn .commit ()
298- logger .info ("Migrated to schema v8: PEGI + user data normalization + future tables " )
274+ logger .info ("Migrated to v8 " )
299275
300- def _migrate_to_v9 (self ) -> None :
301- """Migrate to schema v9: curator tables."""
276+ def _m9 (self ):
302277 self .conn .executescript ("""
303278 CREATE TABLE IF NOT EXISTS curators (
304279 curator_id INTEGER PRIMARY KEY,
@@ -322,4 +297,4 @@ def _migrate_to_v9(self) -> None:
322297 ON curator_recommendations(app_id);
323298 """ )
324299 self .conn .commit ()
325- logger .info ("Migrated to schema v9: curator tables " )
300+ logger .info ("Migrated to v9 " )
0 commit comments