From 2d883fae2197563281316db47804ebd6242f3f3d Mon Sep 17 00:00:00 2001 From: bBlazewavE Date: Fri, 6 Mar 2026 10:03:04 -0300 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=93=9D=20Add=20documentation=20for=20?= =?UTF-8?q?unique=20and=20check=20constraints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a guide covering unique constraints (single and multi-column) and check constraints using __table_args__. Closes #82 Closes #292 --- docs/advanced/constraints.md | 156 ++++++++++++++++++ docs_src/advanced/constraints/__init__.py | 1 + .../advanced/constraints/tutorial001_py310.py | 44 +++++ .../advanced/constraints/tutorial002_py310.py | 46 ++++++ .../advanced/constraints/tutorial003_py310.py | 46 ++++++ .../advanced/constraints/tutorial004_py310.py | 50 ++++++ mkdocs.yml | 1 + 7 files changed, 344 insertions(+) create mode 100644 docs/advanced/constraints.md create mode 100644 docs_src/advanced/constraints/__init__.py create mode 100644 docs_src/advanced/constraints/tutorial001_py310.py create mode 100644 docs_src/advanced/constraints/tutorial002_py310.py create mode 100644 docs_src/advanced/constraints/tutorial003_py310.py create mode 100644 docs_src/advanced/constraints/tutorial004_py310.py diff --git a/docs/advanced/constraints.md b/docs/advanced/constraints.md new file mode 100644 index 0000000000..bead39f595 --- /dev/null +++ b/docs/advanced/constraints.md @@ -0,0 +1,156 @@ +# Database Constraints + +Database constraints are rules that ensure data integrity and prevent invalid data from being inserted into your database. **SQLModel** supports several types of constraints through **SQLAlchemy**. + +These constraints are enforced at the **database level**, which means they work regardless of which application is inserting the data. This is particularly important for data consistency and reliability in production systems. + +/// info + +**SQLModel** uses SQLAlchemy's constraint system under the hood, giving you access to all the powerful constraint options available in SQLAlchemy. + +/// + +## Unique Constraints + +Unique constraints ensure that certain fields or combinations of fields have unique values across all rows in the table. + +### Single Column Unique Constraints + +The simplest way to add a unique constraint is using the `unique` parameter in the `Field()` function: + +{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[6:11] hl[8] *} + +In this example, 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. + +βœ… **Valid examples:** +* Two heroes with names "Deadpond" and "Spider-Boy" +* Heroes with the same age but different names + +🚫 **Invalid examples:** +* Two heroes both named "Deadpond" +* Inserting a hero with a name that already exists in the database + +### Multi-Column Unique Constraints + +Sometimes you want a combination of fields to be unique, even though each individual field can have duplicate values. You can achieve this using `__table_args__` with `UniqueConstraint`: + +{* ./docs_src/advanced/constraints/tutorial002_py310.py ln[7:13] hl[8] *} + +In this example, the combination of `name` and `age` must be unique. This means you can have multiple heroes with the same name (as long as they have different ages), and you can have multiple heroes with the same age (as long as they have different names). + +βœ… **Valid examples:** +* "Spider-Boy" aged 16 and "Spider-Boy" aged 25 (same name, different ages) +* "Spider-Boy" aged 16 and "Iron Man" aged 16 (different names, same age) + +🚫 **Invalid examples:** +* Two heroes both named "Spider-Boy" and both aged 16 + +/// tip + +You can include as many fields as needed in a `UniqueConstraint`. For example: `UniqueConstraint("name", "age", "team")` would make the combination of all three fields unique. + +/// + +## Check Constraints + +Check constraints allow you to define custom validation rules using SQL expressions. These are more flexible than basic type validation and can enforce business rules at the database level. + +{* ./docs_src/advanced/constraints/tutorial003_py310.py ln[7:13] hl[8] *} + +In this example, the check constraint ensures that the `age` field cannot be negative. The constraint has a name (`age_non_negative`) which makes error messages clearer and allows you to reference it later if needed. + +βœ… **Valid examples:** +* Heroes with age 0, 16, 25, 100, etc. +* Any non-negative integer for age + +🚫 **Invalid examples:** +* Heroes with negative ages like -5, -1, etc. + +/// info + +Check constraints can use any valid SQL expression supported by your database. Common examples include: + +- **Range checks:** `age BETWEEN 0 AND 150` +- **String length:** `LENGTH(name) >= 2` +- **Pattern matching:** `email LIKE '%@%'` +- **Value lists:** `status IN ('active', 'inactive', 'pending')` + +/// + +### Naming Check Constraints + +It's a good practice to always give your check constraints descriptive names using the `name` parameter. This makes debugging easier when constraint violations occur: + +```python +CheckConstraint("age >= 0", name="age_non_negative") +CheckConstraint("LENGTH(name) >= 2", name="name_min_length") +CheckConstraint("email LIKE '%@%'", name="email_format") +``` + +## Combining Multiple Constraints + +You can combine different types of constraints in the same table by adding multiple constraint objects to `__table_args__`: + +{* ./docs_src/advanced/constraints/tutorial004_py310.py ln[7:16] hl[8:12] *} + +This example combines: +- A unique constraint on the combination of `name` and `age` +- A check constraint ensuring age is non-negative +- A check constraint ensuring name has at least 2 characters + +/// tip + +When combining constraints, remember that **all constraints must be satisfied** for data to be inserted successfully. Design your constraints carefully to ensure they work together and don't create impossible conditions. + +/// + +## Constraint Violation Errors + +When constraints are violated, SQLAlchemy will raise exceptions. It's good practice to handle these in your application: + +```python +from sqlalchemy.exc import IntegrityError + +try: + with Session(engine) as session: + # Trying to insert duplicate data + hero = Hero(name="Deadpond", age=48, secret_name="Dive Wilson") + session.add(hero) + session.commit() +except IntegrityError as e: + print(f"Constraint violation: {e}") + session.rollback() +``` + +## Database Support + +/// warning + +Not all databases support all types of constraints equally: + +- **SQLite:** Supports unique constraints and basic check constraints, but has limitations with some complex SQL expressions +- **PostgreSQL:** Full support for all constraint types with rich SQL expression support +- **MySQL:** Good support for most constraints, with some syntax differences in check constraints +- **SQL Server:** Full support for all constraint types + +Always test your constraints with your target database to ensure compatibility. + +/// + +## Best Practices + +🎯 **Use meaningful constraint names** - This makes debugging easier when violations occur + +🎯 **Combine field-level and table-level constraints** - Use `Field(unique=True)` for simple cases and `__table_args__` for complex ones + +🎯 **Consider performance** - Unique constraints automatically create indexes, which can improve query performance + +🎯 **Handle constraint violations gracefully** - Always wrap database operations in try-catch blocks when constraints might be violated + +🎯 **Document your constraints** - Make sure your team understands what business rules the constraints enforce + +/// info + +Remember that **SQLModel** constraints are implemented using **SQLAlchemy**, so you have access to all the power and flexibility of SQLAlchemy's constraint system. For more advanced use cases, check the SQLAlchemy constraints documentation. + +/// \ No newline at end of file diff --git a/docs_src/advanced/constraints/__init__.py b/docs_src/advanced/constraints/__init__.py new file mode 100644 index 0000000000..a7506fdc94 --- /dev/null +++ b/docs_src/advanced/constraints/__init__.py @@ -0,0 +1 @@ +# Empty init file \ No newline at end of file diff --git a/docs_src/advanced/constraints/tutorial001_py310.py b/docs_src/advanced/constraints/tutorial001_py310.py new file mode 100644 index 0000000000..21c18cf10b --- /dev/null +++ b/docs_src/advanced/constraints/tutorial001_py310.py @@ -0,0 +1,44 @@ +from sqlmodel import Field, SQLModel, create_engine, Session, select +from sqlalchemy import UniqueConstraint, CheckConstraint +from typing import Optional + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(unique=True) # Single column unique constraint + 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() \ No newline at end of file diff --git a/docs_src/advanced/constraints/tutorial002_py310.py b/docs_src/advanced/constraints/tutorial002_py310.py new file mode 100644 index 0000000000..0e7a1e4c02 --- /dev/null +++ b/docs_src/advanced/constraints/tutorial002_py310.py @@ -0,0 +1,46 @@ +from sqlmodel import Field, SQLModel, create_engine, Session, select +from sqlalchemy import UniqueConstraint +from typing import Optional + + +class Hero(SQLModel, table=True): + __table_args__ = (UniqueConstraint("name", "age"),) + + id: Optional[int] = 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") # Same name, different age - OK! + + 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() \ No newline at end of file diff --git a/docs_src/advanced/constraints/tutorial003_py310.py b/docs_src/advanced/constraints/tutorial003_py310.py new file mode 100644 index 0000000000..04358a6423 --- /dev/null +++ b/docs_src/advanced/constraints/tutorial003_py310.py @@ -0,0 +1,46 @@ +from sqlmodel import Field, SQLModel, create_engine, Session, select +from sqlalchemy import CheckConstraint +from typing import Optional + + +class Hero(SQLModel, table=True): + __table_args__ = (CheckConstraint("age >= 0", name="age_non_negative"),) + + id: Optional[int] = 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") # Age 0 is OK + + 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() \ No newline at end of file diff --git a/docs_src/advanced/constraints/tutorial004_py310.py b/docs_src/advanced/constraints/tutorial004_py310.py new file mode 100644 index 0000000000..bb4b519727 --- /dev/null +++ b/docs_src/advanced/constraints/tutorial004_py310.py @@ -0,0 +1,50 @@ +from sqlmodel import Field, SQLModel, create_engine, Session, select +from sqlalchemy import UniqueConstraint, CheckConstraint +from typing import Optional + + +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: Optional[int] = 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() \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b89516e024..12ed13acd1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: From 14cd3926971538f7bcaae3b01959a4a01ca4b640 Mon Sep 17 00:00:00 2001 From: bBlazewavE Date: Fri, 6 Mar 2026 11:00:41 -0300 Subject: [PATCH 2/5] fix: use py310 syntax, fix line numbers, remove unused imports --- docs/advanced/constraints.md | 8 ++++---- docs_src/advanced/constraints/__init__.py | 2 +- docs_src/advanced/constraints/tutorial001_py310.py | 10 ++++------ docs_src/advanced/constraints/tutorial002_py310.py | 11 +++++------ docs_src/advanced/constraints/tutorial003_py310.py | 11 +++++------ docs_src/advanced/constraints/tutorial004_py310.py | 11 +++++------ 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/docs/advanced/constraints.md b/docs/advanced/constraints.md index bead39f595..9fd7b7382f 100644 --- a/docs/advanced/constraints.md +++ b/docs/advanced/constraints.md @@ -18,7 +18,7 @@ Unique constraints ensure that certain fields or combinations of fields have uni The simplest way to add a unique constraint is using the `unique` parameter in the `Field()` function: -{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[6:11] hl[8] *} +{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[4:8] hl[6] *} In this example, 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. @@ -34,7 +34,7 @@ In this example, the `name` field must be unique across all heroes. If you try t Sometimes you want a combination of fields to be unique, even though each individual field can have duplicate values. You can achieve this using `__table_args__` with `UniqueConstraint`: -{* ./docs_src/advanced/constraints/tutorial002_py310.py ln[7:13] hl[8] *} +{* ./docs_src/advanced/constraints/tutorial002_py310.py ln[5:11] hl[6] *} In this example, the combination of `name` and `age` must be unique. This means you can have multiple heroes with the same name (as long as they have different ages), and you can have multiple heroes with the same age (as long as they have different names). @@ -55,7 +55,7 @@ You can include as many fields as needed in a `UniqueConstraint`. For example: ` Check constraints allow you to define custom validation rules using SQL expressions. These are more flexible than basic type validation and can enforce business rules at the database level. -{* ./docs_src/advanced/constraints/tutorial003_py310.py ln[7:13] hl[8] *} +{* ./docs_src/advanced/constraints/tutorial003_py310.py ln[5:11] hl[6] *} In this example, the check constraint ensures that the `age` field cannot be negative. The constraint has a name (`age_non_negative`) which makes error messages clearer and allows you to reference it later if needed. @@ -91,7 +91,7 @@ CheckConstraint("email LIKE '%@%'", name="email_format") You can combine different types of constraints in the same table by adding multiple constraint objects to `__table_args__`: -{* ./docs_src/advanced/constraints/tutorial004_py310.py ln[7:16] hl[8:12] *} +{* ./docs_src/advanced/constraints/tutorial004_py310.py ln[5:15] hl[6:10] *} This example combines: - A unique constraint on the combination of `name` and `age` diff --git a/docs_src/advanced/constraints/__init__.py b/docs_src/advanced/constraints/__init__.py index a7506fdc94..8b13789179 100644 --- a/docs_src/advanced/constraints/__init__.py +++ b/docs_src/advanced/constraints/__init__.py @@ -1 +1 @@ -# Empty init file \ No newline at end of file + diff --git a/docs_src/advanced/constraints/tutorial001_py310.py b/docs_src/advanced/constraints/tutorial001_py310.py index 21c18cf10b..9ff0c8c34c 100644 --- a/docs_src/advanced/constraints/tutorial001_py310.py +++ b/docs_src/advanced/constraints/tutorial001_py310.py @@ -1,11 +1,9 @@ -from sqlmodel import Field, SQLModel, create_engine, Session, select -from sqlalchemy import UniqueConstraint, CheckConstraint -from typing import Optional +from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(unique=True) # Single column unique constraint + id: int | None = Field(default=None, primary_key=True) + name: str = Field(unique=True) age: int secret_name: str @@ -41,4 +39,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/docs_src/advanced/constraints/tutorial002_py310.py b/docs_src/advanced/constraints/tutorial002_py310.py index 0e7a1e4c02..a3cd30ed20 100644 --- a/docs_src/advanced/constraints/tutorial002_py310.py +++ b/docs_src/advanced/constraints/tutorial002_py310.py @@ -1,12 +1,11 @@ -from sqlmodel import Field, SQLModel, create_engine, Session, select from sqlalchemy import UniqueConstraint -from typing import Optional +from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): __table_args__ = (UniqueConstraint("name", "age"),) - - id: Optional[int] = Field(default=None, primary_key=True) + + id: int | None = Field(default=None, primary_key=True) name: str age: int secret_name: str @@ -24,7 +23,7 @@ def create_db_and_tables(): 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") # Same name, different age - OK! + hero_2 = Hero(name="Spider-Boy", age=25, secret_name="Different Person") with Session(engine) as session: session.add(hero_1) @@ -43,4 +42,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/docs_src/advanced/constraints/tutorial003_py310.py b/docs_src/advanced/constraints/tutorial003_py310.py index 04358a6423..7e81a66e06 100644 --- a/docs_src/advanced/constraints/tutorial003_py310.py +++ b/docs_src/advanced/constraints/tutorial003_py310.py @@ -1,12 +1,11 @@ -from sqlmodel import Field, SQLModel, create_engine, Session, select from sqlalchemy import CheckConstraint -from typing import Optional +from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): __table_args__ = (CheckConstraint("age >= 0", name="age_non_negative"),) - - id: Optional[int] = Field(default=None, primary_key=True) + + id: int | None = Field(default=None, primary_key=True) name: str age: int secret_name: str @@ -24,7 +23,7 @@ def create_db_and_tables(): 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") # Age 0 is OK + hero_2 = Hero(name="Baby Hero", age=0, secret_name="Little One") with Session(engine) as session: session.add(hero_1) @@ -43,4 +42,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/docs_src/advanced/constraints/tutorial004_py310.py b/docs_src/advanced/constraints/tutorial004_py310.py index bb4b519727..cafb4dcc86 100644 --- a/docs_src/advanced/constraints/tutorial004_py310.py +++ b/docs_src/advanced/constraints/tutorial004_py310.py @@ -1,6 +1,5 @@ -from sqlmodel import Field, SQLModel, create_engine, Session, select -from sqlalchemy import UniqueConstraint, CheckConstraint -from typing import Optional +from sqlalchemy import CheckConstraint, UniqueConstraint +from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): @@ -9,8 +8,8 @@ class Hero(SQLModel, table=True): CheckConstraint("age >= 0", name="age_non_negative"), CheckConstraint("LENGTH(name) >= 2", name="name_min_length"), ) - - id: Optional[int] = Field(default=None, primary_key=True) + + id: int | None = Field(default=None, primary_key=True) name: str age: int secret_name: str @@ -47,4 +46,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From 4d223596b68129777c46bc0b53a457df482fedfd Mon Sep 17 00:00:00 2001 From: bBlazewavE Date: Fri, 6 Mar 2026 11:34:45 -0300 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20match=20Tiangolo's=20docs=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/constraints.md | 131 +++++------------------------------ 1 file changed, 19 insertions(+), 112 deletions(-) diff --git a/docs/advanced/constraints.md b/docs/advanced/constraints.md index 9fd7b7382f..8f68f8fd15 100644 --- a/docs/advanced/constraints.md +++ b/docs/advanced/constraints.md @@ -1,156 +1,63 @@ # Database Constraints -Database constraints are rules that ensure data integrity and prevent invalid data from being inserted into your database. **SQLModel** supports several types of constraints through **SQLAlchemy**. +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 constraints are enforced at the **database level**, which means they work regardless of which application is inserting the data. This is particularly important for data consistency and reliability in production systems. +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 SQLAlchemy's constraint system under the hood, giving you access to all the powerful constraint options available in SQLAlchemy. +**SQLModel** uses SQLAlchemy's constraint system under the hood, so you have access to all the powerful constraint options available in SQLAlchemy. /// ## Unique Constraints -Unique constraints ensure that certain fields or combinations of fields have unique values across all rows in the table. +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()`: -### Single Column Unique Constraints +{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[4:8] hl[5] *} -The simplest way to add a unique constraint is using the `unique` parameter in the `Field()` function: +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. -{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[4:8] hl[6] *} +So two heroes named "Deadpond" and "Spider-Boy" would work fine, but trying to add a second "Deadpond" would fail. -In this example, 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. +## Multi-Column Unique Constraints -βœ… **Valid examples:** -* Two heroes with names "Deadpond" and "Spider-Boy" -* Heroes with the same age but different names +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. -🚫 **Invalid examples:** -* Two heroes both named "Deadpond" -* Inserting a hero with a name that already exists in the database - -### Multi-Column Unique Constraints - -Sometimes you want a combination of fields to be unique, even though each individual field can have duplicate values. You can achieve this using `__table_args__` with `UniqueConstraint`: +You can do this using `__table_args__` with a `UniqueConstraint`: {* ./docs_src/advanced/constraints/tutorial002_py310.py ln[5:11] hl[6] *} -In this example, the combination of `name` and `age` must be unique. This means you can have multiple heroes with the same name (as long as they have different ages), and you can have multiple heroes with the same age (as long as they have different names). - -βœ… **Valid examples:** -* "Spider-Boy" aged 16 and "Spider-Boy" aged 25 (same name, different ages) -* "Spider-Boy" aged 16 and "Iron Man" aged 16 (different names, same age) - -🚫 **Invalid examples:** -* Two heroes both named "Spider-Boy" and both aged 16 +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 make the combination of all three fields unique. +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 allow you to define custom validation rules using SQL expressions. These are more flexible than basic type validation and can enforce business rules at the database level. +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] *} -In this example, the check constraint ensures that the `age` field cannot be negative. The constraint has a name (`age_non_negative`) which makes error messages clearer and allows you to reference it later if needed. - -βœ… **Valid examples:** -* Heroes with age 0, 16, 25, 100, etc. -* Any non-negative integer for age - -🚫 **Invalid examples:** -* Heroes with negative ages like -5, -1, etc. - -/// info - -Check constraints can use any valid SQL expression supported by your database. Common examples include: +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. -- **Range checks:** `age BETWEEN 0 AND 150` -- **String length:** `LENGTH(name) >= 2` -- **Pattern matching:** `email LIKE '%@%'` -- **Value lists:** `status IN ('active', 'inactive', 'pending')` - -/// - -### Naming Check Constraints - -It's a good practice to always give your check constraints descriptive names using the `name` parameter. This makes debugging easier when constraint violations occur: - -```python -CheckConstraint("age >= 0", name="age_non_negative") -CheckConstraint("LENGTH(name) >= 2", name="name_min_length") -CheckConstraint("email LIKE '%@%'", name="email_format") -``` +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 combine different types of constraints in the same table by adding multiple constraint objects to `__table_args__`: +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 example combines: -- A unique constraint on the combination of `name` and `age` -- A check constraint ensuring age is non-negative -- A check constraint ensuring name has at least 2 characters - -/// tip - -When combining constraints, remember that **all constraints must be satisfied** for data to be inserted successfully. Design your constraints carefully to ensure they work together and don't create impossible conditions. - -/// - -## Constraint Violation Errors - -When constraints are violated, SQLAlchemy will raise exceptions. It's good practice to handle these in your application: - -```python -from sqlalchemy.exc import IntegrityError - -try: - with Session(engine) as session: - # Trying to insert duplicate data - hero = Hero(name="Deadpond", age=48, secret_name="Dive Wilson") - session.add(hero) - session.commit() -except IntegrityError as e: - print(f"Constraint violation: {e}") - session.rollback() -``` - -## Database Support +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. /// warning -Not all databases support all types of constraints equally: - -- **SQLite:** Supports unique constraints and basic check constraints, but has limitations with some complex SQL expressions -- **PostgreSQL:** Full support for all constraint types with rich SQL expression support -- **MySQL:** Good support for most constraints, with some syntax differences in check constraints -- **SQL Server:** Full support for all constraint types +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. -Always test your constraints with your target database to ensure compatibility. +Most other SQL databases like **PostgreSQL** and **MySQL** have full or near-full support. πŸŽ‰ /// - -## Best Practices - -🎯 **Use meaningful constraint names** - This makes debugging easier when violations occur - -🎯 **Combine field-level and table-level constraints** - Use `Field(unique=True)` for simple cases and `__table_args__` for complex ones - -🎯 **Consider performance** - Unique constraints automatically create indexes, which can improve query performance - -🎯 **Handle constraint violations gracefully** - Always wrap database operations in try-catch blocks when constraints might be violated - -🎯 **Document your constraints** - Make sure your team understands what business rules the constraints enforce - -/// info - -Remember that **SQLModel** constraints are implemented using **SQLAlchemy**, so you have access to all the power and flexibility of SQLAlchemy's constraint system. For more advanced use cases, check the SQLAlchemy constraints documentation. - -/// \ No newline at end of file From a84f8f939e5684fd1c7aa6d514be8e4dc3b01720 Mon Sep 17 00:00:00 2001 From: bBlazewavE Date: Fri, 6 Mar 2026 14:24:18 -0300 Subject: [PATCH 4/5] fix: correct highlight, add constraint violation example, minor style tweaks --- docs/advanced/constraints.md | 31 +++++++++++- .../advanced/constraints/tutorial005_py310.py | 50 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 docs_src/advanced/constraints/tutorial005_py310.py diff --git a/docs/advanced/constraints.md b/docs/advanced/constraints.md index 8f68f8fd15..a76244e40f 100644 --- a/docs/advanced/constraints.md +++ b/docs/advanced/constraints.md @@ -1,6 +1,6 @@ # 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. +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. @@ -14,7 +14,7 @@ These rules are called **constraints**, and because they live in the database, t 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[5] *} +{* ./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. @@ -54,6 +54,33 @@ You can mix different types of constraints in the same model by adding multiple 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. + +
+ +```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) +``` + +
+ +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. diff --git a/docs_src/advanced/constraints/tutorial005_py310.py b/docs_src/advanced/constraints/tutorial005_py310.py new file mode 100644 index 0000000000..77ddf2ce19 --- /dev/null +++ b/docs_src/advanced/constraints/tutorial005_py310.py @@ -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() \ No newline at end of file From 5de95b04dac6ae2b2f868de4fbc4a278cfe87f6b Mon Sep 17 00:00:00 2001 From: bBlazewavE Date: Fri, 6 Mar 2026 14:26:38 -0300 Subject: [PATCH 5/5] fix: add trailing newline to tutorial005 --- docs_src/advanced/constraints/tutorial005_py310.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/advanced/constraints/tutorial005_py310.py b/docs_src/advanced/constraints/tutorial005_py310.py index 77ddf2ce19..63296343c9 100644 --- a/docs_src/advanced/constraints/tutorial005_py310.py +++ b/docs_src/advanced/constraints/tutorial005_py310.py @@ -47,4 +47,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()