Skip to content

Error Handling Demos

These demos show how to handle errors that occur when working with SQLiter.

Duplicate Record Error

Handle unique constraint violations.

from typing import Annotated
from sqliter import SqliterDB
from sqliter.model import BaseDBModel
from sqliter.model.unique import unique
from sqliter.exceptions import RecordInsertionError

class User(BaseDBModel):
    email: Annotated[str, unique()]
    name: str

db = SqliterDB(memory=True)
db.create_table(User)

db.insert(User(email="[email protected]", name="Alice"))
print("Created user with email [email protected]")

try:
    # Try to insert duplicate email
    db.insert(User(email="[email protected]", name="Alice 2"))
except RecordInsertionError as e:
    print(f"\nCaught error: {type(e).__name__}")
    print(f"Message: {e}")

db.close()

Prevention

Check if record exists before inserting:

existing = db.select(User).filter(email__eq="[email protected]").fetch_one()
if not existing:
    db.insert(User(name="Alice", email="[email protected]"))

Record Not Found

Handle missing records gracefully.

from sqliter import SqliterDB
from sqliter.model import BaseDBModel
from sqliter.exceptions import RecordNotFoundError

class User(BaseDBModel):
    name: str

db = SqliterDB(memory=True)
db.create_table(User)

user = db.insert(User(name="Alice"))
print(f"Created user with pk={user.pk}")

try:
    # Try to delete non-existent record (raises RecordNotFoundError)
    db.delete(User, 9999)
except RecordNotFoundError as e:
    print(f"\nCaught error: {type(e).__name__}")
    print(f"Message: {e}")

db.close()

Alternative Using Queries

Use fetch_one() which returns None instead of raising:

user = db.select(User).filter(name__eq="Alice").fetch_one()
if user is None:
    print("User not found")
else:
    print(f"Found: {user.name}")

Validation Errors

Pydantic validates data before it reaches the database, ensuring type safety and data integrity.

from pydantic import ValidationError

from sqliter import SqliterDB
from sqliter.model import BaseDBModel

class Product(BaseDBModel):
    name: str
    price: float
    quantity: int

db = SqliterDB(memory=True)
db.create_table(Product)

product = db.insert(Product(name="Widget", price=19.99, quantity=100))
print(f"Created product: {product.name}, price: ${product.price}")

# Try to create product with invalid data (wrong types)
print("\nAttempting to create product with invalid data...")

try:
    # Wrong types: price should be float, quantity should be int
    # ValidationError is raised by Pydantic during model instantiation
    Product(name="Invalid Widget", price="free", quantity="lots")
except ValidationError as e:
    print(f"\nCaught error: {type(e).__name__}")
    print(f"Message: {e}")

db.close()

Benefits

  • Data never reaches the database in invalid form - Validation happens before insert
  • Clear error messages - Pydantic tells you exactly what's wrong
  • Type safety - Catch type mismatches at model instantiation, not at database insert
  • Automatic - No manual validation code needed, Pydantic handles it

Generic Error Handling

Catch all SQLiter errors with the base SqliterError class when you don't need to distinguish between specific error types.

from sqliter import SqliterDB
from sqliter.model import BaseDBModel
from sqliter.exceptions import SqliterError

class Task(BaseDBModel):
    title: str

db = SqliterDB(memory=True)
db.create_table(Task)

task = db.insert(Task(title="My Task"))
print(f"Created task: {task.title}")

# Try to update a deleted record
try:
    task.title = "Updated"
    db.delete(Task, task.pk)
    db.update(task)  # This will fail
except SqliterError as e:
    print(f"\nCaught SqliterError: {type(e).__name__}")
    print(f"Message: {e}")

db.close()

When to Use Generic Error Handling

  • Simplified error handling: When you don't need to take different actions based on error type
  • Logging or reporting: When you just need to log that an error occurred
  • Top-level error handlers: When you want to catch any SQLiter error at the application boundary

Specific vs Generic

# Specific - handle different error types differently
try:
    db.insert(user)
except RecordInsertionError:
    print("Duplicate user")
except ValidationError as e:
    print(f"Invalid data: {e}")

# Generic - catch all SQLiter errors
try:
    db.insert(user)
except SqliterError as e:
    print(f"Database error: {e}")

Database Connection Errors

Handle connection failures.

from sqliter import SqliterDB
from sqliter.model import BaseDBModel

class User(BaseDBModel):
    name: str

