Skip to content

Commit 2cff18f

Browse files
authored
Merge pull request #21 from codebude/feature/set-of-small-changes
feature/set of small changes
2 parents 0e91018 + 8d3bc3e commit 2cff18f

43 files changed

Lines changed: 1217 additions & 232 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<p align="center">
1414
<a href="https://github.com/codebude/librislog/actions/workflows/tests.yml"><img src="https://github.com/codebude/librislog/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
15-
<a href="https://github.com/codebude/librislog/actions/workflows/docker.yml"><img src="https://github.com/codebude/librislog/actions/workflows/docker.yml/badge.svg" alt="Docker Build"></a>
15+
<a href="https://github.com/codebude?tab=packages&repo_name=librislog"><img src="https://github.com/codebude/librislog/actions/workflows/docker.yml/badge.svg" alt="Docker Build"></a>
1616
<a href="https://codebude.github.io/librislog/"><img src="https://github.com/codebude/librislog/actions/workflows/docs.yml/badge.svg" alt="Docs Build"></a>
1717
<img src="https://img.shields.io/badge/python-3.14-%233776AB?logo=python" alt="Python">
1818
<img src="https://img.shields.io/badge/svelte-5-%23FF3E00?logo=svelte" alt="Svelte">

backend/app/database.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,30 @@
55

66
from app.config import settings
77

8+
9+
def _set_sqlite_pragmas(dbapi_connection, _connection_record) -> None:
10+
"""Apply performance-oriented PRAGMAs to new SQLite connections."""
11+
import sqlite3
12+
if not isinstance(dbapi_connection, sqlite3.Connection):
13+
return
14+
cursor = dbapi_connection.cursor()
15+
cursor.execute("PRAGMA journal_mode=WAL")
16+
cursor.execute("PRAGMA synchronous=NORMAL")
17+
cursor.execute("PRAGMA cache_size=-8000")
18+
cursor.execute("PRAGMA temp_store=MEMORY")
19+
cursor.execute("PRAGMA mmap_size=268435456")
20+
cursor.close()
21+
22+
823
engine = create_engine(
924
settings.database_url,
1025
connect_args={"check_same_thread": False}, # needed for SQLite
26+
pool_pre_ping=True,
1127
)
1228

29+
from sqlalchemy import event # noqa: E402
30+
event.listen(engine, "connect", _set_sqlite_pragmas)
31+
1332

1433
@atexit.register
1534
def _dispose_engine() -> None:

backend/app/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class BookTag(SQLModel, table=True):
103103
__tablename__ = "book_tag"
104104

105105
book_id: int = Field(foreign_key="book.id", primary_key=True)
106-
tag_id: int = Field(foreign_key="tag.id", primary_key=True)
106+
tag_id: int = Field(foreign_key="tag.id", primary_key=True, index=True)
107107

108108

109109
class User(SQLModel, table=True):

backend/app/routers/books.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
)
3232
from app.services.cover_import import import_cover_from_url, is_external_cover_url
3333
from app.services.quote_cache import get_or_fetch_dashboard_quote
34-
from app.services.tags import build_book_read, cleanup_orphan_tags, sync_book_tags
34+
from app.services.tags import build_book_read, cleanup_orphan_tags, load_tags_batch, sync_book_tags
3535
from app.time_utils import utcnow
3636

3737
logger = logging.getLogger(__name__)
@@ -128,6 +128,14 @@ def _raise_integrity_conflict(exc: IntegrityError) -> None:
128128
raise
129129

130130

131+
def _build_book_read_with_tags(book: Book, tags_text: str | None) -> BookRead:
132+
"""Build a BookRead from a Book model with a pre-resolved tags string."""
133+
payload = book.model_dump()
134+
payload.pop("user_id", None)
135+
payload["tags"] = tags_text
136+
return BookRead.model_validate(payload)
137+
138+
131139
@router.get("", response_model=BookListResponse)
132140
def list_books(
133141
status: Optional[ReadingStatus] = Query(default=None),
@@ -206,8 +214,13 @@ def list_books(
206214

207215
books = list(session.exec(statement).all())
208216
logger.debug("list_books — returning %d/%d book(s)", len(books), total)
217+
book_ids = [b.id for b in books if b.id is not None]
218+
book_tags_map = load_tags_batch(session, book_ids) if book_ids else {}
209219
return BookListResponse(
210-
books=[build_book_read(session, book) for book in books],
220+
books=[
221+
_build_book_read_with_tags(book, book_tags_map.get(book.id))
222+
for book in books
223+
],
211224
total=total,
212225
)
213226

@@ -279,12 +292,13 @@ def get_tag_cloud(
279292
session: Session = Depends(get_session),
280293
) -> List[TagCloudEntry]:
281294
"""Return tags sorted by usage count (descending) for the authenticated user."""
295+
count_label = func.count(BookTag.book_id).label("cnt")
282296
rows = session.exec(
283-
select(Tag.name, func.count(BookTag.book_id))
297+
select(Tag.name, count_label)
284298
.join(BookTag, BookTag.tag_id == Tag.id)
285299
.where(Tag.user_id == current_user.id)
286-
.group_by(Tag.id, Tag.name)
287-
.order_by(func.count(BookTag.book_id).desc(), Tag.name.asc())
300+
.group_by(Tag.id)
301+
.order_by(count_label.desc(), Tag.name.asc())
288302
.limit(limit)
289303
).all()
290304
return [TagCloudEntry(tag=name, count=count) for name, count in rows]

backend/app/routers/data.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ async def execute_import_data(
293293
async def event_generator():
294294
with Session(stream_bind) as session:
295295
completed = False
296+
import_failed_rows = 0
296297
final_error: str | None = None
297298
try:
298299
async for event in execute_import(
@@ -305,6 +306,7 @@ async def event_generator():
305306
):
306307
if event.get("event") == "complete":
307308
completed = True
309+
import_failed_rows = event.get("failed", 0)
308310
if event.get("event") == "error":
309311
final_error = str(event.get("message") or "Import failed")
310312
yield f"data: {json.dumps(event)}\n\n"
@@ -321,7 +323,7 @@ async def event_generator():
321323
final_error = 'error.importExecutionFailed'
322324
yield f"data: {json.dumps({'event': 'error', 'message': 'error.importExecutionFailed'})}\n\n"
323325
finally:
324-
if completed or final_error is not None:
326+
if completed and import_failed_rows == 0 and final_error is None:
325327
delete_parsed_upload(body.file_id, current_user.id)
326328

327329
return StreamingResponse(

0 commit comments

Comments
 (0)