Skip to content

Commit 6961d40

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 6961d40

File tree

6 files changed

+128
-61
lines changed

6 files changed

+128
-61
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: 75 additions & 33 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

@@ -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)))

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
),

docs/changelog.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ Bug fixes:
5757
:bug:`5699`
5858
* :doc:`plugins/discogs`: Beets will no longer crash if a release has been
5959
deleted, and returns a 404.
60-
* Beets now creates an index for ``album_id`` on the ``items`` sqlite table.
61-
This speeds up queries that filter items by their album significantly.
60+
* Beets now creates an index for the ``album_id`` field in the ``items`` table.
61+
This significantly speeds up queries that filter items by their album.
6262
:bug:`5809`
6363

6464
For packagers:

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)