try:
    # Try to connect to non-existent directory
    db = SqliterDB(database="/invalid/path/db.sqlite")
    db.create_table(User)
except (OSError, IOError) as e:
    print(f"Connection failed: {e}")

Common Causes

  • Invalid directory (doesn't exist)
  • Permission denied (can't write to directory)
  • Database file corrupted

Table Creation

Always create tables before using them.

from sqliter import SqliterDB
from sqliter.model import BaseDBModel

class User(BaseDBModel):
    name: str

db = SqliterDB(memory=True)

# Always create tables first
db.create_table(User)

# Now inserts will work
db.insert(User(name="Alice"))

Prevention

Always call create_table() before inserting:

db.create_table(User)  # Safe if called multiple times
db.insert(User(name="Alice"))

Foreign Key Constraint Errors

Handle foreign key violations.

from sqliter import SqliterDB
from sqliter.orm import BaseDBModel, ForeignKey
from sqliter.exceptions import ForeignKeyConstraintError

class Author(BaseDBModel):
    name: str

class Book(BaseDBModel):
    title: str
    author: ForeignKey[Author] = ForeignKey(Author, on_delete="RESTRICT")

db = SqliterDB(memory=True)
db.create_table(Author)
db.create_table(Book)

author = db.insert(Author(name="Jane"))
db.insert(Book(title="Book 1", author=author))
print("Created author and linked book")

# Attempt to insert book with non-existent author
print("\nAttempting to insert book with non-existent author...")

try:
    # Create book with invalid author_id (doesn't exist in database)
    invalid_book = Book(title="Orphan Book", author_id=9999)
    db.insert(invalid_book)
except ForeignKeyConstraintError as e:
    print(f"\nCaught error: {type(e).__name__}")
    print(f"Message: {e}")

db.close()

Prevention

Ensure parent record exists:

author = db.insert(Author(name="Jane Austen"))
db.insert(Book(title="Pride and Prejudice", author=author))

Transaction Errors

Handle errors during transactions.

from sqliter import SqliterDB
from sqliter.model import BaseDBModel

class Account(BaseDBModel):
    balance: float

db = SqliterDB(memory=True)
db.create_table(Account)

account = db.insert(Account(balance=100.0))

try:
    with db:
        account.balance -= 200.0  # Would go negative
        db.update(account)
        raise ValueError("Invalid operation")
except ValueError as e:
    print(f"Transaction failed: {e}")
    print("Changes rolled back automatically")

# Verify balance unchanged (rollback restored original value)
reloaded = db.get(Account, account.pk)
if reloaded is not None:
    print(f"Balance: {reloaded.balance}")  # Still 100.0

Error Handling Best Practices

Specific Exceptions

Catch specific exceptions for different error types:

from pydantic import ValidationError
from sqliter.exceptions import (
    RecordInsertionError,
    RecordNotFoundError,
    ForeignKeyConstraintError,
    SqliterError,
)

try:
    db.insert(user)
except RecordInsertionError:
    print("Duplicate record or constraint violation")
except ValidationError as e:
    print(f"Invalid data: {e}")
except SqliterError as e:
    print(f"Database error: {e}")

User-Friendly Messages

Translate technical errors for users:

from typing import Annotated

from sqliter.model import BaseDBModel
from sqliter.model.unique import unique
from sqliter.exceptions import RecordInsertionError

class User(BaseDBModel):
    username: Annotated[str, unique()]

try:
    db.insert(User(username="alice"))
except RecordInsertionError:
    print("Username already taken, please choose another")

Logging

Log errors for debugging:

import logging

from sqliter.model import BaseDBModel
from sqliter.exceptions import RecordInsertionError

logger = logging.getLogger(__name__)

class User(BaseDBModel):
    username: str

try:
    db.insert(User(username="alice"))
except RecordInsertionError as e:
    logger.error(f"Failed to create user: {e}")
    raise  # Re-raise for user-facing error

Exception Hierarchy

Exception
├── SqliterError
│   ├── RecordNotFoundError
│   ├── RecordInsertionError
│   ├── RecordUpdateError
│   ├── RecordDeletionError
│   └── ForeignKeyConstraintError
└── ValidationError (from Pydantic)

Best Practices

DO

  • Catch specific exceptions for different error types
  • Provide user-friendly error messages
  • Log errors for debugging
  • Validate data before database operations
  • Use transactions for multi-step operations

DON'T

  • Catch all exceptions with bare except:
  • Ignore errors silently
  • Expose raw database errors to users
  • Forget that validation errors prevent database writes