From a77145e08ee1534e2bda1d014ff580a71675aff1 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 3 Jan 2026 17:48:46 +0100 Subject: [PATCH 1/2] fix: Use db.insert() for ticket creation to fix SQLite serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation used db.execute() with a raw dict, which failed on SQLite because the 'extra' field (TicketExtra model) was passed as a Python dict that SQLite cannot serialize. Using db.insert() with the Pydantic model ensures proper JSON serialization of the extra field across all database backends (SQLite, PostgreSQL, CockroachDB). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crud.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crud.py b/crud.py index e92e47e..8996a2e 100644 --- a/crud.py +++ b/crud.py @@ -20,7 +20,12 @@ async def create_ticket( ) -> Ticket: now = datetime.now(timezone.utc) - # Handle database constraints: if user_id is provided, use empty strings for name/email + # TODO: Check if this empty string workaround is still needed. + # This converts None to empty strings for database storage because: + # 1. Database may have NOT NULL constraints on name/email columns + # 2. When user_id is provided, name/email are not used (mutually exclusive) + # 3. The get_ticket() functions convert empty strings back to None when reading + # Consider using nullable columns instead of this empty string pattern. if user_id: db_name = "" db_email = "" @@ -28,7 +33,28 @@ async def create_ticket( db_name = name or "" db_email = email or "" - ticket = Ticket( + # Create ticket with database-compatible values for insertion + # Using db.insert() ensures proper serialization of the extra field (TicketExtra) + # across all database backends (SQLite, PostgreSQL, CockroachDB) + db_ticket = Ticket( + id=payment_hash, + wallet=wallet, + event=event, + name=db_name, + email=db_email, + user_id=user_id, + registered=False, + paid=False, + reg_timestamp=now, + time=now, + extra=TicketExtra(**extra) if extra else TicketExtra(), + ) + + await db.insert("events.ticket", db_ticket) + + # Return ticket with original name/email values (not empty strings) + # This maintains consistency with how get_ticket() converts empty strings back to None + return Ticket( id=payment_hash, wallet=wallet, event=event, @@ -42,20 +68,6 @@ async def create_ticket( extra=TicketExtra(**extra) if extra else TicketExtra(), ) - # Create a dict for database insertion with proper handling of constraints - ticket_dict = ticket.dict() - ticket_dict["name"] = db_name - ticket_dict["email"] = db_email - - await db.execute( - """ - INSERT INTO events.ticket (id, wallet, event, name, email, user_id, registered, paid, time, reg_timestamp, extra) - VALUES (:id, :wallet, :event, :name, :email, :user_id, :registered, :paid, :time, :reg_timestamp, :extra) - """, - ticket_dict - ) - return ticket - async def update_ticket(ticket: Ticket) -> Ticket: # Create a new Ticket object with corrected values for database constraints From 68e6e3d02e5bfc353ddc81a890deaab69ccf522e Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 3 Jan 2026 18:03:34 +0100 Subject: [PATCH 2/2] fix: Parse JSON extra field when reading tickets from database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix used db.insert() which serializes the extra field to JSON. However, the read functions (get_ticket, get_tickets, etc.) use fetchone/fetchall without a model parameter, so the extra field comes back as a JSON string. Added _parse_ticket_row() helper that: - Converts empty strings to None for name/email (existing logic) - Parses extra field from JSON string if needed (new) The isinstance(extra, str) check ensures compatibility with both: - SQLite: returns JSON as string - PostgreSQL/CockroachDB: may return native JSONB as dict 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crud.py | 70 ++++++++++++++++++++++++--------------------------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/crud.py b/crud.py index 8996a2e..8abcbbb 100644 --- a/crud.py +++ b/crud.py @@ -1,3 +1,4 @@ +import json from datetime import datetime, timedelta, timezone from typing import Optional @@ -6,6 +7,30 @@ from lnbits.helpers import urlsafe_short_hash from .models import CreateEvent, Event, Ticket, TicketExtra + +def _parse_ticket_row(row) -> dict: + """ + Parse a database row into a dict suitable for Ticket model creation. + Handles: + - Empty string to None conversion for name/email + - JSON string to dict conversion for extra field + """ + ticket_data = dict(row) + + # Convert empty strings back to None for the model + if ticket_data.get("name") == "": + ticket_data["name"] = None + if ticket_data.get("email") == "": + ticket_data["email"] = None + + # Parse extra field from JSON string if needed + # (db.insert() serializes to JSON, but manual fetchone/fetchall returns string) + extra = ticket_data.get("extra") + if isinstance(extra, str): + ticket_data["extra"] = json.loads(extra) + + return ticket_data + db = Database("ext_events") @@ -94,14 +119,7 @@ async def get_ticket(payment_hash: str) -> Optional[Ticket]: if not row: return None - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - - return Ticket(**ticket_data) + return Ticket(**_parse_ticket_row(row)) async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: @@ -110,17 +128,7 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})") - tickets = [] - for row in rows: - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - tickets.append(Ticket(**ticket_data)) - - return tickets + return [Ticket(**_parse_ticket_row(row)) for row in rows] async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: @@ -130,17 +138,7 @@ async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: {"user_id": user_id} ) - tickets = [] - for row in rows: - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - tickets.append(Ticket(**ticket_data)) - - return tickets + return [Ticket(**_parse_ticket_row(row)) for row in rows] async def delete_ticket(payment_hash: str) -> None: @@ -212,14 +210,4 @@ async def get_event_tickets(event_id: str) -> list[Ticket]: {"event": event_id}, ) - tickets = [] - for row in rows: - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - tickets.append(Ticket(**ticket_data)) - - return tickets + return [Ticket(**_parse_ticket_row(row)) for row in rows]