2626from collections import defaultdict
2727from collections .abc import Generator , Iterable , Iterator , Mapping , Sequence
2828from sqlite3 import Connection
29- from typing import TYPE_CHECKING , Any , AnyStr , Callable , Generic
29+ from typing import TYPE_CHECKING , Any , AnyStr , Callable , Generic , NamedTuple
3030
3131from typing_extensions import TypeVar # default value support
3232from unidecode import unidecode
@@ -287,7 +287,7 @@ class Model(ABC, Generic[D]):
287287 terms.
288288 """
289289
290- _indices : Sequence [types . Index ] = ()
290+ _indices : Sequence [Index ] = ()
291291 """A sequence of `Index` objects that describe the indices to be
292292 created for this table.
293293 """
@@ -1036,10 +1036,9 @@ def __init__(self, path, timeout: float = 5.0):
10361036
10371037 # Set up database schema.
10381038 for model_cls in self ._models :
1039- self ._make_table (
1040- model_cls ._table , model_cls ._fields , model_cls ._indices
1041- )
1039+ self ._make_table (model_cls ._table , model_cls ._fields )
10421040 self ._make_attribute_table (model_cls ._flex_table )
1041+ self ._migrate_indices (model_cls ._table , model_cls ._indices )
10431042
10441043 # Primitive access control: connections and transactions.
10451044
@@ -1160,32 +1159,20 @@ def load_extension(self, path: str):
11601159
11611160 # Schema setup and migration.
11621161
1163- def _make_table (
1164- self ,
1165- table : str ,
1166- fields : Mapping [str , types .Type ],
1167- indices : Sequence [types .Index ],
1168- ):
1162+ def _make_table (self , table : str , fields : Mapping [str , types .Type ]):
11691163 """Set up the schema of the database. `fields` is a mapping
11701164 from field names to `Type`s. Columns are added if necessary.
11711165 """
1172- # Get current schema and existing indexes
1166+ # Get current schema.
11731167 with self .transaction () as tx :
11741168 rows = tx .query ("PRAGMA table_info(%s)" % table )
1175- current_fields = {row [1 ] for row in rows }
1176- index_rows = tx .query (f"PRAGMA index_list({ table } )" )
1177- current_indices = {row [1 ] for row in index_rows }
1169+ current_fields = {row [1 ] for row in rows }
11781170
1179- # Skip table creation if the current schema matches the
1180- # requested schema (and no indexes are missing).
11811171 field_names = set (fields .keys ())
1182- index_names = {index .name for index in indices }
1183- if current_fields .issuperset (
1184- field_names
1185- ) and current_indices .issuperset (index_names ):
1172+ if current_fields .issuperset (field_names ):
1173+ # Table exists and has all the required columns.
11861174 return
11871175
1188- # Table schema handling
11891176 if not current_fields :
11901177 # No table exists.
11911178 columns = []
@@ -1208,17 +1195,6 @@ def _make_table(
12081195 with self .transaction () as tx :
12091196 tx .script (setup_sql )
12101197
1211- # Index handling
1212- with self .transaction () as tx :
1213- for index in indices :
1214- if index .name in current_indices :
1215- continue
1216-
1217- columns_str = ", " .join (index .columns )
1218- tx .script (
1219- f"CREATE INDEX { index .name } ON { table } ({ columns_str } )"
1220- )
1221-
12221198 def _make_attribute_table (self , flex_table : str ):
12231199 """Create a table and associated index for flexible attributes
12241200 for the given entity (if they don't exist).
@@ -1237,6 +1213,33 @@ def _make_attribute_table(self, flex_table: str):
12371213 """ .format (flex_table )
12381214 )
12391215
1216+ def _migrate_indices (
1217+ self ,
1218+ table : str ,
1219+ indices : Sequence [Index ],
1220+ ):
1221+ """Create or replace indices for the given table.
1222+
1223+ If the indices already exists and are up to date (i.e., the
1224+ index name and columns match), nothing is done. Otherwise, the
1225+ indices are created or replaced.
1226+ """
1227+ with self .transaction () as tx :
1228+ index_rows = tx .query (f"PRAGMA index_list({ table } )" )
1229+ current_indices = {Index .from_db (tx , row [1 ]) for row in index_rows }
1230+
1231+ _indices = set (indices )
1232+
1233+ if current_indices .issuperset (_indices ):
1234+ return
1235+
1236+ # May also include missing indices.
1237+ changed_indices = _indices - current_indices
1238+
1239+ with self .transaction () as tx :
1240+ for index in changed_indices :
1241+ index .recreate (tx , table )
1242+
12401243 # Querying.
12411244
12421245 def _fetch (
@@ -1306,3 +1309,42 @@ def _get(
13061309 exist.
13071310 """
13081311 return self ._fetch (model_cls , MatchQuery ("id" , id )).get ()
1312+
1313+
1314+ class Index (NamedTuple ):
1315+ """A helper class to represent the index
1316+ information in the database schema.
1317+ """
1318+
1319+ name : str
1320+ columns : Sequence [str ]
1321+
1322+ def recreate (self , tx : Transaction , table : str ) -> None :
1323+ """Recreate the index in the database.
1324+
1325+ This is useful when the index has been changed and needs to be
1326+ updated.
1327+ """
1328+ tx .script (f"DROP INDEX IF EXISTS { self .name } " )
1329+ self .create (tx , table )
1330+
1331+ def create (self , tx : Transaction , table : str ) -> None :
1332+ """Create the index in the database."""
1333+ return tx .script (
1334+ f"CREATE INDEX { self .name } ON { table } ({ ', ' .join (self .columns )} )"
1335+ )
1336+
1337+ @classmethod
1338+ def from_db (cls , tx : Transaction , name : str ) -> Index :
1339+ """Create an Index object from the database if it exists.
1340+
1341+ The name has to exists in the database! Otherwise, an
1342+ Error will be raised.
1343+ """
1344+ rows = tx .query (f"PRAGMA index_info({ name } )" )
1345+ columns = [row [2 ] for row in rows ]
1346+ return cls (name , columns )
1347+
1348+ def __hash__ (self ) -> int :
1349+ """Unique hash for the index based on its name and columns."""
1350+ return hash ((self .name , tuple (self .columns )))
0 commit comments