Skip to content

Commit 40fee27

Browse files
committed
Reverted some changes to _make_table function and added a _make_indices
function. Moved Index into db.py from types.py
1 parent b14be1b commit 40fee27

File tree

5 files changed

+125
-54
lines changed

5 files changed

+125
-54
lines changed

beets/dbcore/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
Library.
1717
"""
1818

19-
from .db import Database, Model, Results
19+
from .db import Database, Index, Model, Results
2020
from .query import (
2121
AndQuery,
2222
FieldQuery,
@@ -43,6 +43,7 @@
4343
"Query",
4444
"Results",
4545
"Type",
46+
"Index",
4647
"parse_sorted_query",
4748
"query_from_strings",
4849
"sort_from_strings",

beets/dbcore/db.py

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from collections import defaultdict
2727
from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence
2828
from 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

3131
from typing_extensions import TypeVar # default value support
3232
from 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

@@ -1164,28 +1163,20 @@ def _make_table(
11641163
self,
11651164
table: str,
11661165
fields: Mapping[str, types.Type],
1167-
indices: Sequence[types.Index],
11681166
):
11691167
"""Set up the schema of the database. `fields` is a mapping
11701168
from field names to `Type`s. Columns are added if necessary.
11711169
"""
1172-
# Get current schema and existing indexes
1170+
# Get current schema.
11731171
with self.transaction() as tx:
11741172
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}
1173+
current_fields = {row[1] for row in rows}
11781174

1179-
# Skip table creation if the current schema matches the
1180-
# requested schema (and no indexes are missing).
11811175
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):
1176+
if current_fields.issuperset(field_names):
1177+
# Table exists and has all the required columns.
11861178
return
11871179

1188-
# Table schema handling
11891180
if not current_fields:
11901181
# No table exists.
11911182
columns = []
@@ -1208,17 +1199,6 @@ def _make_table(
12081199
with self.transaction() as tx:
12091200
tx.script(setup_sql)
12101201

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-
12221202
def _make_attribute_table(self, flex_table: str):
12231203
"""Create a table and associated index for flexible attributes
12241204
for the given entity (if they don't exist).
@@ -1237,6 +1217,33 @@ def _make_attribute_table(self, flex_table: str):
12371217
""".format(flex_table)
12381218
)
12391219

1220+
def _migrate_indices(
1221+
self,
1222+
table: str,
1223+
indices: Sequence[Index],
1224+
):
1225+
"""Create or replace indices for the given table.
1226+
1227+
If the indices already exists and are up to date (i.e., the
1228+
index name and columns match), nothing is done. Otherwise, the
1229+
indices are created or replaced.
1230+
"""
1231+
with self.transaction() as tx:
1232+
index_rows = tx.query(f"PRAGMA index_list({table})")
1233+
current_indices = {Index.from_db(tx, row[1]) for row in index_rows}
1234+
1235+
_indices = set(indices)
1236+
1237+
if current_indices.issuperset(_indices):
1238+
return
1239+
1240+
# May also include missing indices.
1241+
changed_indices = _indices - current_indices
1242+
1243+
with self.transaction() as tx:
1244+
for index in changed_indices:
1245+
index.recreate(tx, table)
1246+
12401247
# Querying.
12411248

12421249
def _fetch(
@@ -1306,3 +1313,42 @@ def _get(
13061313
exist.
13071314
"""
13081315
return self._fetch(model_cls, MatchQuery("id", id)).get()
1316+
1317+
1318+
class Index(NamedTuple):
1319+
"""A helper class to represent the index
1320+
information in the database schema.
1321+
"""
1322+
1323+
name: str
1324+
columns: Sequence[str]
1325+
1326+
def recreate(self, tx: Transaction, table: str) -> None:
1327+
"""Recreate the index in the database.
1328+
1329+
This is useful when the index has been changed and needs to be
1330+
updated.
1331+
"""
1332+
tx.script(f"DROP INDEX IF EXISTS {self.name}")
1333+
self.create(tx, table)
1334+
1335+
def create(self, tx: Transaction, table: str) -> None:
1336+
"""Create the index in the database."""
1337+
return tx.script(
1338+
f"CREATE INDEX {self.name} ON {table} ({', '.join(self.columns)})"
1339+
)
1340+
1341+
@classmethod
1342+
def from_db(cls, tx: Transaction, name: str) -> Index:
1343+
"""Create an Index object from the database if it exists.
1344+
1345+
The name has to exists in the database! Otherwise, an
1346+
Error will be raised.
1347+
"""
1348+
rows = tx.query(f"PRAGMA index_info({name})")
1349+
columns = [row[2] for row in rows]
1350+
return cls(name, columns)
1351+
1352+
def __hash__(self) -> int:
1353+
"""Unique hash for the index based on its name and columns."""
1354+
return hash((self.name, tuple(self.columns)))

beets/dbcore/types.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,7 @@
2020
import time
2121
import typing
2222
from abc import ABC
23-
from typing import (
24-
TYPE_CHECKING,
25-
Any,
26-
Generic,
27-
NamedTuple,
28-
Sequence,
29-
TypeVar,
30-
cast,
31-
)
23+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
3224

3325
import beets
3426
from beets import util
@@ -462,21 +454,6 @@ def parse(self, string):
462454
return self.null
463455

464456

465-
class Index(NamedTuple):
466-
"""A database index.
467-
468-
A helper class to represent the index
469-
information in the schema.
470-
"""
471-
472-
name: str
473-
columns: Sequence[str]
474-
table: str | None = None
475-
"""As a index is normally bound to a table
476-
we may omit it here.
477-
"""
478-
479-
480457
# Shared instances of common types.
481458
DEFAULT = Default()
482459
INTEGER = Integer()

beets/library/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ class Item(LibModel):
717717
"added": types.DATE,
718718
}
719719
_indices = (
720-
types.Index(
720+
dbcore.Index(
721721
name="idx_item_album_id",
722722
columns=("album_id",),
723723
),

test/test_dbcore.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import pytest
2424

2525
from beets import dbcore
26+
from beets.dbcore.db import Index
2627
from beets.library import LibModel
2728
from beets.test import _common
2829
from beets.util import cached_classproperty
@@ -58,6 +59,7 @@ class ModelFixture1(LibModel):
5859
_sorts = {
5960
"some_sort": SortFixture,
6061
}
62+
_indices = (Index("field_one_index", ("field_one",)),)
6163

6264
@cached_classproperty
6365
def _types(cls):
@@ -784,3 +786,48 @@ def test_no_results(self):
784786
self.db._fetch(ModelFixture1, dbcore.query.FalseQuery()).get()
785787
is None
786788
)
789+
790+
791+
class TestIndex:
792+
@pytest.fixture(autouse=True)
793+
def db(self):
794+
"""Set up an in-memory SQLite database."""
795+
db = DatabaseFixture1(":memory:")
796+
yield db
797+
db._connection().close()
798+
799+
@pytest.fixture
800+
def sample_index(self):
801+
"""Fixture for a sample Index object."""
802+
return Index(name="sample_index", columns=["field_one"])
803+
804+
def test_index_creation(self, db, sample_index):
805+
"""Test creating an index and checking its existence."""
806+
with db.transaction() as tx:
807+
sample_index.create(tx, "test")
808+
indexes = (
809+
db._connection().execute("PRAGMA index_list(test)").fetchall()
810+
)
811+
assert any(sample_index.name in index for index in indexes)
812+
813+
def test_from_db(self, db, sample_index):
814+
"""Test retrieving an index from the database."""
815+
with db.transaction() as tx:
816+
sample_index.create(tx, "test")
817+
retrieved = Index.from_db(tx, sample_index.name)
818+
assert retrieved == sample_index
819+
820+
def test_index_hashing_and_set_behavior(self, sample_index):
821+
"""Test the hashing and set behavior of the Index class."""
822+
index_set = {sample_index}
823+
similar_index = Index(name="sample_index", columns=["field_one"])
824+
825+
assert similar_index in index_set # Should recognize similar attributes
826+
827+
different_index = Index(name="other_index", columns=["other_field"])
828+
index_set.add(different_index)
829+
830+
assert len(index_set) == 2 # Should recognize distinct index
831+
832+
index_set.discard(sample_index)
833+
assert similar_index not in index_set # Should remove similar index

0 commit comments

Comments
 (0)