Skip to content

Commit 253e40a

Browse files
committed
feat: use WAL mode for the library database
1 parent 6dde2cc commit 253e40a

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_,
@@ -379,29 +380,21 @@ def open_library(self, library_dir: Path, in_memory: bool = False) -> OpenLibrar
379380

380381
@staticmethod
381382
def __get_engine(library_dir: Path, in_memory: bool, sql_filename: str):
383+
db_path = library_dir / TS_FOLDER_NAME / sql_filename
382384
connection_string = URL.create(
383385
drivername="sqlite",
384-
database=(
385-
":memory:" if in_memory else str(library_dir / TS_FOLDER_NAME / sql_filename)
386-
),
386+
database=":memory:" if in_memory else str(db_path),
387387
)
388-
# NOTE: File-based databases should use NullPool to create new DB connection in order to
389-
# keep connections on separate threads, which prevents the DB files from being locked
390-
# even after a connection has been closed.
391-
# SingletonThreadPool (the default for :memory:) should still be used for in-memory DBs.
392-
# More info can be found on the SQLAlchemy docs:
393-
# https://docs.sqlalchemy.org/en/20/changelog/migration_07.html
394-
# Under -> sqlite-the-sqlite-dialect-now-uses-nullpool-for-file-based-databases
395-
poolclass = None if in_memory else NullPool
396388

397-
logger.info(
398-
"[Library] Creating SQLAlchemy Engine",
399-
connection_string=connection_string,
400-
poolclass=poolclass,
401-
)
402-
return create_engine(
403-
connection_string, poolclass=poolclass, connect_args={"autocommit": False}
404-
)
389+
if not in_memory:
390+
try:
391+
with closing(sqlite3.connect(db_path, autocommit=True)) as conn:
392+
conn.execute("PRAGMA journal_mode=WAL")
393+
except sqlite3.OperationalError as e:
394+
logger.warning("[Library] Could not set journal mode", error=e)
395+
396+
logger.info("[Library] Creating SQLAlchemy Engine", connection_string=connection_string)
397+
return create_engine(connection_string, connect_args={"autocommit": False})
405398

406399
def create_sqlite_library(
407400
self, library_dir: Path, in_memory: bool, sql_filename: str = SQL_FILENAME
@@ -1450,17 +1443,18 @@ def save_library_backup_to_disk(library_dir: Path) -> Path:
14501443
makedirs(str(library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
14511444

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

1454-
target_path = library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
1455-
1456-
shutil.copyfile(
1457-
library_dir / TS_FOLDER_NAME / SQL_FILENAME,
1458-
target_path,
1459-
)
1448+
with (
1449+
closing(sqlite3.connect(library_dir / TS_FOLDER_NAME / SQL_FILENAME)) as library,
1450+
closing(sqlite3.connect(library_backup_path)) as library_backup,
1451+
):
1452+
library.backup(library_backup)
1453+
library_backup.execute("PRAGMA journal_mode=DELETE") # Don't use WAL for backups
14601454

1461-
logger.info("Library backup saved to disk.", path=target_path)
1455+
logger.info("[Library] Library backup saved to disk", path=library_backup_path)
14621456

1463-
return target_path
1457+
return library_backup_path
14641458

14651459
def get_tag(self, tag_id: int) -> Tag | None:
14661460
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)