RpRelPyDBRelational Python

Documentation

16. Errors & Limitations

Exception types, what triggers them, and known constraints you should be aware of.

Exception hierarchy

All RelPyDB exceptions inherit from RelPyError, so you can catch any library error with a single except RelPyError.

from relpy import (
    RelPyError,
    SchemaError,
    TableNotFoundError,
    ColumnNotFoundError,
    ConstraintError,
    QueryError,
    QueryTypeError,
    ViewError,
    RowNotFoundError,
    NoRowsFoundError,
    MultipleRowsFoundError,
    EncryptionError,
)

Error reference

ExceptionTriggered by
TableNotFoundErrorQuerying or inserting into a table name that does not exist.
ColumnNotFoundErrorReferencing a column not defined in the schema.
SchemaErrorCalling add_column on a non-existent table, or defining an invalid schema.
ConstraintErrorInserting a duplicate primary key, violating a NOT NULL rule, breaking a foreign key, or violating a unique index.
QueryErrorUsing update() or delete() without allow_all=True when no where filter is given.
QueryTypeErrorUsing Python and/or instead of &/| in a where condition.
ViewErrorCalling db.view() on a name that was never registered, or saving a database that has views.
NoRowsFoundErrorCalling .one() when the query returns zero rows.
MultipleRowsFoundErrorCalling .one() when the query returns more than one row.
EncryptionErrorCalling to_list(decrypt=True) without a key loaded, or loading a database encrypted with a different key.

Common mistakes and fixes

Using Python and in a condition

❌ Wrong — raises QueryTypeError
db.query("users").where(
    col("age") >= 18 and col("status") == "active"
)
✓ Correct
db.query("users").where(
    (col("age") >= 18) &
    (col("status") == "active")
)

Calling .one() on a query with multiple rows

❌ Wrong — raises MultipleRowsFoundError
user = db.query("users").one()
# Raises if there is more than one user
✓ Correct
user = (
    db.query("users")
      .where(col("id") == 1)
      .one()
)

Deleting all rows without the safety flag

❌ Wrong — raises QueryError
db.delete("users")
✓ Correct
db.delete("users", allow_all=True)

Decrypting without loading a key

❌ Wrong — raises EncryptionError
db = RelPy.load("database.relpy.json")
db.to_list("users", decrypt=True)
# No key loaded!
✓ Correct
db = RelPy.load(
    "database.relpy.json",
    encryption_key=my_key,
)
db.to_list("users", decrypt=True)

Known limitations

  • No concurrent writes. RelPyDB is a single-process in-memory object. It is not safe for multi-threaded mutation without external locking.
  • Views are not persisted. Views contain Python callables and cannot be serialised. Re-register them after RelPy.load().
  • No subquery support. Nested queries are not supported in the current query builder. Use views or Python variables to chain results.
  • Performance degrades on very large datasets. The library is optimised for clarity and correctness, not compiled-engine throughput. For >500 000 rows or heavy analytical queries, DuckDB or SQLite will be substantially faster.
  • AutoNumber columns cannot be manually set. Passing an explicit value for an AutoNumber column in insert() raises a ConstraintError.
  • Primary keys are immutable. update() ignores changes to primary key columns.

Catching errors gracefully

from relpy import RelPyError, ConstraintError, TableNotFoundError

try:
    db.insert("orders", {"user_id": 9999, "amount": 50.0})
except ConstraintError as e:
    print(f"FK violation: {e}")
except TableNotFoundError as e:
    print(f"Table missing: {e}")
except RelPyError as e:
    print(f"Other RelPyDB error: {e}")