Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d7f4ff67 | ||
|
|
438480f128 | ||
|
|
7ba80e47e7 | ||
|
|
1920f07052 | ||
|
|
09adc76e3d | ||
|
|
690f9cf5e1 | ||
|
|
fca0621098 | ||
|
|
95936bb508 | ||
|
|
3b889e09f7 | ||
|
|
600da0a25c | ||
|
|
96bfd855f8 | ||
|
|
8416508d79 | ||
|
|
f1bfebc9e2 | ||
|
|
1263024be5 | ||
|
|
23869cab0d | ||
|
|
24e76c7a13 | ||
|
|
6e7e553963 | ||
|
|
e7c62fc9d9 | ||
|
|
8703539bf0 | ||
|
|
f6ad19b1a7 | ||
|
|
4590963e88 | ||
|
|
883cbe3a8d | ||
|
|
b560e9deb8 | ||
|
|
e2f646dea5 | ||
|
|
b93dd95125 | ||
|
|
ceac7bc2e8 | ||
|
|
1d43bd8b1e | ||
|
|
9f3af8507e | ||
|
|
d165e4b5ad | ||
|
|
d5cba6e358 | ||
|
|
bd1641c9a2 | ||
|
|
71de44daba | ||
|
|
1b275bd6a7 | ||
|
|
866d9ecb29 | ||
|
|
5bb4cffd49 | ||
|
|
a319952be1 | ||
|
|
662bd641b8 | ||
|
|
dcf4f58e81 | ||
|
|
e4013acc54 | ||
|
|
df0f834227 | ||
|
|
5e592c9a0d | ||
|
|
c13b71056e | ||
|
|
900e0d3371 | ||
|
|
a280b58c10 | ||
|
|
9ebbf255f7 | ||
|
|
39cbf27904 | ||
|
|
28d0e76370 |
2
.github/workflows/build-docs.yml
vendored
2
.github/workflows/build-docs.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
run: python ./scripts/docs.py verify-readme
|
||||
- name: Build Docs
|
||||
run: python ./scripts/docs.py build
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-site
|
||||
path: ./site/**
|
||||
|
||||
16
.github/workflows/deploy-docs.yml
vendored
16
.github/workflows/deploy-docs.yml
vendored
@@ -19,18 +19,16 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./site
|
||||
mkdir ./site
|
||||
- name: Download Artifact Docs
|
||||
id: download
|
||||
uses: dawidd6/action-download-artifact@v2.28.0
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
if_no_artifact_found: ignore
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-docs.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs-site
|
||||
path: ./site/
|
||||
pattern: docs-site
|
||||
merge-multiple: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- name: Deploy to Cloudflare Pages
|
||||
if: steps.download.outputs.found_artifact == 'true'
|
||||
# hashFiles returns an empty string if there are no files
|
||||
if: hashFiles('./site/*')
|
||||
id: deploy
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
|
||||
2
.github/workflows/issue-manager.yml
vendored
2
.github/workflows/issue-manager.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
issue-manager:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: tiangolo/issue-manager@0.4.1
|
||||
- uses: tiangolo/issue-manager@0.5.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config: >
|
||||
|
||||
1
.github/workflows/publish.yml
vendored
1
.github/workflows/publish.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
matrix:
|
||||
package:
|
||||
- sqlmodel
|
||||
- sqlmodel-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
|
||||
10
.github/workflows/smokeshow.yml
vendored
10
.github/workflows/smokeshow.yml
vendored
@@ -20,12 +20,14 @@ jobs:
|
||||
|
||||
- run: pip install smokeshow
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2.28.0
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
workflow: test.yml
|
||||
commit: ${{ github.event.workflow_run.head_sha }}
|
||||
name: coverage-html
|
||||
path: htmlcov
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- run: smokeshow upload coverage-html
|
||||
- run: smokeshow upload htmlcov
|
||||
env:
|
||||
SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}
|
||||
SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95
|
||||
|
||||
3
.github/workflows/test-redistribute.yml
vendored
3
.github/workflows/test-redistribute.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
||||
matrix:
|
||||
package:
|
||||
- sqlmodel
|
||||
- sqlmodel-slim
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
@@ -40,6 +41,8 @@ jobs:
|
||||
run: |
|
||||
cd dist/sqlmodel*/
|
||||
pip install -r requirements-tests.txt
|
||||
env:
|
||||
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }}
|
||||
- name: Run source distribution tests
|
||||
run: |
|
||||
cd dist/sqlmodel*/
|
||||
|
||||
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-v01
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: pip install -r requirements-tests.txt
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
run: pip install --upgrade "pydantic>=1.10.0,<2.0.0"
|
||||
- name: Install Pydantic v2
|
||||
if: matrix.pydantic-version == 'pydantic-v2'
|
||||
run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
|
||||
run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1"
|
||||
- name: Lint
|
||||
# Do not run on Python 3.7 as mypy behaves differently
|
||||
if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2'
|
||||
@@ -72,9 +72,9 @@ jobs:
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
|
||||
path: coverage
|
||||
coverage-combine:
|
||||
needs:
|
||||
@@ -89,10 +89,11 @@ jobs:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Get coverage files
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
|
||||
- run: pip install coverage[toml]
|
||||
|
||||
@@ -102,7 +103,7 @@ jobs:
|
||||
- run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}"
|
||||
|
||||
- name: Store coverage HTML
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-html
|
||||
path: htmlcov
|
||||
|
||||
@@ -4,7 +4,7 @@ default_language_version:
|
||||
python: python3.10
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-toml
|
||||
@@ -14,7 +14,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.0
|
||||
rev: v0.5.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
342
docs/advanced/uuid.md
Normal file
342
docs/advanced/uuid.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# UUID (Universally Unique Identifiers)
|
||||
|
||||
We have discussed some data types like `str`, `int`, etc.
|
||||
|
||||
There's another data type called `UUID` (Universally Unique Identifier).
|
||||
|
||||
You might have seen **UUIDs**, for example in URLs. They look something like this:
|
||||
|
||||
```
|
||||
4ff2dab7-bffe-414d-88a5-1826b9fea8df
|
||||
```
|
||||
|
||||
UUIDs can be particularly useful as an alternative to auto-incrementing integers for **primary keys**.
|
||||
|
||||
/// info
|
||||
|
||||
Official support for UUIDs was added in SQLModel version `0.0.20`.
|
||||
|
||||
///
|
||||
|
||||
## About UUIDs
|
||||
|
||||
UUIDs are numbers with 128 bits, that is, 16 bytes.
|
||||
|
||||
They are normally seen as 32 <abbr title="numbers in base 16 (instead of base 10), using letters from A to F to represent the numbers from 10 to 15">hexadecimal</abbr> characters separated by dashes.
|
||||
|
||||
There are several versions of UUID, some versions include the current time in the bytes, but **UUIDs version 4** are mainly random, the way they are generated makes them virtually **unique**.
|
||||
|
||||
### Distributed UUIDs
|
||||
|
||||
You could generate one UUID in one computer, and someone else could generate another UUID in another computer, and it would be almost **impossible** for both UUIDs to be the **same**.
|
||||
|
||||
This means that you don't have to wait for the DB to generate the ID for you, you can **generate it in code before sending it to the database**, because you can be quite certain it will be unique.
|
||||
|
||||
/// note | Technical Details
|
||||
|
||||
Because the number of possible UUIDs is so large (2^128), the probability of generating the same UUID version 4 (the random ones) twice is very low.
|
||||
|
||||
If you had 103 trillion version 4 UUIDs stored in the database, the probability of generating a duplicated new one is one in a billion. 🤓
|
||||
|
||||
///
|
||||
|
||||
For the same reason, if you decided to migrate your database, combine it with another database and mix records, etc. you would most probably be able to **just use the same UUIDs** you had originally.
|
||||
|
||||
/// warning
|
||||
|
||||
There's still a chance you could have a collision, but it's very low. In most cases you could assume you wouldn't have it, but it would be good to be prepared for it.
|
||||
|
||||
///
|
||||
|
||||
### UUIDs Prevent Information Leakage
|
||||
|
||||
Because UUIDs version 4 are **random**, you could give these IDs to the application users or to other systems, **without exposing information** about your application.
|
||||
|
||||
When using **auto-incremented integers** for primary keys, you could implicitly expose information about your system. For example, someone could create a new hero, and by getting the hero ID `20` **they would know that you have 20 heroes** in your system (or even less, if some heroes were already deleted).
|
||||
|
||||
### UUID Storage
|
||||
|
||||
Because UUIDs are 16 bytes, they would **consume more space** in the database than a smaller auto-incremented integer (commonly 4 bytes).
|
||||
|
||||
Depending on the database you use, UUIDs could have **better or worse performance**. If you are concerned about that, you should check the documentation for the specific database.
|
||||
|
||||
SQLite doesn't have a specific UUID type, so it will store the UUID as a string. Other databases like Postgres have a specific UUID type which would result in better performance and space usage than strings.
|
||||
|
||||
## Models with UUIDs
|
||||
|
||||
To use UUIDs as primary keys we need to import `uuid`, which is part of the Python standard library (we don't have to install anything) and use `uuid.UUID` as the **type** for the ID field.
|
||||
|
||||
We also want the Python code to **generate a new UUID** when creating a new instance, so we use `default_factory`.
|
||||
|
||||
The parameter `default_factory` takes a function (or in general, a "<abbr title="Something that can be called as a function.">callable</abbr>"). This function will be **called when creating a new instance** of the model and the value returned by the function will be used as the default value for the field.
|
||||
|
||||
For the function in `default_factory` we pass `uuid.uuid4`, which is a function that generates a **new UUID version 4**.
|
||||
|
||||
/// tip
|
||||
|
||||
We don't call `uuid.uuid4()` ourselves in the code (we don't put the parenthesis). Instead, we pass the function itself, just `uuid.uuid4`, so that SQLModel can call it every time we create a new instance.
|
||||
|
||||
///
|
||||
|
||||
This means that the UUID will be generated in the Python code, **before sending the data to the database**.
|
||||
|
||||
//// 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>.
|
||||
|
||||
For the database, **SQLModel** internally uses <a href="https://docs.sqlalchemy.org/en/20/core/type_basics.html#sqlalchemy.types.Uuid" class="external-link" target="_blank">SQLAlchemy's `Uuid` type</a>.
|
||||
|
||||
### Create a Record with a UUID
|
||||
|
||||
When creating a `Hero` record, the `id` field will be **automatically populated** with a new UUID because we set `default_factory=uuid.uuid4`.
|
||||
|
||||
As `uuid.uuid4` will be called when creating the model instance, even before sending it to the database, we can **access and use the ID right away**.
|
||||
|
||||
And that **same ID (a UUID)** will be saved in the database.
|
||||
|
||||
//// 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
|
||||
|
||||
We can do the same operations we could do with other fields.
|
||||
|
||||
For example we can **select a hero by ID**:
|
||||
|
||||
//// 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
|
||||
|
||||
Even if a database like SQLite stores the UUID as a string, we can select and run comparisons using a Python UUID object and it will work.
|
||||
|
||||
SQLModel (actually SQLAlchemy) will take care of making it work. ✨
|
||||
|
||||
///
|
||||
|
||||
#### Select with `session.get()`
|
||||
|
||||
We could also select by ID with `session.get()`:
|
||||
|
||||
//// 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. 🚀
|
||||
|
||||
### Run the program
|
||||
|
||||
If you run the program, you will see the **UUID** generated in the Python code, and then the record **saved in the database with the same UUID**.
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python app.py
|
||||
|
||||
// Some boilerplate and previous output omitted 😉
|
||||
|
||||
// In SQLite, the UUID will be stored as a string
|
||||
// other DBs like Postgres have a specific UUID type
|
||||
CREATE TABLE hero (
|
||||
id CHAR(32) NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
secret_name VARCHAR NOT NULL,
|
||||
age INTEGER,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
|
||||
// Before saving in the DB we already have the UUID
|
||||
The hero before saving in the DB
|
||||
name='Deadpond' secret_name='Dive Wilson' id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') age=None
|
||||
The hero ID was already set
|
||||
0e44c1a6-88d3-4a35-8b8a-307faa2def28
|
||||
|
||||
// The SQL statement to insert the record uses our UUID
|
||||
INSERT INTO hero (id, name, secret_name, age) VALUES (?, ?, ?, ?)
|
||||
('0e44c1a688d34a358b8a307faa2def28', 'Deadpond', 'Dive Wilson', None)
|
||||
|
||||
// And indeed, the record was saved with the UUID we created 😎
|
||||
After saving in the DB
|
||||
age=None id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') name='Deadpond' secret_name='Dive Wilson'
|
||||
|
||||
// Now we create a new hero (to select it in a bit)
|
||||
Created hero:
|
||||
age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador'
|
||||
Created hero ID:
|
||||
9d90d186-85db-4eaa-891a-def7b4ae2dab
|
||||
|
||||
// And now we select it
|
||||
Selected hero:
|
||||
age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador'
|
||||
Selected hero ID:
|
||||
9d90d186-85db-4eaa-891a-def7b4ae2dab
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more about **UUIDs** in:
|
||||
|
||||
* The official <a href="https://docs.python.org/3/library/uuid.html" class="external-link" target="_blank">Python docs for UUID</a>.
|
||||
* The <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier" class="external-link" target="_blank">Wikipedia for UUID</a>.
|
||||
@@ -8,6 +8,10 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.termy .linenos {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.external-link::after {
|
||||
/* \00A0 is a non-breaking space
|
||||
to make the mark be on the same line as the link
|
||||
|
||||
@@ -36,20 +36,10 @@ You will get completion for everything while writing the **minimum** amount of c
|
||||
|
||||
You won't need to keep guessing the types of different attributes in your models, if they could be `None`, etc. Your editor will be able to help you with everything because **SQLModel** is based on **standard Python type annotations**.
|
||||
|
||||
**SQLModel** even adopts currently <a href="https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md" class="external-link" target="_blank">in development standards</a> for Python type annotations to ensure the **best developer experience**, so you will get inline errors and autocompletion even while creating new model instances.
|
||||
**SQLModel** adopts <a href="https://peps.python.org/pep-0681/" class="external-link" target="_blank">PEP 681</a> for Python type annotations to ensure the **best developer experience**, so you will get inline errors and autocompletion even while creating new model instances.
|
||||
|
||||
<img class="shadow" src="/img/index/autocompletion01.png">
|
||||
|
||||
/// info
|
||||
|
||||
Don't worry, adopting this in-development standard only affects/improves editor support.
|
||||
|
||||
It doesn't affect performance or correctness. And if the in-progress standard was deprecated your code won't be affected.
|
||||
|
||||
Meanwhile, you will get inline errors (like type checks) and autocompletion on places you wouldn't get with any other library. 🎉
|
||||
|
||||
///
|
||||
|
||||
## Short
|
||||
|
||||
**SQLModel** has **sensible defaults** for everything, with **optional configurations** everywhere.
|
||||
|
||||
@@ -13,7 +13,7 @@ function setupTermynal() {
|
||||
|
||||
function createTermynals() {
|
||||
document
|
||||
.querySelectorAll(`.${termynalActivateClass} .highlight`)
|
||||
.querySelectorAll(`.${termynalActivateClass} .highlight code`)
|
||||
.forEach(node => {
|
||||
const text = node.textContent;
|
||||
const lines = text.split("\n");
|
||||
|
||||
@@ -2,6 +2,64 @@
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types. Initial PR [#992](https://github.com/tiangolo/sqlmodel/pull/992) by [@estebanx64](https://github.com/estebanx64).
|
||||
* New docs in the [Advanced User Guide: UUID (Universally Unique Identifiers)](https://sqlmodel.tiangolo.com/advanced/uuid/).
|
||||
|
||||
### Docs
|
||||
|
||||
* ✏️ Fix internal link in `docs/tutorial/create-db-and-table.md`. PR [#911](https://github.com/tiangolo/sqlmodel/pull/911) by [@tfpgh](https://github.com/tfpgh).
|
||||
* ✏️ Add missing step in `create-db-and-table-with-db-browser.md`. PR [#976](https://github.com/tiangolo/sqlmodel/pull/976) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ✏️ Fix typo in `docs/tutorial`. PR [#943](https://github.com/tiangolo/sqlmodel/pull/943) by [@luco17](https://github.com/luco17).
|
||||
* ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1).
|
||||
* ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ✏️ Fix typo in `docs/tutorial/relationship-attributes/index.md`. PR [#880](https://github.com/tiangolo/sqlmodel/pull/880) by [@UncleGoogle](https://github.com/UncleGoogle).
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#979](https://github.com/tiangolo/sqlmodel/pull/979) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
|
||||
* 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#1018](https://github.com/tiangolo/sqlmodel/pull/1018) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.0.19
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix pydantic `EmailStr` support and `max_length` in several String subclasses. PR [#966](https://github.com/tiangolo/sqlmodel/pull/966) by [@estebanx64](https://github.com/estebanx64).
|
||||
* 🐛 Fix set varchar limit when `max_length` is set on Pydantic models using Pydantic v2. PR [#963](https://github.com/tiangolo/sqlmodel/pull/963) by [@estebanx64](https://github.com/estebanx64).
|
||||
|
||||
### Refactors
|
||||
|
||||
* ♻️ Refactor generate select template to isolate templated code to the minimum. PR [#967](https://github.com/tiangolo/sqlmodel/pull/967) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Upgrades
|
||||
|
||||
* ⬆️ Update minimum SQLAlchemy version to 2.0.14 as that one includes `TryCast` used internally. PR [#964](https://github.com/tiangolo/sqlmodel/pull/964) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
|
||||
* ✏️ Fix broken link to `@dataclass_transform` (now PEP 681) in `docs/features.md`. PR [#753](https://github.com/tiangolo/sqlmodel/pull/753) by [@soof-golan](https://github.com/soof-golan).
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆️ Upgrade Ruff and Black. PR [#968](https://github.com/tiangolo/sqlmodel/pull/968) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆ Bump tiangolo/issue-manager from 0.4.1 to 0.5.0. PR [#922](https://github.com/tiangolo/sqlmodel/pull/922) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* 📌 Pin typing-extensions in tests for compatiblity with Python 3.8, dirty-equals, Pydantic. PR [#965](https://github.com/tiangolo/sqlmodel/pull/965) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.0.18
|
||||
|
||||
### Internal
|
||||
|
||||
* ✨ Add `sqlmodel-slim` setup. PR [#916](https://github.com/tiangolo/sqlmodel/pull/916) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
In the future SQLModel will include the standard default recommended packages, and `sqlmodel-slim` will come without those recommended standard packages and with a group of optional dependencies `sqlmodel-slim[standard]`, equivalent to `sqlmodel`, for those that want to opt out of those packages.
|
||||
|
||||
* 🔧 Re-enable MkDocs Material Social plugin. PR [#915](https://github.com/tiangolo/sqlmodel/pull/915) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Refactors
|
||||
|
||||
@@ -125,6 +125,8 @@ And delete that `./database.db` file in your project directory.
|
||||
|
||||
And click again on <kbd>New Database</kbd>.
|
||||
|
||||
Save the file with the name `database.db` again.
|
||||
|
||||
This time, if you see the dialog to create a new table, just close it by clicking the <kbd>Cancel</kbd> button.
|
||||
|
||||
And now, go to the tab <kbd>Execute SQL</kbd>.
|
||||
|
||||
@@ -354,7 +354,7 @@ But we will talk about it later.
|
||||
|
||||
### Engine Database URL
|
||||
|
||||
Each supported database has it's own URL type. For example, for **SQLite** it is `sqlite:///` followed by the file path. For example:
|
||||
Each supported database has its own URL type. For example, for **SQLite** it is `sqlite:///` followed by the file path. For example:
|
||||
|
||||
* `sqlite:///database.db`
|
||||
* `sqlite:///databases/local/application.db`
|
||||
@@ -470,7 +470,7 @@ If you didn't know about SQLAlchemy before and are just learning **SQLModel**, y
|
||||
|
||||
You can read a lot more about the engine in the <a href="https://docs.sqlalchemy.org/en/14/tutorial/engine.html" class="external-link" target="_blank">SQLAlchemy documentation</a>.
|
||||
|
||||
**SQLModel** defines it's own `create_engine()` function. It is the same as SQLAlchemy's `create_engine()`, but with the difference that it defaults to use `future=True` (which means that it uses the style of the latest SQLAlchemy, 1.4, and the future 2.0).
|
||||
**SQLModel** defines its own `create_engine()` function. It is the same as SQLAlchemy's `create_engine()`, but with the difference that it defaults to use `future=True` (which means that it uses the style of the latest SQLAlchemy, 1.4, and the future 2.0).
|
||||
|
||||
And SQLModel's version of `create_engine()` is type annotated internally, so your editor will be able to help you with autocompletion and inline errors.
|
||||
|
||||
@@ -688,7 +688,7 @@ In the example in the previous chapter we created the table using `TEXT` for som
|
||||
|
||||
But in this output SQLAlchemy is using `VARCHAR` instead. Let's see what's going on.
|
||||
|
||||
Remember that [each SQL Database has some different variations in what they support?](../databases/#sql-the-language){.internal-link target=_blank}
|
||||
Remember that [each SQL Database has some different variations in what they support?](../databases.md#sql-the-language){.internal-link target=_blank}
|
||||
|
||||
This is one of the differences. Each database supports some particular **data types**, like `INTEGER` and `TEXT`.
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ Now, after making sure we are inside of a virtual environment in some way, we ca
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
# (env) $$ python -m pip install sqlmodel
|
||||
# (env) $$ pip install sqlmodel
|
||||
---> 100%
|
||||
Successfully installed sqlmodel pydantic sqlalchemy
|
||||
```
|
||||
|
||||
@@ -90,7 +90,7 @@ Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅
|
||||
|
||||
That <abbr title="a recipe, a sequence of predefined steps that achieve a result">algorithm</abbr> 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.
|
||||
It's called 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.
|
||||
|
||||
///
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ In the previous chapters we discussed how to manage databases with tables that h
|
||||
|
||||
And then we read the data together with `select()` and using `.where()` or `.join()` to connect it.
|
||||
|
||||
Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy) to work with the data in the database in way much more familiar way, and closer to normal Python code.
|
||||
Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy), to work with the data in the database in a much more familiar way, and closer to normal Python code.
|
||||
|
||||
/// info
|
||||
|
||||
|
||||
@@ -713,7 +713,7 @@ In this chapter we are touching some of them.
|
||||
|
||||
When importing from `sqlmodel` the `select()` function, you are using **SQLModel**'s version of `select`.
|
||||
|
||||
SQLAchemy also has it's own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally.
|
||||
SQLAchemy also has its own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally.
|
||||
|
||||
But SQLModel's version does a lot of **tricks** with type annotations to make sure you get the best **editor support** possible, no matter if you use **VS Code**, **PyCharm**, or something else. ✨
|
||||
|
||||
|
||||
0
docs_src/advanced/uuid/__init__.py
Normal file
0
docs_src/advanced/uuid/__init__.py
Normal file
65
docs_src/advanced/uuid/tutorial001.py
Normal file
65
docs_src/advanced/uuid/tutorial001.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import uuid
|
||||
from typing import Union
|
||||
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
|
||||
|
||||
class Hero(SQLModel, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
name: str = Field(index=True)
|
||||
secret_name: str
|
||||
age: Union[int, None] = 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_hero():
|
||||
with Session(engine) as session:
|
||||
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
|
||||
print("The hero before saving in the DB")
|
||||
print(hero)
|
||||
print("The hero ID was already set")
|
||||
print(hero.id)
|
||||
session.add(hero)
|
||||
session.commit()
|
||||
session.refresh(hero)
|
||||
print("After saving in the DB")
|
||||
print(hero)
|
||||
|
||||
|
||||
def select_hero():
|
||||
with Session(engine) as session:
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
|
||||
session.add(hero_2)
|
||||
session.commit()
|
||||
session.refresh(hero_2)
|
||||
hero_id = hero_2.id
|
||||
print("Created hero:")
|
||||
print(hero_2)
|
||||
print("Created hero ID:")
|
||||
print(hero_id)
|
||||
|
||||
statement = select(Hero).where(Hero.id == hero_id)
|
||||
selected_hero = session.exec(statement).one()
|
||||
print("Selected hero:")
|
||||
print(selected_hero)
|
||||
print("Selected hero ID:")
|
||||
print(selected_hero.id)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
create_db_and_tables()
|
||||
create_hero()
|
||||
select_hero()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
64
docs_src/advanced/uuid/tutorial001_py310.py
Normal file
64
docs_src/advanced/uuid/tutorial001_py310.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
|
||||
|
||||
class Hero(SQLModel, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
name: str = Field(index=True)
|
||||
secret_name: str
|
||||
age: int | None = 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_hero():
|
||||
with Session(engine) as session:
|
||||
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
|
||||
print("The hero before saving in the DB")
|
||||
print(hero)
|
||||
print("The hero ID was already set")
|
||||
print(hero.id)
|
||||
session.add(hero)
|
||||
session.commit()
|
||||
session.refresh(hero)
|
||||
print("After saving in the DB")
|
||||
print(hero)
|
||||
|
||||
|
||||
def select_hero():
|
||||
with Session(engine) as session:
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
|
||||
session.add(hero_2)
|
||||
session.commit()
|
||||
session.refresh(hero_2)
|
||||
hero_id = hero_2.id
|
||||
print("Created hero:")
|
||||
print(hero_2)
|
||||
print("Created hero ID:")
|
||||
print(hero_id)
|
||||
|
||||
statement = select(Hero).where(Hero.id == hero_id)
|
||||
selected_hero = session.exec(statement).one()
|
||||
print("Selected hero:")
|
||||
print(selected_hero)
|
||||
print("Selected hero ID:")
|
||||
print(selected_hero.id)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
create_db_and_tables()
|
||||
create_hero()
|
||||
select_hero()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
64
docs_src/advanced/uuid/tutorial002.py
Normal file
64
docs_src/advanced/uuid/tutorial002.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
from typing import Union
|
||||
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine
|
||||
|
||||
|
||||
class Hero(SQLModel, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
name: str = Field(index=True)
|
||||
secret_name: str
|
||||
age: Union[int, None] = 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_hero():
|
||||
with Session(engine) as session:
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
|
||||
print("The hero before saving in the DB")
|
||||
print(hero_1)
|
||||
print("The hero ID was already set")
|
||||
print(hero_1.id)
|
||||
session.add(hero_1)
|
||||
session.commit()
|
||||
session.refresh(hero_1)
|
||||
print("After saving in the DB")
|
||||
print(hero_1)
|
||||
|
||||
|
||||
def select_hero():
|
||||
with Session(engine) as session:
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
|
||||
session.add(hero_2)
|
||||
session.commit()
|
||||
session.refresh(hero_2)
|
||||
hero_id = hero_2.id
|
||||
print("Created hero:")
|
||||
print(hero_2)
|
||||
print("Created hero ID:")
|
||||
print(hero_id)
|
||||
|
||||
selected_hero = session.get(Hero, hero_id)
|
||||
print("Selected hero:")
|
||||
print(selected_hero)
|
||||
print("Selected hero ID:")
|
||||
print(selected_hero.id)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
create_db_and_tables()
|
||||
create_hero()
|
||||
select_hero()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
63
docs_src/advanced/uuid/tutorial002_py310.py
Normal file
63
docs_src/advanced/uuid/tutorial002_py310.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import uuid
|
||||
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine
|
||||
|
||||
|
||||
class Hero(SQLModel, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
name: str = Field(index=True)
|
||||
secret_name: str
|
||||
age: int | None = 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_hero():
|
||||
with Session(engine) as session:
|
||||
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
|
||||
print("The hero before saving in the DB")
|
||||
print(hero)
|
||||
print("The hero ID was already set")
|
||||
print(hero.id)
|
||||
session.add(hero)
|
||||
session.commit()
|
||||
session.refresh(hero)
|
||||
print("After saving in the DB")
|
||||
print(hero)
|
||||
|
||||
|
||||
def select_hero():
|
||||
with Session(engine) as session:
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
|
||||
session.add(hero_2)
|
||||
session.commit()
|
||||
session.refresh(hero_2)
|
||||
hero_id = hero_2.id
|
||||
print("Created hero:")
|
||||
print(hero_2)
|
||||
print("Created hero ID:")
|
||||
print(hero_id)
|
||||
|
||||
selected_hero = session.get(Hero, hero_id)
|
||||
print("Selected hero:")
|
||||
print(selected_hero)
|
||||
print("Selected hero ID:")
|
||||
print(selected_hero.id)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
create_db_and_tables()
|
||||
create_hero()
|
||||
select_hero()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,3 @@
|
||||
plugins:
|
||||
# TODO: Re-enable once this is fixed: https://github.com/squidfunk/mkdocs-material/issues/6983
|
||||
# social:
|
||||
social:
|
||||
typeset:
|
||||
|
||||
@@ -99,6 +99,7 @@ nav:
|
||||
- Advanced User Guide:
|
||||
- advanced/index.md
|
||||
- advanced/decimal.md
|
||||
- advanced/uuid.md
|
||||
- alternatives.md
|
||||
- help.md
|
||||
- contributing.md
|
||||
|
||||
39
pdm_build.py
Normal file
39
pdm_build.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pdm.backend.hooks import Context
|
||||
|
||||
TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE", "sqlmodel")
|
||||
|
||||
|
||||
def pdm_build_initialize(context: Context) -> None:
|
||||
metadata = context.config.metadata
|
||||
# Get custom config for the current package, from the env var
|
||||
config: Dict[str, Any] = context.config.data["tool"]["tiangolo"][
|
||||
"_internal-slim-build"
|
||||
]["packages"][TIANGOLO_BUILD_PACKAGE]
|
||||
project_config: Dict[str, Any] = config["project"]
|
||||
# Get main optional dependencies, extras
|
||||
optional_dependencies: Dict[str, List[str]] = metadata.get(
|
||||
"optional-dependencies", {}
|
||||
)
|
||||
# Get custom optional dependencies name to always include in this (non-slim) package
|
||||
include_optional_dependencies: List[str] = config.get(
|
||||
"include-optional-dependencies", []
|
||||
)
|
||||
# Override main [project] configs with custom configs for this package
|
||||
for key, value in project_config.items():
|
||||
metadata[key] = value
|
||||
# Get custom build config for the current package
|
||||
build_config: Dict[str, Any] = (
|
||||
config.get("tool", {}).get("pdm", {}).get("build", {})
|
||||
)
|
||||
# Override PDM build config with custom build config for this package
|
||||
for key, value in build_config.items():
|
||||
context.config.build_config[key] = value
|
||||
# Get main dependencies
|
||||
dependencies: List[str] = metadata.get("dependencies", [])
|
||||
# Add optional dependencies to the default dependencies for this (non-slim) package
|
||||
for include_optional in include_optional_dependencies:
|
||||
optional_dependencies_group = optional_dependencies.get(include_optional, [])
|
||||
dependencies.extend(optional_dependencies_group)
|
||||
@@ -35,7 +35,7 @@ classifiers = [
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"SQLAlchemy >=2.0.0,<2.1.0",
|
||||
"SQLAlchemy >=2.0.14,<2.1.0",
|
||||
"pydantic >=1.10.13,<3.0.0",
|
||||
]
|
||||
|
||||
@@ -57,6 +57,18 @@ source-includes = [
|
||||
"sqlmodel/sql/expression.py.jinja2",
|
||||
]
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.sqlmodel-slim.project]
|
||||
name = "sqlmodel-slim"
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.sqlmodel]
|
||||
# include-optional-dependencies = ["standard"]
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.sqlmodel.project]
|
||||
optional-dependencies = {}
|
||||
|
||||
# [tool.tiangolo._internal-slim-build.packages.sqlmodel.project.scripts]
|
||||
# sqlmodel = "sqlmodel.cli:main"
|
||||
|
||||
[tool.coverage.run]
|
||||
parallel = true
|
||||
source = [
|
||||
@@ -78,7 +90,7 @@ exclude_lines = [
|
||||
strict = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "sqlmodel.sql.expression"
|
||||
module = "sqlmodel.sql._expression_select_gen"
|
||||
warn_unused_ignores = false
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# For mkdocstrings and code generator using templates
|
||||
black >=22.10,<24.0
|
||||
black >=22.10
|
||||
|
||||
@@ -12,7 +12,7 @@ pillow==10.1.0
|
||||
# For image processing by Material for MkDocs
|
||||
cairosvg==2.7.0
|
||||
mkdocstrings[python]==0.23.0
|
||||
griffe-typingdoc==0.2.2
|
||||
# Enable griffe-typingdoc once dropping Python 3.7 and upgrading typing-extensions
|
||||
# griffe-typingdoc==0.2.5
|
||||
# For griffe, it formats with black
|
||||
black==23.3.0
|
||||
typer == 0.12.3
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
pytest >=7.0.1,<8.0.0
|
||||
coverage[toml] >=6.2,<8.0
|
||||
mypy ==1.4.1
|
||||
ruff ==0.2.0
|
||||
ruff ==0.4.7
|
||||
# For FastAPI tests
|
||||
fastapi >=0.103.2
|
||||
httpx ==0.24.1
|
||||
# TODO: upgrade when deprecating Python 3.7
|
||||
dirty-equals ==0.6.0
|
||||
jinja2 ==3.1.3
|
||||
# Pin typing-extensions until Python 3.8 is deprecated or the issue with dirty-equals
|
||||
# is fixed, maybe fixed after dropping Python 3.7 and upgrading dirty-equals
|
||||
typing-extensions ==4.6.1
|
||||
|
||||
@@ -7,8 +7,10 @@ import black
|
||||
from jinja2 import Template
|
||||
from pydantic import BaseModel
|
||||
|
||||
template_path = Path(__file__).parent.parent / "sqlmodel/sql/expression.py.jinja2"
|
||||
destiny_path = Path(__file__).parent.parent / "sqlmodel/sql/expression.py"
|
||||
template_path = (
|
||||
Path(__file__).parent.parent / "sqlmodel/sql/_expression_select_gen.py.jinja2"
|
||||
)
|
||||
destiny_path = Path(__file__).parent.parent / "sqlmodel/sql/_expression_select_gen.py"
|
||||
|
||||
|
||||
number_of_types = 4
|
||||
@@ -48,7 +50,7 @@ result = template.render(number_of_types=number_of_types, signatures=signatures)
|
||||
|
||||
result = (
|
||||
"# WARNING: do not modify this code, it is generated by "
|
||||
"expression.py.jinja2\n\n" + result
|
||||
"_expression_select_gen.py.jinja2\n\n" + result
|
||||
)
|
||||
|
||||
result = black.format_str(result, mode=black.Mode())
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
set -e
|
||||
set -x
|
||||
|
||||
CHECK_JINJA=1 python scripts/generate_select.py
|
||||
coverage run -m pytest tests
|
||||
coverage combine
|
||||
coverage report --show-missing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = "0.0.17"
|
||||
__version__ = "0.0.20"
|
||||
|
||||
# Re-export from SQLAlchemy
|
||||
from sqlalchemy.engine import create_engine as create_engine
|
||||
@@ -140,5 +140,4 @@ from .sql.expression import select as select
|
||||
from .sql.expression import tuple_ as tuple_
|
||||
from .sql.expression import type_coerce as type_coerce
|
||||
from .sql.expression import within_group as within_group
|
||||
from .sql.sqltypes import GUID as GUID
|
||||
from .sql.sqltypes import AutoString as AutoString
|
||||
|
||||
@@ -72,6 +72,7 @@ def partial_init() -> Generator[None, None, None]:
|
||||
|
||||
|
||||
if IS_PYDANTIC_V2:
|
||||
from annotated_types import MaxLen
|
||||
from pydantic import ConfigDict as BaseConfig
|
||||
from pydantic._internal._fields import PydanticMetadata
|
||||
from pydantic._internal._model_construction import ModelMetaclass
|
||||
@@ -193,7 +194,7 @@ if IS_PYDANTIC_V2:
|
||||
# Non optional unions are not allowed
|
||||
if bases[0] is not NoneType and bases[1] is not NoneType:
|
||||
raise ValueError(
|
||||
"Cannot have a (non-optional) union as a SQLlchemy field"
|
||||
"Cannot have a (non-optional) union as a SQLAlchemy field"
|
||||
)
|
||||
# Optional unions are allowed
|
||||
return bases[0] if bases[0] is not NoneType else bases[1]
|
||||
@@ -201,7 +202,7 @@ if IS_PYDANTIC_V2:
|
||||
|
||||
def get_field_metadata(field: Any) -> Any:
|
||||
for meta in field.metadata:
|
||||
if isinstance(meta, PydanticMetadata):
|
||||
if isinstance(meta, (PydanticMetadata, MaxLen)):
|
||||
return meta
|
||||
return FakeMetadata()
|
||||
|
||||
|
||||
@@ -43,8 +43,7 @@ class AsyncSession(_AsyncSession):
|
||||
bind_arguments: Optional[Dict[str, Any]] = None,
|
||||
_parent_execute_state: Optional[Any] = None,
|
||||
_add_event: Optional[Any] = None,
|
||||
) -> TupleResult[_TSelectParam]:
|
||||
...
|
||||
) -> TupleResult[_TSelectParam]: ...
|
||||
|
||||
@overload
|
||||
async def exec(
|
||||
@@ -56,8 +55,7 @@ class AsyncSession(_AsyncSession):
|
||||
bind_arguments: Optional[Dict[str, Any]] = None,
|
||||
_parent_execute_state: Optional[Any] = None,
|
||||
_add_event: Optional[Any] = None,
|
||||
) -> ScalarResult[_TSelectParam]:
|
||||
...
|
||||
) -> ScalarResult[_TSelectParam]: ...
|
||||
|
||||
async def exec(
|
||||
self,
|
||||
|
||||
@@ -25,7 +25,7 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic.fields import FieldInfo as PydanticFieldInfo
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
@@ -51,7 +51,7 @@ from sqlalchemy.orm.attributes import set_attribute
|
||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
||||
from sqlalchemy.orm.instrumentation import is_instrumented
|
||||
from sqlalchemy.sql.schema import MetaData
|
||||
from sqlalchemy.sql.sqltypes import LargeBinary, Time
|
||||
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
|
||||
from typing_extensions import Literal, deprecated, get_origin
|
||||
|
||||
from ._compat import ( # type: ignore[attr-defined]
|
||||
@@ -80,7 +80,7 @@ from ._compat import ( # type: ignore[attr-defined]
|
||||
sqlmodel_init,
|
||||
sqlmodel_validate,
|
||||
)
|
||||
from .sql.sqltypes import GUID, AutoString
|
||||
from .sql.sqltypes import AutoString
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass
|
||||
@@ -231,8 +231,7 @@ def Field(
|
||||
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
|
||||
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
|
||||
schema_extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Any:
|
||||
...
|
||||
) -> Any: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -268,8 +267,7 @@ def Field(
|
||||
repr: bool = True,
|
||||
sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore
|
||||
schema_extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Any:
|
||||
...
|
||||
) -> Any: ...
|
||||
|
||||
|
||||
def Field(
|
||||
@@ -361,8 +359,7 @@ def Relationship(
|
||||
link_model: Optional[Any] = None,
|
||||
sa_relationship_args: Optional[Sequence[Any]] = None,
|
||||
sa_relationship_kwargs: Optional[Mapping[str, Any]] = None,
|
||||
) -> Any:
|
||||
...
|
||||
) -> Any: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -371,8 +368,7 @@ def Relationship(
|
||||
back_populates: Optional[str] = None,
|
||||
link_model: Optional[Any] = None,
|
||||
sa_relationship: Optional[RelationshipProperty[Any]] = None,
|
||||
) -> Any:
|
||||
...
|
||||
) -> Any: ...
|
||||
|
||||
|
||||
def Relationship(
|
||||
@@ -574,7 +570,18 @@ def get_sqlalchemy_type(field: Any) -> Any:
|
||||
# Check enums first as an enum can also be a str, needed by Pydantic/FastAPI
|
||||
if issubclass(type_, Enum):
|
||||
return sa_Enum(type_)
|
||||
if issubclass(type_, str):
|
||||
if issubclass(
|
||||
type_,
|
||||
(
|
||||
str,
|
||||
ipaddress.IPv4Address,
|
||||
ipaddress.IPv4Network,
|
||||
ipaddress.IPv6Address,
|
||||
ipaddress.IPv6Network,
|
||||
Path,
|
||||
EmailStr,
|
||||
),
|
||||
):
|
||||
max_length = getattr(metadata, "max_length", None)
|
||||
if max_length:
|
||||
return AutoString(length=max_length)
|
||||
@@ -600,18 +607,8 @@ def get_sqlalchemy_type(field: Any) -> Any:
|
||||
precision=getattr(metadata, "max_digits", None),
|
||||
scale=getattr(metadata, "decimal_places", None),
|
||||
)
|
||||
if issubclass(type_, ipaddress.IPv4Address):
|
||||
return AutoString
|
||||
if issubclass(type_, ipaddress.IPv4Network):
|
||||
return AutoString
|
||||
if issubclass(type_, ipaddress.IPv6Address):
|
||||
return AutoString
|
||||
if issubclass(type_, ipaddress.IPv6Network):
|
||||
return AutoString
|
||||
if issubclass(type_, Path):
|
||||
return AutoString
|
||||
if issubclass(type_, uuid.UUID):
|
||||
return GUID
|
||||
return Uuid
|
||||
raise ValueError(f"{type_} has no matching SQLAlchemy type")
|
||||
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ class Session(_Session):
|
||||
bind_arguments: Optional[Dict[str, Any]] = None,
|
||||
_parent_execute_state: Optional[Any] = None,
|
||||
_add_event: Optional[Any] = None,
|
||||
) -> TupleResult[_TSelectParam]:
|
||||
...
|
||||
) -> TupleResult[_TSelectParam]: ...
|
||||
|
||||
@overload
|
||||
def exec(
|
||||
@@ -48,8 +47,7 @@ class Session(_Session):
|
||||
bind_arguments: Optional[Dict[str, Any]] = None,
|
||||
_parent_execute_state: Optional[Any] = None,
|
||||
_add_event: Optional[Any] = None,
|
||||
) -> ScalarResult[_TSelectParam]:
|
||||
...
|
||||
) -> ScalarResult[_TSelectParam]: ...
|
||||
|
||||
def exec(
|
||||
self,
|
||||
|
||||
43
sqlmodel/sql/_expression_select_cls.py
Normal file
43
sqlmodel/sql/_expression_select_cls.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from typing import (
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from sqlalchemy.sql._typing import (
|
||||
_ColumnExpressionArgument,
|
||||
)
|
||||
from sqlalchemy.sql.expression import Select as _Select
|
||||
from typing_extensions import Self
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share
|
||||
# where and having without having type overlap incompatibility in session.exec().
|
||||
class SelectBase(_Select[Tuple[_T]]):
|
||||
inherit_cache = True
|
||||
|
||||
def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self:
|
||||
"""Return a new `Select` construct with the given expression added to
|
||||
its `WHERE` clause, joined to the existing clause via `AND`, if any.
|
||||
"""
|
||||
return super().where(*whereclause) # type: ignore[arg-type]
|
||||
|
||||
def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self:
|
||||
"""Return a new `Select` construct with the given expression added to
|
||||
its `HAVING` clause, joined to the existing clause via `AND`, if any.
|
||||
"""
|
||||
return super().having(*having) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class Select(SelectBase[_T]):
|
||||
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(SelectBase[_T]):
|
||||
inherit_cache = True
|
||||
367
sqlmodel/sql/_expression_select_gen.py
Normal file
367
sqlmodel/sql/_expression_select_gen.py
Normal file
@@ -0,0 +1,367 @@
|
||||
# WARNING: do not modify this code, it is generated by _expression_select_gen.py.jinja2
|
||||
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
)
|
||||
from sqlalchemy.sql.elements import (
|
||||
SQLCoreOperations,
|
||||
)
|
||||
from sqlalchemy.sql.roles import TypedColumnsClauseRole
|
||||
|
||||
from ._expression_select_cls import Select, SelectOfScalar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
_TCCA = Union[
|
||||
TypedColumnsClauseRole[_T],
|
||||
SQLCoreOperations[_T],
|
||||
Type[_T],
|
||||
]
|
||||
|
||||
# Generated TypeVars start
|
||||
|
||||
|
||||
_TScalar_0 = TypeVar(
|
||||
"_TScalar_0",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T0 = TypeVar("_T0")
|
||||
|
||||
|
||||
_TScalar_1 = TypeVar(
|
||||
"_TScalar_1",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T1 = TypeVar("_T1")
|
||||
|
||||
|
||||
_TScalar_2 = TypeVar(
|
||||
"_TScalar_2",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T2 = TypeVar("_T2")
|
||||
|
||||
|
||||
_TScalar_3 = TypeVar(
|
||||
"_TScalar_3",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T3 = TypeVar("_T3")
|
||||
|
||||
|
||||
# Generated TypeVars end
|
||||
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore
|
||||
...
|
||||
|
||||
|
||||
# Generated overloads start
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
) -> Select[Tuple[_T0, _T1]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
) -> Select[Tuple[_T0, _TScalar_1]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
) -> Select[Tuple[_TScalar_0, _T1]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_T0, _T1, _T2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ...
|
||||
|
||||
|
||||
# Generated overloads end
|
||||
|
||||
|
||||
def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore
|
||||
if len(entities) == 1:
|
||||
return SelectOfScalar(*entities)
|
||||
return Select(*entities)
|
||||
84
sqlmodel/sql/_expression_select_gen.py.jinja2
Normal file
84
sqlmodel/sql/_expression_select_gen.py.jinja2
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
)
|
||||
from sqlalchemy.sql.elements import (
|
||||
SQLCoreOperations,
|
||||
)
|
||||
from sqlalchemy.sql.roles import TypedColumnsClauseRole
|
||||
|
||||
from ._expression_select_cls import Select, SelectOfScalar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
_TCCA = Union[
|
||||
TypedColumnsClauseRole[_T],
|
||||
SQLCoreOperations[_T],
|
||||
Type[_T],
|
||||
]
|
||||
|
||||
# Generated TypeVars start
|
||||
|
||||
|
||||
{% for i in range(number_of_types) %}
|
||||
_TScalar_{{ i }} = TypeVar(
|
||||
"_TScalar_{{ i }}",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T{{ i }} = TypeVar("_T{{ i }}")
|
||||
|
||||
{% endfor %}
|
||||
|
||||
# Generated TypeVars end
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore
|
||||
...
|
||||
|
||||
|
||||
# Generated overloads start
|
||||
|
||||
{% for signature in signatures %}
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
{% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %}
|
||||
) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ...
|
||||
|
||||
{% endfor %}
|
||||
|
||||
# Generated overloads end
|
||||
|
||||
|
||||
def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore
|
||||
if len(entities) == 1:
|
||||
return SelectOfScalar(*entities)
|
||||
return Select(*entities)
|
||||
@@ -1,6 +1,3 @@
|
||||
# WARNING: do not modify this code, it is generated by expression.py.jinja2
|
||||
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
Iterable,
|
||||
@@ -11,9 +8,7 @@ from typing import (
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import (
|
||||
@@ -39,14 +34,15 @@ from sqlalchemy.sql.elements import (
|
||||
Cast,
|
||||
CollectionAggregate,
|
||||
ColumnClause,
|
||||
SQLCoreOperations,
|
||||
TryCast,
|
||||
UnaryExpression,
|
||||
)
|
||||
from sqlalchemy.sql.expression import Select as _Select
|
||||
from sqlalchemy.sql.roles import TypedColumnsClauseRole
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
from typing_extensions import Literal, Self
|
||||
from typing_extensions import Literal
|
||||
|
||||
from ._expression_select_cls import Select as Select
|
||||
from ._expression_select_cls import SelectOfScalar as SelectOfScalar
|
||||
from ._expression_select_gen import select as select
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
@@ -89,7 +85,7 @@ def between(
|
||||
upper_bound: Any,
|
||||
symmetric: bool = False,
|
||||
) -> BinaryExpression[bool]:
|
||||
return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type]
|
||||
return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric)
|
||||
|
||||
|
||||
def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]:
|
||||
@@ -110,14 +106,14 @@ def cast(
|
||||
expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any],
|
||||
type_: "_TypeEngineArgument[_T]",
|
||||
) -> Cast[_T]:
|
||||
return sqlalchemy.cast(expression, type_) # type: ignore[arg-type]
|
||||
return sqlalchemy.cast(expression, type_)
|
||||
|
||||
|
||||
def try_cast(
|
||||
expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any],
|
||||
type_: "_TypeEngineArgument[_T]",
|
||||
) -> TryCast[_T]:
|
||||
return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type]
|
||||
return sqlalchemy.try_cast(expression, type_)
|
||||
|
||||
|
||||
def desc(
|
||||
@@ -135,7 +131,7 @@ def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpressi
|
||||
|
||||
|
||||
def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract:
|
||||
return sqlalchemy.extract(field, expr) # type: ignore[arg-type]
|
||||
return sqlalchemy.extract(field, expr)
|
||||
|
||||
|
||||
def funcfilter(
|
||||
@@ -162,7 +158,7 @@ def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpress
|
||||
return sqlalchemy.nulls_last(column) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def or_( # type: ignore[empty-body]
|
||||
def or_(
|
||||
initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool],
|
||||
*clauses: Union[_ColumnExpressionArgument[bool], bool],
|
||||
) -> ColumnElement[bool]:
|
||||
@@ -190,7 +186,7 @@ def over(
|
||||
) -> Over[_T]:
|
||||
return sqlalchemy.over(
|
||||
element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows
|
||||
) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def tuple_(
|
||||
@@ -204,413 +200,13 @@ def type_coerce(
|
||||
expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any],
|
||||
type_: "_TypeEngineArgument[_T]",
|
||||
) -> TypeCoerce[_T]:
|
||||
return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type]
|
||||
return sqlalchemy.type_coerce(expression, type_)
|
||||
|
||||
|
||||
def within_group(
|
||||
element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any]
|
||||
) -> WithinGroup[_T]:
|
||||
return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share
|
||||
# where and having without having type overlap incompatibility in session.exec().
|
||||
class SelectBase(_Select[Tuple[_T]]):
|
||||
inherit_cache = True
|
||||
|
||||
def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self:
|
||||
"""Return a new `Select` construct with the given expression added to
|
||||
its `WHERE` clause, joined to the existing clause via `AND`, if any.
|
||||
"""
|
||||
return super().where(*whereclause) # type: ignore[arg-type]
|
||||
|
||||
def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self:
|
||||
"""Return a new `Select` construct with the given expression added to
|
||||
its `HAVING` clause, joined to the existing clause via `AND`, if any.
|
||||
"""
|
||||
return super().having(*having) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class Select(SelectBase[_T]):
|
||||
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(SelectBase[_T]):
|
||||
inherit_cache = True
|
||||
|
||||
|
||||
_TCCA = Union[
|
||||
TypedColumnsClauseRole[_T],
|
||||
SQLCoreOperations[_T],
|
||||
Type[_T],
|
||||
]
|
||||
|
||||
# Generated TypeVars start
|
||||
|
||||
|
||||
_TScalar_0 = TypeVar(
|
||||
"_TScalar_0",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T0 = TypeVar("_T0")
|
||||
|
||||
|
||||
_TScalar_1 = TypeVar(
|
||||
"_TScalar_1",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T1 = TypeVar("_T1")
|
||||
|
||||
|
||||
_TScalar_2 = TypeVar(
|
||||
"_TScalar_2",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T2 = TypeVar("_T2")
|
||||
|
||||
|
||||
_TScalar_3 = TypeVar(
|
||||
"_TScalar_3",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T3 = TypeVar("_T3")
|
||||
|
||||
|
||||
# Generated TypeVars end
|
||||
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore
|
||||
...
|
||||
|
||||
|
||||
# Generated overloads start
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
) -> Select[Tuple[_T0, _T1]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
) -> Select[Tuple[_T0, _TScalar_1]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
) -> Select[Tuple[_TScalar_0, _T1]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_T0, _T1, _T2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_T0, _T1, _TScalar_2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _T2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _T2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _T1, _T2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
__ent0: _TCCA[_T0],
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
__ent1: _TCCA[_T1],
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
__ent2: _TCCA[_T2],
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
__ent3: _TCCA[_T3],
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
entity_0: _TScalar_0,
|
||||
entity_1: _TScalar_1,
|
||||
entity_2: _TScalar_2,
|
||||
entity_3: _TScalar_3,
|
||||
) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]:
|
||||
...
|
||||
|
||||
|
||||
# Generated overloads end
|
||||
|
||||
|
||||
def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore
|
||||
if len(entities) == 1:
|
||||
return SelectOfScalar(*entities)
|
||||
return Select(*entities)
|
||||
return sqlalchemy.within_group(element, *order_by)
|
||||
|
||||
|
||||
def col(column_expression: _T) -> Mapped[_T]:
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
ColumnElement,
|
||||
Extract,
|
||||
FunctionElement,
|
||||
FunctionFilter,
|
||||
Label,
|
||||
Over,
|
||||
TypeCoerce,
|
||||
WithinGroup,
|
||||
)
|
||||
from sqlalchemy.orm import InstrumentedAttribute, Mapped
|
||||
from sqlalchemy.sql._typing import (
|
||||
_ColumnExpressionArgument,
|
||||
_ColumnExpressionOrLiteralArgument,
|
||||
_ColumnExpressionOrStrLabelArgument,
|
||||
)
|
||||
from sqlalchemy.sql.elements import (
|
||||
BinaryExpression,
|
||||
Case,
|
||||
Cast,
|
||||
CollectionAggregate,
|
||||
ColumnClause,
|
||||
SQLCoreOperations,
|
||||
TryCast,
|
||||
UnaryExpression,
|
||||
)
|
||||
from sqlalchemy.sql.expression import Select as _Select
|
||||
from sqlalchemy.sql.roles import TypedColumnsClauseRole
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
from typing_extensions import Literal, Self
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]]
|
||||
|
||||
# Redefine operatos that would only take a column expresion to also take the (virtual)
|
||||
# types of Pydantic models, e.g. str instead of only Mapped[str].
|
||||
|
||||
|
||||
def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]:
|
||||
return sqlalchemy.all_(expr) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def and_(
|
||||
initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool],
|
||||
*clauses: Union[_ColumnExpressionArgument[bool], bool],
|
||||
) -> ColumnElement[bool]:
|
||||
return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]:
|
||||
return sqlalchemy.any_(expr) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def asc(
|
||||
column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T],
|
||||
) -> UnaryExpression[_T]:
|
||||
return sqlalchemy.asc(column) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def collate(
|
||||
expression: Union[_ColumnExpressionArgument[str], str], collation: str
|
||||
) -> BinaryExpression[str]:
|
||||
return sqlalchemy.collate(expression, collation) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def between(
|
||||
expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T],
|
||||
lower_bound: Any,
|
||||
upper_bound: Any,
|
||||
symmetric: bool = False,
|
||||
) -> BinaryExpression[bool]:
|
||||
return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]:
|
||||
return sqlalchemy.not_(clause) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def case(
|
||||
*whens: Union[
|
||||
Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any]
|
||||
],
|
||||
value: Optional[Any] = None,
|
||||
else_: Optional[Any] = None,
|
||||
) -> Case[Any]:
|
||||
return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def cast(
|
||||
expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any],
|
||||
type_: "_TypeEngineArgument[_T]",
|
||||
) -> Cast[_T]:
|
||||
return sqlalchemy.cast(expression, type_) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def try_cast(
|
||||
expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any],
|
||||
type_: "_TypeEngineArgument[_T]",
|
||||
) -> TryCast[_T]:
|
||||
return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def desc(
|
||||
column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T],
|
||||
) -> UnaryExpression[_T]:
|
||||
return sqlalchemy.desc(column) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]:
|
||||
return sqlalchemy.distinct(expr) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]:
|
||||
return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract:
|
||||
return sqlalchemy.extract(field, expr) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def funcfilter(
|
||||
func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool]
|
||||
) -> FunctionFilter[_T]:
|
||||
return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def label(
|
||||
name: str,
|
||||
element: Union[_ColumnExpressionArgument[_T], _T],
|
||||
type_: Optional["_TypeEngineArgument[_T]"] = None,
|
||||
) -> Label[_T]:
|
||||
return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def nulls_first(
|
||||
column: Union[_ColumnExpressionArgument[_T], _T],
|
||||
) -> UnaryExpression[_T]:
|
||||
return sqlalchemy.nulls_first(column) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]:
|
||||
return sqlalchemy.nulls_last(column) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def or_( # type: ignore[empty-body]
|
||||
initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool],
|
||||
*clauses: Union[_ColumnExpressionArgument[bool], bool],
|
||||
) -> ColumnElement[bool]:
|
||||
return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def over(
|
||||
element: FunctionElement[_T],
|
||||
partition_by: Optional[
|
||||
Union[
|
||||
Iterable[Union[_ColumnExpressionArgument[Any], Any]],
|
||||
_ColumnExpressionArgument[Any],
|
||||
Any,
|
||||
]
|
||||
] = None,
|
||||
order_by: Optional[
|
||||
Union[
|
||||
Iterable[Union[_ColumnExpressionArgument[Any], Any]],
|
||||
_ColumnExpressionArgument[Any],
|
||||
Any,
|
||||
]
|
||||
] = None,
|
||||
range_: Optional[Tuple[Optional[int], Optional[int]]] = None,
|
||||
rows: Optional[Tuple[Optional[int], Optional[int]]] = None,
|
||||
) -> Over[_T]:
|
||||
return sqlalchemy.over(
|
||||
element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows
|
||||
) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def tuple_(
|
||||
*clauses: Union[_ColumnExpressionArgument[Any], Any],
|
||||
types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None,
|
||||
) -> Tuple[Any, ...]:
|
||||
return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value]
|
||||
|
||||
|
||||
def type_coerce(
|
||||
expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any],
|
||||
type_: "_TypeEngineArgument[_T]",
|
||||
) -> TypeCoerce[_T]:
|
||||
return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def within_group(
|
||||
element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any]
|
||||
) -> WithinGroup[_T]:
|
||||
return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share
|
||||
# where and having without having type overlap incompatibility in session.exec().
|
||||
class SelectBase(_Select[Tuple[_T]]):
|
||||
inherit_cache = True
|
||||
|
||||
def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self:
|
||||
"""Return a new `Select` construct with the given expression added to
|
||||
its `WHERE` clause, joined to the existing clause via `AND`, if any.
|
||||
"""
|
||||
return super().where(*whereclause) # type: ignore[arg-type]
|
||||
|
||||
def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self:
|
||||
"""Return a new `Select` construct with the given expression added to
|
||||
its `HAVING` clause, joined to the existing clause via `AND`, if any.
|
||||
"""
|
||||
return super().having(*having) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class Select(SelectBase[_T]):
|
||||
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(SelectBase[_T]):
|
||||
inherit_cache = True
|
||||
|
||||
|
||||
_TCCA = Union[
|
||||
TypedColumnsClauseRole[_T],
|
||||
SQLCoreOperations[_T],
|
||||
Type[_T],
|
||||
]
|
||||
|
||||
# Generated TypeVars start
|
||||
|
||||
|
||||
{% for i in range(number_of_types) %}
|
||||
_TScalar_{{ i }} = TypeVar(
|
||||
"_TScalar_{{ i }}",
|
||||
Column, # type: ignore
|
||||
Sequence, # type: ignore
|
||||
Mapping, # type: ignore
|
||||
UUID,
|
||||
datetime,
|
||||
float,
|
||||
int,
|
||||
bool,
|
||||
bytes,
|
||||
str,
|
||||
None,
|
||||
)
|
||||
|
||||
_T{{ i }} = TypeVar("_T{{ i }}")
|
||||
|
||||
{% endfor %}
|
||||
|
||||
# Generated TypeVars end
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore
|
||||
...
|
||||
|
||||
|
||||
# Generated overloads start
|
||||
|
||||
{% for signature in signatures %}
|
||||
|
||||
@overload
|
||||
def select( # type: ignore
|
||||
{% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %}
|
||||
) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]:
|
||||
...
|
||||
|
||||
{% endfor %}
|
||||
|
||||
# Generated overloads end
|
||||
|
||||
|
||||
def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore
|
||||
if len(entities) == 1:
|
||||
return SelectOfScalar(*entities)
|
||||
return Select(*entities)
|
||||
|
||||
|
||||
def col(column_expression: _T) -> Mapped[_T]:
|
||||
if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)):
|
||||
raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}")
|
||||
return column_expression # type: ignore
|
||||
@@ -1,10 +1,7 @@
|
||||
import uuid
|
||||
from typing import Any, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from sqlalchemy import CHAR, types
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy.engine.interfaces import Dialect
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
|
||||
class AutoString(types.TypeDecorator): # type: ignore
|
||||
@@ -17,43 +14,3 @@ class AutoString(types.TypeDecorator): # type: ignore
|
||||
if impl.length is None and dialect.name == "mysql":
|
||||
return dialect.type_descriptor(types.String(self.mysql_default_length))
|
||||
return super().load_dialect_impl(dialect)
|
||||
|
||||
|
||||
# Reference form SQLAlchemy docs: https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type
|
||||
# with small modifications
|
||||
class GUID(types.TypeDecorator): # type: ignore
|
||||
"""Platform-independent GUID type.
|
||||
|
||||
Uses PostgreSQL's UUID type, otherwise uses
|
||||
CHAR(32), storing as stringified hex values.
|
||||
|
||||
"""
|
||||
|
||||
impl = CHAR
|
||||
cache_ok = True
|
||||
|
||||
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]:
|
||||
if dialect.name == "postgresql":
|
||||
return dialect.type_descriptor(UUID())
|
||||
else:
|
||||
return dialect.type_descriptor(CHAR(32))
|
||||
|
||||
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 uuid.UUID(value).hex
|
||||
else:
|
||||
# hexstring
|
||||
return value.hex
|
||||
|
||||
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 cast(uuid.UUID, value)
|
||||
|
||||
0
tests/test_advanced/test_uuid/__init__.py
Normal file
0
tests/test_advanced/test_uuid/__init__.py
Normal file
71
tests/test_advanced/test_uuid/test_tutorial001.py
Normal file
71
tests/test_advanced/test_uuid/test_tutorial001.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from dirty_equals import IsUUID
|
||||
from sqlmodel import create_engine
|
||||
|
||||
from ...conftest import get_testing_print_function
|
||||
|
||||
|
||||
def test_tutorial(clear_sqlmodel) -> None:
|
||||
from docs_src.advanced.uuid 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()
|
||||
first_uuid = calls[1][0]["id"]
|
||||
assert first_uuid == IsUUID(4)
|
||||
|
||||
second_uuid = calls[7][0]["id"]
|
||||
assert second_uuid == IsUUID(4)
|
||||
|
||||
assert first_uuid != second_uuid
|
||||
|
||||
assert calls == [
|
||||
["The hero before saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"id": first_uuid,
|
||||
"age": None,
|
||||
}
|
||||
],
|
||||
["The hero ID was already set"],
|
||||
[first_uuid],
|
||||
["After saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"age": None,
|
||||
"id": first_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero ID:"],
|
||||
[second_uuid],
|
||||
["Selected hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Selected hero ID:"],
|
||||
[second_uuid],
|
||||
]
|
||||
72
tests/test_advanced/test_uuid/test_tutorial001_py310.py
Normal file
72
tests/test_advanced/test_uuid/test_tutorial001_py310.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from dirty_equals import IsUUID
|
||||
from sqlmodel import create_engine
|
||||
|
||||
from ...conftest import get_testing_print_function, needs_py310
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_tutorial(clear_sqlmodel) -> None:
|
||||
from docs_src.advanced.uuid import tutorial001_py310 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()
|
||||
first_uuid = calls[1][0]["id"]
|
||||
assert first_uuid == IsUUID(4)
|
||||
|
||||
second_uuid = calls[7][0]["id"]
|
||||
assert second_uuid == IsUUID(4)
|
||||
|
||||
assert first_uuid != second_uuid
|
||||
|
||||
assert calls == [
|
||||
["The hero before saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"id": first_uuid,
|
||||
"age": None,
|
||||
}
|
||||
],
|
||||
["The hero ID was already set"],
|
||||
[first_uuid],
|
||||
["After saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"age": None,
|
||||
"id": first_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero ID:"],
|
||||
[second_uuid],
|
||||
["Selected hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Selected hero ID:"],
|
||||
[second_uuid],
|
||||
]
|
||||
71
tests/test_advanced/test_uuid/test_tutorial002.py
Normal file
71
tests/test_advanced/test_uuid/test_tutorial002.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from dirty_equals import IsUUID
|
||||
from sqlmodel import create_engine
|
||||
|
||||
from ...conftest import get_testing_print_function
|
||||
|
||||
|
||||
def test_tutorial(clear_sqlmodel) -> None:
|
||||
from docs_src.advanced.uuid 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()
|
||||
first_uuid = calls[1][0]["id"]
|
||||
assert first_uuid == IsUUID(4)
|
||||
|
||||
second_uuid = calls[7][0]["id"]
|
||||
assert second_uuid == IsUUID(4)
|
||||
|
||||
assert first_uuid != second_uuid
|
||||
|
||||
assert calls == [
|
||||
["The hero before saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"id": first_uuid,
|
||||
"age": None,
|
||||
}
|
||||
],
|
||||
["The hero ID was already set"],
|
||||
[first_uuid],
|
||||
["After saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"age": None,
|
||||
"id": first_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero ID:"],
|
||||
[second_uuid],
|
||||
["Selected hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Selected hero ID:"],
|
||||
[second_uuid],
|
||||
]
|
||||
72
tests/test_advanced/test_uuid/test_tutorial002_py310.py
Normal file
72
tests/test_advanced/test_uuid/test_tutorial002_py310.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from dirty_equals import IsUUID
|
||||
from sqlmodel import create_engine
|
||||
|
||||
from ...conftest import get_testing_print_function, needs_py310
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_tutorial(clear_sqlmodel) -> None:
|
||||
from docs_src.advanced.uuid import tutorial002_py310 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()
|
||||
first_uuid = calls[1][0]["id"]
|
||||
assert first_uuid == IsUUID(4)
|
||||
|
||||
second_uuid = calls[7][0]["id"]
|
||||
assert second_uuid == IsUUID(4)
|
||||
|
||||
assert first_uuid != second_uuid
|
||||
|
||||
assert calls == [
|
||||
["The hero before saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"id": first_uuid,
|
||||
"age": None,
|
||||
}
|
||||
],
|
||||
["The hero ID was already set"],
|
||||
[first_uuid],
|
||||
["After saving in the DB"],
|
||||
[
|
||||
{
|
||||
"name": "Deadpond",
|
||||
"secret_name": "Dive Wilson",
|
||||
"age": None,
|
||||
"id": first_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Created hero ID:"],
|
||||
[second_uuid],
|
||||
["Selected hero:"],
|
||||
[
|
||||
{
|
||||
"name": "Spider-Boy",
|
||||
"secret_name": "Pedro Parqueador",
|
||||
"age": None,
|
||||
"id": second_uuid,
|
||||
}
|
||||
],
|
||||
["Selected hero ID:"],
|
||||
[second_uuid],
|
||||
]
|
||||
19
tests/test_select_gen.py
Normal file
19
tests/test_select_gen.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from .conftest import needs_py39
|
||||
|
||||
root_path = Path(__file__).parent.parent
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_select_gen() -> None:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "scripts/generate_select.py"],
|
||||
env={"CHECK_JINJA": "1"},
|
||||
check=True,
|
||||
cwd=root_path,
|
||||
capture_output=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
Reference in New Issue
Block a user