diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b38df29f46..cd972a0ba4 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,16 @@
version: 2
updates:
+ # GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ commit-message:
+ prefix: ⬆
+ # Python
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
+ commit-message:
+ prefix: ⬆
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index 82402f537a..6400691533 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -1,6 +1,8 @@
name: Build Docs
on:
push:
+ branches:
+ - main
pull_request:
types: [opened, synchronize]
workflow_dispatch:
@@ -17,9 +19,9 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.1.0
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: "3.7"
# Allow debugging with tmate
@@ -28,7 +30,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
with:
limit-access-to-actor: true
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
id: cache
with:
path: ${{ env.pythonLocation }}
@@ -51,14 +53,22 @@ jobs:
- name: Install Material for MkDocs Insiders
if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true'
run: python -m poetry run pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git
+ - uses: actions/cache@v3
+ with:
+ key: mkdocs-cards-${{ github.ref }}
+ path: .cache
- name: Build Docs
+ if: github.event.pull_request.head.repo.fork == true
run: python -m poetry run mkdocs build
+ - name: Build Docs with Insiders
+ if: github.event.pull_request.head.repo.fork == false
+ run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml
- name: Zip docs
run: python -m poetry run bash ./scripts/zip-docs.sh
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v3
with:
name: docs-zip
- path: ./docs.zip
+ path: ./site/docs.zip
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1.1.5
with:
diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml
index 48fb6dc833..9c3edccbf3 100644
--- a/.github/workflows/latest-changes.yml
+++ b/.github/workflows/latest-changes.yml
@@ -20,7 +20,7 @@ jobs:
latest-changes:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.1.0
with:
# To allow latest-changes to commit to the main branch
token: ${{ secrets.ACTIONS_TOKEN }}
diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml
index e335e81f91..3550a9b441 100644
--- a/.github/workflows/preview-docs.yml
+++ b/.github/workflows/preview-docs.yml
@@ -10,17 +10,22 @@ jobs:
preview-docs:
runs-on: ubuntu-20.04
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.1.0
+ - name: Clean site
+ run: |
+ rm -rf ./site
+ mkdir ./site
- name: Download Artifact Docs
- uses: dawidd6/action-download-artifact@v2.9.0
+ uses: dawidd6/action-download-artifact@v2.24.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: build-docs.yml
run_id: ${{ github.event.workflow_run.id }}
name: docs-zip
+ path: ./site/
- name: Unzip docs
run: |
- rm -rf ./site
+ cd ./site
unzip docs.zip
rm -f docs.zip
- name: Deploy to Netlify
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 105dbdd4cc..f3c1e980a6 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -15,9 +15,9 @@ jobs:
publish:
runs-on: ubuntu-20.04
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.1.0
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: "3.7"
# Allow debugging with tmate
@@ -26,7 +26,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
with:
limit-access-to-actor: true
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
id: cache
with:
path: ${{ env.pythonLocation }}
diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml
new file mode 100644
index 0000000000..7ee17cac55
--- /dev/null
+++ b/.github/workflows/smokeshow.yml
@@ -0,0 +1,35 @@
+name: Smokeshow
+
+on:
+ workflow_run:
+ workflows: [Test]
+ types: [completed]
+
+permissions:
+ statuses: write
+
+jobs:
+ smokeshow:
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.9'
+
+ - run: pip install smokeshow
+
+ - uses: dawidd6/action-download-artifact@v2.24.2
+ with:
+ workflow: test.yml
+ commit: ${{ github.event.workflow_run.head_sha }}
+
+ - run: smokeshow upload coverage-html
+ env:
+ SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}
+ SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95
+ SMOKESHOW_GITHUB_CONTEXT: coverage
+ SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
+ SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6e15a7d6f7..585ffc0455 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -2,6 +2,8 @@ name: Test
on:
push:
+ branches:
+ - main
pull_request:
types: [opened, synchronize]
workflow_dispatch:
@@ -16,13 +18,13 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
- python-version: [3.6, 3.7, 3.8, 3.9]
+ python-version: ["3.6.15", "3.7", "3.8", "3.9", "3.10"]
fail-fast: false
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.1.0
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
# Allow debugging with tmate
@@ -31,7 +33,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
with:
limit-access-to-actor: true
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
id: cache
with:
path: ${{ env.pythonLocation }}
@@ -52,9 +54,45 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: python -m poetry install
- name: Lint
- if: ${{ matrix.python-version != '3.6' }}
+ if: ${{ matrix.python-version != '3.6.15' }}
run: python -m poetry run bash scripts/lint.sh
+ - run: mkdir coverage
- name: Test
run: python -m poetry run bash scripts/test.sh
- - name: Upload coverage
- uses: codecov/codecov-action@v1
+ env:
+ COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
+ CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
+ - name: Store coverage files
+ uses: actions/upload-artifact@v3
+ with:
+ name: coverage
+ path: coverage
+ coverage-combine:
+ needs: [test]
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.8'
+
+ - name: Get coverage files
+ uses: actions/download-artifact@v3
+ with:
+ name: coverage
+ path: coverage
+
+ - run: pip install coverage[toml]
+
+ - run: ls -la coverage
+ - run: coverage combine coverage
+ - run: coverage report
+ - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}"
+
+ - name: Store coverage HTML
+ uses: actions/upload-artifact@v3
+ with:
+ name: coverage-html
+ path: htmlcov
diff --git a/.gitignore b/.gitignore
index 909f50ed81..4006069389 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ htmlcov
coverage.xml
site
*.db
+.cache
diff --git a/README.md b/README.md
index 21e7e5fe85..165cb6876c 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,8 @@
-
-
-
+
+
@@ -212,4 +211,4 @@ And at the same time, ✨ the class is also a **Pydantic** model ✨. You can us
## License
-This project is licensed under the terms of the MIT license.
+This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE).
diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md
new file mode 100644
index 0000000000..c0541b75df
--- /dev/null
+++ b/docs/advanced/decimal.md
@@ -0,0 +1,148 @@
+# Decimal Numbers
+
+In some cases you might need to be able to store decimal numbers with guarantees about the precision.
+
+This is particularly important if you are storing things like **currencies**, **prices**, **accounts**, and others, as you would want to know that you wouldn't have rounding errors.
+
+As an example, if you open Python and sum `1.1` + `2.2` you would expect to see `3.3`, but you will actually get `3.3000000000000003`:
+
+```Python
+>>> 1.1 + 2.2
+3.3000000000000003
+```
+
+This is because of the way numbers are stored in "ones and zeros" (binary). But Python has a module and some types to have strict decimal values. You can read more about it in the official Python docs for Decimal.
+
+Because databases store data in the same ways as computers (in binary), they would have the same types of issues. And because of that, they also have a special **decimal** type.
+
+In most cases this would probably not be a problem, for example measuring views in a video, or the life bar in a videogame. But as you can imagine, this is particularly important when dealing with **money** and **finances**.
+
+## Decimal Types
+
+Pydantic has special support for `Decimal` types using the `condecimal()` special function.
+
+!!! tip
+ Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function.
+
+ But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here.
+
+When you use `condecimal()` you can specify the number of digits and decimal places to support. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns.
+
+!!! info
+ For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type.
+
+## Decimals in SQLModel
+
+Let's say that each hero in the database will have an amount of money. We could make that field a `Decimal` type using the `condecimal()` function:
+
+```{.python .annotate hl_lines="12" }
+{!./docs_src/advanced/decimal/tutorial001.py[ln:1-12]!}
+
+# More code here later 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/advanced/decimal/tutorial001.py!}
+```
+
+
+
+Here we are saying that `money` can have at most `5` digits with `max_digits`, **this includes the integers** (to the left of the decimal dot) **and the decimals** (to the right of the decimal dot).
+
+We are also saying that the number of decimal places (to the right of the decimal dot) is `3`, so we can have **3 decimal digits** for these numbers in the `money` field. This means that we will have **2 digits for the integer part** and **3 digits for the decimal part**.
+
+✅ So, for example, these are all valid numbers for the `money` field:
+
+* `12.345`
+* `12.3`
+* `12`
+* `1.2`
+* `0.123`
+* `0`
+
+🚫 But these are all invalid numbers for that `money` field:
+
+* `1.2345`
+ * This number has more than 3 decimal places.
+* `123.234`
+ * This number has more than 5 digits in total (integer and decimal part).
+* `123`
+ * Even though this number doesn't have any decimals, we still have 3 places saved for them, which means that we can **only use 2 places** for the **integer part**, and this number has 3 integer digits. So, the allowed number of integer digits is `max_digits` - `decimal_places` = 2.
+
+!!! tip
+ Make sure you adjust the number of digits and decimal places for your own needs, in your own application. 🤓
+
+## Create models with Decimals
+
+When creating new models you can actually pass normal (`float`) numbers, Pydantic will automatically convert them to `Decimal` types, and **SQLModel** will store them as `Decimal` types in the database (using SQLAlchemy).
+
+```Python hl_lines="4-6"
+# Code above omitted 👆
+
+{!./docs_src/advanced/decimal/tutorial001.py[ln:25-35]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/advanced/decimal/tutorial001.py!}
+```
+
+
+
+## Select Decimal data
+
+Then, when working with Decimal types, you can confirm that they indeed avoid those rounding errors from floats:
+
+```Python hl_lines="15-16"
+# Code above omitted 👆
+
+{!./docs_src/advanced/decimal/tutorial001.py[ln:38-51]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/advanced/decimal/tutorial001.py!}
+```
+
+
+
+## Review the results
+
+Now if you run this, instead of printing the unexpected number `3.3000000000000003`, it prints `3.300`:
+
+
+
+```console
+$ python app.py
+
+// Some boilerplate and previous output omitted 😉
+
+// The type of money is Decimal('1.100')
+Hero 1: id=1 secret_name='Dive Wilson' age=None name='Deadpond' money=Decimal('1.100')
+
+// More output omitted here 🤓
+
+// The type of money is Decimal('1.100')
+Hero 2: id=3 secret_name='Tommy Sharp' age=48 name='Rusty-Man' money=Decimal('2.200')
+
+// No rounding errors, just 3.3! 🎉
+Total money: 3.300
+```
+
+
+
+!!! warning
+ Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports.
+
+ But decimals are supported by most of the other SQL databases. 🎉
diff --git a/docs/advanced/index.md b/docs/advanced/index.md
index 588ac1d0e0..f6178249ce 100644
--- a/docs/advanced/index.md
+++ b/docs/advanced/index.md
@@ -1,12 +1,10 @@
# Advanced User Guide
-The **Advanced User Guide** will be coming soon to a theater **documentation** near you... 😅
+The **Advanced User Guide** is gradually growing, you can already read about some advanced topics.
-I just have to `add` it, `commit` it, and `refresh` it. 😉
+At some point it will include:
-It will include:
-
-* How to use the `async` and `await` with the async session.
+* How to use `async` and `await` with the async session.
* How to run migrations.
* How to combine **SQLModel** models with SQLAlchemy.
-* ...and more.
+* ...and more. 🤓
diff --git a/docs/contributing.md b/docs/contributing.md
index 2cfa5331df..f2964fba9b 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -42,7 +42,7 @@ $ poetry shell
-That will set up the environment variables needed dand will start a new shell with them.
+That will set up the environment variables needed and start a new shell with them.
#### Using your local SQLModel
diff --git a/docs/databases.md b/docs/databases.md
index cb085c67d2..f1aaf663ab 100644
--- a/docs/databases.md
+++ b/docs/databases.md
@@ -85,7 +85,7 @@ Some examples of databases that work like this could be **PostgreSQL**, **MySQL*
### Distributed servers
-In some cases, the database could even be a group server applications running on different machines, working together and communicating between them to be more efficient and handle more data.
+In some cases, the database could even be a group of server applications running on different machines, working together and communicating between them to be more efficient and handle more data.
In this case, your code would talk to one or more of these server applications running on different machines.
@@ -250,7 +250,7 @@ As these **primary key** IDs can uniquely identify each row on the table for tea
-So, in the table for heroes, we use the `team_id` column to define a relationship to the *foreign* table for teams. Each value in the `team_id` column on the table with heroes will be the same value as the `id` column of one row in the table wiwth teams.
+So, in the table for heroes, we use the `team_id` column to define a relationship to the *foreign* table for teams. Each value in the `team_id` column on the table with heroes will be the same value as the `id` column of one row in the table with teams.
In the table for heroes we have a **primary key** that is the `id`. But we also have another column `team_id` that refers to a **key** in a **foreign** table. There's a technical term for that too, the `team_id` is a "**foreign key**".
@@ -274,7 +274,7 @@ The language is called **SQL**, the name comes from for **Structured Query Langu
Nevertheless, the language is not only used to *query* for data. It is also used to create records/rows, to update them, to delete them. And to manipulate the database, create tables, etc.
-This language is supported by all these databases that handle multiple tables, that's why they are called **SQL Databases**. Although, each database has small variations in the SQL language they support.
+This language is supported by all these databases that handle multiple tables, that's why they are called **SQL Databases**. Although, each database has small variations in the SQL language they support (*dialect*).
Let's imagine that the table holding the heroes is called the `hero` table. An example of a SQL query to get all the data from it could look like:
diff --git a/docs/db-to-code.md b/docs/db-to-code.md
index d4b182d26e..2e0fb1babc 100644
--- a/docs/db-to-code.md
+++ b/docs/db-to-code.md
@@ -62,7 +62,7 @@ The user is probably, in some way, telling your application:
2
```
-And the would be this table (with a single row):
+And the result would be this table (with a single row):
@@ -143,7 +143,7 @@ If the user provides this ID:
2
```
-...the would be this table (with a single row):
+...the result would be this table (with a single row):
diff --git a/docs/help.md b/docs/help.md
index bf2360bd60..6cde4c6142 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -12,7 +12,7 @@ And there are several ways to get help too.
## Subscribe to the FastAPI and Friends newsletter
-You can subscribe to the (infrequent) [**FastAPI and friends** newsletter](/newsletter/){.internal-link target=_blank} to stay updated about:
+You can subscribe to the (infrequent) **FastAPI and friends** newsletter to stay updated about:
* News about FastAPI and friends, including SQLModel 🚀
* Guides 📝
diff --git a/docs/img/tutorial/indexes/dictionary001.drawio b/docs/img/tutorial/indexes/dictionary001.drawio
new file mode 100644
index 0000000000..659f6b52a4
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary001.drawio
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary001.svg b/docs/img/tutorial/indexes/dictionary001.svg
new file mode 100644
index 0000000000..b543793a25
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary001.svg
@@ -0,0 +1,57 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary002.drawio b/docs/img/tutorial/indexes/dictionary002.drawio
new file mode 100644
index 0000000000..cb1857b1ad
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary002.drawio
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary002.svg b/docs/img/tutorial/indexes/dictionary002.svg
new file mode 100644
index 0000000000..677687d248
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary002.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary003.drawio b/docs/img/tutorial/indexes/dictionary003.drawio
new file mode 100644
index 0000000000..845eb065cd
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary003.drawio
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary003.svg b/docs/img/tutorial/indexes/dictionary003.svg
new file mode 100644
index 0000000000..d667a68893
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary003.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary004.drawio b/docs/img/tutorial/indexes/dictionary004.drawio
new file mode 100644
index 0000000000..14bbb1e26e
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary004.drawio
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary004.svg b/docs/img/tutorial/indexes/dictionary004.svg
new file mode 100644
index 0000000000..f881d6c9c2
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary004.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary005.drawio b/docs/img/tutorial/indexes/dictionary005.drawio
new file mode 100644
index 0000000000..9e339c177e
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary005.drawio
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary005.svg b/docs/img/tutorial/indexes/dictionary005.svg
new file mode 100644
index 0000000000..9d376245c0
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary005.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary006.drawio b/docs/img/tutorial/indexes/dictionary006.drawio
new file mode 100644
index 0000000000..3c669d323f
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary006.drawio
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary006.svg b/docs/img/tutorial/indexes/dictionary006.svg
new file mode 100644
index 0000000000..30be80ea8b
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary006.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary007.drawio b/docs/img/tutorial/indexes/dictionary007.drawio
new file mode 100644
index 0000000000..89b32cabaf
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary007.drawio
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary007.svg b/docs/img/tutorial/indexes/dictionary007.svg
new file mode 100644
index 0000000000..79e795060e
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary007.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary008.drawio b/docs/img/tutorial/indexes/dictionary008.drawio
new file mode 100644
index 0000000000..ac08ad04d4
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary008.drawio
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/dictionary008.svg b/docs/img/tutorial/indexes/dictionary008.svg
new file mode 100644
index 0000000000..013a4d64a3
--- /dev/null
+++ b/docs/img/tutorial/indexes/dictionary008.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/techbook001.drawio b/docs/img/tutorial/indexes/techbook001.drawio
new file mode 100644
index 0000000000..de1c25668c
--- /dev/null
+++ b/docs/img/tutorial/indexes/techbook001.drawio
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/img/tutorial/indexes/techbook001.svg b/docs/img/tutorial/indexes/techbook001.svg
new file mode 100644
index 0000000000..8b0c09ddcf
--- /dev/null
+++ b/docs/img/tutorial/indexes/techbook001.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 09a2819a46..5721f1cdb0 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -11,9 +11,8 @@
-
-
-
+
+
@@ -212,4 +211,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher
## License
-This project is licensed under the terms of the MIT license.
+This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE).
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
new file mode 100644
index 0000000000..fc5bce873f
--- /dev/null
+++ b/docs/overrides/main.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+{%- block scripts %}
+{{ super() }}
+
+
+
+
+
+
You can ask questions about SQLModel. Try:
+
Which Python version is supported?
+
How SQLModel interacts with the database?
+
How can I link tables?
+
+
+
+{%- endblock %}
\ No newline at end of file
diff --git a/docs/release-notes.md b/docs/release-notes.md
index d27692deab..4a4788f3bd 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -2,6 +2,192 @@
## Latest Changes
+* 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo).
+* ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu).
+* ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart).
+* ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine).
+* ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro).
+* 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update black requirement from ^21.5-beta.1 to ^22.10.0. PR [#460](https://github.com/tiangolo/sqlmodel/pull/460) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ➕ Add extra dev dependencies for MkDocs Material. PR [#485](https://github.com/tiangolo/sqlmodel/pull/485) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Update mypy requirement from 0.930 to 0.971. PR [#380](https://github.com/tiangolo/sqlmodel/pull/380) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update coverage requirement from ^5.5 to ^6.2. PR [#171](https://github.com/tiangolo/sqlmodel/pull/171) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump codecov/codecov-action from 2 to 3. PR [#415](https://github.com/tiangolo/sqlmodel/pull/415) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/upload-artifact from 2 to 3. PR [#412](https://github.com/tiangolo/sqlmodel/pull/412) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update flake8 requirement from ^3.9.2 to ^5.0.4. PR [#396](https://github.com/tiangolo/sqlmodel/pull/396) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Update pytest requirement from ^6.2.4 to ^7.0.1. PR [#242](https://github.com/tiangolo/sqlmodel/pull/242) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump actions/checkout from 2 to 3.1.0. PR [#458](https://github.com/tiangolo/sqlmodel/pull/458) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.0.8
+
+### Fixes
+
+* 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs).
+* ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo).
+
+### Docs
+
+* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo).
+* ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox).
+
+## 0.0.7
+
+### Features
+
+* ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson).
+* ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster).
+* ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10).
+* ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca).
+* ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo).
+
+### Fixes
+
+* 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday).
+* 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1).
+* 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman).
+* 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900).
+* 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199).
+* 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51).
+* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman).
+
+### Docs
+
+* 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli).
+* ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos).
+* 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura).
+* ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87).
+* ✏ Fix typo in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#239](https://github.com/tiangolo/sqlmodel/pull/239) by [@jalvaradosegura](https://github.com/jalvaradosegura).
+* ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#80](https://github.com/tiangolo/sqlmodel/pull/80) by [@joemudryk](https://github.com/joemudryk).
+* ✏ Fix typos in multiple files in the docs. PR [#400](https://github.com/tiangolo/sqlmodel/pull/400) by [@VictorGambarini](https://github.com/VictorGambarini).
+* ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#344](https://github.com/tiangolo/sqlmodel/pull/344) by [@marciomazza](https://github.com/marciomazza).
+* ✏ Fix typo in `docs/db-to-code.md`. PR [#155](https://github.com/tiangolo/sqlmodel/pull/155) by [@gr8jam](https://github.com/gr8jam).
+* ✏ Fix typo in `docs/contributing.md`. PR [#323](https://github.com/tiangolo/sqlmodel/pull/323) by [@Fardad13](https://github.com/Fardad13).
+* ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#265](https://github.com/tiangolo/sqlmodel/pull/265) by [@johnhoman](https://github.com/johnhoman).
+* ✏ Fix typo in `docs/tutorial/where.md`. PR [#286](https://github.com/tiangolo/sqlmodel/pull/286) by [@jalvaradosegura](https://github.com/jalvaradosegura).
+* ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#268](https://github.com/tiangolo/sqlmodel/pull/268) by [@cirrusj](https://github.com/cirrusj).
+* ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#247](https://github.com/tiangolo/sqlmodel/pull/247) by [@hao-wang](https://github.com/hao-wang).
+* ✏ Fix typos in `docs/tutorial/automatic-id-none-refresh.md`, `docs/tutorial/fastapi/update.md`, `docs/tutorial/select.md`. PR [#185](https://github.com/tiangolo/sqlmodel/pull/185) by [@rootux](https://github.com/rootux).
+* ✏ Fix typo in `docs/databases.md`. PR [#177](https://github.com/tiangolo/sqlmodel/pull/177) by [@seandlg](https://github.com/seandlg).
+* ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#162](https://github.com/tiangolo/sqlmodel/pull/162) by [@wmcgee3](https://github.com/wmcgee3).
+* ✏ Fix typos in `docs/tutorial/code-structure.md`, `docs/tutorial/fastapi/multiple-models.md`, `docs/tutorial/fastapi/simple-hero-api.md`, `docs/tutorial/many-to-many/index.md`. PR [#116](https://github.com/tiangolo/sqlmodel/pull/116) by [@moonso](https://github.com/moonso).
+* ✏ Fix typo in `docs/tutorial/fastapi/teams.md`. PR [#154](https://github.com/tiangolo/sqlmodel/pull/154) by [@chrisgoddard](https://github.com/chrisgoddard).
+* ✏ Fix typo variable in example about relationships and `back_populates`, always use `hero` instead of `owner`. PR [#120](https://github.com/tiangolo/sqlmodel/pull/120) by [@onionj](https://github.com/onionj).
+* ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#113](https://github.com/tiangolo/sqlmodel/pull/113) by [@feanil](https://github.com/feanil).
+* ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet).
+* ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj).
+* ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus).
+* ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8).
+* ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose).
+
+### Internal
+
+* ♻ Refactor internal statements to simplify code. PR [#53](https://github.com/tiangolo/sqlmodel/pull/53) by [@yezz123](https://github.com/yezz123).
+* ♻ Refactor internal imports to reduce redundancy. PR [#272](https://github.com/tiangolo/sqlmodel/pull/272) by [@aminalaee](https://github.com/aminalaee).
+* ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta).
+* ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo).
+* ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo).
+* 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo).
+* 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo).
+* 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.0.6
+
+### Breaking Changes
+
+**SQLModel** no longer creates indexes by default for every column, indexes are now opt-in. You can read more about it in PR [#205](https://github.com/tiangolo/sqlmodel/pull/205).
+
+Before this change, if you had a model like this:
+
+```Python
+from typing import Optional
+
+from sqlmodel import Field, SQLModel
+
+
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str
+ secret_name: str
+ age: Optional[int] = None
+```
+
+...when creating the tables, SQLModel version `0.0.5` and below, would also create an index for `name`, one for `secret_name`, and one for `age` (`id` is the primary key, so it doesn't need an additional index).
+
+If you depended on having an index for each one of those columns, now you can (and would have to) define them explicitly:
+
+```Python
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str = Field(index=True)
+ secret_name: str = Field(index=True)
+ age: Optional[int] = Field(default=None, index=True)
+```
+
+There's a high chance you don't need indexes for all the columns. For example, you might only need indexes for `name` and `age`, but not for `secret_name`. In that case, you could define the model as:
+
+```Python
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str = Field(index=True)
+ secret_name: str
+ age: Optional[int] = Field(default=None, index=True)
+```
+
+If you already created your database tables with SQLModel using versions `0.0.5` or below, it would have also created those indexes in the database. In that case, you might want to manually drop (remove) some of those indexes, if they are unnecessary, to avoid the extra cost in performance and space.
+
+Depending on the database you are using, there will be a different way to find the available indexes.
+
+For example, let's say you no longer need the index for `secret_name`. You could check the current indexes in the database and find the one for `secret_name`, it could be named `ix_hero_secret_name`. Then you can remove it with SQL:
+
+```SQL
+DROP INDEX ix_hero_secret_name
+```
+
+or
+
+```SQL
+DROP INDEX ix_hero_secret_name ON hero;
+```
+
+Here's the new, extensive documentation explaining indexes and how to use them: [Indexes - Optimize Queries](https://sqlmodel.tiangolo.com/tutorial/indexes/).
+
+### Docs
+
+* ✨ Document indexes and make them opt-in. Here's the new documentation: [Indexes - Optimize Queries](https://sqlmodel.tiangolo.com/tutorial/indexes/). This is the same change described above in **Breaking Changes**. PR [#205](https://github.com/tiangolo/sqlmodel/pull/205) by [@tiangolo](https://github.com/tiangolo).
+* ✏ Fix typo in FastAPI tutorial. PR [#192](https://github.com/tiangolo/sqlmodel/pull/192) by [@yaquelinehoyos](https://github.com/yaquelinehoyos).
+* 📝 Add links to the license file. PR [#29](https://github.com/tiangolo/sqlmodel/pull/29) by [@sobolevn](https://github.com/sobolevn).
+* ✏ Fix typos in docs titles. PR [#28](https://github.com/tiangolo/sqlmodel/pull/28) by [@Batalex](https://github.com/Batalex).
+* ✏ Fix multiple typos and some rewording. PR [#22](https://github.com/tiangolo/sqlmodel/pull/22) by [@egrim](https://github.com/egrim).
+* ✏ Fix typo in `docs/tutorial/automatic-id-none-refresh.md`. PR [#14](https://github.com/tiangolo/sqlmodel/pull/14) by [@leynier](https://github.com/leynier).
+* ✏ Fix typos in `docs/tutorial/index.md` and `docs/databases.md`. PR [#5](https://github.com/tiangolo/sqlmodel/pull/5) by [@sebastianmarines](https://github.com/sebastianmarines).
+
+## 0.0.5
+
+### Features
+
+* ✨ Add support for Decimal fields from Pydantic and SQLAlchemy. Original PR [#103](https://github.com/tiangolo/sqlmodel/pull/103) by [@robcxyz](https://github.com/robcxyz). New docs: [Advanced User Guide - Decimal Numbers](https://sqlmodel.tiangolo.com/advanced/decimal/).
+
+### Docs
+
+* ✏ Update decimal tutorial source for consistency. PR [#188](https://github.com/tiangolo/sqlmodel/pull/188) by [@tiangolo](https://github.com/tiangolo).
+
+### Internal
+
+* 🔧 Split MkDocs insiders build in CI to support building from PRs. PR [#186](https://github.com/tiangolo/sqlmodel/pull/186) by [@tiangolo](https://github.com/tiangolo).
+* 🎨 Format `expression.py` and expression template, currently needed by CI. PR [#187](https://github.com/tiangolo/sqlmodel/pull/187) by [@tiangolo](https://github.com/tiangolo).
+* 🐛Fix docs light/dark theme switcher. PR [#1](https://github.com/tiangolo/sqlmodel/pull/1) by [@Lehoczky](https://github.com/Lehoczky).
+* 🔧 Add MkDocs Material social cards. PR [#90](https://github.com/tiangolo/sqlmodel/pull/90) by [@tiangolo](https://github.com/tiangolo).
+* ✨ Update type annotations and upgrade mypy. PR [#173](https://github.com/tiangolo/sqlmodel/pull/173) by [@tiangolo](https://github.com/tiangolo).
## 0.0.4
diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md
index 215735e387..bbf74dd307 100644
--- a/docs/tutorial/automatic-id-none-refresh.md
+++ b/docs/tutorial/automatic-id-none-refresh.md
@@ -1,6 +1,6 @@
# Automatic IDs, None Defaults, and Refreshing Data
-In the previous chapter we saw how to add rows to the database using **SQLModel**.
+In the previous chapter, we saw how to add rows to the database using **SQLModel**.
Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`.
@@ -11,7 +11,7 @@ But the same `id` field actually **can be `None`** in the Python code, so we dec
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:6-10]!}
-# Code below ommitted 👇
+# Code below omitted 👇
```
@@ -68,7 +68,7 @@ If we ran this code before saving the hero to the database and the `hero_1.id` w
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
```
-But by declaring it with `Optional[int]` the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍
+But by declaring it with `Optional[int]`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍
## Print the Default `id` Values
@@ -79,7 +79,7 @@ We can confirm that by printing our heroes before adding them to the database:
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-31]!}
-# Code below ommitted 👇
+# Code below omitted 👇
```
@@ -98,7 +98,7 @@ That will output:
```console
$ python app.py
-// Output above ommitted 👆
+// Output above omitted 👆
Before interacting with the database
Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None
@@ -118,7 +118,7 @@ What happens when we `add` these objects to the **session**?
After we add the `Hero` instance objects to the **session**, the IDs are *still* `None`.
-We can verify by creating a session using a `with` block, and adding the objects. And then printing them again:
+We can verify by creating a session using a `with` block and adding the objects. And then printing them again:
```Python hl_lines="19-21"
# Code above omitted 👆
@@ -144,7 +144,7 @@ This will, again, output the `id`s of the objects as `None`:
```console
$ python app.py
-// Output above ommitted 👆
+// Output above omitted 👆
After adding to the session
Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None
@@ -165,7 +165,7 @@ Then we can `commit` the changes in the session, and print again:
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-48]!}
-# Code below ommitted 👇
+# Code below omitted 👇
```
@@ -184,7 +184,7 @@ And now, something unexpected happens, look at the output, it seems as if the `H
```console
$ python app.py
-// Output above ommitted 👆
+// Output above omitted 👆
// Here the engine talks to the database, the SQL part
INFO Engine BEGIN (implicit)
@@ -399,7 +399,7 @@ In this case, after committing the object to the database with the **session**,
## Print Data After Closing the Session
-Now, as a fnal experiment, we can also print data after the **session** is closed.
+Now, as a final experiment, we can also print data after the **session** is closed.
There are no surprises here, it still works:
@@ -450,7 +450,7 @@ Now let's review all this code once again.
And as we created the **engine** with `echo=True`, we can see the SQL statements being executed at each step.
-```{ .python .annotate hl_lines="54" }
+```{ .python .annotate }
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!}
```
diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md
index 0d91b4d5f5..59a9e4bd9a 100644
--- a/docs/tutorial/code-structure.md
+++ b/docs/tutorial/code-structure.md
@@ -8,7 +8,7 @@ The class `Hero` has a reference to the class `Team` internally.
But the class `Team` also has a reference to the class `Hero`.
-So, if those two classes where in separate files and you tried to import the classes in each other's file directly, it would result in a **circular import**. 🔄
+So, if those two classes were in separate files and you tried to import the classes in each other's file directly, it would result in a **circular import**. 🔄
And Python will not be able to handle it and will throw an error. 🚨
@@ -170,7 +170,7 @@ Let's assume that now the file structure is:
The problem with circular imports is that Python can't resolve them at *runtime*.
-but when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files.
+But when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files.
And the files with those classes might **also need to import** more things from the first files.
@@ -198,7 +198,7 @@ It has a value of `True` for editors and tools that analyze the code with the ty
But when Python is executing, its value is `False`.
-So, we can us it in an `if` block and import things inside the `if` block. And they will be "imported" only for editors, but not at runtime.
+So, we can use it in an `if` block and import things inside the `if` block. And they will be "imported" only for editors, but not at runtime.
### Hero Model File
diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md
index 3485ccf087..452c904ebe 100644
--- a/docs/tutorial/connect/create-connected-tables.md
+++ b/docs/tutorial/connect/create-connected-tables.md
@@ -78,6 +78,7 @@ The `Team` model will be in a table automatically named `"team"`, and it will ha
* `id`, the primary key, automatically generated by the database
* `name`, the name of the team
+ * We also tell **SQLModel** to create an index for this column
* `headquarters`, the headquarters of the team
And finally we mark it as a table in the config.
@@ -107,7 +108,7 @@ Most of that should look familiar:
The column will be named `team_id`. It will be an integer, and it could be `NULL` in the database (or `None` in Python), becase there could be some heroes that don't belong to any team.
-As we don't have to explicitly pass `team_id=None` when creating a hero, we add a default of `None` to the `Field()`.
+We add a default of `None` to the `Field()` so we don't have to explicitly pass `team_id=None` when creating a hero.
Now, here's the new part:
diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md
index 1153b51f32..f44559b3d1 100644
--- a/docs/tutorial/connect/remove-data-connections.md
+++ b/docs/tutorial/connect/remove-data-connections.md
@@ -46,7 +46,7 @@ We will continue with the code from the previous chapter.
## Break a Connection
-We don't really have to delete anyting to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`.
+We don't really have to delete anything to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`.
Let's say **Spider-Boy** is tired of the lack of friendly neighbors and wants to get out of the **Preventers**.
diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md
index 2bdecaac67..abd73cb797 100644
--- a/docs/tutorial/create-db-and-table.md
+++ b/docs/tutorial/create-db-and-table.md
@@ -415,7 +415,7 @@ Now run the program with Python:
// We set echo=True, so this will show the SQL code
$ python app.py
-// First, some boilerplate SQL that we are not that intereted in
+// First, some boilerplate SQL that we are not that interested in
INFO Engine BEGIN (implicit)
INFO Engine PRAGMA main.table_info("hero")
@@ -498,7 +498,7 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`.
Let's put it in a function `create_db_and_tables()`:
-```Python hl_lines="22-23"
+```Python hl_lines="19-20"
{!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!}
# More code here later 👇
@@ -513,9 +513,9 @@ Let's put it in a function `create_db_and_tables()`:
-If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time**.
+If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module.
-We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function.
+We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function, because we can make sure that the tables are created only when we call that function, and not when this module is imported somewhere else.
Now we would be able to, for example, import the `Hero` class in some other file without having those **side effects**.
diff --git a/docs/tutorial/fastapi/delete.md b/docs/tutorial/fastapi/delete.md
index 2ce3fe5b8b..a48122304b 100644
--- a/docs/tutorial/fastapi/delete.md
+++ b/docs/tutorial/fastapi/delete.md
@@ -39,6 +39,6 @@ After deleting it successfully, we just return a response of:
## Recap
-That's it, feel free to try it out in the interactve docs UI to delete some heroes. 💥
+That's it, feel free to try it out in the interactive docs UI to delete some heroes. 💥
Using **FastAPI** to read data and combining it with **SQLModel** makes it quite straightforward to delete data from the database.
diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md
index 6df18f429a..92bbfc7ee0 100644
--- a/docs/tutorial/fastapi/limit-and-offset.md
+++ b/docs/tutorial/fastapi/limit-and-offset.md
@@ -1,15 +1,15 @@
-# Read Heroes with Limit and Offset wtih FastAPI
+# Read Heroes with Limit and Offset with FastAPI
When a client sends a request to get all the heroes, we have been returning them all.
-But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwith, etc.
+But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwidth, etc.
-So we probably want to limit it.
+So, we probably want to limit it.
Let's use the same **offset** and **limit** we learned about in the previous tutorial chapters for the API.
!!! info
- In many cases this is also called **pagination**.
+ In many cases, this is also called **pagination**.
## Add a Limit and Offset to the Query Parameters
@@ -38,13 +38,13 @@ And by default, we will return a maximum of `100` heroes, so `limit` will have a
-We want to allow clients to set a different `offset` and `limit` values.
+We want to allow clients to set different `offset` and `limit` values.
But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱
So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess **t**han or **e**qual to `100` with `lte=100`.
-This way, a client can decide to take less heroes if they want, but not more.
+This way, a client can decide to take fewer heroes if they want, but not more.
!!! info
If you need to refresh how query parameters and their validation work, check out the docs in FastAPI:
diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md
index 5cac6dda91..c37fad386b 100644
--- a/docs/tutorial/fastapi/multiple-models.md
+++ b/docs/tutorial/fastapi/multiple-models.md
@@ -2,7 +2,7 @@
We have been using the same `Hero` model to declare the schema of the data we receive in the API, the table model in the database, and the schema of the data we send back in responses.
-But in most of the cases there are slight differences, let's use multiple models to solve it.
+But in most of the cases, there are slight differences. Let's use multiple models to solve it.
Here you will see the main and biggest feature of **SQLModel**. 😎
@@ -10,7 +10,7 @@ Here you will see the main and biggest feature of **SQLModel**. 😎
Let's start by reviewing the automatically generated schemas from the docs UI.
-For input we have:
+For input, we have:
@@ -20,7 +20,7 @@ This means that the client could try to use the same ID that already exists in t
That's not what we want.
-We want the client to only send the data that is needed to create a new hero:
+We want the client only to send the data that is needed to create a new hero:
* `name`
* `secret_name`
@@ -63,7 +63,7 @@ The ultimate goal of an API is for some **clients to use it**.
The clients could be a frontend application, a command line program, a graphical user interface, a mobile application, another backend application, etc.
-And the code those clients write depend on what our API tells them they **need to send**, and what they can **expect to receive**.
+And the code those clients write depends on what our API tells them they **need to send**, and what they can **expect to receive**.
Making both sides very clear will make it much easier to interact with the API.
@@ -164,7 +164,7 @@ Let's first check how is the process to create a hero now:
Let's check that in detail.
-Now we use the type annotation `HeroCreate` for the request JSON data, in the `hero` parameter of the **path operation function**.
+Now we use the type annotation `HeroCreate` for the request JSON data in the `hero` parameter of the **path operation function**.
```Python hl_lines="3"
# Code above omitted 👆
@@ -180,9 +180,9 @@ The method `.from_orm()` reads data from another object with attributes and crea
The alternative is `Hero.parse_obj()` that reads data from a dictionary.
-But as in this case we have a `HeroCreate` instance in the `hero` variable, this is an object with attributes, so we use `.from_orm()` to read those attributes.
+But as in this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.from_orm()` to read those attributes.
-With this we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
+With this, we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
```Python hl_lines="3"
# Code above omitted 👆
@@ -192,7 +192,7 @@ With this we create a new `Hero` instance (the one for the database) and put it
# Code below omitted 👇
```
-Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally we return the same `db_hero` variable that has the just refreshed `Hero` instance.
+Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally, we return the same `db_hero` variable that has the just refreshed `Hero` instance.
Because it is just refreshed, it has the `id` field set with a new ID taken from the database.
@@ -206,30 +206,30 @@ And now that we return it, FastAPI will validate the data with the `response_mod
# Code below omitted 👇
```
-This will validate that all the data that we promised is there, and will remove any data we didn't declare.
+This will validate that all the data that we promised is there and will remove any data we didn't declare.
!!! tip
- This filtering could be very important, and could be a very good security feature, for example to make sure you filter private data, hashed passwords, etc.
+ This filtering could be very important and could be a very good security feature, for example, to make sure you filter private data, hashed passwords, etc.
You can read more about it in the FastAPI docs about Response Model.
-In particular, it will make sure that the `id` is there, and that it is indeed an integer (and not `None`).
+In particular, it will make sure that the `id` is there and that it is indeed an integer (and not `None`).
## Shared Fields
But looking closely, we could see that these models have a lot of **duplicated information**.
-All **the 3 models** declare that thay share some **common fields** that look exactly the same:
+All **the 3 models** declare that they share some **common fields** that look exactly the same:
* `name`, required
* `secret_name`, required
* `age`, optional
-And then they declare other fields with some differences (in this case only about the `id`).
+And then they declare other fields with some differences (in this case, only about the `id`).
We want to **avoid duplicated information** if possible.
-This is important if, for example, in the future we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`.
+This is important if, for example, in the future, we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`.
If we have that duplicated in multiple models, we could easily forget to update one of them. But if we **avoid duplication**, there's only one place that would need updating. ✨
@@ -305,6 +305,31 @@ And of course, all these fields will be in the columns for the resulting `hero`
And those inherited fields will also be in the **autocompletion** and **inline errors** in editors, etc.
+### Columns and Inheritance with Multiple Models
+
+Notice that the parent model `HeroBase` is not a **table model**, but still, we can declare `name` and `age` using `Field(index=True)`.
+
+```Python hl_lines="4 6 9"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
+```
+
+
+
+This won't affect this parent **data model** `HeroBase`.
+
+But once the child model `Hero` (the actual **table model**) inherits those fields, it will use those field configurations to create the indexes when creating the tables in the database.
+
### The `HeroCreate` **Data Model**
Now let's see the `HeroCreate` model that will be used to define the data that we want to receive in the API when creating a new hero.
@@ -336,9 +361,9 @@ And because we can't leave the empty space when creating a new class, but we don
This means that there's nothing else special in this class apart from the fact that it is named `HeroCreate` and that it inherits from `HeroBase`.
-As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the auomatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for.
+As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the automatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for.
-On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example a password), and now we already have the class to put those extra fields.
+On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example, a password), and now we already have the class to put those extra fields.
### The `HeroRead` **Data Model**
@@ -365,7 +390,7 @@ This one just declares that the `id` field is required when reading a hero from
## Review the Updated Docs UI
-The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now we define them in a smarter way with inheritance.
+The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now, we define them in a smarter way with inheritance.
So, we can jump to the docs UI right away and see how they look with the updated data.
@@ -375,7 +400,7 @@ Let's see the new UI for creating a hero:
-Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optinally `age`.
+Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optionally `age`.
We no longer pass an `id`.
@@ -391,7 +416,7 @@ And if we check the schema for the **Read Heroes** *path operation* it will also
## Inheritance and Table Models
-We just saw how powerful inheritance of these models can be.
+We just saw how powerful the inheritance of these models could be.
This is a very simple example, and it might look a bit... meh. 😅
diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md
index b503546298..8eea6488b1 100644
--- a/docs/tutorial/fastapi/read-one.md
+++ b/docs/tutorial/fastapi/read-one.md
@@ -42,7 +42,7 @@ But if the integer is not the ID of any hero in the database, it will not find a
So, we check it in an `if` block, if it's `None`, we raise an `HTTPException` with a `404` status code.
-And to use it we first import `HTTPException` from `fastapi`.
+And to use it, we first import `HTTPException` from `fastapi`.
This will let the client know that they probably made a mistake on their side and requested a hero that doesn't exist in the database.
diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md
index 3aa8863f2f..6921b5ac85 100644
--- a/docs/tutorial/fastapi/relationships.md
+++ b/docs/tutorial/fastapi/relationships.md
@@ -55,11 +55,11 @@ And the same way, we declared the `TeamRead` with only the same base fields of t
# Code here omitted 👈
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-37]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-36]!}
# Code here omitted 👈
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:46-47]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:45-46]!}
# Code below omitted 👇
```
@@ -80,11 +80,11 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s
```Python hl_lines="3 8 12 17"
# Code above omitted 👆
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:105-110]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:104-109]!}
# Code here omitted 👈
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:160-165]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:159-164]!}
# Code below omitted 👇
```
@@ -102,7 +102,7 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s
Now let's stop for a second and think about it.
-We cannot simply include *all* the data including all the internal relationships, because each **hero** has an attribute `team` with their team, and then that **team** also has an attribute `heroes` with all the **heroes** in the team, including this one.
+We cannot simply include *all* the data, including all the internal relationships, because each **hero** has an attribute `team` with their team, and then that **team** also has an attribute `heroes` with all the **heroes** in the team, including this one.
If we tried to include everything, we could make the server application **crash** trying to extract **infinite data**, going through the same hero and team over and over again internally, something like this:
@@ -152,7 +152,7 @@ If we tried to include everything, we could make the server application **crash*
}
```
-As you can see, in this example we would get the hero **Rusty-Man**, and from this hero we would get the team **Preventers**, and then from this team we would get its heroes, of course, including **Rusty-Man**... 😱
+As you can see, in this example, we would get the hero **Rusty-Man**, and from this hero we would get the team **Preventers**, and then from this team we would get its heroes, of course, including **Rusty-Man**... 😱
So we start again, and in the end, the server would just crash trying to get all the data with a `"Maximum recursion error"`, we would not even get a response like the one above.
@@ -164,7 +164,7 @@ This is a decision that will depend on **each application**.
In our case, let's say that if we get a **list of heroes**, we don't want to also include each of their teams in each one.
-And if we get a **list of teams**, we don't want to get a a list of the heroes for each one.
+And if we get a **list of teams**, we don't want to get a list of the heroes for each one.
But if we get a **single hero**, we want to include the team data (without the team's heroes).
@@ -195,7 +195,7 @@ We'll add them **after** the other models so that we can easily reference the pr
-These two models are very **simple in code**, but there's a lot happening here, let's check it out.
+These two models are very **simple in code**, but there's a lot happening here. Let's check it out.
### Inheritance and Type Annotations
@@ -203,7 +203,7 @@ The `HeroReadWithTeam` **inherits** from `HeroRead`, which means that it will ha
And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamRead` with the base fields for reading a team.
-Then we do the same for the `TeamReadWithHeroes`, it **inherits** from `TeamRead`, and declare the **new field** `heroes` which is a list of `HeroRead`.
+Then we do the same for the `TeamReadWithHeroes`, it **inherits** from `TeamRead`, and declares the **new field** `heroes`, which is a list of `HeroRead`.
### Data Models Without Relationship Attributes
@@ -213,7 +213,7 @@ Instead, here these are only **data models** that will tell FastAPI **which attr
### Reference to Other Models
-Also notice that the field `team` is not declared with this new `TeamReadWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamRead` model.
+Also, notice that the field `team` is not declared with this new `TeamReadWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamRead` model.
And the same for `TeamReadWithHeroes`, the model used for the new field `heroes` uses `HeroRead` to get only each hero's data.
@@ -326,7 +326,7 @@ Now we get the list of **heroes** included:
## Recap
-Using the same techniques to declare additonal **data models** we can tell FastAPI what data to return in the responses, even when we return **table models**.
+Using the same techniques to declare additional **data models**, we can tell FastAPI what data to return in the responses, even when we return **table models**.
Here we almost **didn't have to change the FastAPI app** code, but of course, there will be cases where you need to get the data and process it in different ways in the *path operation function* before returning it.
@@ -334,4 +334,4 @@ But even in those cases, you will be able to define the **data models** to use i
By this point, you already have a very robust API to handle data in a SQL database combining **SQLModel** with **FastAPI**, and implementing **best practices**, like data validation, conversion, filtering, and documentation. ✨
-In the next chapter I'll tell you how to implement automated **testing** for your application using FastAPI and SQLModel. ✅
+In the next chapter, I'll tell you how to implement automated **testing** for your application using FastAPI and SQLModel. ✅
diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md
index b4e0b6701e..c019f4580b 100644
--- a/docs/tutorial/fastapi/response-model.md
+++ b/docs/tutorial/fastapi/response-model.md
@@ -22,7 +22,7 @@ You can see that there's a possible "Successful Response" with a code `200`, but
-Right now we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back.
+Right now, we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back.
Let's do that now. 🤓
diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md
index 7f049f5002..52a800b9ea 100644
--- a/docs/tutorial/fastapi/session-with-dependency.md
+++ b/docs/tutorial/fastapi/session-with-dependency.md
@@ -90,7 +90,7 @@ We import `Depends()` from `fastapi`. Then we use it in the *path operation func
You can read more about it in the FastAPI documentation Path Parameters and Numeric Validations - Order the parameters as you need, tricks
-The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code, and will give you the value from that dependency.
+The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code and will give you the value from that dependency.
If it had `yield`, then it will continue the rest of the execution once you are done sending the response. In the case of the **session**, it will finish the cleanup code from the `with` block, closing the session, etc.
diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md
index 8759bce2c2..53a5fa7d38 100644
--- a/docs/tutorial/fastapi/simple-hero-api.md
+++ b/docs/tutorial/fastapi/simple-hero-api.md
@@ -23,7 +23,7 @@ $ python -m pip install fastapi "uvicorn[standard]"
```
-s
+
## **SQLModel** Code - Models, Engine
Now let's start with the SQLModel code.
@@ -152,13 +152,13 @@ It will be called when a user sends a request with a `POST` **operation** to the
## The **SQLModel** Advantage
-Here's where having our **SQLModel** class models be both **SQLAlchemy** models and **Pydantic** models at the same tieme shine. ✨
+Here's where having our **SQLModel** class models be both **SQLAlchemy** models and **Pydantic** models at the same time shine. ✨
Here we use the **same** class model to define the **request body** that will be received by our API.
Because **FastAPI** is based on Pydantic, it will use the same model (the Pydantic part) to do automatic data validation and conversion from the JSON request to an object that is an actual instance of the `Hero` class.
-And then because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database.
+And then, because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database.
So we can use intuitive standard Python **type annotations**, and we don't have to duplicate a lot of the code for the database models and the API data models. 🎉
@@ -190,13 +190,13 @@ When a client sends a request to the **path** `/heroes/` with a `GET` HTTP **ope
## One Session per Request
-Remember that we shoud use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session?
+Remember that we should use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session?
Here it is much more obvious.
We should normally have **one session per request** in most of the cases.
-In some isolated cases we would want to have new sessions inside, so, **more than one session** per request.
+In some isolated cases, we would want to have new sessions inside, so, **more than one session** per request.
But we would **never want to *share* the same session** among different requests.
@@ -277,7 +277,7 @@ And then you can get them back with the **Read Heroes** *path operation*:
Now you can terminate that Uvicorn server by going back to the terminal and pressing Ctrl+C.
-And then you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉
+And then, you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉
@@ -287,4 +287,4 @@ Good job! This is already a FastAPI **web API** application to interact with the
There are several things we can improve and extend. For example, we want the database to decide the ID of each new hero, we don't want to allow a user to send it.
-We will do all those improvements in the next chapters. 🚀
+We will make all those improvements in the next chapters. 🚀
diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md
index f0bce4c839..0b19a95cb3 100644
--- a/docs/tutorial/fastapi/teams.md
+++ b/docs/tutorial/fastapi/teams.md
@@ -1,4 +1,4 @@
-# FastAPI Path Opeartions for Teams - Other Models
+# FastAPI Path Operations for Teams - Other Models
Let's now update the **FastAPI** application to handle data for teams.
@@ -12,14 +12,14 @@ Let's add the models for the teams.
It's the same process we did for heroes, with a base model, a **table model**, and some other **data models**.
-We have a `TeamBase` **data model**, and from it we inherit with a `Team` **table model**.
+We have a `TeamBase` **data model**, and from it, we inherit with a `Team` **table model**.
Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **data models**.
And we also create a `TeamUpdate` **data model**.
-```Python hl_lines="7-9 12-15 18-19 22-23 26-29"
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-29]!}
+```Python hl_lines="7-9 12-15 18-19 22-23 26-28"
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-28]!}
# Code below omitted 👇
```
@@ -42,7 +42,7 @@ Let's now update the `Hero` models too.
```Python hl_lines="3-8 11-15 17-18 21-22 25-29"
# Code above omitted 👆
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-58]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-57]!}
# Code below omitted 👇
```
@@ -66,10 +66,10 @@ And even though the `HeroBase` is *not* a **table model**, we can declare `team_
Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them.
-```Python hl_lines="11 39"
+```Python hl_lines="11 38"
# Code above omitted 👆
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-58]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-57]!}
# Code below omitted 👇
```
@@ -92,7 +92,7 @@ These are equivalent and very similar to the **path operations** for the **heroe
```Python hl_lines="3-9 12-20 23-28 31-47 50-57"
# Code above omitted 👆
-{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:140-194]!}
+{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:139-193]!}
# Code below omitted 👇
```
@@ -108,9 +108,9 @@ These are equivalent and very similar to the **path operations** for the **heroe
## Using Relationships Attributes
-Up to this point we are actually not using the **relationship attributes**, but we could access them in our code.
+Up to this point, we are actually not using the **relationship attributes**, but we could access them in our code.
-In the next chapter we will play more with them.
+In the next chapter, we will play more with them.
## Check the Docs UI
diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md
index eaf3ef380f..f817a883a1 100644
--- a/docs/tutorial/fastapi/tests.md
+++ b/docs/tutorial/fastapi/tests.md
@@ -76,13 +76,13 @@ Let's start with a simple test, with just the basic test code we need the check
That's the **core** of the code we need for all the tests later.
-But now we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓
+But now, we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓
## Testing Database
This test looks fine, but there's a problem.
-If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding adding unnecesary data to it, or even worse, in future tests we could end up removing production data.
+If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding unnecesary data to it, or even worse, in future tests we could end up removing production data.
So, we should use an independent **testing database**, just for the tests.
@@ -155,7 +155,7 @@ That way, when we call `.create_all()` all the **table models** are correctly re
## Memory Database
-Now we are not using the production database, instead we use a **new testing database** with the `testing.db` file, which is great.
+Now we are not using the production database. Instead, we use a **new testing database** with the `testing.db` file, which is great.
But SQLite also supports having an **in memory** database. This means that all the database is only in memory, and it is never saved in a file on disk.
@@ -171,7 +171,7 @@ Other alternatives and ideas 👀
Before arriving at the idea of using an **in-memory database** we could have explored other alternatives and ideas.
-The first, is that we are not deleting the file after we finish the test, so, the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥
+The first is that we are not deleting the file after we finish the test, so the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥
But if each test has to create a new file and then delete it afterwards, running all the tests could be **a bit slow**.
@@ -179,7 +179,7 @@ Right now, we have a file `testing.db` that is used by all the tests (we only ha
So, if we tried to run the tests at the same time **in parallel** to try to speed things up a bit, they would clash trying to use the *same* `testing.db` file.
-Of couse, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative with just using an **in-memory database**. ✨
+Of course, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative by just using an **in-memory database**. ✨
@@ -208,7 +208,7 @@ And all the other tests can do the same.
Great, that works, and you could replicate all that process in each of the test functions.
-But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, the dependency override.
+But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, and the dependency override.
Do we really have to duplicate all that for **each test**? No, we can do better! 😎
@@ -242,12 +242,12 @@ Let's see the first code example with a fixture:
**pytest** fixtures work in a very similar way to FastAPI dependencies, but have some minor differences:
-* In pytest fixtures we need to add a decorator of `@pytest.fixture()` on top.
+* In pytest fixtures, we need to add a decorator of `@pytest.fixture()` on top.
* To use a pytest fixture in a function, we have to declare the parameter with the **exact same name**. In FastAPI we have to **explicitly use `Depends()`** with the actual function inside it.
But apart from the way we declare them and how we tell the framework that we want to have them in the function, they **work in a very similar way**.
-Now we create lot's of tests, and re-use that same fixture in all of them, saving us that **boilerplate code**.
+Now we create lot's of tests and re-use that same fixture in all of them, saving us that **boilerplate code**.
**pytest** will make sure to run them right before (and finish them right after) each test function. So, each test function will actually have its own database, engine, and session.
@@ -255,7 +255,7 @@ Now we create lot's of tests, and re-use that same fixture in all of them, savin
Awesome, that fixture helps us prevent a lot of duplicated code.
-But currently we still have to write some code in the test function that will be repetitive for other tests, right now we:
+But currently, we still have to write some code in the test function that will be repetitive for other tests, right now we:
* create the **dependency override**
* put it in the `app.dependency_overrides`
@@ -277,7 +277,7 @@ So, we can create a **client fixture** that will be used in all the tests, and i
!!! tip
Check out the number bubbles to see what is done by each line of code.
-Now we have a **client fixture** that in turns uses the **session fixture**.
+Now we have a **client fixture** that, in turn, uses the **session fixture**.
And in the actual test function, we just have to declare that we require this **client fixture**.
@@ -285,7 +285,7 @@ And in the actual test function, we just have to declare that we require this **
At this point, it all might seem like we just did a lot of changes for nothing, to get **the same result**. 🤔
-But normally we will create **lots of other test functions**. And now all the boilerplate and complexity is **writen only once**, in those two fixtures.
+But normally we will create **lots of other test functions**. And now all the boilerplate and complexity is **written only once**, in those two fixtures.
Let's add some more tests:
@@ -311,13 +311,13 @@ Let's add some more tests:
That's why we add these two extra tests here.
-Now, any additional test functions can be as **simple** as the first one, they just have to **declate the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎
+Now, any additional test functions can be as **simple** as the first one, they just have to **declare the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎
## Why Two Fixtures
-Now, seeing the code we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense!
+Now, seeing the code, we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense!
-For these examples, **that would have been simpler**, there's no need to separate that code in two fixtures for them...
+For these examples, **that would have been simpler**, there's no need to separate that code into two fixtures for them...
But for the next test function, we will require **both fixtures**, the **client** and the **session**.
@@ -340,7 +340,7 @@ But for the next test function, we will require **both fixtures**, the **client*
-In this test function we want to check that the *path operation* to **read a list of heroes** actually sends us heroes.
+In this test function, we want to check that the *path operation* to **read a list of heroes** actually sends us heroes.
But if the **database is empty**, we would get an **empty list**, and we wouldn't know if the hero data is being sent correctly or not.
@@ -362,7 +362,7 @@ The function for the **client fixture** and the actual testing function will **b
## Add the Rest of the Tests
-Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc. we can now add the rest of the tests, they look quite similar to what we have done up to now.
+Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc., we can now add the rest of the tests. They look quite similar to what we have done up to now.
```Python hl_lines="3 18 33"
# Code above omitted 👆
@@ -406,9 +406,9 @@ project/test_main.py ....... [100%]
Did you read all that? Wow, I'm impressed! 😎
-Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you indended.
+Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you intended.
-And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests they can help catch a lot of errors that can be easily introduced by refactoring.
+And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests can help catch a lot of errors that can be easily introduced by refactoring.
And they will give you the confidence to work faster and **more efficiently**, because you know that you are checking if you are **not breaking anything**. 😅
diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md
index b32e58281d..0b5292bd29 100644
--- a/docs/tutorial/fastapi/update.md
+++ b/docs/tutorial/fastapi/update.md
@@ -4,7 +4,7 @@ Now let's see how to update data in the database with a **FastAPI** *path operat
## `HeroUpdate` Model
-We want clients to be able to udpate the `name`, the `secret_name`, and the `age` of a hero.
+We want clients to be able to update the `name`, the `secret_name`, and the `age` of a hero.
But we don't want them to have to include all the data again just to **update a single field**.
@@ -61,7 +61,7 @@ We will use a `PATCH` HTTP operation. This is used to **partially update data**,
-We also read the `hero_id` from the *path parameter* an the request body, a `HeroUpdate`.
+We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`.
### Read the Existing Hero
@@ -100,7 +100,7 @@ But that also means that if we just call `hero.dict()` we will get a dictionary
}
```
-And then if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**.
+And then, if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**.
But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`.
@@ -200,7 +200,7 @@ We are **not simply omitting** the data that has the **default values**.
And we are **not simply omitting** anything that is `None`.
-This means that, if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀
+This means that if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀
So, if the client wanted to intentionally remove the `age` of a hero, they could just send a JSON with:
@@ -218,11 +218,11 @@ And when getting the data with `hero.dict(exclude_unset=True)`, we would get:
}
```
-So, we would use that value and upate the `age` to `None` in the database, **just as the client intended**.
+So, we would use that value and update the `age` to `None` in the database, **just as the client intended**.
Notice that `age` here is `None`, and **we still detected it**.
-Also that `name` was not even sent, and we don't *accidentaly* set it to `None` or something, we just didn't touch it, because the client didn't sent it, so we are **pefectly fine**, even in these corner cases. ✨
+Also, that `name` was not even sent, and we don't *accidentally* set it to `None` or something. We just didn't touch it because the client didn't send it, so we are **perfectly fine**, even in these corner cases. ✨
These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉
diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md
index 6deb258802..33cf6226c4 100644
--- a/docs/tutorial/index.md
+++ b/docs/tutorial/index.md
@@ -2,11 +2,11 @@
## Type hints
-If you need a refreshed about how to use Python type hints (type annotations), check FastAPI's Python types intro.
+If you need a refresher about how to use Python type hints (type annotations), check FastAPI's Python types intro.
You can also check the mypy cheat sheet.
-**SQLModel** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support posible, with autocompletion and in-editor error checking.
+**SQLModel** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support possible, with autocompletion and in-editor error checking.
## Intro
@@ -97,7 +97,7 @@ $ python3 --version
// This is too old! 😱
Python 3.5.6
// Let's see if python3.10 is available
-$ python3.10 --verson
+$ python3.10 --version
// Oh, no, this one is not available 😔
command not found: python3.10
$ python3.9 --version
diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md
new file mode 100644
index 0000000000..6513d7d462
--- /dev/null
+++ b/docs/tutorial/indexes.md
@@ -0,0 +1,406 @@
+# Indexes - Optimize Queries
+
+We just saw how to get some data `WHERE` a **condition** is true. For example, where the hero **name is "Deadpond"**.
+
+If we just create the tables and the data as we have been doing, when we `SELECT` some data using `WHERE`, the database would have to **scan** through **each one of the records** to find the ones that **match**. This is not a problem with 3 heroes as in these examples.
+
+But imagine that your database has **thousands** or **millions** of **records**, if every time you want to find the heroes with the name "Deadpond" it has to scan through **all** of the records to find all the possible matches, then that becomes problematic, as it would be too slow.
+
+I'll show you how to handle it with a database **index**.
+
+The change in the code is **extremely small**, but it's useful to understand what's happening behind the scenes, so I'll show you **how it all works** and what it means.
+
+---
+
+If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results.
+
+## No Time to Explain
+
+Are you already a **SQL expert** and don't have time for all my explanations?
+
+Fine, in that case, you can **sneak peek** the final code to create indexes here.
+
+
+👀 Full file preview
+
+```Python hl_lines="8 10"
+{!./docs_src/tutorial/indexes/tutorial002.py!}
+```
+
+
+
+..but if you are not an expert, **continue reading**, this will probably be useful. 🤓
+
+## What is an Index
+
+In general, an **index** is just something we can have to help us **find things faster**. It normally works by having things in **order**. Let's think about some real-life examples before even thinking about databases and code.
+
+### An Index and a Dictionary
+
+Imagine a **dictionary**, a book with definitions of words. 📔 ...not a Python `dict`. 😅
+
+Let's say that you want to **find a word**, for example the word "**database**". You take the dictionary, and open it somewhere, for example in the middle. Maybe you see some definitions of words that start with `m`, like `manual`, so you conclude that you are in the letter `m` in the dictionary.
+
+
+
+You know that in the alphabet, the letter `d` for `database` comes **before** the letter `m` for `manual`.
+
+
+
+So, you know you have to search in the dictionary **before** the point you currently are. You still don't know where the word `database` is, because you don't know exactly where the letter `d` is in the dictionary, but you know that **it is not after** that point, you can now **discard the right half** of the dictionary in your search.
+
+
+
+Next, you **open the dictionary again**, but only taking into account the **half of the dictionary** that can contain the word you want, the **left part of the dictionary**. You open it in the middle of that left part and now you arrive maybe at the letter `f`.
+
+
+
+You know that `d` from `database` comes before `f`. So it has to be **before** that. But now you know that `database` **is not after** that point, and you can discard the dictionary from that point onward.
+
+
+
+Now you have a **small section of dictionary** to search (only a **quarter** of dictionary can have your word). You take that **quarter** of the pages at the start of the dictionary that can contain your word, and open it in the middle of that section. Maybe you arrive at the letter `c`.
+
+
+
+You know the word `database` has to be **after** that and **not before** that point, so you can discard the left part of that block of pages.
+
+
+
+You repeat this process **a few more times**, and you finally arrive at the letter `d`, you continue with the same process in that section for the letter `d` and you finally **find the word** `database`. 🎉
+
+
+
+You had to open the dictionary a few times, maybe **5 or 10**. That's actually **very little work** compared to what it could have been.
+
+!!! note "Technical Details"
+ Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅
+
+ That algorithm I showed you above is called **Binary Search**.
+
+ It's called like that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want.
+
+### An Index and a Novel
+
+Let's now imagine you are reading a **novel book**. And someone told you that at some point, they mention a **database**, and you want to find that chapter.
+
+How do you find the word "*database*" there? You might have to read **the entire book** to find where the word "*database*" is located in the book. So, instead of opening the book 5 or 10 times, you would have to open each of the **500 pages** and read them one by one until you find the word. You might enjoy the book, though. 😅
+
+But if we are only interested in **quickly finding information** (as when working with SQL databases), then reading each of the 500 pages is **too inefficient** when there could be an option to open the book in 5 or 10 places and find what you're looking for.
+
+### A Technical Book with an Index
+
+Now let's imagine you are reading a technical book. For example, with several topics about programming. And there's a couple of sections where it talks about a **database**.
+
+This book might have a **book index**: a section in the book that has some **names of topics covered** and the **page numbers** in the book where you can read about them. And the topic names are **sorted** in alphabetic order, pretty much like a dictionary (a book with words, as in the previous example).
+
+In this case, you can open that book in the end (or in the beginning) to find the **book index** section, it would have only a few pages. And then, you can do the same process as with the **dictionary** example above.
+
+Open the index, and after **5 or 10 steps**, quickly find the topic "**database**" with the page numbers where that is covered, for example "page 253 in Chapter 5". Now you used the dictionary technique to find the **topic**, and that topic gave you a **page number**.
+
+Now you know that you need to find "**page 253**". But by looking at the closed book you still don't know where that page is, so you have to **find that page**. To find it, you can do the same process again, but this time, instead of searching for a **topic** in the **index**, you are searching for a **page number** in the **entire book**. And after **5 or 10 more steps**, you find the page 253 in Chapter 5.
+
+
+
+After this, even though this book is not a dictionary and has some particular content, you were able to **find the section** in the book that talks about a "**database**" in a **few steps** (say 10 or 20, instead of reading all the 500 pages).
+
+The main point is that the index is **sorted**, so we can use the same process we used for the **dictionary** to find the topic. And then that gives us a page number, and the **page numbers are also sorted**! 😅
+
+When we have a list of sorted things we can apply the same technique, and that's the whole trick here, we use the same technique first for the **topics** in the index and then for the **page numbers** to find the actual chapter.
+
+Such efficiency! 😎
+
+## What are Database Indexes
+
+**Database indexes** are very similar to **book indexes**.
+
+Database indexes store some info, some keys, in a way that makes it **easy and fast to find** (for example sorted), and then for each key they **point to some data somewhere else** in the database.
+
+Let's see a more clear example. Let's say you have this table in a database:
+
+
+
+
id
name
secret_name
age
+
+
+
1
Deadpond
Dive Wilson
null
+
+
+
2
Spider-Boy
Pedro Parqueador
null
+
+
+
3
Rusty-Man
Tommy Sharp
48
+
+
+
+And let's imagine you have **many more rows**, many more heroes. Probably **thousands**.
+
+If you tell the SQL database to get you a hero by a specific name, for example `Spider-Boy` (by using the `name` in the `WHERE` part of the SQL query), the database will have to **scan** all the heroes, checking **one by one** to find all the ones with a name of `Spider-Boy`.
+
+In this case, there's only one, but there's nothing limiting the database from having **more records with the same name**. And because of that, the database would **continue searching** and checking each one of the records, which would be very slow.
+
+But now let's say that the database has an index for the column `name`. The index could look something like this, we could imagine that the index is like an additional special table that the database manages automatically:
+
+
+
+
name
id
+
+
+
Deadpond
1
+
+
+
Rusty-Man
3
+
+
+
Spider-Boy
2
+
+
+
+It would have each `name` field from the `hero` table **in order**. It would not be sorted by `id`, but by `name` (in alphabetical order, as the `name` is a string). So, first it would have `Deadpond`, then `Rusty-Man`, and last `Spider-Boy`. It would also include the `id` of each hero. Remember that this could have **thousands** of heroes.
+
+Then the database would be able to use more or less the same ideas in the examples above with the **dictionary** and the **book index**.
+
+It could start somewhere (for example, in the middle of the index). It could arrive at some hero there in the middle, like `Rusty-Man`. And because the **index** has the `name` fields in order, the database would know that it can **discard all the previous index rows** and **only search** in the following index rows.
+
+
+
+
name
id
+
+
+
Deadpond
1
+
+
+
Rusty-Man
3
+
+
+
Spider-Boy
2
+
+
+
+And that way, as with the example with the dictionary above, **instead of reading thousands of heroes**, the database would be able to do a few steps, say **5 or 10 steps**, and arrive at the row of the index that has `Spider-Boy`, even if the table (and index) has thousands of rows:
+
+
+
+
name
id
+
+
+
Deadpond
1
+
+
+
Rusty-Man
3
+
+
+
✨ Spider-Boy ✨
2
+
+
+
+Then by looking at **this index row**, it would know that the `id` for `Spider-Boy` in the `hero` table is `2`.
+
+So then it could **search that `id`** in the `hero` table using more or less the **same technique**.
+
+That way, in the end, instead of reading thousands of records, the database only had to do **a few steps** to find the hero we wanted.
+
+## Updating the Index
+
+As you can imagine, for all this to work, the index would need to be **up to date** with the data in the database.
+
+If you had to update it **manually** in code, it would be very cumbersome and **error-prone**, as it would be easy to end up in a state where the index is not up to date and points to incorrect data. 😱
+
+Here's the good news: when you create an index in a **SQL Database**, the database takes care of **updating** it **automatically** whenever it's necessary. 😎🎉
+
+If you **add new records** to the `hero` table, the database will **automatically** update the index. It will do the **same process** of **finding** the right place to put the new index data (those **5 or 10 steps** described above), and then it will save the new index information there. The same would happen when you **update** or **delete** data.
+
+Defining and creating an index is very **easy** with SQL databases. And then **using it** is even easier... it's transparent. The database will figure out which index to use automatically, the SQL queries don't even change.
+
+So, in SQL databases **indexes are great**! And are super **easy to use**. Why not just have indexes for everything? .....Because indexes also have a "**cost**" in computation and storage (disk space).
+
+## Index Cost
+
+There's a **cost** associated with **indexes**. 💰
+
+When you don't have an index and add a **new row** to the table `hero`, the database has to perform **1 operation** to add the new hero row at the end of the table.
+
+But if you have an **index** for the **hero names**, now the database has to perform the same **1 operation** to add that row **plus** some extra **5 or 10 operations** in the index, to find the right spot for the name, to then add that **index record** there.
+
+And if you have an index for the `name`, one for the `age`, and one for the `secret_name`, now the database has to perform the same **1 operation** to add that row **plus** some extra **5 or 10 operations** in the index **times 3**, for each of the indexes. This means that now adding one row takes something like **31 operations**.
+
+This also means that you are **exchanging** the time it takes to **read** data for the time it takes to **write** data plus some extra **space** in the database.
+
+If you have queries that get data out of the database comparing each one of those fields (for example using `WHERE`), then it makes total sense to have indexes for each one of them. Because **31 operations** while creating or updating data (plus the space of the index) is much, much better than the possible **500 or 1000 operations** to read all the rows to be able to compare them using each field.
+
+But if you **never** have queries that find records by the `secret_name` (you never use `secret_name` in the `WHERE` part) it probably doesn't make sense to have an index for the `secret_name` field/column, as that will increase the computational and space **cost** of writing and updating the database.
+
+## Create an Index with SQL
+
+Phew, that was a lot of theory and explanations. 😅
+
+The most important thing about indexes is **understanding** them, how, and when to use them.
+
+Let's now see the **SQL** syntax to create an **index**. It is very simple:
+
+```SQL hl_lines="3"
+CREATE INDEX ix_hero_name
+ON hero (name)
+```
+
+This means, more or less:
+
+> Hey SQL database 👋, please `CREATE` an `INDEX` for me.
+>
+> I want the name of the index to be `ix_hero_name`.
+>
+> This index should be `ON` the table `hero`, it refers to that table.
+>
+> The column I want you to use for it is `name`.
+
+## Declare Indexes with SQLModel
+
+And now let's see how to define indexes in **SQLModel**.
+
+The change in code is underwhelming, it's very simple. 😆
+
+Here's the `Hero` model we had before:
+
+```Python hl_lines="8"
+{!./docs_src/tutorial/where/tutorial001.py[ln:1-10]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/where/tutorial001.py!}
+```
+
+
+
+Let's now update it to tell **SQLModel** to create an index for the `name` field when creating the table:
+
+```Python hl_lines="8"
+{!./docs_src/tutorial/indexes/tutorial001.py[ln:1-10]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/indexes/tutorial001.py!}
+```
+
+
+
+We use the same `Field()` again as we did before, and set `index=True`. That's it! 🚀
+
+Notice that we didn't set an argument of `default=None` or anything similar. This means that **SQLModel** (thanks to Pydantic) will keep it as a **required** field.
+
+!!! info
+ SQLModel (actually SQLAlchemy) will **automatically generate the index name** for you.
+
+ In this case the generated name would be `ix_hero_name`.
+
+## Query Data
+
+Now, to query the data using the field `name` and the new index we don't have to do anything special or different in the code, it's just **the same code**.
+
+The SQL database will figure it out **automatically**. ✨
+
+This is great because it means that indexes are very **simple to use**. But it might also feel counterintuitive at first, as you are **not doing anything** explicitly in the code to make it obvious that the index is useful, it all happens in the database behind the scenes.
+
+```Python hl_lines="5"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/indexes/tutorial001.py[ln:36-41]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/indexes/tutorial001.py!}
+```
+
+
+
+This is exactly the same code as we had before, but now the database will **use the index** underneath.
+
+## Run the Program
+
+If you run the program now, you will see an output like this:
+
+
+
+```console
+$ python app.py
+
+// Some boilerplate output omitted 😉
+
+// Create the table
+CREATE TABLE hero (
+ id INTEGER,
+ name VARCHAR NOT NULL,
+ secret_name VARCHAR NOT NULL,
+ age INTEGER,
+ PRIMARY KEY (id)
+)
+
+// Create the index 🤓🎉
+CREATE INDEX ix_hero_name ON hero (name)
+
+// The SELECT with WHERE looks the same
+INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
+FROM hero
+WHERE hero.name = ?
+INFO Engine [no key 0.00014s] ('Deadpond',)
+
+// The resulting hero
+secret_name='Dive Wilson' age=None id=1 name='Deadpond'
+```
+
+
+
+## More Indexes
+
+We are going to query the `hero` table doing comparisons on the `age` field too, so we should **define an index** for that one as well:
+
+```Python hl_lines="10"
+{!./docs_src/tutorial/indexes/tutorial002.py[ln:1-10]!}
+
+# Code below omitted 👇
+```
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/indexes/tutorial002.py!}
+```
+
+
+
+In this case, we want the default value of `age` to continue being `None`, so we set `default=None` when using `Field()`.
+
+Now when we use **SQLModel** to create the database and tables, it will also create the **indexes** for these two columns in the `hero` table.
+
+So, when we query the database for the `hero` table and use those **two columns** to define what data we get, the database will be able to **use those indexes** to improve the **reading performance**. 🚀
+
+## Primary Key and Indexes
+
+You probably noticed that we didn't set `index=True` for the `id` field.
+
+Because the `id` is already the **primary key**, the database will automatically create an internal **index** for it.
+
+The database always creates an internal index for **primary keys** automatically, as those are the primary way to organize, store, and retrieve data. 🤓
+
+But if you want to be **frequently querying** the SQL database for any **other field** (e.g. using any other field in the `WHERE` section), you will probably want to have at least an **index** for that.
+
+## Recap
+
+**Indexes** are very important to improve **reading performance** and speed when querying the database. 🏎
+
+Creating and using them is very **simple** and easy. The most important part is to understand **how** they work, **when** to create them, and for **which columns**.
diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md
index 0a964cc508..3fb001cf97 100644
--- a/docs/tutorial/limit-and-offset.md
+++ b/docs/tutorial/limit-and-offset.md
@@ -81,7 +81,7 @@ In this case, instead of getting all the 7 rows, we are limiting them to only ge
-## Run the Program on the Comamnd Line
+## Run the Program on the Command Line
If we run it on the command line, it will output:
@@ -153,7 +153,7 @@ Each of those methods applies the change in the internal special select statemen
**Offset** means "skip this many rows", and as we want to skip the ones we already saw, the first three, we use `.offset(3)`.
-## Run the Program with Offset on the Comamnd Line
+## Run the Program with Offset on the Command Line
Now we can run the program on the command line, and it will output:
@@ -207,9 +207,9 @@ The database right now has **only 7 rows**, so this query can only get 1 row.
But don't worry, the database won't throw an error trying to get 3 rows when there's only one (as would happen with a Python list).
-The database knows that we want to **limit** the number of results, but it doesn't necessarily has to find those many results.
+The database knows that we want to **limit** the number of results, but it doesn't necessarily have to find that many results.
-## Run the Program with the Last Batch on the Comamnd Line
+## Run the Program with the Last Batch on the Command Line
And if we run it in the command line, it will output:
@@ -271,7 +271,7 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o
-## Run the Program with Limit and Where on the Comamnd Line
+## Run the Program with Limit and Where on the Command Line
If we run it on the command line, it will find all the heroes in the database with an age above 32. That would normally be 4 heroes.
diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md
index 2b5fb8cf73..bc4481f73d 100644
--- a/docs/tutorial/many-to-many/create-models-with-link.md
+++ b/docs/tutorial/many-to-many/create-models-with-link.md
@@ -40,7 +40,7 @@ And **both fields are primary keys**. We hadn't used this before. 🤓
Let's see the `Team` model, it's almost identical as before, but with a little change:
```Python hl_lines="8"
-# Code above ommited 👆
+# Code above omitted 👆
{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:15-20]!}
@@ -56,7 +56,7 @@ Let's see the `Team` model, it's almost identical as before, but with a little c
-The **relationship attribute `heroes`** is still a list of heroes, annotatted as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that).
+The **relationship attribute `heroes`** is still a list of heroes, annotated as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that).
We use the same **`Relationship()`** function.
@@ -69,7 +69,7 @@ And here's the important part to allow the **many-to-many** relationship, we use
Let's see the other side, here's the `Hero` model:
```Python hl_lines="9"
-# Code above ommited 👆
+# Code above omitted 👆
{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:23-29]!}
@@ -102,7 +102,7 @@ And now we have a **`link_model=HeroTeamLink`**. ✨
The same as before, we will have the rest of the code to create the **engine**, and a function to create all the tables `create_db_and_tables()`.
```Python hl_lines="9"
-# Code above ommited 👆
+# Code above omitted 👆
{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:32-39]!}
@@ -122,7 +122,7 @@ The same as before, we will have the rest of the code to create the **engine**,
And as in previous examples, we will add that function to a function `main()`, and we will call that `main()` function in the main block:
```Python hl_lines="4"
-# Code above ommited 👆
+# Code above omitted 👆
{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-79]!}
# We will do more stuff here later 👈
@@ -149,7 +149,7 @@ If you run the code in the command line, it would output:
```console
$ python app.py
-// Boilerplate ommited 😉
+// Boilerplate omitted 😉
INFO Engine
CREATE TABLE team (
diff --git a/docs/tutorial/many-to-many/index.md b/docs/tutorial/many-to-many/index.md
index 24d7824fe0..e2e34777c0 100644
--- a/docs/tutorial/many-to-many/index.md
+++ b/docs/tutorial/many-to-many/index.md
@@ -60,7 +60,7 @@ Notice that each hero can only have **one** connection. But each team can receiv
## Introduce Many-to-Many
-But let's say that as **Deadpond** is a great chracter, they recruit him to the new **Preventers** team, but he's still part of the **Z-Force** team too.
+But let's say that as **Deadpond** is a great character, they recruit him to the new **Preventers** team, but he's still part of the **Z-Force** team too.
So, now, we need to be able to have a hero that is connected to **many** teams. And then, each team, should still be able to receive **many** heroes. So we need a **Many-to-Many** relationship.
diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md
index 901199bc42..3b60653ed9 100644
--- a/docs/tutorial/one.md
+++ b/docs/tutorial/one.md
@@ -18,7 +18,7 @@ We'll continue with the same examples we have been using in the previous chapter
👀 Full file preview
```Python
-{!./docs_src/tutorial/where/tutorial006.py!}
+{!./docs_src/tutorial/indexes/tutorial002.py!}
```
@@ -32,7 +32,7 @@ We have been iterating over the rows in a `result` object like:
```Python hl_lines="7-8"
# Code above omitted 👆
-{!./docs_src/tutorial/where/tutorial006.py[ln:44-49]!}
+{!./docs_src/tutorial/indexes/tutorial002.py[ln:44-49]!}
# Code below omitted 👇
```
@@ -41,7 +41,7 @@ We have been iterating over the rows in a `result` object like:
👀 Full file preview
```Python
-{!./docs_src/tutorial/where/tutorial006.py!}
+{!./docs_src/tutorial/indexes/tutorial002.py!}
```
diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md
index 09d7b2765b..0531ec53e5 100644
--- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md
+++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md
@@ -96,7 +96,7 @@ Next, use that `Relationship` to declare a new attribute in the model classes:
## What Are These Relationship Attributes
-This new attributes are not the same as fields, they **don't represent a column** directly in the database, and their value is not a singular value like an integer. Their value is the actual **entire object** that is related.
+These new attributes are not the same as fields, they **don't represent a column** directly in the database, and their value is not a singular value like an integer. Their value is the actual **entire object** that is related.
So, in the case of a `Hero` instance, if you call `hero.team`, you will get the entire `Team` instance object that this hero belongs to. ✨
diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md
index 5f917f69ad..fb638c1212 100644
--- a/docs/tutorial/select.md
+++ b/docs/tutorial/select.md
@@ -88,7 +88,7 @@ You can try that out in **DB Browser for SQLite**:
### A SQL Shortcut
-If we want to get all the columns like in this case above, in SQL there's a shortcut, instead of specifying each of the column names wew could write a `*`:
+If we want to get all the columns like in this case above, in SQL there's a shortcut, instead of specifying each of the column names we could write a `*`:
```SQL
SELECT *
@@ -97,9 +97,9 @@ FROM hero
That would end up in the same result. Although we won't use that for **SQLModel**.
-### `SELECT` Less Columns
+### `SELECT` Fewer Columns
-We can also `SELECT` less columns, for example:
+We can also `SELECT` fewer columns, for example:
```SQL
SELECT id, name
@@ -150,7 +150,7 @@ Another variation is that most of the SQL keywords like `SELECT` can also be wri
This is the interesting part. The tables returned by SQL databases **don't have to exist** in the database as independent tables. 🧙
-For example, in our database, we only have one table that has all the columns, `id`, `name`, `secret_name`, `age`. And here we are getting a result table with less columns.
+For example, in our database, we only have one table that has all the columns, `id`, `name`, `secret_name`, `age`. And here we are getting a result table with fewer columns.
One of the main points of SQL is to be able to keep the data structured in different tables, without repeating data, etc, and then query the database in many ways and get many different tables as a result.
diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md
index 3348615762..b3099f5a16 100644
--- a/docs/tutorial/update.md
+++ b/docs/tutorial/update.md
@@ -10,7 +10,7 @@ As before, we'll continue from where we left off with the previous code.
👀 Full file preview
```Python
-{!./docs_src/tutorial/where/tutorial006.py!}
+{!./docs_src/tutorial/indexes/tutorial002.py!}
```
@@ -39,7 +39,7 @@ In a similar way to `SELECT` statements, the first part defines the columns to w
And the second part, with the `WHERE`, defines to which rows it should apply that update.
-In this case, as we only have one hero with the name `"Spider-Boy"`, it will only apply the udpate in that row.
+In this case, as we only have one hero with the name `"Spider-Boy"`, it will only apply the update in that row.
!!! info
Notice that in the `UPDATE` the single equals sign (`=`) means **assignment**, setting a column to some value.
@@ -70,7 +70,7 @@ After that update, the data in the table will look like this, with the new age f
!!! tip
- It will probably be more common to find the row to update by Id, for example:
+ It will probably be more common to find the row to update by `id`, for example:
```SQL
UPDATE hero
@@ -340,7 +340,7 @@ Now let's review all that code:
The update process with **SQLModel** is more or less the same as with creating new objects, you add them to the session, and then commit them.
-This also means that you can update several fields (atributes, columns) at once, and you can also update several objects (heroes) at once:
+This also means that you can update several fields (attributes, columns) at once, and you can also update several objects (heroes) at once:
```{ .python .annotate hl_lines="15-17 19-21 23" }
# Code above omitted 👆
diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md
index e52173128b..ca85a4dd00 100644
--- a/docs/tutorial/where.md
+++ b/docs/tutorial/where.md
@@ -204,7 +204,7 @@ We care specially about the **select** statement:
-## Filter Rows Using `WHERE` wtih **SQLModel**
+## Filter Rows Using `WHERE` with **SQLModel**
Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statment to filter rows, which will filter the objects returned:
@@ -233,7 +233,7 @@ The object returned by `select(Hero)` is a special type of object with some meth
One of those methods is `.where()` used to (unsurprisingly) add a `WHERE` to the SQL statement in that **select** object.
-There are other methods that will will explore later. 💡
+There are other methods that we will explore later. 💡
Most of these methods return the same object again after modifying it.
@@ -271,7 +271,7 @@ In the example above we are using two equal signs (`==`). That's called the "**e
!!! tip
An **operator** is just a symbol that is put beside one value or in the middle of two values to do something with them.
- `==` is called **equality** operator because it checks if two things are **equal**.
+ `==` is called the **equality** operator because it checks if two things are **equal**.
When writing Python, if you write something using this equality operator (`==`) like:
@@ -311,7 +311,7 @@ Instead, it results in a special type of object. If you tried that in an interac
```
-So, that result value is an **expession** object. 💡
+So, that result value is an **expression** object. 💡
And `.where()` takes one (or more) of these **expression** objects to update the SQL statement.
@@ -421,7 +421,7 @@ Of course, the keyword arguments would have been a bit shorter.
But with the **expressions** your editor can help you a lot with autocompletion and inline error checks. ✨
-Let me give you an example. Let's imagine that keword arguments were supported in SQLModel and you wanted to filter using the secret identity of Spider-Boy.
+Let me give you an example. Let's imagine that keyword arguments were supported in SQLModel and you wanted to filter using the secret identity of Spider-Boy.
You could write:
@@ -436,7 +436,7 @@ Maybe your code could even run and seem like it's all fine, and then some months
And maybe finally you would realize that we wrote the code using `secret_identity` which is not a column in the table. We should have written `secret_name` instead.
-Now, with the the expressions, your editor would show you an error right away if you tried this:
+Now, with the expressions, your editor would show you an error right away if you tried this:
```Python
# Expression ✨
@@ -694,11 +694,11 @@ age=35 id=5 name='Black Lion' secret_name='Trevor Challa'
!!! tip
We get `Black Lion` here too because although the age is not *strictly* less than `35` it is *equal* to `35`.
-### Benefits of Expresions
+### Benefits of Expressions
Here's a good moment to see that being able to use these pure Python expressions instead of keyword arguments can help a lot. ✨
-We can use the same standard Python comparison operators like `.
+We can use the same standard Python comparison operators like `<`, `<=`, `>`, `>=`, `==`, etc.
## Multiple `.where()`
@@ -865,7 +865,7 @@ It would be an error telling you that
This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `Optional[int]`, which means `int` or `None`.
-By using this simple and standard Python type annotations We get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨
+By using this simple and standard Python type annotations we get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨
And when we use these special **class attributes** in a `.where()`, during execution of the program, the special class attribute will know that the comparison only applies for the values that are not `NULL` in the database, and it will work correctly.
@@ -933,3 +933,7 @@ And with that the editor knows this code is actually fine, because this is a spe
## Recap
You can use `.where()` with powerful expressions using **SQLModel** columns (the special class attributes) to filter the rows that you want. 🚀
+
+Up to now, the database would have been **looking through each one of the records** (rows) to find the ones that match what you want. If you have thousands or millions of records, this could be very **slow**. 😱
+
+In the next section I'll tell you how to add **indexes** to the database, this is what will make the queries **very efficient**. 😎
diff --git a/docs_src/advanced/__init__.py b/docs_src/advanced/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs_src/advanced/decimal/__init__.py b/docs_src/advanced/decimal/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001.py
new file mode 100644
index 0000000000..b803119d9e
--- /dev/null
+++ b/docs_src/advanced/decimal/tutorial001.py
@@ -0,0 +1,61 @@
+from typing import Optional
+
+from pydantic import condecimal
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str = Field(index=True)
+ secret_name: str
+ age: Optional[int] = Field(default=None, index=True)
+ money: condecimal(max_digits=5, decimal_places=3) = Field(default=0)
+
+
+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", secret_name="Dive Wilson", money=1.1)
+ hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
+ hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)
+
+ with Session(engine) as session:
+ session.add(hero_1)
+ session.add(hero_2)
+ session.add(hero_3)
+
+ session.commit()
+
+
+def select_heroes():
+ with Session(engine) as session:
+ statement = select(Hero).where(Hero.name == "Deadpond")
+ results = session.exec(statement)
+ hero_1 = results.one()
+ print("Hero 1:", hero_1)
+
+ statement = select(Hero).where(Hero.name == "Rusty-Man")
+ results = session.exec(statement)
+ hero_2 = results.one()
+ print("Hero 2:", hero_2)
+
+ total_money = hero_1.money + hero_2.money
+ print(f"Total money: {total_money}")
+
+
+def main():
+ create_db_and_tables()
+ create_heroes()
+ select_heroes()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs_src/tutorial/code_structure/tutorial001/models.py b/docs_src/tutorial/code_structure/tutorial001/models.py
index 9bd1fa93f2..8e2647b3c4 100644
--- a/docs_src/tutorial/code_structure/tutorial001/models.py
+++ b/docs_src/tutorial/code_structure/tutorial001/models.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
diff --git a/docs_src/tutorial/code_structure/tutorial002/hero_model.py b/docs_src/tutorial/code_structure/tutorial002/hero_model.py
index 84fc7f276b..06dd9c6dfd 100644
--- a/docs_src/tutorial/code_structure/tutorial002/hero_model.py
+++ b/docs_src/tutorial/code_structure/tutorial002/hero_model.py
@@ -8,9 +8,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional["Team"] = Relationship(back_populates="heroes")
diff --git a/docs_src/tutorial/code_structure/tutorial002/team_model.py b/docs_src/tutorial/code_structure/tutorial002/team_model.py
index 54974a01e5..c8a008bf4c 100644
--- a/docs_src/tutorial/code_structure/tutorial002/team_model.py
+++ b/docs_src/tutorial/code_structure/tutorial002/team_model.py
@@ -8,7 +8,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
diff --git a/docs_src/tutorial/connect/create_tables/tutorial001.py b/docs_src/tutorial/connect/create_tables/tutorial001.py
index 86dcc9adb8..ef24ec77d0 100644
--- a/docs_src/tutorial/connect/create_tables/tutorial001.py
+++ b/docs_src/tutorial/connect/create_tables/tutorial001.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/delete/tutorial001.py b/docs_src/tutorial/connect/delete/tutorial001.py
index 57bbd0ee91..eeb376a0cc 100644
--- a/docs_src/tutorial/connect/delete/tutorial001.py
+++ b/docs_src/tutorial/connect/delete/tutorial001.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/insert/tutorial001.py b/docs_src/tutorial/connect/insert/tutorial001.py
index d64d37f6a7..dc3661d7c7 100644
--- a/docs_src/tutorial/connect/insert/tutorial001.py
+++ b/docs_src/tutorial/connect/insert/tutorial001.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/select/tutorial001.py b/docs_src/tutorial/connect/select/tutorial001.py
index 18c4f402d4..d4cdf413f1 100644
--- a/docs_src/tutorial/connect/select/tutorial001.py
+++ b/docs_src/tutorial/connect/select/tutorial001.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/select/tutorial002.py b/docs_src/tutorial/connect/select/tutorial002.py
index f7df277d65..59edbf7fd9 100644
--- a/docs_src/tutorial/connect/select/tutorial002.py
+++ b/docs_src/tutorial/connect/select/tutorial002.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/select/tutorial003.py b/docs_src/tutorial/connect/select/tutorial003.py
index 110cace997..fb5b8aa0c9 100644
--- a/docs_src/tutorial/connect/select/tutorial003.py
+++ b/docs_src/tutorial/connect/select/tutorial003.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/select/tutorial004.py b/docs_src/tutorial/connect/select/tutorial004.py
index 87e739a91e..d1d260b3f4 100644
--- a/docs_src/tutorial/connect/select/tutorial004.py
+++ b/docs_src/tutorial/connect/select/tutorial004.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/select/tutorial005.py b/docs_src/tutorial/connect/select/tutorial005.py
index 0e696d0382..a61ef8a015 100644
--- a/docs_src/tutorial/connect/select/tutorial005.py
+++ b/docs_src/tutorial/connect/select/tutorial005.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/connect/update/tutorial001.py b/docs_src/tutorial/connect/update/tutorial001.py
index 3c9726f9ef..0080340532 100644
--- a/docs_src/tutorial/connect/update/tutorial001.py
+++ b/docs_src/tutorial/connect/update/tutorial001.py
@@ -5,15 +5,15 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/delete/tutorial001.py b/docs_src/tutorial/delete/tutorial001.py
index 0f7c056174..7c911df50e 100644
--- a/docs_src/tutorial/delete/tutorial001.py
+++ b/docs_src/tutorial/delete/tutorial001.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/delete/tutorial002.py b/docs_src/tutorial/delete/tutorial002.py
index 1f2671162e..202d63b6d3 100644
--- a/docs_src/tutorial/delete/tutorial002.py
+++ b/docs_src/tutorial/delete/tutorial002.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py
index 02f5ab8497..88b8fbbcea 100644
--- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py
+++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py
index 19ab2bb3ca..3c15efbb2d 100644
--- a/docs_src/tutorial/fastapi/delete/tutorial001.py
+++ b/docs_src/tutorial/fastapi/delete/tutorial001.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py
index 9bdf60446a..aef21332a7 100644
--- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py
+++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py
index c46448be80..df20123333 100644
--- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py
+++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py
@@ -6,9 +6,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class HeroCreate(SQLModel):
diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py
index 537e89637a..392c2c5829 100644
--- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py
+++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001.py
index bc83db5e04..4d66e471a5 100644
--- a/docs_src/tutorial/fastapi/read_one/tutorial001.py
+++ b/docs_src/tutorial/fastapi/read_one/tutorial001.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py
index da21e4686f..97220b95e5 100644
--- a/docs_src/tutorial/fastapi/relationships/tutorial001.py
+++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py
@@ -5,7 +5,7 @@
class TeamBase(SQLModel):
- name: str
+ name: str = Field(index=True)
headquarters: str
@@ -30,9 +30,9 @@ class TeamUpdate(SQLModel):
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001.py b/docs_src/tutorial/fastapi/response_model/tutorial001.py
index 5f7a19064e..57d8738395 100644
--- a/docs_src/tutorial/fastapi/response_model/tutorial001.py
+++ b/docs_src/tutorial/fastapi/response_model/tutorial001.py
@@ -6,9 +6,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py
index 02f5ab8497..88b8fbbcea 100644
--- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py
+++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py
index 715836ce57..41eaa621d3 100644
--- a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py
+++ b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py
@@ -6,9 +6,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py
index dc3e0939e2..2a0bd600fc 100644
--- a/docs_src/tutorial/fastapi/teams/tutorial001.py
+++ b/docs_src/tutorial/fastapi/teams/tutorial001.py
@@ -5,7 +5,7 @@
class TeamBase(SQLModel):
- name: str
+ name: str = Field(index=True)
headquarters: str
@@ -24,15 +24,14 @@ class TeamRead(TeamBase):
class TeamUpdate(SQLModel):
- id: Optional[int] = None
name: Optional[str] = None
headquarters: Optional[str] = None
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py
index 9309d626b7..35554878db 100644
--- a/docs_src/tutorial/fastapi/update/tutorial001.py
+++ b/docs_src/tutorial/fastapi/update/tutorial001.py
@@ -5,9 +5,9 @@
class HeroBase(SQLModel):
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
diff --git a/docs_src/tutorial/indexes/__init__.py b/docs_src/tutorial/indexes/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs_src/tutorial/indexes/tutorial001.py b/docs_src/tutorial/indexes/tutorial001.py
new file mode 100644
index 0000000000..539220c9b7
--- /dev/null
+++ b/docs_src/tutorial/indexes/tutorial001.py
@@ -0,0 +1,51 @@
+from typing import Optional
+
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str = Field(index=True)
+ secret_name: str
+ age: Optional[int] = Field(default=None, index=True)
+
+
+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", secret_name="Dive Wilson")
+ hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
+ hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
+
+ with Session(engine) as session:
+ session.add(hero_1)
+ session.add(hero_2)
+ session.add(hero_3)
+
+ session.commit()
+
+
+def select_heroes():
+ with Session(engine) as session:
+ statement = select(Hero).where(Hero.name == "Deadpond")
+ results = session.exec(statement)
+ for hero in results:
+ print(hero)
+
+
+def main():
+ create_db_and_tables()
+ create_heroes()
+ select_heroes()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs_src/tutorial/indexes/tutorial002.py b/docs_src/tutorial/indexes/tutorial002.py
new file mode 100644
index 0000000000..ebc8d64f65
--- /dev/null
+++ b/docs_src/tutorial/indexes/tutorial002.py
@@ -0,0 +1,59 @@
+from typing import Optional
+
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str = Field(index=True)
+ secret_name: str
+ age: Optional[int] = Field(default=None, index=True)
+
+
+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", secret_name="Dive Wilson")
+ hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
+ hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
+ hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32)
+ hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35)
+ hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36)
+ hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93)
+
+ with Session(engine) as session:
+ session.add(hero_1)
+ session.add(hero_2)
+ session.add(hero_3)
+ session.add(hero_4)
+ session.add(hero_5)
+ session.add(hero_6)
+ session.add(hero_7)
+
+ session.commit()
+
+
+def select_heroes():
+ with Session(engine) as session:
+ statement = select(Hero).where(Hero.age <= 35)
+ results = session.exec(statement)
+ for hero in results:
+ print(hero)
+
+
+def main():
+ create_db_and_tables()
+ create_heroes()
+ select_heroes()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs_src/tutorial/many_to_many/tutorial001.py b/docs_src/tutorial/many_to_many/tutorial001.py
index aee77af3b1..bb4e9d0896 100644
--- a/docs_src/tutorial/many_to_many/tutorial001.py
+++ b/docs_src/tutorial/many_to_many/tutorial001.py
@@ -14,7 +14,7 @@ class HeroTeamLink(SQLModel, table=True):
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink)
@@ -22,9 +22,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
diff --git a/docs_src/tutorial/many_to_many/tutorial002.py b/docs_src/tutorial/many_to_many/tutorial002.py
index 123fa5a523..dc4aa0b770 100644
--- a/docs_src/tutorial/many_to_many/tutorial002.py
+++ b/docs_src/tutorial/many_to_many/tutorial002.py
@@ -14,7 +14,7 @@ class HeroTeamLink(SQLModel, table=True):
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink)
@@ -22,9 +22,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003.py
index c2f3d56d33..1e03c4af89 100644
--- a/docs_src/tutorial/many_to_many/tutorial003.py
+++ b/docs_src/tutorial/many_to_many/tutorial003.py
@@ -18,7 +18,7 @@ class HeroTeamLink(SQLModel, table=True):
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
hero_links: List[HeroTeamLink] = Relationship(back_populates="team")
@@ -26,9 +26,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_links: List[HeroTeamLink] = Relationship(back_populates="hero")
diff --git a/docs_src/tutorial/offset_and_limit/tutorial001.py b/docs_src/tutorial/offset_and_limit/tutorial001.py
index 5413c17ea3..1b033accb9 100644
--- a/docs_src/tutorial/offset_and_limit/tutorial001.py
+++ b/docs_src/tutorial/offset_and_limit/tutorial001.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/offset_and_limit/tutorial002.py b/docs_src/tutorial/offset_and_limit/tutorial002.py
index 9d85a1342b..65a62369f4 100644
--- a/docs_src/tutorial/offset_and_limit/tutorial002.py
+++ b/docs_src/tutorial/offset_and_limit/tutorial002.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/offset_and_limit/tutorial003.py b/docs_src/tutorial/offset_and_limit/tutorial003.py
index 5d2c3bffca..36cae9c42c 100644
--- a/docs_src/tutorial/offset_and_limit/tutorial003.py
+++ b/docs_src/tutorial/offset_and_limit/tutorial003.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/offset_and_limit/tutorial004.py b/docs_src/tutorial/offset_and_limit/tutorial004.py
index bfa882d352..a95715cd98 100644
--- a/docs_src/tutorial/offset_and_limit/tutorial004.py
+++ b/docs_src/tutorial/offset_and_limit/tutorial004.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial001.py b/docs_src/tutorial/one/tutorial001.py
index 9fa5f0b503..072f4a3bf5 100644
--- a/docs_src/tutorial/one/tutorial001.py
+++ b/docs_src/tutorial/one/tutorial001.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial002.py b/docs_src/tutorial/one/tutorial002.py
index a1d86e0bbf..c24490659f 100644
--- a/docs_src/tutorial/one/tutorial002.py
+++ b/docs_src/tutorial/one/tutorial002.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial003.py b/docs_src/tutorial/one/tutorial003.py
index fe8c05cf50..f8fb0bcd7d 100644
--- a/docs_src/tutorial/one/tutorial003.py
+++ b/docs_src/tutorial/one/tutorial003.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial004.py b/docs_src/tutorial/one/tutorial004.py
index 32bc9b9efc..8688fc4799 100644
--- a/docs_src/tutorial/one/tutorial004.py
+++ b/docs_src/tutorial/one/tutorial004.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial005.py b/docs_src/tutorial/one/tutorial005.py
index 238213670e..f624d4cb68 100644
--- a/docs_src/tutorial/one/tutorial005.py
+++ b/docs_src/tutorial/one/tutorial005.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial006.py b/docs_src/tutorial/one/tutorial006.py
index cf408c494a..afc48547d9 100644
--- a/docs_src/tutorial/one/tutorial006.py
+++ b/docs_src/tutorial/one/tutorial006.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial007.py b/docs_src/tutorial/one/tutorial007.py
index 8a36d978a4..15df5baa94 100644
--- a/docs_src/tutorial/one/tutorial007.py
+++ b/docs_src/tutorial/one/tutorial007.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial008.py b/docs_src/tutorial/one/tutorial008.py
index 1b0d6ef501..9aa077ca38 100644
--- a/docs_src/tutorial/one/tutorial008.py
+++ b/docs_src/tutorial/one/tutorial008.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/one/tutorial009.py b/docs_src/tutorial/one/tutorial009.py
index 70deed28a8..f4ee23b355 100644
--- a/docs_src/tutorial/one/tutorial009.py
+++ b/docs_src/tutorial/one/tutorial009.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py
index d9851b4e43..fc4eb97934 100644
--- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py
+++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship()
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship()
diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py
index b33fabe716..a25df4e75d 100644
--- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py
+++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py
index cbd1581b10..98e197002e 100644
--- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py
+++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py
@@ -5,14 +5,14 @@
class Weapon(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
hero: "Hero" = Relationship(back_populates="weapon")
class Power(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
hero_id: int = Field(foreign_key="hero.id")
hero: "Hero" = Relationship(back_populates="powers")
@@ -20,7 +20,7 @@ class Power(SQLModel, table=True):
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -28,15 +28,15 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id")
- weapon: Optional[Weapon] = Relationship(back_populates="owner")
+ weapon: Optional[Weapon] = Relationship(back_populates="hero")
powers: List[Power] = Relationship(back_populates="hero")
diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py
index 2bf2041ff9..ec9c909d73 100644
--- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py
+++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py
index 98c1919209..71cb3f6136 100644
--- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py
+++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py
index e5c23a7264..3b130072b7 100644
--- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py
+++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
@@ -99,7 +99,7 @@ def select_heroes():
result = session.exec(statement)
hero_spider_boy = result.one()
- statement = select(Team).where(Team.id == hero_spider_boy.id)
+ statement = select(Team).where(Team.id == hero_spider_boy.team_id)
result = session.exec(statement)
team = result.first()
print("Spider-Boy's team:", team)
diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py
index efae8e556c..fdb436eb5f 100644
--- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py
+++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py
@@ -5,7 +5,7 @@
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
@@ -13,9 +13,9 @@ class Team(SQLModel, table=True):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
diff --git a/docs_src/tutorial/update/tutorial001.py b/docs_src/tutorial/update/tutorial001.py
index 96c72088fa..37868acc96 100644
--- a/docs_src/tutorial/update/tutorial001.py
+++ b/docs_src/tutorial/update/tutorial001.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/update/tutorial002.py b/docs_src/tutorial/update/tutorial002.py
index 04185f8e76..4880f73f90 100644
--- a/docs_src/tutorial/update/tutorial002.py
+++ b/docs_src/tutorial/update/tutorial002.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/update/tutorial003.py b/docs_src/tutorial/update/tutorial003.py
index c3199152bd..fd2ed75f0b 100644
--- a/docs_src/tutorial/update/tutorial003.py
+++ b/docs_src/tutorial/update/tutorial003.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/docs_src/tutorial/update/tutorial004.py b/docs_src/tutorial/update/tutorial004.py
index e61a04fbec..868c66c7d4 100644
--- a/docs_src/tutorial/update/tutorial004.py
+++ b/docs_src/tutorial/update/tutorial004.py
@@ -5,9 +5,9 @@
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- name: str
+ name: str = Field(index=True)
secret_name: str
- age: Optional[int] = None
+ age: Optional[int] = Field(default=None, index=True)
sqlite_file_name = "database.db"
diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml
new file mode 100644
index 0000000000..9f2775ff97
--- /dev/null
+++ b/mkdocs.insiders.yml
@@ -0,0 +1,4 @@
+INHERIT: mkdocs.yml
+plugins:
+ - search
+ - social
diff --git a/mkdocs.yml b/mkdocs.yml
index 5ebc361083..a27bbde8a1 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -3,22 +3,24 @@ site_description: SQLModel, SQL databases in Python, designed for simplicity, co
site_url: https://sqlmodel.tiangolo.com/
theme:
name: material
+ custom_dir: docs/overrides
palette:
- scheme: default
primary: deep purple
accent: amber
toggle:
- icon: material/lightbulb-outline
- name: Switch to light mode
+ icon: material/lightbulb
+ name: Switch to dark mode
- scheme: slate
primary: deep purple
accent: amber
toggle:
- icon: material/lightbulb
- name: Switch to dark mode
+ icon: material/lightbulb-outline
+ name: Switch to light mode
features:
- search.suggest
- search.highlight
+ - content.tabs.link
icon:
repo: fontawesome/brands/github-alt
logo: img/icon-white.svg
@@ -27,11 +29,6 @@ theme:
repo_name: tiangolo/sqlmodel
repo_url: https://github.com/tiangolo/sqlmodel
edit_uri: ''
-google_analytics:
- - UA-205713594-2
- - auto
-plugins:
-- search
nav:
- SQLModel: index.md
- features.md
@@ -45,6 +42,7 @@ nav:
- tutorial/automatic-id-none-refresh.md
- tutorial/select.md
- tutorial/where.md
+ - tutorial/indexes.md
- tutorial/one.md
- tutorial/limit-and-offset.md
- tutorial/update.md
@@ -86,6 +84,7 @@ nav:
- tutorial/fastapi/tests.md
- Advanced User Guide:
- advanced/index.md
+ - advanced/decimal.md
- alternatives.md
- help.md
- contributing.md
@@ -103,11 +102,15 @@ markdown_extensions:
custom_fences:
- name: mermaid
class: mermaid
- format: !!python/name:pymdownx.superfences.fence_div_format ''
-- pymdownx.tabbed
+ format: !!python/name:pymdownx.superfences.fence_code_format ''
+- pymdownx.tabbed:
+ alternate_style: true
- mdx_include
extra:
+ analytics:
+ provider: google
+ property: UA-205713594-2
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/tiangolo/sqlmodel
@@ -127,6 +130,5 @@ extra_css:
- css/custom.css
extra_javascript:
- - https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js
- js/termynal.js
- js/custom.js
diff --git a/pyproject.toml b/pyproject.toml
index fc567909a8..e3b1d3c279 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,20 +31,22 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.6.1"
-SQLAlchemy = ">=1.4.17,<1.5.0"
+SQLAlchemy = ">=1.4.17,<=1.4.41"
pydantic = "^1.8.2"
sqlalchemy2-stubs = {version = "*", allow-prereleases = true}
[tool.poetry.dev-dependencies]
-pytest = "^6.2.4"
-mypy = "^0.812"
-flake8 = "^3.9.2"
-black = {version = "^21.5-beta.1", python = "^3.7"}
+pytest = "^7.0.1"
+mypy = "0.971"
+flake8 = "^5.0.4"
+black = {version = "^22.10.0", python = "^3.7"}
mkdocs = "^1.2.1"
-mkdocs-material = "^7.1.9"
+mkdocs-material = "^8.1.4"
+pillow = {version = "^9.3.0", python = "^3.7"}
+cairosvg = {version = "^2.5.2", python = "^3.7"}
mdx-include = "^1.4.1"
-coverage = {extras = ["toml"], version = "^5.5"}
-fastapi = "^0.68.0"
+coverage = {extras = ["toml"], version = "^6.2"}
+fastapi = "^0.68.1"
requests = "^2.26.0"
autoflake = "^1.4"
isort = "^5.9.3"
@@ -65,6 +67,7 @@ source = [
"tests",
"sqlmodel"
]
+context = '${CONTEXT}'
[tool.coverage.report]
exclude_lines = [
@@ -98,3 +101,9 @@ warn_return_any = true
implicit_reexport = false
strict_equality = true
# --strict end
+
+[[tool.mypy.overrides]]
+module = "sqlmodel.sql.expression"
+warn_unused_ignores = false
+
+# invalidate CI cache: 1
diff --git a/scripts/generate_select.py b/scripts/generate_select.py
index b66a1673c4..f8aa30023f 100644
--- a/scripts/generate_select.py
+++ b/scripts/generate_select.py
@@ -1,3 +1,4 @@
+import os
from itertools import product
from pathlib import Path
from typing import List, Tuple
@@ -52,4 +53,11 @@ class Arg(BaseModel):
result = black.format_str(result, mode=black.Mode())
+current_content = destiny_path.read_text()
+
+if current_content != result and os.getenv("CHECK_JINJA"):
+ raise RuntimeError(
+ "sqlmodel/sql/expression.py content not update with Jinja2 template"
+ )
+
destiny_path.write_text(result)
diff --git a/scripts/lint.sh b/scripts/lint.sh
index 4191d90f1f..02568cda6b 100755
--- a/scripts/lint.sh
+++ b/scripts/lint.sh
@@ -7,3 +7,5 @@ mypy sqlmodel
flake8 sqlmodel tests docs_src
black sqlmodel tests docs_src --check
isort sqlmodel tests docs_src scripts --check-only
+# TODO: move this to test.sh after deprecating Python 3.6
+CHECK_JINJA=1 python scripts/generate_select.py
diff --git a/scripts/test-cov-html.sh b/scripts/test-cov-html.sh
deleted file mode 100755
index b15445faaa..0000000000
--- a/scripts/test-cov-html.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-set -x
-
-bash ./scripts/test.sh
-coverage html
diff --git a/scripts/test.sh b/scripts/test.sh
index 7fce865bd6..9b758bdbdf 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -6,4 +6,4 @@ set -x
coverage run -m pytest tests
coverage combine
coverage report --show-missing
-coverage xml
+coverage html
diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh
index f2b7ba3be3..69315f5ddd 100644
--- a/scripts/zip-docs.sh
+++ b/scripts/zip-docs.sh
@@ -3,7 +3,9 @@
set -x
set -e
+cd ./site
+
if [ -f docs.zip ]; then
rm -rf docs.zip
fi
-zip -r docs.zip ./site
+zip -r docs.zip ./
diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py
index bbe86ee2ae..720aa8c929 100644
--- a/sqlmodel/__init__.py
+++ b/sqlmodel/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.0.4"
+__version__ = "0.0.8"
# Re-export from SQLAlchemy
from sqlalchemy.engine import create_mock_engine as create_mock_engine
diff --git a/sqlmodel/engine/create.py b/sqlmodel/engine/create.py
index 97481259e2..b2d567b1b1 100644
--- a/sqlmodel/engine/create.py
+++ b/sqlmodel/engine/create.py
@@ -136,4 +136,4 @@ def create_engine(
if not isinstance(query_cache_size, _DefaultPlaceholder):
current_kwargs["query_cache_size"] = query_cache_size
current_kwargs.update(kwargs)
- return _create_engine(url, **current_kwargs)
+ return _create_engine(url, **current_kwargs) # type: ignore
diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py
index d521427581..7a25422227 100644
--- a/sqlmodel/engine/result.py
+++ b/sqlmodel/engine/result.py
@@ -23,7 +23,7 @@ def __iter__(self) -> Iterator[_T]:
return super().__iter__()
def __next__(self) -> _T:
- return super().__next__()
+ return super().__next__() # type: ignore
def first(self) -> Optional[_T]:
return super().first()
@@ -32,7 +32,7 @@ def one_or_none(self) -> Optional[_T]:
return super().one_or_none()
def one(self) -> _T:
- return super().one()
+ return super().one() # type: ignore
class Result(_Result, Generic[_T]):
@@ -70,10 +70,10 @@ def scalar_one(self) -> _T:
return super().scalar_one() # type: ignore
def scalar_one_or_none(self) -> Optional[_T]:
- return super().scalar_one_or_none() # type: ignore
+ return super().scalar_one_or_none()
def one(self) -> _T: # type: ignore
return super().one() # type: ignore
def scalar(self) -> Optional[_T]:
- return super().scalar() # type: ignore
+ return super().scalar()
diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py
index 40e5b766e9..80267b25e5 100644
--- a/sqlmodel/ext/asyncio/session.py
+++ b/sqlmodel/ext/asyncio/session.py
@@ -21,7 +21,7 @@ def __init__(
self,
bind: Optional[Union[AsyncConnection, AsyncEngine]] = None,
binds: Optional[Mapping[object, Union[AsyncConnection, AsyncEngine]]] = None,
- **kw,
+ **kw: Any,
):
# All the same code of the original AsyncSession
kw["future"] = True
@@ -52,7 +52,7 @@ async def exec(
# util.immutabledict has the union() method. Is this a bug in SQLAlchemy?
execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore
- return await greenlet_spawn( # type: ignore
+ return await greenlet_spawn(
self.sync_session.exec,
statement,
params=params,
diff --git a/sqlmodel/main.py b/sqlmodel/main.py
index 661276b31d..d95c498507 100644
--- a/sqlmodel/main.py
+++ b/sqlmodel/main.py
@@ -6,7 +6,6 @@
from enum import Enum
from pathlib import Path
from typing import (
- TYPE_CHECKING,
AbstractSet,
Any,
Callable,
@@ -24,25 +23,17 @@
cast,
)
-from pydantic import BaseModel
+from pydantic import BaseConfig, BaseModel
from pydantic.errors import ConfigError, DictError
+from pydantic.fields import SHAPE_SINGLETON
from pydantic.fields import FieldInfo as PydanticFieldInfo
from pydantic.fields import ModelField, Undefined, UndefinedType
-from pydantic.main import BaseConfig, ModelMetaclass, validate_model
+from pydantic.main import ModelMetaclass, validate_model
from pydantic.typing import ForwardRef, NoArgAnyCallable, resolve_annotations
from pydantic.utils import ROOT_KEY, Representation
-from sqlalchemy import (
- Boolean,
- Column,
- Date,
- DateTime,
- Float,
- ForeignKey,
- Integer,
- Interval,
- Numeric,
- inspect,
-)
+from sqlalchemy import Boolean, Column, Date, DateTime
+from sqlalchemy import Enum as sa_Enum
+from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect
from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship
from sqlalchemy.orm.attributes import set_attribute
from sqlalchemy.orm.decl_api import DeclarativeMeta
@@ -70,6 +61,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None:
primary_key = kwargs.pop("primary_key", False)
nullable = kwargs.pop("nullable", Undefined)
foreign_key = kwargs.pop("foreign_key", Undefined)
+ unique = kwargs.pop("unique", False)
index = kwargs.pop("index", Undefined)
sa_column = kwargs.pop("sa_column", Undefined)
sa_column_args = kwargs.pop("sa_column_args", Undefined)
@@ -89,6 +81,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None:
self.primary_key = primary_key
self.nullable = nullable
self.foreign_key = foreign_key
+ self.unique = unique
self.index = index
self.sa_column = sa_column
self.sa_column_args = sa_column_args
@@ -101,7 +94,7 @@ def __init__(
*,
back_populates: Optional[str] = None,
link_model: Optional[Any] = None,
- sa_relationship: Optional[RelationshipProperty] = None,
+ sa_relationship: Optional[RelationshipProperty] = None, # type: ignore
sa_relationship_args: Optional[Sequence[Any]] = None,
sa_relationship_kwargs: Optional[Mapping[str, Any]] = None,
) -> None:
@@ -127,32 +120,33 @@ def Field(
default: Any = Undefined,
*,
default_factory: Optional[NoArgAnyCallable] = None,
- alias: str = None,
- title: str = None,
- description: str = None,
+ alias: Optional[str] = None,
+ title: Optional[str] = None,
+ description: Optional[str] = None,
exclude: Union[
AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any
] = None,
include: Union[
AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any
] = None,
- const: bool = None,
- gt: float = None,
- ge: float = None,
- lt: float = None,
- le: float = None,
- multiple_of: float = None,
- min_items: int = None,
- max_items: int = None,
- min_length: int = None,
- max_length: int = None,
+ const: Optional[bool] = None,
+ gt: Optional[float] = None,
+ ge: Optional[float] = None,
+ lt: Optional[float] = None,
+ le: Optional[float] = None,
+ multiple_of: Optional[float] = None,
+ min_items: Optional[int] = None,
+ max_items: Optional[int] = None,
+ min_length: Optional[int] = None,
+ max_length: Optional[int] = None,
allow_mutation: bool = True,
- regex: str = None,
+ regex: Optional[str] = None,
primary_key: bool = False,
foreign_key: Optional[Any] = None,
+ unique: bool = False,
nullable: Union[bool, UndefinedType] = Undefined,
index: Union[bool, UndefinedType] = Undefined,
- sa_column: Union[Column, UndefinedType] = Undefined,
+ sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
schema_extra: Optional[Dict[str, Any]] = None,
@@ -180,6 +174,7 @@ def Field(
regex=regex,
primary_key=primary_key,
foreign_key=foreign_key,
+ unique=unique,
nullable=nullable,
index=index,
sa_column=sa_column,
@@ -195,7 +190,7 @@ def Relationship(
*,
back_populates: Optional[str] = None,
link_model: Optional[Any] = None,
- sa_relationship: Optional[RelationshipProperty] = None,
+ sa_relationship: Optional[RelationshipProperty] = None, # type: ignore
sa_relationship_args: Optional[Sequence[Any]] = None,
sa_relationship_kwargs: Optional[Mapping[str, Any]] = None,
) -> Any:
@@ -217,19 +212,25 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
# Replicate SQLAlchemy
def __setattr__(cls, name: str, value: Any) -> None:
- if getattr(cls.__config__, "table", False): # type: ignore
+ if getattr(cls.__config__, "table", False):
DeclarativeMeta.__setattr__(cls, name, value)
else:
super().__setattr__(name, value)
def __delattr__(cls, name: str) -> None:
- if getattr(cls.__config__, "table", False): # type: ignore
+ if getattr(cls.__config__, "table", False):
DeclarativeMeta.__delattr__(cls, name)
else:
super().__delattr__(name)
# From Pydantic
- def __new__(cls, name, bases, class_dict: dict, **kwargs) -> Any:
+ def __new__(
+ cls,
+ name: str,
+ bases: Tuple[Type[Any], ...],
+ class_dict: Dict[str, Any],
+ **kwargs: Any,
+ ) -> Any:
relationships: Dict[str, RelationshipInfo] = {}
dict_for_pydantic = {}
original_annotations = resolve_annotations(
@@ -342,7 +343,7 @@ def __init__(
)
relationship_to = temp_field.type_
if isinstance(temp_field.type_, ForwardRef):
- relationship_to = temp_field.type_.__forward_arg__ # type: ignore
+ relationship_to = temp_field.type_.__forward_arg__
rel_kwargs: Dict[str, Any] = {}
if rel_info.back_populates:
rel_kwargs["back_populates"] = rel_info.back_populates
@@ -360,16 +361,17 @@ def __init__(
rel_args.extend(rel_info.sa_relationship_args)
if rel_info.sa_relationship_kwargs:
rel_kwargs.update(rel_info.sa_relationship_kwargs)
- rel_value: RelationshipProperty = relationship(
+ rel_value: RelationshipProperty = relationship( # type: ignore
relationship_to, *rel_args, **rel_kwargs
)
dict_used[rel_name] = rel_value
+ setattr(cls, rel_name, rel_value) # Fix #315
DeclarativeMeta.__init__(cls, classname, bases, dict_used, **kw)
else:
ModelMetaclass.__init__(cls, classname, bases, dict_, **kw)
-def get_sqlachemy_type(field: ModelField) -> Any:
+def get_sqlalchemy_type(field: ModelField) -> Any:
if issubclass(field.type_, str):
if field.field_info.max_length:
return AutoString(length=field.field_info.max_length)
@@ -389,11 +391,14 @@ def get_sqlachemy_type(field: ModelField) -> Any:
if issubclass(field.type_, time):
return Time
if issubclass(field.type_, Enum):
- return Enum
+ return sa_Enum(field.type_)
if issubclass(field.type_, bytes):
return LargeBinary
if issubclass(field.type_, Decimal):
- return Numeric
+ return Numeric(
+ precision=getattr(field.type_, "max_digits", None),
+ scale=getattr(field.type_, "decimal_places", None),
+ )
if issubclass(field.type_, ipaddress.IPv4Address):
return AutoString
if issubclass(field.type_, ipaddress.IPv4Network):
@@ -406,30 +411,35 @@ def get_sqlachemy_type(field: ModelField) -> Any:
return AutoString
if issubclass(field.type_, uuid.UUID):
return GUID
+ raise ValueError(f"The field {field.name} has no matching SQLAlchemy type")
-def get_column_from_field(field: ModelField) -> Column:
+def get_column_from_field(field: ModelField) -> Column: # type: ignore
sa_column = getattr(field.field_info, "sa_column", Undefined)
if isinstance(sa_column, Column):
return sa_column
- sa_type = get_sqlachemy_type(field)
+ sa_type = get_sqlalchemy_type(field)
primary_key = getattr(field.field_info, "primary_key", False)
- nullable = not field.required
index = getattr(field.field_info, "index", Undefined)
if index is Undefined:
- index = True
+ index = False
+ nullable = not primary_key and _is_field_noneable(field)
+ # Override derived nullability if the nullable property is set explicitly
+ # on the field
if hasattr(field.field_info, "nullable"):
field_nullable = getattr(field.field_info, "nullable")
if field_nullable != Undefined:
nullable = field_nullable
args = []
foreign_key = getattr(field.field_info, "foreign_key", None)
+ unique = getattr(field.field_info, "unique", False)
if foreign_key:
args.append(ForeignKey(foreign_key))
kwargs = {
"primary_key": primary_key,
"nullable": nullable,
"index": index,
+ "unique": unique,
}
sa_default = Undefined
if field.field_info.default_factory:
@@ -440,11 +450,11 @@ def get_column_from_field(field: ModelField) -> Column:
kwargs["default"] = sa_default
sa_column_args = getattr(field.field_info, "sa_column_args", Undefined)
if sa_column_args is not Undefined:
- args.extend(list(cast(Sequence, sa_column_args)))
+ args.extend(list(cast(Sequence[Any], sa_column_args)))
sa_column_kwargs = getattr(field.field_info, "sa_column_kwargs", Undefined)
if sa_column_kwargs is not Undefined:
- kwargs.update(cast(dict, sa_column_kwargs))
- return Column(sa_type, *args, **kwargs)
+ kwargs.update(cast(Dict[Any, Any], sa_column_kwargs))
+ return Column(sa_type, *args, **kwargs) # type: ignore
class_registry = weakref.WeakValueDictionary() # type: ignore
@@ -452,24 +462,27 @@ def get_column_from_field(field: ModelField) -> Column:
default_registry = registry()
-def _value_items_is_true(v) -> bool:
+def _value_items_is_true(v: Any) -> bool:
# Re-implement Pydantic's ValueItems.is_true() as it hasn't been released as of
# the current latest, Pydantic 1.8.2
return v is True or v is ...
+_TSQLModel = TypeVar("_TSQLModel", bound="SQLModel")
+
+
class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry):
# SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values
__slots__ = ("__weakref__",)
__tablename__: ClassVar[Union[str, Callable[..., str]]]
- __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]]
+ __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] # type: ignore
__name__: ClassVar[str]
metadata: ClassVar[MetaData]
class Config:
orm_mode = True
- def __new__(cls, *args, **kwargs) -> Any:
+ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
new_object = super().__new__(cls)
# SQLAlchemy doesn't call __init__ on the base class
# Ref: https://docs.sqlalchemy.org/en/14/orm/constructors.html
@@ -482,9 +495,6 @@ def __new__(cls, *args, **kwargs) -> Any:
def __init__(__pydantic_self__, **data: Any) -> None:
# Uses something other than `self` the first arg to allow "self" as a
# settable attribute
- if TYPE_CHECKING:
- __pydantic_self__.__dict__: Dict[str, Any] = {}
- __pydantic_self__.__fields_set__: Set[str] = set()
values, fields_set, validation_error = validate_model(
__pydantic_self__.__class__, data
)
@@ -497,9 +507,9 @@ def __init__(__pydantic_self__, **data: Any) -> None:
# Do not set values as in Pydantic, pass them through setattr, so SQLAlchemy
# can handle them
# object.__setattr__(__pydantic_self__, '__dict__', values)
- object.__setattr__(__pydantic_self__, "__fields_set__", fields_set)
for key, value in values.items():
setattr(__pydantic_self__, key, value)
+ object.__setattr__(__pydantic_self__, "__fields_set__", fields_set)
non_pydantic_keys = data.keys() - values.keys()
for key in non_pydantic_keys:
if key in __pydantic_self__.__sqlmodel_relationships__:
@@ -511,16 +521,17 @@ def __setattr__(self, name: str, value: Any) -> None:
return
else:
# Set in SQLAlchemy, before Pydantic to trigger events and updates
- if getattr(self.__config__, "table", False):
- if is_instrumented(self, name):
- set_attribute(self, name, value)
+ if getattr(self.__config__, "table", False) and is_instrumented(self, name):
+ set_attribute(self, name, value)
# Set in Pydantic model to trigger possible validation changes, only for
# non relationship values
if name not in self.__sqlmodel_relationships__:
super().__setattr__(name, value)
@classmethod
- def from_orm(cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None):
+ def from_orm(
+ cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None
+ ) -> _TSQLModel:
# Duplicated from Pydantic
if not cls.__config__.orm_mode:
raise ConfigError(
@@ -533,7 +544,7 @@ def from_orm(cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None):
# End SQLModel support dict
if not getattr(cls.__config__, "table", False):
# If not table, normal Pydantic code
- m = cls.__new__(cls)
+ m: _TSQLModel = cls.__new__(cls)
else:
# If table, create the new instance normally to make SQLAlchemy create
# the _sa_instance_state attribute
@@ -554,8 +565,8 @@ def from_orm(cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None):
@classmethod
def parse_obj(
- cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None
- ) -> "SQLModel":
+ cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None
+ ) -> _TSQLModel:
obj = cls._enforce_dict_if_root(obj)
# SQLModel, support update dict
if update is not None:
@@ -569,7 +580,7 @@ def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]:
# From Pydantic, override to enforce validation with dict
@classmethod
- def validate(cls: Type["SQLModel"], value: Any) -> "SQLModel":
+ def validate(cls: Type[_TSQLModel], value: Any) -> _TSQLModel:
if isinstance(value, cls):
return value.copy() if cls.__config__.copy_on_model_validation else value
@@ -578,7 +589,7 @@ def validate(cls: Type["SQLModel"], value: Any) -> "SQLModel":
values, fields_set, validation_error = validate_model(cls, value)
if validation_error:
raise validation_error
- model = cls(**values)
+ model = cls(**value)
# Reset fields set, this would have been done in Pydantic in __init__
object.__setattr__(model, "__fields_set__", fields_set)
return model
@@ -594,14 +605,14 @@ def validate(cls: Type["SQLModel"], value: Any) -> "SQLModel":
return cls(**value_as_dict)
# From Pydantic, override to only show keys from fields, omit SQLAlchemy attributes
- def _calculate_keys( # type: ignore
+ def _calculate_keys(
self,
include: Optional[Mapping[Union[int, str], Any]],
exclude: Optional[Mapping[Union[int, str], Any]],
exclude_unset: bool,
update: Optional[Dict[str, Any]] = None,
) -> Optional[AbstractSet[str]]:
- if include is None and exclude is None and exclude_unset is False:
+ if include is None and exclude is None and not exclude_unset:
# Original in Pydantic:
# return None
# Updated to not return SQLAlchemy attributes
@@ -619,7 +630,6 @@ def _calculate_keys( # type: ignore
# Do not include relationships as that would easily lead to infinite
# recursion, or traversing the whole database
keys = self.__fields__.keys() # | self.__sqlmodel_relationships__.keys()
-
if include is not None:
keys &= include.keys()
@@ -634,3 +644,12 @@ def _calculate_keys( # type: ignore
@declared_attr # type: ignore
def __tablename__(cls) -> str:
return cls.__name__.lower()
+
+
+def _is_field_noneable(field: ModelField) -> bool:
+ if not field.required:
+ # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947)
+ return field.allow_none and (
+ field.shape != SHAPE_SINGLETON or not field.sub_fields
+ )
+ return False
diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py
index a5a63e2c69..1692fdcbcb 100644
--- a/sqlmodel/orm/session.py
+++ b/sqlmodel/orm/session.py
@@ -60,7 +60,7 @@ def exec(
results = super().execute(
statement,
params=params,
- execution_options=execution_options, # type: ignore
+ execution_options=execution_options,
bind_arguments=bind_arguments,
_parent_execute_state=_parent_execute_state,
_add_event=_add_event,
@@ -74,7 +74,7 @@ def execute(
self,
statement: _Executable,
params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None,
- execution_options: Mapping[str, Any] = util.EMPTY_DICT,
+ execution_options: Optional[Mapping[str, Any]] = util.EMPTY_DICT,
bind_arguments: Optional[Mapping[str, Any]] = None,
_parent_execute_state: Optional[Any] = None,
_add_event: Optional[Any] = None,
@@ -101,7 +101,7 @@ def execute(
return super().execute( # type: ignore
statement,
params=params,
- execution_options=execution_options, # type: ignore
+ execution_options=execution_options,
bind_arguments=bind_arguments,
_parent_execute_state=_parent_execute_state,
_add_event=_add_event,
@@ -128,6 +128,7 @@ def get(
populate_existing: bool = False,
with_for_update: Optional[Union[Literal[True], Mapping[str, Any]]] = None,
identity_token: Optional[Any] = None,
+ execution_options: Optional[Mapping[Any, Any]] = util.EMPTY_DICT,
) -> Optional[_TSelectParam]:
return super().get(
entity,
@@ -136,4 +137,5 @@ def get(
populate_existing=populate_existing,
with_for_update=with_for_update,
identity_token=identity_token,
+ execution_options=execution_options,
)
diff --git a/sqlmodel/sql/base.py b/sqlmodel/sql/base.py
index 129e4d43d7..3764a9721d 100644
--- a/sqlmodel/sql/base.py
+++ b/sqlmodel/sql/base.py
@@ -6,6 +6,4 @@
class Executable(_Executable, Generic[_T]):
- def __init__(self, *args, **kwargs):
- self.__dict__["_exec_options"] = kwargs.pop("_exec_options", None)
- super(_Executable, self).__init__(*args, **kwargs)
+ pass
diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py
index 66063bf236..31c0bc1a1e 100644
--- a/sqlmodel/sql/expression.py
+++ b/sqlmodel/sql/expression.py
@@ -29,15 +29,14 @@
if sys.version_info.minor >= 7:
class Select(_Select, Generic[_TSelect]):
- pass
+ inherit_cache = True
# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different
# purpose. This is the same as a normal SQLAlchemy Select class where there's only one
# entity, so the result will be converted to a scalar by default. This way writing
# for loops on the results will feel natural.
class SelectOfScalar(_Select, Generic[_TSelect]):
- pass
-
+ inherit_cache = True
else:
from typing import GenericMeta # type: ignore
@@ -45,11 +44,11 @@ class SelectOfScalar(_Select, Generic[_TSelect]):
class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore
pass
- class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): # type: ignore
- pass
+ class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
+ inherit_cache = True
- class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): # type: ignore
- pass
+ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
+ inherit_cache = True
# Cast them for editors to work correctly, from several tricks tried, this works
# for both VS Code and PyCharm
@@ -65,9 +64,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet
_TScalar_0 = TypeVar(
"_TScalar_0",
- Column,
- Sequence,
- Mapping,
+ Column, # type: ignore
+ Sequence, # type: ignore
+ Mapping, # type: ignore
UUID,
datetime,
float,
@@ -83,9 +82,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet
_TScalar_1 = TypeVar(
"_TScalar_1",
- Column,
- Sequence,
- Mapping,
+ Column, # type: ignore
+ Sequence, # type: ignore
+ Mapping, # type: ignore
UUID,
datetime,
float,
@@ -101,9 +100,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet
_TScalar_2 = TypeVar(
"_TScalar_2",
- Column,
- Sequence,
- Mapping,
+ Column, # type: ignore
+ Sequence, # type: ignore
+ Mapping, # type: ignore
UUID,
datetime,
float,
@@ -119,9 +118,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet
_TScalar_3 = TypeVar(
"_TScalar_3",
- Column,
- Sequence,
- Mapping,
+ Column, # type: ignore
+ Sequence, # type: ignore
+ Mapping, # type: ignore
UUID,
datetime,
float,
@@ -446,14 +445,14 @@ def select( # type: ignore
# Generated overloads end
-def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]:
+def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore
if len(entities) == 1:
return SelectOfScalar._create(*entities, **kw) # type: ignore
return Select._create(*entities, **kw) # type: ignore
# TODO: add several @overload from Python types to SQLAlchemy equivalents
-def col(column_expression: Any) -> ColumnClause:
+def col(column_expression: Any) -> ColumnClause: # type: ignore
if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)):
raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}")
return column_expression
diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2
index b39d636ea2..51f04a215d 100644
--- a/sqlmodel/sql/expression.py.jinja2
+++ b/sqlmodel/sql/expression.py.jinja2
@@ -27,15 +27,14 @@ _TSelect = TypeVar("_TSelect")
if sys.version_info.minor >= 7:
class Select(_Select, Generic[_TSelect]):
- pass
+ inherit_cache = True
# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different
# purpose. This is the same as a normal SQLAlchemy Select class where there's only one
# entity, so the result will be converted to a scalar by default. This way writing
# for loops on the results will feel natural.
class SelectOfScalar(_Select, Generic[_TSelect]):
- pass
-
+ inherit_cache = True
else:
from typing import GenericMeta # type: ignore
@@ -44,10 +43,10 @@ else:
pass
class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
- pass
+ inherit_cache = True
class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
- pass
+ inherit_cache = True
# Cast them for editors to work correctly, from several tricks tried, this works
# for both VS Code and PyCharm
@@ -63,9 +62,9 @@ if TYPE_CHECKING: # pragma: no cover
{% for i in range(number_of_types) %}
_TScalar_{{ i }} = TypeVar(
"_TScalar_{{ i }}",
- Column,
- Sequence,
- Mapping,
+ Column, # type: ignore
+ Sequence, # type: ignore
+ Mapping, # type: ignore
UUID,
datetime,
float,
@@ -106,14 +105,14 @@ def select( # type: ignore
# Generated overloads end
-def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]:
+def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore
if len(entities) == 1:
return SelectOfScalar._create(*entities, **kw) # type: ignore
- return Select._create(*entities, **kw)
+ return Select._create(*entities, **kw) # type: ignore
# TODO: add several @overload from Python types to SQLAlchemy equivalents
-def col(column_expression: Any) -> ColumnClause:
+def col(column_expression: Any) -> ColumnClause: # type: ignore
if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)):
raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}")
return column_expression
diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py
index e7b77b8c52..09b8239476 100644
--- a/sqlmodel/sql/sqltypes.py
+++ b/sqlmodel/sql/sqltypes.py
@@ -1,13 +1,13 @@
import uuid
-from typing import Any, cast
+from typing import Any, Optional, cast
-from sqlalchemy import types
+from sqlalchemy import CHAR, types
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.engine.interfaces import Dialect
-from sqlalchemy.types import CHAR, TypeDecorator
+from sqlalchemy.sql.type_api import TypeEngine
-class AutoString(types.TypeDecorator):
+class AutoString(types.TypeDecorator): # type: ignore
impl = types.String
cache_ok = True
@@ -22,7 +22,7 @@ def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]":
# Reference form SQLAlchemy docs: https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type
# with small modifications
-class GUID(TypeDecorator):
+class GUID(types.TypeDecorator): # type: ignore
"""Platform-independent GUID type.
Uses PostgreSQL's UUID type, otherwise uses
@@ -33,28 +33,28 @@ class GUID(TypeDecorator):
impl = CHAR
cache_ok = True
- def load_dialect_impl(self, dialect):
+ def load_dialect_impl(self, dialect: Dialect) -> TypeEngine: # type: ignore
if dialect.name == "postgresql":
- return dialect.type_descriptor(UUID())
+ return dialect.type_descriptor(UUID()) # type: ignore
else:
- return dialect.type_descriptor(CHAR(32))
+ return dialect.type_descriptor(CHAR(32)) # type: ignore
- def process_bind_param(self, value, dialect):
+ def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]:
if value is None:
return value
elif dialect.name == "postgresql":
return str(value)
else:
if not isinstance(value, uuid.UUID):
- return f"{uuid.UUID(value).int:x}"
+ return uuid.UUID(value).hex
else:
# hexstring
- return f"{value.int:x}"
+ return value.hex
- def process_result_value(self, value, dialect):
+ def process_result_value(self, value: Any, dialect: Dialect) -> Optional[uuid.UUID]:
if value is None:
return value
else:
if not isinstance(value, uuid.UUID):
value = uuid.UUID(value)
- return value
+ return cast(uuid.UUID, value)
diff --git a/tests/test_advanced/__init__.py b/tests/test_advanced/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_advanced/test_decimal/__init__.py b/tests/test_advanced/test_decimal/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py
new file mode 100644
index 0000000000..1dafdfb322
--- /dev/null
+++ b/tests/test_advanced/test_decimal/test_tutorial001.py
@@ -0,0 +1,44 @@
+from decimal import Decimal
+from unittest.mock import patch
+
+from sqlmodel import create_engine
+
+from ...conftest import get_testing_print_function
+
+expected_calls = [
+ [
+ "Hero 1:",
+ {
+ "name": "Deadpond",
+ "age": None,
+ "id": 1,
+ "secret_name": "Dive Wilson",
+ "money": Decimal("1.100"),
+ },
+ ],
+ [
+ "Hero 2:",
+ {
+ "name": "Rusty-Man",
+ "age": 48,
+ "id": 3,
+ "secret_name": "Tommy Sharp",
+ "money": Decimal("2.200"),
+ },
+ ],
+ ["Total money: 3.300"],
+]
+
+
+def test_tutorial(clear_sqlmodel):
+ from docs_src.advanced.decimal import tutorial001 as mod
+
+ mod.sqlite_url = "sqlite://"
+ mod.engine = create_engine(mod.sqlite_url)
+ calls = []
+
+ new_print = get_testing_print_function(calls)
+
+ with patch("builtins.print", new=new_print):
+ mod.main()
+ assert calls == expected_calls
diff --git a/tests/test_enums.py b/tests/test_enums.py
new file mode 100644
index 0000000000..aeec6456da
--- /dev/null
+++ b/tests/test_enums.py
@@ -0,0 +1,72 @@
+import enum
+import uuid
+
+from sqlalchemy import create_mock_engine
+from sqlalchemy.sql.type_api import TypeEngine
+from sqlmodel import Field, SQLModel
+
+"""
+Tests related to Enums
+
+Associated issues:
+* https://github.com/tiangolo/sqlmodel/issues/96
+* https://github.com/tiangolo/sqlmodel/issues/164
+"""
+
+
+class MyEnum1(enum.Enum):
+ A = "A"
+ B = "B"
+
+
+class MyEnum2(enum.Enum):
+ C = "C"
+ D = "D"
+
+
+class BaseModel(SQLModel):
+ id: uuid.UUID = Field(primary_key=True)
+ enum_field: MyEnum2
+
+
+class FlatModel(SQLModel, table=True):
+ id: uuid.UUID = Field(primary_key=True)
+ enum_field: MyEnum1
+
+
+class InheritModel(BaseModel, table=True):
+ pass
+
+
+def pg_dump(sql: TypeEngine, *args, **kwargs):
+ dialect = sql.compile(dialect=postgres_engine.dialect)
+ sql_str = str(dialect).rstrip()
+ if sql_str:
+ print(sql_str + ";")
+
+
+def sqlite_dump(sql: TypeEngine, *args, **kwargs):
+ dialect = sql.compile(dialect=sqlite_engine.dialect)
+ sql_str = str(dialect).rstrip()
+ if sql_str:
+ print(sql_str + ";")
+
+
+postgres_engine = create_mock_engine("postgresql://", pg_dump)
+sqlite_engine = create_mock_engine("sqlite://", sqlite_dump)
+
+
+def test_postgres_ddl_sql(capsys):
+ SQLModel.metadata.create_all(bind=postgres_engine, checkfirst=False)
+
+ captured = capsys.readouterr()
+ assert "CREATE TYPE myenum1 AS ENUM ('A', 'B');" in captured.out
+ assert "CREATE TYPE myenum2 AS ENUM ('C', 'D');" in captured.out
+
+
+def test_sqlite_ddl_sql(capsys):
+ SQLModel.metadata.create_all(bind=sqlite_engine, checkfirst=False)
+
+ captured = capsys.readouterr()
+ assert "enum_field VARCHAR(1) NOT NULL" in captured.out
+ assert "CREATE TYPE" not in captured.out
diff --git a/tests/test_fields_set.py b/tests/test_fields_set.py
new file mode 100644
index 0000000000..56f4ad0144
--- /dev/null
+++ b/tests/test_fields_set.py
@@ -0,0 +1,21 @@
+from datetime import datetime, timedelta
+
+from sqlmodel import Field, SQLModel
+
+
+def test_fields_set():
+ class User(SQLModel):
+ username: str
+ email: str = "test@test.com"
+ last_updated: datetime = Field(default_factory=datetime.now)
+
+ user = User(username="bob")
+ assert user.__fields_set__ == {"username"}
+ user = User(username="bob", email="bob@test.com")
+ assert user.__fields_set__ == {"username", "email"}
+ user = User(
+ username="bob",
+ email="bob@test.com",
+ last_updated=datetime.now() - timedelta(days=1),
+ )
+ assert user.__fields_set__ == {"username", "email", "last_updated"}
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000000..22c62327da
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,93 @@
+from typing import Optional
+
+import pytest
+from sqlalchemy.exc import IntegrityError
+from sqlmodel import Field, Session, SQLModel, create_engine
+
+
+def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel):
+ class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str
+ secret_name: str
+ age: Optional[int] = None
+
+ hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
+ hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson")
+
+ engine = create_engine("sqlite://")
+
+ SQLModel.metadata.create_all(engine)
+
+ with Session(engine) as session:
+ session.add(hero_1)
+ session.commit()
+ session.refresh(hero_1)
+
+ with Session(engine) as session:
+ session.add(hero_2)
+ session.commit()
+ session.refresh(hero_2)
+
+ with Session(engine) as session:
+ heroes = session.query(Hero).all()
+ assert len(heroes) == 2
+ assert heroes[0].name == heroes[1].name
+
+
+def test_should_allow_duplicate_row_if_unique_constraint_is_false(clear_sqlmodel):
+ class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str
+ secret_name: str = Field(unique=False)
+ age: Optional[int] = None
+
+ hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
+ hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson")
+
+ engine = create_engine("sqlite://")
+
+ SQLModel.metadata.create_all(engine)
+
+ with Session(engine) as session:
+ session.add(hero_1)
+ session.commit()
+ session.refresh(hero_1)
+
+ with Session(engine) as session:
+ session.add(hero_2)
+ session.commit()
+ session.refresh(hero_2)
+
+ with Session(engine) as session:
+ heroes = session.query(Hero).all()
+ assert len(heroes) == 2
+ assert heroes[0].name == heroes[1].name
+
+
+def test_should_raise_exception_when_try_to_duplicate_row_if_unique_constraint_is_true(
+ clear_sqlmodel,
+):
+ class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str
+ secret_name: str = Field(unique=True)
+ age: Optional[int] = None
+
+ hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
+ hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson")
+
+ engine = create_engine("sqlite://")
+
+ SQLModel.metadata.create_all(engine)
+
+ with Session(engine) as session:
+ session.add(hero_1)
+ session.commit()
+ session.refresh(hero_1)
+
+ with pytest.raises(IntegrityError):
+ with Session(engine) as session:
+ session.add(hero_2)
+ session.commit()
+ session.refresh(hero_2)
diff --git a/tests/test_missing_type.py b/tests/test_missing_type.py
new file mode 100644
index 0000000000..2185fa43e9
--- /dev/null
+++ b/tests/test_missing_type.py
@@ -0,0 +1,21 @@
+from typing import Optional
+
+import pytest
+from sqlmodel import Field, SQLModel
+
+
+def test_missing_sql_type():
+ class CustomType:
+ @classmethod
+ def __get_validators__(cls):
+ yield cls.validate
+
+ @classmethod
+ def validate(cls, v):
+ return v
+
+ with pytest.raises(ValueError):
+
+ class Item(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ item: CustomType
diff --git a/tests/test_nullable.py b/tests/test_nullable.py
new file mode 100644
index 0000000000..1c8b37b218
--- /dev/null
+++ b/tests/test_nullable.py
@@ -0,0 +1,125 @@
+from typing import Optional
+
+import pytest
+from sqlalchemy.exc import IntegrityError
+from sqlmodel import Field, Session, SQLModel, create_engine
+
+
+def test_nullable_fields(clear_sqlmodel, caplog):
+ class Hero(SQLModel, table=True):
+ primary_key: Optional[int] = Field(
+ default=None,
+ primary_key=True,
+ )
+ required_value: str
+ optional_default_ellipsis: Optional[str] = Field(default=...)
+ optional_default_none: Optional[str] = Field(default=None)
+ optional_non_nullable: Optional[str] = Field(
+ nullable=False,
+ )
+ optional_nullable: Optional[str] = Field(
+ nullable=True,
+ )
+ optional_default_ellipses_non_nullable: Optional[str] = Field(
+ default=...,
+ nullable=False,
+ )
+ optional_default_ellipses_nullable: Optional[str] = Field(
+ default=...,
+ nullable=True,
+ )
+ optional_default_none_non_nullable: Optional[str] = Field(
+ default=None,
+ nullable=False,
+ )
+ optional_default_none_nullable: Optional[str] = Field(
+ default=None,
+ nullable=True,
+ )
+ default_ellipses_non_nullable: str = Field(default=..., nullable=False)
+ optional_default_str: Optional[str] = "default"
+ optional_default_str_non_nullable: Optional[str] = Field(
+ default="default", nullable=False
+ )
+ optional_default_str_nullable: Optional[str] = Field(
+ default="default", nullable=True
+ )
+ str_default_str: str = "default"
+ str_default_str_non_nullable: str = Field(default="default", nullable=False)
+ str_default_str_nullable: str = Field(default="default", nullable=True)
+ str_default_ellipsis_non_nullable: str = Field(default=..., nullable=False)
+ str_default_ellipsis_nullable: str = Field(default=..., nullable=True)
+
+ engine = create_engine("sqlite://", echo=True)
+ SQLModel.metadata.create_all(engine)
+
+ create_table_log = [
+ message for message in caplog.messages if "CREATE TABLE hero" in message
+ ][0]
+ assert "primary_key INTEGER NOT NULL," in create_table_log
+ assert "required_value VARCHAR NOT NULL," in create_table_log
+ assert "optional_default_ellipsis VARCHAR NOT NULL," in create_table_log
+ assert "optional_default_none VARCHAR," in create_table_log
+ assert "optional_non_nullable VARCHAR NOT NULL," in create_table_log
+ assert "optional_nullable VARCHAR," in create_table_log
+ assert (
+ "optional_default_ellipses_non_nullable VARCHAR NOT NULL," in create_table_log
+ )
+ assert "optional_default_ellipses_nullable VARCHAR," in create_table_log
+ assert "optional_default_none_non_nullable VARCHAR NOT NULL," in create_table_log
+ assert "optional_default_none_nullable VARCHAR," in create_table_log
+ assert "default_ellipses_non_nullable VARCHAR NOT NULL," in create_table_log
+ assert "optional_default_str VARCHAR," in create_table_log
+ assert "optional_default_str_non_nullable VARCHAR NOT NULL," in create_table_log
+ assert "optional_default_str_nullable VARCHAR," in create_table_log
+ assert "str_default_str VARCHAR NOT NULL," in create_table_log
+ assert "str_default_str_non_nullable VARCHAR NOT NULL," in create_table_log
+ assert "str_default_str_nullable VARCHAR," in create_table_log
+ assert "str_default_ellipsis_non_nullable VARCHAR NOT NULL," in create_table_log
+ assert "str_default_ellipsis_nullable VARCHAR," in create_table_log
+
+
+# Test for regression in https://github.com/tiangolo/sqlmodel/issues/420
+def test_non_nullable_optional_field_with_no_default_set(clear_sqlmodel, caplog):
+ class Hero(SQLModel, table=True):
+ primary_key: Optional[int] = Field(
+ default=None,
+ primary_key=True,
+ )
+
+ optional_non_nullable_no_default: Optional[str] = Field(nullable=False)
+
+ engine = create_engine("sqlite://", echo=True)
+ SQLModel.metadata.create_all(engine)
+
+ create_table_log = [
+ message for message in caplog.messages if "CREATE TABLE hero" in message
+ ][0]
+ assert "primary_key INTEGER NOT NULL," in create_table_log
+ assert "optional_non_nullable_no_default VARCHAR NOT NULL," in create_table_log
+
+ # We can create a hero with `None` set for the optional non-nullable field
+ hero = Hero(primary_key=123, optional_non_nullable_no_default=None)
+ # But we cannot commit it.
+ with Session(engine) as session:
+ session.add(hero)
+ with pytest.raises(IntegrityError):
+ session.commit()
+
+
+def test_nullable_primary_key(clear_sqlmodel, caplog):
+ # Probably the weirdest corner case, it shouldn't happen anywhere, but let's test it
+ class Hero(SQLModel, table=True):
+ nullable_integer_primary_key: Optional[int] = Field(
+ default=None,
+ primary_key=True,
+ nullable=True,
+ )
+
+ engine = create_engine("sqlite://", echo=True)
+ SQLModel.metadata.create_all(engine)
+
+ create_table_log = [
+ message for message in caplog.messages if "CREATE TABLE hero" in message
+ ][0]
+ assert "nullable_integer_primary_key INTEGER," in create_table_log
diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py
index 591a51cc22..b6a2e72628 100644
--- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py
+++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py
@@ -9,7 +9,7 @@ def test_create_db_and_table(cov_tmp_path: Path):
assert "BEGIN" in result.stdout
assert 'PRAGMA main.table_info("hero")' in result.stdout
assert "CREATE TABLE hero (" in result.stdout
- assert "id INTEGER," in result.stdout
+ assert "id INTEGER NOT NULL," in result.stdout
assert "name VARCHAR NOT NULL," in result.stdout
assert "secret_name VARCHAR NOT NULL," in result.stdout
assert "age INTEGER," in result.stdout
diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py
index ac85eca2d5..cf008563f4 100644
--- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py
+++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py
@@ -1,4 +1,6 @@
from fastapi.testclient import TestClient
+from sqlalchemy import inspect
+from sqlalchemy.engine.reflection import Inspector
from sqlmodel import create_engine
from sqlmodel.pool import StaticPool
@@ -166,3 +168,16 @@ def test_tutorial(clear_sqlmodel):
assert response.status_code == 200, response.text
assert data == openapi_schema
+
+ # Test inherited indexes
+ insp: Inspector = inspect(mod.engine)
+ indexes = insp.get_indexes(str(mod.Hero.__tablename__))
+ expected_indexes = [
+ {"name": "ix_hero_name", "column_names": ["name"], "unique": 0},
+ {"name": "ix_hero_age", "column_names": ["age"], "unique": 0},
+ ]
+ for index in expected_indexes:
+ assert index in indexes, "This expected index should be in the indexes in DB"
+ # Now that this index was checked, remove it from the list of indexes
+ indexes.pop(indexes.index(index))
+ assert len(indexes) == 0, "The database should only have the expected indexes"
diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py
index 421a1cad53..57393a7ddc 100644
--- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py
+++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py
@@ -1,4 +1,6 @@
from fastapi.testclient import TestClient
+from sqlalchemy import inspect
+from sqlalchemy.engine.reflection import Inspector
from sqlmodel import create_engine
from sqlmodel.pool import StaticPool
@@ -166,3 +168,16 @@ def test_tutorial(clear_sqlmodel):
assert response.status_code == 200, response.text
assert data == openapi_schema
+
+ # Test inherited indexes
+ insp: Inspector = inspect(mod.engine)
+ indexes = insp.get_indexes(str(mod.Hero.__tablename__))
+ expected_indexes = [
+ {"name": "ix_hero_age", "column_names": ["age"], "unique": 0},
+ {"name": "ix_hero_name", "column_names": ["name"], "unique": 0},
+ ]
+ for index in expected_indexes:
+ assert index in indexes, "This expected index should be in the indexes in DB"
+ # Now that this index was checked, remove it from the list of indexes
+ indexes.pop(indexes.index(index))
+ assert len(indexes) == 0, "The database should only have the expected indexes"
diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py
index 852839b2a1..6ac1cffc5e 100644
--- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py
+++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py
@@ -442,7 +442,6 @@
"title": "TeamUpdate",
"type": "object",
"properties": {
- "id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"headquarters": {"title": "Headquarters", "type": "string"},
},
diff --git a/tests/test_tutorial/test_indexes/__init__.py b/tests/test_tutorial/test_indexes/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py
new file mode 100644
index 0000000000..596207737d
--- /dev/null
+++ b/tests/test_tutorial/test_indexes/test_tutorial001.py
@@ -0,0 +1,35 @@
+from unittest.mock import patch
+
+from sqlalchemy import inspect
+from sqlalchemy.engine.reflection import Inspector
+from sqlmodel import create_engine
+
+from ...conftest import get_testing_print_function
+
+
+def test_tutorial(clear_sqlmodel):
+ from docs_src.tutorial.indexes import tutorial001 as mod
+
+ mod.sqlite_url = "sqlite://"
+ mod.engine = create_engine(mod.sqlite_url)
+ calls = []
+
+ new_print = get_testing_print_function(calls)
+
+ with patch("builtins.print", new=new_print):
+ mod.main()
+ assert calls == [
+ [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}]
+ ]
+
+ insp: Inspector = inspect(mod.engine)
+ indexes = insp.get_indexes(str(mod.Hero.__tablename__))
+ expected_indexes = [
+ {"name": "ix_hero_name", "column_names": ["name"], "unique": 0},
+ {"name": "ix_hero_age", "column_names": ["age"], "unique": 0},
+ ]
+ for index in expected_indexes:
+ assert index in indexes, "This expected index should be in the indexes in DB"
+ # Now that this index was checked, remove it from the list of indexes
+ indexes.pop(indexes.index(index))
+ assert len(indexes) == 0, "The database should only have the expected indexes"
diff --git a/tests/test_tutorial/test_indexes/test_tutorial006.py b/tests/test_tutorial/test_indexes/test_tutorial006.py
new file mode 100644
index 0000000000..e26f8b2ed8
--- /dev/null
+++ b/tests/test_tutorial/test_indexes/test_tutorial006.py
@@ -0,0 +1,36 @@
+from unittest.mock import patch
+
+from sqlalchemy import inspect
+from sqlalchemy.engine.reflection import Inspector
+from sqlmodel import create_engine
+
+from ...conftest import get_testing_print_function
+
+
+def test_tutorial(clear_sqlmodel):
+ from docs_src.tutorial.indexes import tutorial002 as mod
+
+ mod.sqlite_url = "sqlite://"
+ mod.engine = create_engine(mod.sqlite_url)
+ calls = []
+
+ new_print = get_testing_print_function(calls)
+
+ with patch("builtins.print", new=new_print):
+ mod.main()
+ assert calls == [
+ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}],
+ [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}],
+ ]
+
+ insp: Inspector = inspect(mod.engine)
+ indexes = insp.get_indexes(str(mod.Hero.__tablename__))
+ expected_indexes = [
+ {"name": "ix_hero_name", "column_names": ["name"], "unique": 0},
+ {"name": "ix_hero_age", "column_names": ["age"], "unique": 0},
+ ]
+ for index in expected_indexes:
+ assert index in indexes, "This expected index should be in the indexes in DB"
+ # Now that this index was checked, remove it from the list of indexes
+ indexes.pop(indexes.index(index))
+ assert len(indexes) == 0, "The database should only have the expected indexes"
diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py
index 887c98ee6f..9fc70012d8 100644
--- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py
+++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py
@@ -81,7 +81,7 @@
],
[
"Spider-Boy's team:",
- {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"},
+ {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"},
],
[
"Spider-Boy's team again:",
diff --git a/tests/test_tutorial/test_where/test_tutorial003.py b/tests/test_tutorial/test_where/test_tutorial003.py
index a01955e6b7..4794d846ff 100644
--- a/tests/test_tutorial/test_where/test_tutorial003.py
+++ b/tests/test_tutorial/test_where/test_tutorial003.py
@@ -17,7 +17,7 @@ def test_tutorial(clear_sqlmodel):
with patch("builtins.print", new=new_print):
mod.main()
- assert calls == [
+ expected_calls = [
[{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}],
[{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}],
[
@@ -29,3 +29,8 @@ def test_tutorial(clear_sqlmodel):
}
],
]
+ for call in expected_calls:
+ assert call in calls, "This expected item should be in the list"
+ # Now that this item was checked, remove it from the list
+ calls.pop(calls.index(call))
+ assert len(calls) == 0, "The list should only have the expected items"
diff --git a/tests/test_tutorial/test_where/test_tutorial004.py b/tests/test_tutorial/test_where/test_tutorial004.py
index 9f4f80c201..682babd43a 100644
--- a/tests/test_tutorial/test_where/test_tutorial004.py
+++ b/tests/test_tutorial/test_where/test_tutorial004.py
@@ -16,7 +16,7 @@ def test_tutorial(clear_sqlmodel):
with patch("builtins.print", new=new_print):
mod.main()
- assert calls == [
+ expected_calls = [
[{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}],
[{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}],
[{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}],
@@ -29,3 +29,8 @@ def test_tutorial(clear_sqlmodel):
}
],
]
+ for call in expected_calls:
+ assert call in calls, "This expected item should be in the list"
+ # Now that this item was checked, remove it from the list
+ calls.pop(calls.index(call))
+ assert len(calls) == 0, "The list should only have the expected items"
diff --git a/tests/test_tutorial/test_where/test_tutorial011.py b/tests/test_tutorial/test_where/test_tutorial011.py
index 743ecd5402..8006cd0708 100644
--- a/tests/test_tutorial/test_where/test_tutorial011.py
+++ b/tests/test_tutorial/test_where/test_tutorial011.py
@@ -16,7 +16,7 @@ def test_tutorial(clear_sqlmodel):
with patch("builtins.print", new=new_print):
mod.main()
- assert calls == [
+ expected_calls = [
[{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}],
[{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}],
[{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}],
@@ -29,3 +29,8 @@ def test_tutorial(clear_sqlmodel):
}
],
]
+ for call in expected_calls:
+ assert call in calls, "This expected item should be in the list"
+ # Now that this item was checked, remove it from the list
+ calls.pop(calls.index(call))
+ assert len(calls) == 0, "The list should only have the expected items"
diff --git a/tests/test_validation.py b/tests/test_validation.py
new file mode 100644
index 0000000000..a3ff6e39ba
--- /dev/null
+++ b/tests/test_validation.py
@@ -0,0 +1,33 @@
+from typing import Optional
+
+import pytest
+from pydantic import validator
+from pydantic.error_wrappers import ValidationError
+from sqlmodel import SQLModel
+
+
+def test_validation(clear_sqlmodel):
+ """Test validation of implicit and explict None values.
+
+ # For consistency with pydantic, validators are not to be called on
+ # arguments that are not explicitly provided.
+
+ https://github.com/tiangolo/sqlmodel/issues/230
+ https://github.com/samuelcolvin/pydantic/issues/1223
+
+ """
+
+ class Hero(SQLModel):
+ name: Optional[str] = None
+ secret_name: Optional[str] = None
+ age: Optional[int] = None
+
+ @validator("name", "secret_name", "age")
+ def reject_none(cls, v):
+ assert v is not None
+ return v
+
+ Hero.validate({"age": 25})
+
+ with pytest.raises(ValidationError):
+ Hero.validate({"name": None, "age": 25})