Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
1fe31a31ac
⬆ Bump httpx from 0.24.1 to 0.27.2
Bumps [httpx](https://github.com/encode/httpx) from 0.24.1 to 0.27.2.
- [Release notes](https://github.com/encode/httpx/releases)
- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpx/compare/0.24.1...0.27.2)

---
updated-dependencies:
- dependency-name: httpx
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 22:39:35 +00:00
26 changed files with 645 additions and 257 deletions

View File

@ -7,11 +7,6 @@ on:
types: types:
- opened - opened
- synchronize - synchronize
env:
UV_SYSTEM_PYTHON: 1
jobs: jobs:
changes: changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -57,19 +52,17 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
- name: Setup uv - uses: actions/cache@v4
uses: astral-sh/setup-uv@v3 id: cache
with: with:
version: "0.4.15" path: ${{ env.pythonLocation }}
enable-cache: true key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-insiders.txt', 'requirements-docs-tests.txt') }}-v02
cache-dependency-glob: |
requirements**.txt
pyproject.toml
- name: Install docs extras - name: Install docs extras
run: uv pip install -r requirements-docs.txt if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-docs.txt
- name: Install Material for MkDocs Insiders - name: Install Material for MkDocs Insiders
if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true'
run: uv pip install -r requirements-docs-insiders.txt run: pip install -r requirements-docs-insiders.txt
env: env:
TOKEN: ${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }} TOKEN: ${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}
- uses: actions/cache@v4 - uses: actions/cache@v4
@ -84,7 +77,6 @@ jobs:
with: with:
name: docs-site name: docs-site
path: ./site/** path: ./site/**
include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why # https://github.com/marketplace/actions/alls-green#why
docs-all-green: # This job does nothing and is only used for the branch protection docs-all-green: # This job does nothing and is only used for the branch protection

View File

@ -12,9 +12,6 @@ permissions:
pull-requests: write pull-requests: write
statuses: write statuses: write
env:
UV_SYSTEM_PYTHON: 1
jobs: jobs:
deploy-docs: deploy-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -28,16 +25,14 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
- name: Setup uv - uses: actions/cache@v4
uses: astral-sh/setup-uv@v3 id: cache
with: with:
version: "0.4.15" path: ${{ env.pythonLocation }}
enable-cache: true key: ${{ runner.os }}-python-github-actions-${{ env.pythonLocation }}-${{ hashFiles('requirements-github-actions.txt') }}-v01
cache-dependency-glob: |
requirements**.txt
pyproject.toml
- name: Install GitHub Actions dependencies - name: Install GitHub Actions dependencies
run: uv pip install -r requirements-github-actions.txt if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-github-actions.txt
- name: Deploy Docs Status Pending - name: Deploy Docs Status Pending
run: python ./scripts/deploy_docs_status.py run: python ./scripts/deploy_docs_status.py
env: env:
@ -60,19 +55,19 @@ jobs:
# hashFiles returns an empty string if there are no files # hashFiles returns an empty string if there are no files
if: hashFiles('./site/*') if: hashFiles('./site/*')
id: deploy id: deploy
env: uses: cloudflare/pages-action@v1
PROJECT_NAME: sqlmodel
BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }}
uses: cloudflare/wrangler-action@v3
with: with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} projectName: sqlmodel
directory: './site'
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }}
- name: Comment Deploy - name: Comment Deploy
run: python ./scripts/deploy_docs_status.py run: python ./scripts/deploy_docs_status.py
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} DEPLOY_URL: ${{ steps.deploy.outputs.url }}
COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_ID: ${{ github.run_id }} RUN_ID: ${{ github.run_id }}
IS_DONE: "true" IS_DONE: "true"

View File

@ -2,7 +2,7 @@ name: Issue Manager
on: on:
schedule: schedule:
- cron: "13 18 * * *" - cron: "11 4 * * *"
issue_comment: issue_comment:
types: types:
- created - created
@ -16,7 +16,6 @@ on:
permissions: permissions:
issues: write issues: write
pull-requests: write
jobs: jobs:
issue-manager: issue-manager:
@ -27,7 +26,7 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: tiangolo/issue-manager@0.5.1 - uses: tiangolo/issue-manager@0.5.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
config: > config: >
@ -36,12 +35,8 @@ jobs:
"delay": 864000, "delay": 864000,
"message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs."
}, },
"waiting": { "changes-requested": {
"delay": 2628000, "delay": 2628000,
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR."
},
"invalid": {
"delay": 0,
"message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details."
} }
} }

View File

@ -17,8 +17,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@v5 - uses: actions/labeler@v5
if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
- run: echo "Done adding labels"
# Run this after labeler applied labels # Run this after labeler applied labels
check-labels: check-labels:
needs: needs:

View File

@ -8,33 +8,25 @@ on:
permissions: permissions:
statuses: write statuses: write
env:
UV_SYSTEM_PYTHON: 1
jobs: jobs:
smokeshow: smokeshow:
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.9' python-version: '3.9'
- name: Setup uv
uses: astral-sh/setup-uv@v3 - run: pip install smokeshow
with:
version: "0.4.15"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
pyproject.toml
- run: uv pip install -r requirements-github-actions.txt
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: coverage-html name: coverage-html
path: htmlcov path: htmlcov
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
- run: smokeshow upload htmlcov - run: smokeshow upload htmlcov
env: env:
SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}

View File

@ -18,9 +18,6 @@ on:
# cron every week on monday # cron every week on monday
- cron: "0 0 * * 1" - cron: "0 0 * * 1"
env:
UV_SYSTEM_PYTHON: 1
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -37,34 +34,33 @@ jobs:
- pydantic-v1 - pydantic-v1
- pydantic-v2 - pydantic-v2
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Setup uv
uses: astral-sh/setup-uv@v3
with:
version: "0.4.15"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
pyproject.toml
# Allow debugging with tmate # Allow debugging with tmate
- name: Setup tmate session - name: Setup tmate session
uses: mxschmitt/action-tmate@v3 uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
with: with:
limit-access-to-actor: true limit-access-to-actor: true
- uses: actions/cache@v4
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-v01
- name: Install Dependencies - name: Install Dependencies
run: uv pip install -r requirements-tests.txt if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
- name: Install Pydantic v1 - name: Install Pydantic v1
if: matrix.pydantic-version == 'pydantic-v1' if: matrix.pydantic-version == 'pydantic-v1'
run: uv pip install --upgrade "pydantic>=1.10.0,<2.0.0" run: pip install --upgrade "pydantic>=1.10.0,<2.0.0"
- name: Install Pydantic v2 - name: Install Pydantic v2
if: matrix.pydantic-version == 'pydantic-v2' if: matrix.pydantic-version == 'pydantic-v2'
run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1" run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1"
- name: Lint - name: Lint
# Do not run on Python 3.7 as mypy behaves differently # Do not run on Python 3.7 as mypy behaves differently
if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2'
@ -73,50 +69,44 @@ jobs:
- name: Test - name: Test
run: bash scripts/test.sh run: bash scripts/test.sh
env: env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
- name: Store coverage files - name: Store coverage files
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
path: coverage path: coverage
include-hidden-files: true
coverage-combine: coverage-combine:
needs: needs:
- test - test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: '3.12'
- name: Setup uv
uses: astral-sh/setup-uv@v3
with:
version: "0.4.15"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
pyproject.toml
- name: Get coverage files - name: Get coverage files
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
pattern: coverage-* pattern: coverage-*
path: coverage path: coverage
merge-multiple: true merge-multiple: true
- name: Install Dependencies
run: uv pip install -r requirements-tests.txt - run: pip install coverage[toml]
- run: ls -la coverage - run: ls -la coverage
- run: coverage combine coverage - run: coverage combine coverage
- run: coverage report - run: coverage report
- run: coverage html --title "Coverage for ${{ github.sha }}" - run: coverage html --title "Coverage for ${{ github.sha }}"
- name: Store coverage HTML - name: Store coverage HTML
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: coverage-html name: coverage-html
path: htmlcov path: htmlcov
include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why # https://github.com/marketplace/actions/alls-green#why
alls-green: # This job does nothing and is only used for the branch protection alls-green: # This job does nothing and is only used for the branch protection

View File

@ -14,7 +14,7 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.5 rev: v0.6.1
hooks: hooks:
- id: ruff - id: ruff
args: args:

View File

@ -80,7 +80,45 @@ We don't call `uuid.uuid4()` ourselves in the code (we don't put the parenthesis
This means that the UUID will be generated in the Python code, **before sending the data to the database**. This means that the UUID will be generated in the Python code, **before sending the data to the database**.
{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[1:10] hl[1,7] *} //// tab | Python 3.10+
```Python hl_lines="1 7"
{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:1-10]!}
# Code below omitted 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="1 8"
{!./docs_src/advanced/uuid/tutorial001.py[ln:1-11]!}
# Code below omitted 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/advanced/uuid/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/advanced/uuid/tutorial001.py!}
```
////
///
Pydantic has support for <a href="https://docs.pydantic.dev/latest/api/standard_library_types/#uuid" class="external-link" target="_blank">`UUID` types</a>. Pydantic has support for <a href="https://docs.pydantic.dev/latest/api/standard_library_types/#uuid" class="external-link" target="_blank">`UUID` types</a>.
@ -94,7 +132,49 @@ As `uuid.uuid4` will be called when creating the model instance, even before sen
And that **same ID (a UUID)** will be saved in the database. And that **same ID (a UUID)** will be saved in the database.
{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[23:34] hl[25,27,29,34] *} //// tab | Python 3.10+
```Python hl_lines="5 7 9 14"
# Code above omitted 👆
{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:23-34]!}
# Code below omitted 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="5 7 9 14"
# Code above omitted 👆
{!./docs_src/advanced/uuid/tutorial001.py[ln:24-35]!}
# Code below omitted 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/advanced/uuid/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/advanced/uuid/tutorial001.py!}
```
////
///
### Select a Hero ### Select a Hero
@ -102,7 +182,49 @@ We can do the same operations we could do with other fields.
For example we can **select a hero by ID**: For example we can **select a hero by ID**:
{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[37:54] hl[49] *} //// tab | Python 3.10+
```Python hl_lines="15"
# Code above omitted 👆
{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:37-54]!}
# Code below omitted 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="15"
# Code above omitted 👆
{!./docs_src/advanced/uuid/tutorial001.py[ln:38-55]!}
# Code below omitted 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/advanced/uuid/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/advanced/uuid/tutorial001.py!}
```
////
///
/// tip /// tip
@ -116,7 +238,49 @@ SQLModel (actually SQLAlchemy) will take care of making it work. ✨
We could also select by ID with `session.get()`: We could also select by ID with `session.get()`:
{* ./docs_src/advanced/uuid/tutorial002_py310.py ln[37:53] hl[49] *} //// tab | Python 3.10+
```Python hl_lines="15"
# Code above omitted 👆
{!./docs_src/advanced/uuid/tutorial002_py310.py[ln:37-54]!}
# Code below omitted 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="15"
# Code above omitted 👆
{!./docs_src/advanced/uuid/tutorial002.py[ln:38-55]!}
# Code below omitted 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/advanced/uuid/tutorial002_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/advanced/uuid/tutorial002.py!}
```
////
///
The same way as with other fields, we could update, delete, etc. 🚀 The same way as with other fields, we could update, delete, etc. 🚀

View File

@ -68,7 +68,7 @@ There are many databases of many types.
A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that on a bit. A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that on a bit.
![database as a single file](img/databases/single-file.svg) ![database as a single file](/img/databases/single-file.svg)
### A server database ### A server database
@ -80,11 +80,11 @@ In this case, your code would talk to this server application instead of reading
The database could be located in a different server/machine: The database could be located in a different server/machine:
![database in an external server](img/databases/external-server.svg) ![database in an external server](/img/databases/external-server.svg)
Or the database could be located in the same server/machine: Or the database could be located in the same server/machine:
![database in the same server](img/databases/same-server.svg) ![database in the same server](/img/databases/same-server.svg)
The most important aspect of these types of databases is that **your code doesn't read or modify** the files containing the data directly. The most important aspect of these types of databases is that **your code doesn't read or modify** the files containing the data directly.
@ -98,7 +98,7 @@ In some cases, the database could even be a group of server applications running
In this case, your code would talk to one or more of these server applications running on different machines. In this case, your code would talk to one or more of these server applications running on different machines.
![distributed database in multiple servers](img/databases/multiple-servers.svg) ![distributed database in multiple servers](/img/databases/multiple-servers.svg)
Most of the databases that work as server applications also support multiple servers in one way or another. Most of the databases that work as server applications also support multiple servers in one way or another.
@ -257,7 +257,7 @@ For example, the table for the teams has the ID `1` for the team `Preventers` an
As these **primary key** IDs can uniquely identify each row on the table for teams, we can now go to the table for heroes and refer to those IDs in the table for teams. As these **primary key** IDs can uniquely identify each row on the table for teams, we can now go to the table for heroes and refer to those IDs in the table for teams.
![table relationships](img/databases/relationships.svg) <img alt="table relationships" src="/img/databases/relationships.svg">
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. 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.

View File

@ -236,7 +236,8 @@ database.execute(
).all() ).all()
``` ```
![](img/db-to-code/autocompletion01.png){class="shadow"} <img class="shadow" src="/img/db-to-code/autocompletion01.png">
## ORMs and SQL ## ORMs and SQL
@ -279,7 +280,7 @@ For example this **Relation** or table:
* **Mapper**: this comes from Math, when there's something that can convert from some set of things to another, that's called a "**mapping function**". That's where the **Mapper** comes from. * **Mapper**: this comes from Math, when there's something that can convert from some set of things to another, that's called a "**mapping function**". That's where the **Mapper** comes from.
![Squares to Triangles Mapper](img/db-to-code/mapper.svg) ![Squares to Triangles Mapper](/img/db-to-code/mapper.svg)
We could also write a **mapping function** in Python that converts from the *set of lowercase letters* to the *set of uppercase letters*, like this: We could also write a **mapping function** in Python that converts from the *set of lowercase letters* to the *set of uppercase letters*, like this:

View File

@ -16,11 +16,11 @@ As **SQLModel** is built on top of <a href="https://www.sqlalchemy.org/" class="
## Install DB Browser for SQLite ## Install DB Browser for SQLite
Remember that [SQLite is a simple database in a single file](databases.md#a-single-file-database){.internal-link target=_blank}? Remember that [SQLite is a simple database in a single file](../databases.md#a-single-file-database){.internal-link target=_blank}?
For most of the tutorial I'll use SQLite for the examples. For most of the tutorial I'll use SQLite for the examples.
Python has integrated support for SQLite, it is a single file read and processed from Python. And it doesn't need an [External Database Server](databases.md#a-server-database){.internal-link target=_blank}, so it will be perfect for learning. Python has integrated support for SQLite, it is a single file read and processed from Python. And it doesn't need an [External Database Server](../databases.md#a-server-database){.internal-link target=_blank}, so it will be perfect for learning.
In fact, SQLite is perfectly capable of handling quite big applications. At some point you might want to migrate to a server-based database like <a href="https://www.postgresql.org/" class="external-link" target="_blank">PostgreSQL</a> (which is also free). But for now we'll stick to SQLite. In fact, SQLite is perfectly capable of handling quite big applications. At some point you might want to migrate to a server-based database like <a href="https://www.postgresql.org/" class="external-link" target="_blank">PostgreSQL</a> (which is also free). But for now we'll stick to SQLite.

View File

@ -2,43 +2,6 @@
## Latest Changes ## Latest Changes
### Refactors
* 🚨 Fix types for new Pydantic. PR [#1131](https://github.com/fastapi/sqlmodel/pull/1131) by [@tiangolo](https://github.com/tiangolo).
### Docs
* ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight).
* 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo).
* 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo).
* ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey).
* 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev).
### Internal
* ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev).
* 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo).
* 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo).
* Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo).
* 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo).
* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
## 0.0.22
### Fixes
* 🐛 Fix support for types with `Optional[Annotated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo).
### Docs ### Docs
* ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#1085](https://github.com/fastapi/sqlmodel/pull/1085) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#1085](https://github.com/fastapi/sqlmodel/pull/1085) by [@tiangolo](https://github.com/tiangolo).
@ -48,7 +11,6 @@
### Internal ### Internal
* ✅ Refactor test_enums to make them independent of previous imports. PR [#1095](https://github.com/fastapi/sqlmodel/pull/1095) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update `latest-changes` GitHub Action. PR [#1087](https://github.com/fastapi/sqlmodel/pull/1087) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `latest-changes` GitHub Action. PR [#1087](https://github.com/fastapi/sqlmodel/pull/1087) by [@tiangolo](https://github.com/tiangolo).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1028](https://github.com/fastapi/sqlmodel/pull/1028) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1028](https://github.com/fastapi/sqlmodel/pull/1028) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot).
@ -69,7 +31,7 @@
* 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). * 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo).
* 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). * 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). * 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). * 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo).

View File

@ -41,7 +41,45 @@ That's why this package is called `SQLModel`. Because it's mainly used to create
For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes: For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes:
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,4] *} //// tab | Python 3.10+
```Python hl_lines="1 4"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="3 6"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table. This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table.
@ -63,7 +101,45 @@ The name of each of these variables will be the name of the column in the table.
And the type of each of them will also be the type of table column: And the type of each of them will also be the type of table column:
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5:8] *} //// tab | Python 3.10+
```Python hl_lines="1 5-8"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="1 3 7-10"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
Let's now see with more detail these field/column declarations. Let's now see with more detail these field/column declarations.
@ -77,7 +153,45 @@ That is the standard way to declare that something "could be an `int` or `None`"
And we also set the default value of `age` to `None`. And we also set the default value of `age` to `None`.
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[8] *} //// tab | Python 3.10+
```Python hl_lines="8"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="1 10"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
/// tip /// tip
@ -107,7 +221,45 @@ So, we need to mark `id` as the **primary key**.
To do that, we use the special `Field` function from `sqlmodel` and set the argument `primary_key=True`: To do that, we use the special `Field` function from `sqlmodel` and set the argument `primary_key=True`:
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5] *} //// tab | Python 3.10+
```Python hl_lines="1 5"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="3 7"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
That way, we tell **SQLModel** that this `id` field/column is the primary key of the table. That way, we tell **SQLModel** that this `id` field/column is the primary key of the table.
@ -150,7 +302,45 @@ If you have a server database (for example PostgreSQL or MySQL), the **engine**
Creating the **engine** is very simple, just call `create_engine()` with a URL for the database to use: Creating the **engine** is very simple, just call `create_engine()` with a URL for the database to use:
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[1,14] *} //// tab | Python 3.10+
```Python hl_lines="1 14"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="3 16"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
You should normally have a single **engine** object for your whole application and re-use it everywhere. You should normally have a single **engine** object for your whole application and re-use it everywhere.
@ -174,7 +364,45 @@ SQLite supports a special database that lives all *in memory*. Hence, it's very
* `sqlite://` * `sqlite://`
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[11:12,14] *} //// tab | Python 3.10+
```Python hl_lines="11-12 14"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="13-14 16"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the <a href="https://docs.sqlalchemy.org/en/14/core/engines.html" class="external-link" target="_blank">SQLAlchemy documentation</a>. You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the <a href="https://docs.sqlalchemy.org/en/14/core/engines.html" class="external-link" target="_blank">SQLAlchemy documentation</a>.
@ -186,7 +414,45 @@ It will make the engine print all the SQL statements it executes, which can help
It is particularly useful for **learning** and **debugging**: It is particularly useful for **learning** and **debugging**:
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[14] *} //// tab | Python 3.10+
```Python hl_lines="14"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="16"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
But in production, you would probably want to remove `echo=True`: But in production, you would probably want to remove `echo=True`:
@ -212,7 +478,21 @@ And SQLModel's version of `create_engine()` is type annotated internally, so you
Now everything is in place to finally create the database and table: Now everything is in place to finally create the database and table:
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py hl[16] *} //// tab | Python 3.10+
```Python hl_lines="16"
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python hl_lines="18"
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
/// tip /// tip
@ -323,7 +603,25 @@ Let's run the program to see it all working.
Put the code it in a file `app.py` if you haven't already. Put the code it in a file `app.py` if you haven't already.
{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py *} /// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial001.py!}
```
////
///
/// tip /// tip
@ -428,7 +726,45 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`.
Let's put it in a function `create_db_and_tables()`: Let's put it in a function `create_db_and_tables()`:
{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py ln[1:18] hl[17:18] *} //// tab | Python 3.10+
```Python hl_lines="17-18"
{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py[ln:1-18]!}
# More code here later 👇
```
////
//// tab | Python 3.7+
```Python hl_lines="19-20"
{!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!}
# More code here later 👇
```
////
/// details | 👀 Full file preview
//// tab | Python 3.10+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!}
```
////
//// tab | Python 3.7+
```Python
{!./docs_src/tutorial/create_db_and_table/tutorial002.py!}
```
////
///
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. 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.
@ -458,7 +794,21 @@ The word **script** often implies that the code could be run independently and e
For that we can use the special variable `__name__` in an `if` block: For that we can use the special variable `__name__` in an `if` block:
{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py hl[21:22] *} //// tab | Python 3.10+
```Python hl_lines="21-22"
{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!}
```
////
//// tab | Python 3.7+
```Python hl_lines="23-24"
{!./docs_src/tutorial/create_db_and_table/tutorial002.py!}
```
////
### About `__name__ == "__main__"` ### About `__name__ == "__main__"`

View File

@ -24,7 +24,7 @@ Fine, in that case, you can **sneak peek** the final code to create indexes here
//// tab | Python 3.10+ //// tab | Python 3.10+
```Python ```Python hl_lines="8 10"
{!./docs_src/tutorial/indexes/tutorial002_py310.py!} {!./docs_src/tutorial/indexes/tutorial002_py310.py!}
``` ```
@ -32,7 +32,7 @@ Fine, in that case, you can **sneak peek** the final code to create indexes here
//// tab | Python 3.7+ //// tab | Python 3.7+
```Python ```Python hl_lines="8 10"
{!./docs_src/tutorial/indexes/tutorial002.py!} {!./docs_src/tutorial/indexes/tutorial002.py!}
``` ```

View File

@ -710,4 +710,4 @@ Hero: None
## Recap ## Recap
As querying the SQL database for a single row is a common operation, you now have several tools to do it in a short and simple way. 🎉 As querying the SQL database for a single row is a common operation, you know have several tools to do it in a short and simple way. 🎉

View File

@ -184,7 +184,6 @@ markdown_extensions:
# Other extensions # Other extensions
mdx_include: mdx_include:
markdown_include_variants:
extra: extra:
analytics: analytics:

View File

@ -16,4 +16,3 @@ cairosvg==2.7.1
# For griffe, it formats with black # For griffe, it formats with black
typer == 0.12.3 typer == 0.12.3
mkdocs-macros-plugin==1.0.5 mkdocs-macros-plugin==1.0.5
markdown-include-variants==0.0.3

View File

@ -2,4 +2,3 @@ PyGithub>=2.3.0,<3.0.0
pydantic>=2.5.3,<3.0.0 pydantic>=2.5.3,<3.0.0
pydantic-settings>=2.1.0,<3.0.0 pydantic-settings>=2.1.0,<3.0.0
httpx>=0.27.0,<0.28.0 httpx>=0.27.0,<0.28.0
smokeshow

View File

@ -6,7 +6,7 @@ mypy ==1.4.1
ruff ==0.6.2 ruff ==0.6.2
# For FastAPI tests # For FastAPI tests
fastapi >=0.103.2 fastapi >=0.103.2
httpx ==0.24.1 httpx ==0.27.2
# TODO: upgrade when deprecating Python 3.7 # TODO: upgrade when deprecating Python 3.7
dirty-equals ==0.6.0 dirty-equals ==0.6.0
jinja2 ==3.1.4 jinja2 ==3.1.4

View File

@ -1,6 +1,4 @@
#!/usr/bin/env bash #!/bin/sh -e
set -e
set -x set -x
ruff check sqlmodel tests docs_src scripts --fix ruff check sqlmodel tests docs_src scripts --fix

View File

@ -1,4 +1,4 @@
__version__ = "0.0.22" __version__ = "0.0.21"
# Re-export from SQLAlchemy # Re-export from SQLAlchemy
from sqlalchemy.engine import create_engine as create_engine from sqlalchemy.engine import create_engine as create_engine

View File

@ -21,7 +21,7 @@ from typing import (
from pydantic import VERSION as P_VERSION from pydantic import VERSION as P_VERSION
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing_extensions import Annotated, get_args, get_origin from typing_extensions import get_args, get_origin
# Reassign variable to make it reexported for mypy # Reassign variable to make it reexported for mypy
PYDANTIC_VERSION = P_VERSION PYDANTIC_VERSION = P_VERSION
@ -177,17 +177,16 @@ if IS_PYDANTIC_V2:
return False return False
return False return False
def get_sa_type_from_type_annotation(annotation: Any) -> Any: def get_type_from_field(field: Any) -> Any:
type_: Any = field.annotation
# Resolve Optional fields # Resolve Optional fields
if annotation is None: if type_ is None:
raise ValueError("Missing field type") raise ValueError("Missing field type")
origin = get_origin(annotation) origin = get_origin(type_)
if origin is None: if origin is None:
return annotation return type_
elif origin is Annotated:
return get_sa_type_from_type_annotation(get_args(annotation)[0])
if _is_union_type(origin): if _is_union_type(origin):
bases = get_args(annotation) bases = get_args(type_)
if len(bases) > 2: if len(bases) > 2:
raise ValueError( raise ValueError(
"Cannot have a (non-optional) union as a SQLAlchemy field" "Cannot have a (non-optional) union as a SQLAlchemy field"
@ -198,14 +197,9 @@ if IS_PYDANTIC_V2:
"Cannot have a (non-optional) union as a SQLAlchemy field" "Cannot have a (non-optional) union as a SQLAlchemy field"
) )
# Optional unions are allowed # Optional unions are allowed
use_type = bases[0] if bases[0] is not NoneType else bases[1] return bases[0] if bases[0] is not NoneType else bases[1]
return get_sa_type_from_type_annotation(use_type)
return origin return origin
def get_sa_type_from_field(field: Any) -> Any:
type_: Any = field.annotation
return get_sa_type_from_type_annotation(type_)
def get_field_metadata(field: Any) -> Any: def get_field_metadata(field: Any) -> Any:
for meta in field.metadata: for meta in field.metadata:
if isinstance(meta, (PydanticMetadata, MaxLen)): if isinstance(meta, (PydanticMetadata, MaxLen)):
@ -450,7 +444,7 @@ else:
) )
return field.allow_none # type: ignore[no-any-return, attr-defined] return field.allow_none # type: ignore[no-any-return, attr-defined]
def get_sa_type_from_field(field: Any) -> Any: def get_type_from_field(field: Any) -> Any:
if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON:
return field.type_ return field.type_
raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") raise ValueError(f"The field {field.name} has no matching SQLAlchemy type")

View File

@ -52,7 +52,7 @@ from sqlalchemy.orm.decl_api import DeclarativeMeta
from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.orm.instrumentation import is_instrumented
from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.schema import MetaData
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
from typing_extensions import Literal, TypeAlias, deprecated, get_origin from typing_extensions import Literal, deprecated, get_origin
from ._compat import ( # type: ignore[attr-defined] from ._compat import ( # type: ignore[attr-defined]
IS_PYDANTIC_V2, IS_PYDANTIC_V2,
@ -71,7 +71,7 @@ from ._compat import ( # type: ignore[attr-defined]
get_field_metadata, get_field_metadata,
get_model_fields, get_model_fields,
get_relationship_to, get_relationship_to,
get_sa_type_from_field, get_type_from_field,
init_pydantic_private_attrs, init_pydantic_private_attrs,
is_field_noneable, is_field_noneable,
is_table_model_class, is_table_model_class,
@ -90,12 +90,7 @@ if TYPE_CHECKING:
_T = TypeVar("_T") _T = TypeVar("_T")
NoArgAnyCallable = Callable[[], Any] NoArgAnyCallable = Callable[[], Any]
IncEx: TypeAlias = Union[ IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None]
Set[int],
Set[str],
Mapping[int, Union["IncEx", Literal[True]]],
Mapping[str, Union["IncEx", Literal[True]]],
]
OnDeleteType = Literal["CASCADE", "SET NULL", "RESTRICT"] OnDeleteType = Literal["CASCADE", "SET NULL", "RESTRICT"]
@ -654,7 +649,7 @@ def get_sqlalchemy_type(field: Any) -> Any:
if sa_type is not Undefined: if sa_type is not Undefined:
return sa_type return sa_type
type_ = get_sa_type_from_field(field) type_ = get_type_from_field(field)
metadata = get_field_metadata(field) metadata = get_field_metadata(field)
# Check enums first as an enum can also be a str, needed by Pydantic/FastAPI # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI
@ -863,8 +858,8 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
self, self,
*, *,
mode: Union[Literal["json", "python"], str] = "python", mode: Union[Literal["json", "python"], str] = "python",
include: Union[IncEx, None] = None, include: IncEx = None,
exclude: Union[IncEx, None] = None, exclude: IncEx = None,
context: Union[Dict[str, Any], None] = None, context: Union[Dict[str, Any], None] = None,
by_alias: bool = False, by_alias: bool = False,
exclude_unset: bool = False, exclude_unset: bool = False,
@ -913,8 +908,8 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
def dict( def dict(
self, self,
*, *,
include: Union[IncEx, None] = None, include: IncEx = None,
exclude: Union[IncEx, None] = None, exclude: IncEx = None,
by_alias: bool = False, by_alias: bool = False,
exclude_unset: bool = False, exclude_unset: bool = False,
exclude_defaults: bool = False, exclude_defaults: bool = False,

View File

@ -1,26 +0,0 @@
import uuid
from typing import Optional
from sqlmodel import Field, Session, SQLModel, create_engine, select
from tests.conftest import needs_pydanticv2
@needs_pydanticv2
def test_annotated_optional_types(clear_sqlmodel) -> None:
from pydantic import UUID4
class Hero(SQLModel, table=True):
# Pydantic UUID4 is: Annotated[UUID, UuidVersion(4)]
id: Optional[UUID4] = Field(default_factory=uuid.uuid4, primary_key=True)
engine = create_engine("sqlite:///:memory:")
SQLModel.metadata.create_all(engine)
with Session(engine) as db:
hero = Hero()
db.add(hero)
db.commit()
statement = select(Hero)
result = db.exec(statement).all()
assert len(result) == 1
assert isinstance(hero.id, uuid.UUID)

View File

@ -1,11 +1,10 @@
import importlib import enum
import uuid
import pytest
from sqlalchemy import create_mock_engine from sqlalchemy import create_mock_engine
from sqlalchemy.sql.type_api import TypeEngine from sqlalchemy.sql.type_api import TypeEngine
from sqlmodel import SQLModel from sqlmodel import Field, SQLModel
from . import test_enums_models
from .conftest import needs_pydanticv1, needs_pydanticv2 from .conftest import needs_pydanticv1, needs_pydanticv2
""" """
@ -17,6 +16,30 @@ Associated issues:
""" """
class MyEnum1(str, enum.Enum):
A = "A"
B = "B"
class MyEnum2(str, 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): def pg_dump(sql: TypeEngine, *args, **kwargs):
dialect = sql.compile(dialect=postgres_engine.dialect) dialect = sql.compile(dialect=postgres_engine.dialect)
sql_str = str(dialect).rstrip() sql_str = str(dialect).rstrip()
@ -35,9 +58,7 @@ postgres_engine = create_mock_engine("postgresql://", pg_dump)
sqlite_engine = create_mock_engine("sqlite://", sqlite_dump) sqlite_engine = create_mock_engine("sqlite://", sqlite_dump)
def test_postgres_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]): def test_postgres_ddl_sql(capsys):
assert test_enums_models, "Ensure the models are imported and registered"
importlib.reload(test_enums_models)
SQLModel.metadata.create_all(bind=postgres_engine, checkfirst=False) SQLModel.metadata.create_all(bind=postgres_engine, checkfirst=False)
captured = capsys.readouterr() captured = capsys.readouterr()
@ -45,19 +66,17 @@ def test_postgres_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]):
assert "CREATE TYPE myenum2 AS ENUM ('C', 'D');" in captured.out assert "CREATE TYPE myenum2 AS ENUM ('C', 'D');" in captured.out
def test_sqlite_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]): def test_sqlite_ddl_sql(capsys):
assert test_enums_models, "Ensure the models are imported and registered"
importlib.reload(test_enums_models)
SQLModel.metadata.create_all(bind=sqlite_engine, checkfirst=False) SQLModel.metadata.create_all(bind=sqlite_engine, checkfirst=False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "enum_field VARCHAR(1) NOT NULL" in captured.out, captured assert "enum_field VARCHAR(1) NOT NULL" in captured.out
assert "CREATE TYPE" not in captured.out assert "CREATE TYPE" not in captured.out
@needs_pydanticv1 @needs_pydanticv1
def test_json_schema_flat_model_pydantic_v1(): def test_json_schema_flat_model_pydantic_v1():
assert test_enums_models.FlatModel.schema() == { assert FlatModel.schema() == {
"title": "FlatModel", "title": "FlatModel",
"type": "object", "type": "object",
"properties": { "properties": {
@ -78,7 +97,7 @@ def test_json_schema_flat_model_pydantic_v1():
@needs_pydanticv1 @needs_pydanticv1
def test_json_schema_inherit_model_pydantic_v1(): def test_json_schema_inherit_model_pydantic_v1():
assert test_enums_models.InheritModel.schema() == { assert InheritModel.schema() == {
"title": "InheritModel", "title": "InheritModel",
"type": "object", "type": "object",
"properties": { "properties": {
@ -99,7 +118,7 @@ def test_json_schema_inherit_model_pydantic_v1():
@needs_pydanticv2 @needs_pydanticv2
def test_json_schema_flat_model_pydantic_v2(): def test_json_schema_flat_model_pydantic_v2():
assert test_enums_models.FlatModel.model_json_schema() == { assert FlatModel.model_json_schema() == {
"title": "FlatModel", "title": "FlatModel",
"type": "object", "type": "object",
"properties": { "properties": {
@ -115,7 +134,7 @@ def test_json_schema_flat_model_pydantic_v2():
@needs_pydanticv2 @needs_pydanticv2
def test_json_schema_inherit_model_pydantic_v2(): def test_json_schema_inherit_model_pydantic_v2():
assert test_enums_models.InheritModel.model_json_schema() == { assert InheritModel.model_json_schema() == {
"title": "InheritModel", "title": "InheritModel",
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -1,28 +0,0 @@
import enum
import uuid
from sqlmodel import Field, SQLModel
class MyEnum1(str, enum.Enum):
A = "A"
B = "B"
class MyEnum2(str, 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