Skip to content

Commit 5df7768

Browse files
authored
feat: use WAL mode for the library database (#1525)
1 parent 3b13281 commit 5df7768

3 files changed

Lines changed: 34 additions & 33 deletions

File tree

‎src/tagstudio/core/library/alchemy/library.py‎

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
import re
66
import shutil
7+
import sqlite3
78
import sys
89
import time
910
import unicodedata
1011
from collections.abc import Iterable, Iterator, Sequence
12+
from contextlib import closing
1113
from dataclasses import dataclass
1214
from datetime import UTC, datetime
1315
from os import makedirs
@@ -20,7 +22,6 @@
2022
URL,
2123
ColumnExpressionArgument,
2224
Engine,
23-
NullPool,
2425
ScalarResult,
2526
Update,
2627
and_,
@@ -393,29 +394,21 @@ def open_library(self, library_dir: Path, in_memory: bool = False) -> OpenLibrar
393394

394395
@staticmethod
395396
def __get_engine(library_dir: Path, in_memory: bool, sql_filename: str):
397+
db_path = library_dir / TS_FOLDER_NAME / sql_filename
396398
connection_string = URL.create(
397399
drivername="sqlite",
398-
database=(
399-
":memory:" if in_memory else str(library_dir / TS_FOLDER_NAME / sql_filename)
400-
),
400+
database=":memory:" if in_memory else str(db_path),
401401
)
402-
# NOTE: File-based databases should use NullPool to create new DB connection in order to
403-
# keep connections on separate threads, which prevents the DB files from being locked
404-
# even after a connection has been closed.
405-
# SingletonThreadPool (the default for :memory:) should still be used for in-memory DBs.
406-
# More info can be found on the SQLAlchemy docs:
407-
# https://docs.sqlalchemy.org/en/20/changelog/migration_07.html
408-
# Under -> sqlite-the-sqlite-dialect-now-uses-nullpool-for-file-based-databases
409-
poolclass = None if in_memory else NullPool
410402

411-
logger.info(
412-
"[Library] Creating SQLAlchemy Engine",
413-
connection_string=connection_string,
414-
poolclass=poolclass,
415-
)
416-
return create_engine(
417-
connection_string, poolclass=poolclass, connect_args={"autocommit": False}
418-
)
403+
if not in_memory:
404+
try:
405+
with closing(sqlite3.connect(db_path, autocommit=True)) as conn:
406+
conn.execute("PRAGMA journal_mode=WAL")
407+
except sqlite3.OperationalError as e:
408+
logger.warning("[Library] Could not set journal mode", error=e)
409+
410+
logger.info("[Library] Creating SQLAlchemy Engine", connection_string=connection_string)
411+
return create_engine(connection_string, connect_args={"autocommit": False})
419412

420413
def create_sqlite_library(
421414
self, library_dir: Path, in_memory: bool, sql_filename: str = SQL_FILENAME
@@ -1619,17 +1612,18 @@ def save_library_backup_to_disk(library_dir: Path) -> Path:
16191612
makedirs(str(library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
16201613

16211614
filename = f"ts_library_backup_{datetime.now(UTC).strftime('%Y_%m_%d_%H%M%S')}.sqlite"
1615+
library_backup_path = library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
16221616

1623-
target_path = library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
1624-
1625-
shutil.copyfile(
1626-
library_dir / TS_FOLDER_NAME / SQL_FILENAME,
1627-
target_path,
1628-
)
1617+
with (
1618+
closing(sqlite3.connect(library_dir / TS_FOLDER_NAME / SQL_FILENAME)) as library,
1619+
closing(sqlite3.connect(library_backup_path)) as library_backup,
1620+
):
1621+
library.backup(library_backup)
1622+
library_backup.execute("PRAGMA journal_mode=DELETE") # Don't use WAL for backups
16291623

1630-
logger.info("Library backup saved to disk.", path=target_path)
1624+
logger.info("[Library] Library backup saved to disk", path=library_backup_path)
16311625

1632-
return target_path
1626+
return library_backup_path
16331627

16341628
def get_tag(self, tag_id: int) -> Tag | None:
16351629
with Session(self.engine) as session:

‎src/tagstudio/qt/mixed/migration_modal.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,6 @@ def update_parity_ui(self):
438438
self.update_parity_value(self.aliases_row, self.alias_parity)
439439
self.update_parity_value(self.colors_row, self.color_parity)
440440
self.update_parity_value(self.ext_row, self.ext_parity)
441-
self.sql_lib.close()
442441

443442
def update_sql_value_ui(self, show_msg_box: bool = True):
444443
"""Update the SQL value count UI."""
@@ -469,6 +468,7 @@ def update_sql_value_ui(self, show_msg_box: bool = True):
469468

470469
def finish_migration(self):
471470
"""Finish the migration upon user approval."""
471+
self.sql_lib.close()
472472
final_name = self.json_lib.library_dir / TS_FOLDER_NAME / SQL_FILENAME
473473
if self.temp_path.exists():
474474
self.temp_path.rename(final_name)

‎tests/conftest.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# pyright: reportUnusedFunction=false
66

77

8+
import shutil
89
import sys
910
from collections.abc import Callable, Generator
1011
from pathlib import Path
@@ -22,7 +23,7 @@
2223
# this needs to be above `src` imports
2324
sys.path.insert(0, str(CWD.parent))
2425

25-
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
26+
from tagstudio.core.constants import BACKUP_FOLDER_NAME, THUMB_CACHE_NAME, TS_FOLDER_NAME
2627
from tagstudio.core.library.alchemy.library import Library
2728
from tagstudio.core.library.alchemy.models import Entry, Tag
2829
from tagstudio.qt.qt_driver import QtDriver
@@ -134,11 +135,17 @@ def library(request, library_dir: Path): # pyright: ignore
134135

135136

136137
@pytest.fixture
137-
def search_library() -> Library:
138+
def search_library(tmp_path: Path) -> Generator[Library]:
139+
shutil.copytree(
140+
CWD / "fixtures" / "search_library" / TS_FOLDER_NAME,
141+
tmp_path / TS_FOLDER_NAME,
142+
ignore=shutil.ignore_patterns(BACKUP_FOLDER_NAME),
143+
)
138144
lib = Library()
139-
status = lib.open_library(Path(CWD / "fixtures" / "search_library"))
145+
status = lib.open_library(tmp_path)
140146
assert status.success
141-
return lib
147+
yield lib
148+
lib.close()
142149

143150

144151
@pytest.fixture

0 commit comments

Comments
 (0)