From 5756e8fd393609f2f38517575c95a8aa0acbb668 Mon Sep 17 00:00:00 2001 From: robcxyz Date: Sat, 18 Sep 2021 22:18:42 -0700 Subject: [PATCH 01/10] Add precision and scale attributes for decimal field type --- sqlmodel/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 661276b31d..5b48afff76 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -393,7 +393,10 @@ def get_sqlachemy_type(field: ModelField) -> Any: if issubclass(field.type_, bytes): return LargeBinary if issubclass(field.type_, Decimal): - return Numeric + return Numeric( + precision=getattr(field.type_, "max_digits", None), + scale=getattr(field.type_, "decimal_places", None), + ) if issubclass(field.type_, ipaddress.IPv4Address): return AutoString if issubclass(field.type_, ipaddress.IPv4Network): From 3bb988b002fa9dc8afb081742bfdb42bfd62a154 Mon Sep 17 00:00:00 2001 From: robcxyz Date: Sat, 18 Sep 2021 22:20:17 -0700 Subject: [PATCH 02/10] Add a decimal attribute in code structure tutorial and test --- docs_src/tutorial/code_structure/tutorial001/app.py | 2 +- docs_src/tutorial/code_structure/tutorial001/models.py | 3 ++- docs_src/tutorial/code_structure/tutorial002/app.py | 2 +- docs_src/tutorial/code_structure/tutorial002/hero_model.py | 2 ++ tests/test_tutorial/test_code_structure/test_tutorial001.py | 2 ++ tests/test_tutorial/test_code_structure/test_tutorial002.py | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs_src/tutorial/code_structure/tutorial001/app.py b/docs_src/tutorial/code_structure/tutorial001/app.py index 065f8a78b5..10e1bc94b8 100644 --- a/docs_src/tutorial/code_structure/tutorial001/app.py +++ b/docs_src/tutorial/code_structure/tutorial001/app.py @@ -9,7 +9,7 @@ def create_heroes(): team_z_force = Team(name="Z-Force", headquarters="Sister Margaretโ€™s Bar") hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force + name="Deadpond", secret_name="Dive Wilson", team=team_z_force, experience_points=1 ) session.add(hero_deadpond) session.commit() diff --git a/docs_src/tutorial/code_structure/tutorial001/models.py b/docs_src/tutorial/code_structure/tutorial001/models.py index 9bd1fa93f2..5d83e19e97 100644 --- a/docs_src/tutorial/code_structure/tutorial001/models.py +++ b/docs_src/tutorial/code_structure/tutorial001/models.py @@ -1,8 +1,8 @@ +from pydantic import condecimal from typing import List, Optional from sqlmodel import Field, Relationship, SQLModel - class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str @@ -16,6 +16,7 @@ class Hero(SQLModel, table=True): name: str secret_name: str age: Optional[int] = None + experience_points: condecimal(max_digits=6, decimal_places=3) = Field(default=0, nullable=False, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002/app.py b/docs_src/tutorial/code_structure/tutorial002/app.py index 8afaee7c16..ce3097adcb 100644 --- a/docs_src/tutorial/code_structure/tutorial002/app.py +++ b/docs_src/tutorial/code_structure/tutorial002/app.py @@ -10,7 +10,7 @@ def create_heroes(): team_z_force = Team(name="Z-Force", headquarters="Sister Margaretโ€™s Bar") hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force + name="Deadpond", secret_name="Dive Wilson", team=team_z_force, experience_points=1 ) session.add(hero_deadpond) session.commit() diff --git a/docs_src/tutorial/code_structure/tutorial002/hero_model.py b/docs_src/tutorial/code_structure/tutorial002/hero_model.py index 84fc7f276b..064e16ef30 100644 --- a/docs_src/tutorial/code_structure/tutorial002/hero_model.py +++ b/docs_src/tutorial/code_structure/tutorial002/hero_model.py @@ -1,3 +1,4 @@ +from pydantic import condecimal from typing import TYPE_CHECKING, Optional from sqlmodel import Field, Relationship, SQLModel @@ -11,6 +12,7 @@ class Hero(SQLModel, table=True): name: str secret_name: str age: Optional[int] = None + experience_points: condecimal(max_digits=6, decimal_places=3) = Field(default=0, nullable=False, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional["Team"] = Relationship(back_populates="heroes") diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index 4192e00434..e98a25c756 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -4,6 +4,7 @@ from ...conftest import get_testing_print_function + expected_calls = [ [ "Created hero:", @@ -13,6 +14,7 @@ "age": None, "secret_name": "Dive Wilson", "team_id": 1, + "experience_points": 1, }, ], [ diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index 37e4e54de6..00ebcb0d90 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -13,6 +13,7 @@ "age": None, "secret_name": "Dive Wilson", "team_id": 1, + "experience_points": 1, }, ], [ From c6b21f1ca943112416bc0b06d43e0114c03f94e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 10:51:30 +0100 Subject: [PATCH 03/10] =?UTF-8?q?=E2=8F=AA=20Revert=20decimals=20in=20tuto?= =?UTF-8?q?rial=20about=20code=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/tutorial/code_structure/tutorial001/app.py | 2 +- docs_src/tutorial/code_structure/tutorial001/models.py | 3 +-- docs_src/tutorial/code_structure/tutorial002/app.py | 2 +- docs_src/tutorial/code_structure/tutorial002/hero_model.py | 2 -- tests/test_tutorial/test_code_structure/test_tutorial001.py | 2 -- tests/test_tutorial/test_code_structure/test_tutorial002.py | 1 - 6 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docs_src/tutorial/code_structure/tutorial001/app.py b/docs_src/tutorial/code_structure/tutorial001/app.py index 10e1bc94b8..065f8a78b5 100644 --- a/docs_src/tutorial/code_structure/tutorial001/app.py +++ b/docs_src/tutorial/code_structure/tutorial001/app.py @@ -9,7 +9,7 @@ def create_heroes(): team_z_force = Team(name="Z-Force", headquarters="Sister Margaretโ€™s Bar") hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force, experience_points=1 + name="Deadpond", secret_name="Dive Wilson", team=team_z_force ) session.add(hero_deadpond) session.commit() diff --git a/docs_src/tutorial/code_structure/tutorial001/models.py b/docs_src/tutorial/code_structure/tutorial001/models.py index 5d83e19e97..9bd1fa93f2 100644 --- a/docs_src/tutorial/code_structure/tutorial001/models.py +++ b/docs_src/tutorial/code_structure/tutorial001/models.py @@ -1,8 +1,8 @@ -from pydantic import condecimal from typing import List, Optional from sqlmodel import Field, Relationship, SQLModel + class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str @@ -16,7 +16,6 @@ class Hero(SQLModel, table=True): name: str secret_name: str age: Optional[int] = None - experience_points: condecimal(max_digits=6, decimal_places=3) = Field(default=0, nullable=False, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002/app.py b/docs_src/tutorial/code_structure/tutorial002/app.py index ce3097adcb..8afaee7c16 100644 --- a/docs_src/tutorial/code_structure/tutorial002/app.py +++ b/docs_src/tutorial/code_structure/tutorial002/app.py @@ -10,7 +10,7 @@ def create_heroes(): team_z_force = Team(name="Z-Force", headquarters="Sister Margaretโ€™s Bar") hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force, experience_points=1 + name="Deadpond", secret_name="Dive Wilson", team=team_z_force ) session.add(hero_deadpond) session.commit() diff --git a/docs_src/tutorial/code_structure/tutorial002/hero_model.py b/docs_src/tutorial/code_structure/tutorial002/hero_model.py index 064e16ef30..84fc7f276b 100644 --- a/docs_src/tutorial/code_structure/tutorial002/hero_model.py +++ b/docs_src/tutorial/code_structure/tutorial002/hero_model.py @@ -1,4 +1,3 @@ -from pydantic import condecimal from typing import TYPE_CHECKING, Optional from sqlmodel import Field, Relationship, SQLModel @@ -12,7 +11,6 @@ class Hero(SQLModel, table=True): name: str secret_name: str age: Optional[int] = None - experience_points: condecimal(max_digits=6, decimal_places=3) = Field(default=0, nullable=False, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional["Team"] = Relationship(back_populates="heroes") diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index e98a25c756..4192e00434 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -4,7 +4,6 @@ from ...conftest import get_testing_print_function - expected_calls = [ [ "Created hero:", @@ -14,7 +13,6 @@ "age": None, "secret_name": "Dive Wilson", "team_id": 1, - "experience_points": 1, }, ], [ diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index 00ebcb0d90..37e4e54de6 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -13,7 +13,6 @@ "age": None, "secret_name": "Dive Wilson", "team_id": 1, - "experience_points": 1, }, ], [ From 78cedad4f1dc18e07d887c3c031c81310182618a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 10:52:20 +0100 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=93=9D=20Add=20example=20for=20usin?= =?UTF-8?q?g=20Decimals=20in=20Advanced=20User=20Guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/advanced/__init__.py | 0 docs_src/advanced/decimal/__init__.py | 0 docs_src/advanced/decimal/tutorial001.py | 61 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 docs_src/advanced/__init__.py create mode 100644 docs_src/advanced/decimal/__init__.py create mode 100644 docs_src/advanced/decimal/tutorial001.py diff --git a/docs_src/advanced/__init__.py b/docs_src/advanced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/decimal/__init__.py b/docs_src/advanced/decimal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001.py new file mode 100644 index 0000000000..fe5936f579 --- /dev/null +++ b/docs_src/advanced/decimal/tutorial001.py @@ -0,0 +1,61 @@ +from typing import Optional + +from pydantic import condecimal +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + money: condecimal(max_digits=6, decimal_places=3) = Field(default=0) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1) + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001) + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Rusty-Man") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + total_money = hero_1.money + hero_2.money + print(f"Total money: {total_money}") + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() From 82306b84fa17d8699896a27edc9d864d4a587abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 10:53:11 +0100 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20for=20Decimal?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/decimal.md | 123 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/advanced/decimal.md diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md new file mode 100644 index 0000000000..40bcc727e9 --- /dev/null +++ b/docs/advanced/decimal.md @@ -0,0 +1,123 @@ +# Decimal Numbers + +In some cases you might need to be able to store decimal numbers with guarantees about the precision. + +This is particularly important if you are storing things like **currencies**, **prices**, **accounts**, and others, as you would want to know that you wouldn't have rounding errors. + +As an example, if you open Python and sum `1.1` + `2.2` you would expect to see `3.3`, but you will actually get `3.3000000000000003`: + +```Python +>>> 1.1 + 2.2 +3.3000000000000003 +``` + +This is because of the way numbers are stored in "ones and zeros" (binary). But Python has a module and some types to have strict decimal values. You can read more about it in the official Python docs for Decimal. + +Because databases store data in the same ways as computers (in binary), they would have the same types of issues. And because of that, they also have a special **decimal** type. + +In most cases this would probably not be a problem, for example measuring views in a video, or the life bar in a videogame. But as you can imagine, this is particularly important when dealing with **money** and **finances**. + +## Decimal Types + +Pydantic has special support for `Decimal` types using the `condecimal()` special function. + +!!! tip + Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function. + + But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here. + +When you use `condecimal()` you can specify the number of decimal places and digits to support. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. + +!!! info + For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. + +## Decimals in SQLModel + +Let's say that each hero in the database will have an amount of money. We could make that field a `Decimal` type using the `condecimal()` function: + +```{.python .annotate hl_lines="12" } +{!./docs_src/advanced/decimal/tutorial001.py[ln:1-12]!} + +# More code here later ๐Ÿ‘‡ +``` + +
+๐Ÿ‘€ Full file preview + +```Python +{!./docs_src/advanced/decimal/tutorial001.py!} +``` + +
+ +## Create models with Decimals + +When creating new models you can actually pass normal (`float`) numbers, Pydantic will automatically convert them to `Decimal` types, and **SQLModel** will store them as `Decimal` types in the database (using SQLAlchemy). + +```Python hl_lines="4-6" +# Code above omitted ๐Ÿ‘† + +{!./docs_src/advanced/decimal/tutorial001.py[ln:25-35]!} + +# Code below omitted ๐Ÿ‘‡ +``` + +
+๐Ÿ‘€ Full file preview + +```Python +{!./docs_src/advanced/decimal/tutorial001.py!} +``` + +
+ +## Select Decimal data + +Then, when working with Decimal types, you can confirm that they indeed avoid those rounding errors from floats: + +```Python hl_lines="15-16" +# Code above omitted ๐Ÿ‘† + +{!./docs_src/advanced/decimal/tutorial001.py[ln:38-51]!} + +# Code below omitted ๐Ÿ‘‡ +``` + +
+๐Ÿ‘€ Full file preview + +```Python +{!./docs_src/advanced/decimal/tutorial001.py!} +``` + +
+ +## Review the results + +Now if you run this, instead of printing the unexpected number `3.3000000000000003`, it prints `3.300`: + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted ๐Ÿ˜‰ + +// The type of money is Decimal('1.100') +Hero 1: id=1 secret_name='Dive Wilson' age=None name='Deadpond' money=Decimal('1.100') + +// More output omitted here ๐Ÿค“ + +// The type of money is Decimal('1.100') +Hero 2: id=3 secret_name='Tommy Sharp' age=48 name='Rusty-Man' money=Decimal('2.200') + +// No rounding errors, just 3.3! ๐ŸŽ‰ +Total money: 3.300 +``` + +
+ +!!! warning + Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports. + + But decimals are supported by most of the other SQL databases. ๐ŸŽ‰ From 8836629bd817dc5b905521c9ff22872f61b04c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 10:53:30 +0100 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20index=20fo?= =?UTF-8?q?r=20Advanced=20User=20Guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/index.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/advanced/index.md b/docs/advanced/index.md index 588ac1d0e0..f6178249ce 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -1,12 +1,10 @@ # Advanced User Guide -The **Advanced User Guide** will be coming soon to a theater **documentation** near you... ๐Ÿ˜… +The **Advanced User Guide** is gradually growing, you can already read about some advanced topics. -I just have to `add` it, `commit` it, and `refresh` it. ๐Ÿ˜‰ +At some point it will include: -It will include: - -* How to use the `async` and `await` with the async session. +* How to use `async` and `await` with the async session. * How to run migrations. * How to combine **SQLModel** models with SQLAlchemy. -* ...and more. +* ...and more. ๐Ÿค“ From d8739bddcc551e36259a6ba3b01e1dc6d6b7e906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 10:54:07 +0100 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=94=A7=20Add=20Decimal=20docs=20to?= =?UTF-8?q?=20MkDocs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 5ebc361083..7c4520e9a8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,7 @@ nav: - tutorial/fastapi/tests.md - Advanced User Guide: - advanced/index.md + - advanced/decimal.md - alternatives.md - help.md - contributing.md From 03c562b3a16d383d16d39b7122f7b59a56b51ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 11:14:13 +0100 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=8E=A8=20Format=20expression=20code?= =?UTF-8?q?=20and=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/sql/expression.py | 1 - sqlmodel/sql/expression.py.jinja2 | 1 - 2 files changed, 2 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index bf6ea38ec6..e7317bcdd8 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -38,7 +38,6 @@ class Select(_Select, Generic[_TSelect]): class SelectOfScalar(_Select, Generic[_TSelect]): pass - else: from typing import GenericMeta # type: ignore diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 9cd5d3f33e..033130393a 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -36,7 +36,6 @@ if sys.version_info.minor >= 7: class SelectOfScalar(_Select, Generic[_TSelect]): pass - else: from typing import GenericMeta # type: ignore From f3ee8b939b0549432dbc56907f8e4c9e1ad8efab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 11:53:36 +0100 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20decimal?= =?UTF-8?q?=20tutorial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_advanced/__init__.py | 0 tests/test_advanced/test_decimal/__init__.py | 0 .../test_decimal/test_tutorial001.py | 44 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tests/test_advanced/__init__.py create mode 100644 tests/test_advanced/test_decimal/__init__.py create mode 100644 tests/test_advanced/test_decimal/test_tutorial001.py diff --git a/tests/test_advanced/__init__.py b/tests/test_advanced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_decimal/__init__.py b/tests/test_advanced/test_decimal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py new file mode 100644 index 0000000000..1dafdfb322 --- /dev/null +++ b/tests/test_advanced/test_decimal/test_tutorial001.py @@ -0,0 +1,44 @@ +from decimal import Decimal +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + +expected_calls = [ + [ + "Hero 1:", + { + "name": "Deadpond", + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "money": Decimal("1.100"), + }, + ], + [ + "Hero 2:", + { + "name": "Rusty-Man", + "age": 48, + "id": 3, + "secret_name": "Tommy Sharp", + "money": Decimal("2.200"), + }, + ], + ["Total money: 3.300"], +] + + +def test_tutorial(clear_sqlmodel): + from docs_src.advanced.decimal import tutorial001 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls From 0a83e2bcbea96fcf736b247e740885df369eb95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 12:26:58 +0100 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20deci?= =?UTF-8?q?mals,=20include=20explanation=20and=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/decimal.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index 40bcc727e9..c0541b75df 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -26,7 +26,7 @@ Pydantic has special support for `Decimal` types using the SQLAlchemy's `DECIMAL` type. @@ -50,6 +50,31 @@ Let's say that each hero in the database will have an amount of money. We could +Here we are saying that `money` can have at most `5` digits with `max_digits`, **this includes the integers** (to the left of the decimal dot) **and the decimals** (to the right of the decimal dot). + +We are also saying that the number of decimal places (to the right of the decimal dot) is `3`, so we can have **3 decimal digits** for these numbers in the `money` field. This means that we will have **2 digits for the integer part** and **3 digits for the decimal part**. + +โœ… So, for example, these are all valid numbers for the `money` field: + +* `12.345` +* `12.3` +* `12` +* `1.2` +* `0.123` +* `0` + +๐Ÿšซ But these are all invalid numbers for that `money` field: + +* `1.2345` + * This number has more than 3 decimal places. +* `123.234` + * This number has more than 5 digits in total (integer and decimal part). +* `123` + * Even though this number doesn't have any decimals, we still have 3 places saved for them, which means that we can **only use 2 places** for the **integer part**, and this number has 3 integer digits. So, the allowed number of integer digits is `max_digits` - `decimal_places` = 2. + +!!! tip + Make sure you adjust the number of digits and decimal places for your own needs, in your own application. ๐Ÿค“ + ## Create models with Decimals When creating new models you can actually pass normal (`float`) numbers, Pydantic will automatically convert them to `Decimal` types, and **SQLModel** will store them as `Decimal` types in the database (using SQLAlchemy).