Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/advanced/constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Database Constraints

In some cases you might want to enforce rules about your data directly at the **database level**. For example, making sure that a hero's name is unique, or that their age is never negative. 🦸‍♀️

These rules are called **constraints**, and because they live in the database, they work regardless of which application is inserting the data. This is particularly important for data consistency in production systems.

/// info

**SQLModel** uses <a href="https://docs.sqlalchemy.org/en/20/core/constraints.html" class="external-link" target="_blank">SQLAlchemy's constraint system</a> under the hood, so you have access to all the powerful constraint options available in SQLAlchemy.

///

## Unique Constraints

Let's say you want to make sure that no two heroes can have the same name. The simplest way to do this is with the `unique` parameter in `Field()`:

{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[4:8] hl[6] *}

Now the `name` field must be unique across all heroes. If you try to insert a hero with a name that already exists, the database will raise an error.

So two heroes named "Deadpond" and "Spider-Boy" would work fine, but trying to add a second "Deadpond" would fail.

## Multi-Column Unique Constraints

Sometimes you don't need each individual field to be unique, but you want a **combination** of fields to be unique. For example, you might allow multiple heroes named "Spider-Boy" as long as they have different ages.

You can do this using `__table_args__` with a `UniqueConstraint`:

{* ./docs_src/advanced/constraints/tutorial002_py310.py ln[5:11] hl[6] *}

With this setup, "Spider-Boy" aged 16 and "Spider-Boy" aged 25 are both allowed, because the **combination** of name and age is different. But two heroes both named "Spider-Boy" and both aged 16 would be rejected.

/// tip

You can include as many fields as needed in a `UniqueConstraint`. For example, `UniqueConstraint("name", "age", "team")` would require the combination of all three fields to be unique.

///

## Check Constraints

Check constraints let you define custom validation rules using SQL expressions. This is handy for enforcing business rules, like making sure a hero's age is never negative:

{* ./docs_src/advanced/constraints/tutorial003_py310.py ln[5:11] hl[6] *}

Here we're saying that `age` must be greater than or equal to zero. The `name` parameter gives the constraint a descriptive label, which makes error messages much easier to understand.

So heroes with age 0, 16, or 100 would all be fine, but trying to insert a hero with age -5 would fail.

## Combining Multiple Constraints

You can mix different types of constraints in the same model by adding multiple constraint objects to `__table_args__`:

{* ./docs_src/advanced/constraints/tutorial004_py310.py ln[5:15] hl[6:10] *}

This model has three constraints working together: the combination of `name` and `age` must be unique, age cannot be negative, and the name must be at least 2 characters long. All constraints must be satisfied for data to be inserted successfully.

## What Happens When a Constraint is Violated?

If you try to insert data that breaks a constraint, the database will raise an error. SQLAlchemy wraps this as an `IntegrityError`. Here's what that looks like in practice:

{* ./docs_src/advanced/constraints/tutorial005_py310.py ln[25:38] hl[32:37] *}

When you run this code, you'll see that the first hero is created successfully, but the attempt to create a duplicate fails with a clear error message.

<div class="termy">

```console
$ python app.py

// Some boilerplate and previous output omitted 😉

✅ Created hero: id=1 age=48 secret_name='Dive Wilson' name='Deadpond'
🚫 Constraint violation caught:
Error: (sqlite3.IntegrityError) UNIQUE constraint failed: hero.name
[SQL: INSERT INTO hero (name, age, secret_name) VALUES (?, ?, ?)]
[parameters: ('Deadpond', 25, 'Wade Wilson')]
(Background on this error at: https://sqlalche.me/e/20/gkpj)
```

</div>

This error handling lets you gracefully manage constraint violations in your application instead of having your program crash unexpectedly. 🛡️

/// warning

Not all databases support all types of constraints equally. In particular, **SQLite** has limitations with some complex SQL expressions in check constraints. Make sure to test your constraints with your target database.

Most other SQL databases like **PostgreSQL** and **MySQL** have full or near-full support. 🎉

///
1 change: 1 addition & 0 deletions docs_src/advanced/constraints/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

42 changes: 42 additions & 0 deletions docs_src/advanced/constraints/tutorial001_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from sqlmodel import Field, Session, SQLModel, create_engine


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(unique=True)
age: int
secret_name: str


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
hero_1 = Hero(name="Deadpond", age=48, secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", age=16, secret_name="Pedro Parqueador")

with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.commit()
session.refresh(hero_1)
session.refresh(hero_2)

print("Created hero:", hero_1)
print("Created hero:", hero_2)


def main():
create_db_and_tables()
create_heroes()


if __name__ == "__main__":
main()
45 changes: 45 additions & 0 deletions docs_src/advanced/constraints/tutorial002_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from sqlalchemy import UniqueConstraint
from sqlmodel import Field, Session, SQLModel, create_engine


class Hero(SQLModel, table=True):
__table_args__ = (UniqueConstraint("name", "age"),)

id: int | None = Field(default=None, primary_key=True)
name: str
age: int
secret_name: str


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
hero_1 = Hero(name="Spider-Boy", age=16, secret_name="Pedro Parqueador")
hero_2 = Hero(name="Spider-Boy", age=25, secret_name="Different Person")

with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.commit()
session.refresh(hero_1)
session.refresh(hero_2)

print("Created hero:", hero_1)
print("Created hero:", hero_2)


def main():
create_db_and_tables()
create_heroes()


if __name__ == "__main__":
main()
45 changes: 45 additions & 0 deletions docs_src/advanced/constraints/tutorial003_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from sqlalchemy import CheckConstraint
from sqlmodel import Field, Session, SQLModel, create_engine


class Hero(SQLModel, table=True):
__table_args__ = (CheckConstraint("age >= 0", name="age_non_negative"),)

id: int | None = Field(default=None, primary_key=True)
name: str
age: int
secret_name: str


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
hero_1 = Hero(name="Spider-Boy", age=16, secret_name="Pedro Parqueador")
hero_2 = Hero(name="Baby Hero", age=0, secret_name="Little One")

with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.commit()
session.refresh(hero_1)
session.refresh(hero_2)

print("Created hero:", hero_1)
print("Created hero:", hero_2)


def main():
create_db_and_tables()
create_heroes()


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions docs_src/advanced/constraints/tutorial004_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from sqlalchemy import CheckConstraint, UniqueConstraint
from sqlmodel import Field, Session, SQLModel, create_engine


class Hero(SQLModel, table=True):
__table_args__ = (
UniqueConstraint("name", "age"),
CheckConstraint("age >= 0", name="age_non_negative"),
CheckConstraint("LENGTH(name) >= 2", name="name_min_length"),
)

id: int | None = Field(default=None, primary_key=True)
name: str
age: int
secret_name: str


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
hero_1 = Hero(name="Spider-Boy", age=16, secret_name="Pedro Parqueador")
hero_2 = Hero(name="Captain Marvel", age=25, secret_name="Carol Danvers")

with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.commit()
session.refresh(hero_1)
session.refresh(hero_2)

print("Created hero:", hero_1)
print("Created hero:", hero_2)


def main():
create_db_and_tables()
create_heroes()


if __name__ == "__main__":
main()
50 changes: 50 additions & 0 deletions docs_src/advanced/constraints/tutorial005_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from sqlalchemy.exc import IntegrityError
from sqlmodel import Field, Session, SQLModel, create_engine


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(unique=True)
age: int
secret_name: str


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
hero_1 = Hero(name="Deadpond", age=48, secret_name="Dive Wilson")

with Session(engine) as session:
session.add(hero_1)
session.commit()
session.refresh(hero_1)
print("✅ Created hero:", hero_1)

# Now try to create another hero with the same name
duplicate_hero = Hero(name="Deadpond", age=25, secret_name="Wade Wilson")
session.add(duplicate_hero)

try:
session.commit()
print("❌ This shouldn't happen - duplicate was allowed!")
except IntegrityError as e:
session.rollback()
print("🚫 Constraint violation caught:")
print(f" Error: {e}")


def main():
create_db_and_tables()
create_heroes()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ nav:
- tutorial/fastapi/tests.md
- Advanced User Guide:
- advanced/index.md
- advanced/constraints.md
- advanced/decimal.md
- advanced/uuid.md
- Resources:
Expand Down