Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ce45588b | ||
|
|
b3e1a66a21 | ||
|
|
c94db7b8a0 | ||
|
|
a67326d358 | ||
|
|
e88b5d3691 | ||
|
|
fdb049bee3 | ||
|
|
ae144e0a39 | ||
|
|
85f5e7fc45 | ||
|
|
b51ebaf658 | ||
|
|
f232166db5 | ||
|
|
4143edd251 |
@@ -3,6 +3,18 @@
|
||||
## Latest Changes
|
||||
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs).
|
||||
* ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox).
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Features
|
||||
|
||||
@@ -46,7 +46,7 @@ We will continue with the code from the previous chapter.
|
||||
|
||||
## Break a Connection
|
||||
|
||||
We don't really have to delete anyting to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`.
|
||||
We don't really have to delete anything to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`.
|
||||
|
||||
Let's say **Spider-Boy** is tired of the lack of friendly neighbors and wants to get out of the **Preventers**.
|
||||
|
||||
|
||||
@@ -498,7 +498,7 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`.
|
||||
|
||||
Let's put it in a function `create_db_and_tables()`:
|
||||
|
||||
```Python hl_lines="22-23"
|
||||
```Python hl_lines="19-20"
|
||||
{!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!}
|
||||
|
||||
# More code here later 👇
|
||||
@@ -513,9 +513,9 @@ Let's put it in a function `create_db_and_tables()`:
|
||||
|
||||
</details>
|
||||
|
||||
If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time**.
|
||||
If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module.
|
||||
|
||||
We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function.
|
||||
We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function, because we can make sure that the tables are created only when we call that function, and not when this module is imported somewhere else.
|
||||
|
||||
Now we would be able to, for example, import the `Hero` class in some other file without having those **side effects**.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
@@ -52,4 +53,11 @@ result = (
|
||||
|
||||
result = black.format_str(result, mode=black.Mode())
|
||||
|
||||
current_content = destiny_path.read_text()
|
||||
|
||||
if current_content != result and os.getenv("CHECK_JINJA"):
|
||||
raise RuntimeError(
|
||||
"sqlmodel/sql/expression.py content not update with Jinja2 template"
|
||||
)
|
||||
|
||||
destiny_path.write_text(result)
|
||||
|
||||
@@ -7,3 +7,5 @@ mypy sqlmodel
|
||||
flake8 sqlmodel tests docs_src
|
||||
black sqlmodel tests docs_src --check
|
||||
isort sqlmodel tests docs_src scripts --check-only
|
||||
# TODO: move this to test.sh after deprecating Python 3.6
|
||||
CHECK_JINJA=1 python scripts/generate_select.py
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = "0.0.7"
|
||||
__version__ = "0.0.8"
|
||||
|
||||
# Re-export from SQLAlchemy
|
||||
from sqlalchemy.engine import create_mock_engine as create_mock_engine
|
||||
|
||||
@@ -423,11 +423,13 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore
|
||||
index = getattr(field.field_info, "index", Undefined)
|
||||
if index is Undefined:
|
||||
index = False
|
||||
nullable = not primary_key and _is_field_noneable(field)
|
||||
# Override derived nullability if the nullable property is set explicitly
|
||||
# on the field
|
||||
if hasattr(field.field_info, "nullable"):
|
||||
field_nullable = getattr(field.field_info, "nullable")
|
||||
if field_nullable != Undefined:
|
||||
nullable = field_nullable
|
||||
nullable = not primary_key and _is_field_nullable(field)
|
||||
args = []
|
||||
foreign_key = getattr(field.field_info, "foreign_key", None)
|
||||
unique = getattr(field.field_info, "unique", False)
|
||||
@@ -644,11 +646,10 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
|
||||
return cls.__name__.lower()
|
||||
|
||||
|
||||
def _is_field_nullable(field: ModelField) -> bool:
|
||||
def _is_field_noneable(field: ModelField) -> bool:
|
||||
if not field.required:
|
||||
# Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947)
|
||||
is_optional = field.allow_none and (
|
||||
return field.allow_none and (
|
||||
field.shape != SHAPE_SINGLETON or not field.sub_fields
|
||||
)
|
||||
return is_optional and field.default is None and field.default_factory is None
|
||||
return False
|
||||
|
||||
@@ -29,14 +29,14 @@ _TSelect = TypeVar("_TSelect")
|
||||
if sys.version_info.minor >= 7:
|
||||
|
||||
class Select(_Select, Generic[_TSelect]):
|
||||
pass
|
||||
inherit_cache = True
|
||||
|
||||
# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different
|
||||
# purpose. This is the same as a normal SQLAlchemy Select class where there's only one
|
||||
# entity, so the result will be converted to a scalar by default. This way writing
|
||||
# for loops on the results will feel natural.
|
||||
class SelectOfScalar(_Select, Generic[_TSelect]):
|
||||
pass
|
||||
inherit_cache = True
|
||||
|
||||
else:
|
||||
from typing import GenericMeta # type: ignore
|
||||
@@ -45,10 +45,10 @@ else:
|
||||
pass
|
||||
|
||||
class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
|
||||
pass
|
||||
inherit_cache = True
|
||||
|
||||
class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
|
||||
pass
|
||||
inherit_cache = True
|
||||
|
||||
# Cast them for editors to work correctly, from several tricks tried, this works
|
||||
# for both VS Code and PyCharm
|
||||
|
||||
125
tests/test_nullable.py
Normal file
125
tests/test_nullable.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine
|
||||
|
||||
|
||||
def test_nullable_fields(clear_sqlmodel, caplog):
|
||||
class Hero(SQLModel, table=True):
|
||||
primary_key: Optional[int] = Field(
|
||||
default=None,
|
||||
primary_key=True,
|
||||
)
|
||||
required_value: str
|
||||
optional_default_ellipsis: Optional[str] = Field(default=...)
|
||||
optional_default_none: Optional[str] = Field(default=None)
|
||||
optional_non_nullable: Optional[str] = Field(
|
||||
nullable=False,
|
||||
)
|
||||
optional_nullable: Optional[str] = Field(
|
||||
nullable=True,
|
||||
)
|
||||
optional_default_ellipses_non_nullable: Optional[str] = Field(
|
||||
default=...,
|
||||
nullable=False,
|
||||
)
|
||||
optional_default_ellipses_nullable: Optional[str] = Field(
|
||||
default=...,
|
||||
nullable=True,
|
||||
)
|
||||
optional_default_none_non_nullable: Optional[str] = Field(
|
||||
default=None,
|
||||
nullable=False,
|
||||
)
|
||||
optional_default_none_nullable: Optional[str] = Field(
|
||||
default=None,
|
||||
nullable=True,
|
||||
)
|
||||
default_ellipses_non_nullable: str = Field(default=..., nullable=False)
|
||||
optional_default_str: Optional[str] = "default"
|
||||
optional_default_str_non_nullable: Optional[str] = Field(
|
||||
default="default", nullable=False
|
||||
)
|
||||
optional_default_str_nullable: Optional[str] = Field(
|
||||
default="default", nullable=True
|
||||
)
|
||||
str_default_str: str = "default"
|
||||
str_default_str_non_nullable: str = Field(default="default", nullable=False)
|
||||
str_default_str_nullable: str = Field(default="default", nullable=True)
|
||||
str_default_ellipsis_non_nullable: str = Field(default=..., nullable=False)
|
||||
str_default_ellipsis_nullable: str = Field(default=..., nullable=True)
|
||||
|
||||
engine = create_engine("sqlite://", echo=True)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
create_table_log = [
|
||||
message for message in caplog.messages if "CREATE TABLE hero" in message
|
||||
][0]
|
||||
assert "primary_key INTEGER NOT NULL," in create_table_log
|
||||
assert "required_value VARCHAR NOT NULL," in create_table_log
|
||||
assert "optional_default_ellipsis VARCHAR NOT NULL," in create_table_log
|
||||
assert "optional_default_none VARCHAR," in create_table_log
|
||||
assert "optional_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
assert "optional_nullable VARCHAR," in create_table_log
|
||||
assert (
|
||||
"optional_default_ellipses_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
)
|
||||
assert "optional_default_ellipses_nullable VARCHAR," in create_table_log
|
||||
assert "optional_default_none_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
assert "optional_default_none_nullable VARCHAR," in create_table_log
|
||||
assert "default_ellipses_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
assert "optional_default_str VARCHAR," in create_table_log
|
||||
assert "optional_default_str_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
assert "optional_default_str_nullable VARCHAR," in create_table_log
|
||||
assert "str_default_str VARCHAR NOT NULL," in create_table_log
|
||||
assert "str_default_str_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
assert "str_default_str_nullable VARCHAR," in create_table_log
|
||||
assert "str_default_ellipsis_non_nullable VARCHAR NOT NULL," in create_table_log
|
||||
assert "str_default_ellipsis_nullable VARCHAR," in create_table_log
|
||||
|
||||
|
||||
# Test for regression in https://github.com/tiangolo/sqlmodel/issues/420
|
||||
def test_non_nullable_optional_field_with_no_default_set(clear_sqlmodel, caplog):
|
||||
class Hero(SQLModel, table=True):
|
||||
primary_key: Optional[int] = Field(
|
||||
default=None,
|
||||
primary_key=True,
|
||||
)
|
||||
|
||||
optional_non_nullable_no_default: Optional[str] = Field(nullable=False)
|
||||
|
||||
engine = create_engine("sqlite://", echo=True)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
create_table_log = [
|
||||
message for message in caplog.messages if "CREATE TABLE hero" in message
|
||||
][0]
|
||||
assert "primary_key INTEGER NOT NULL," in create_table_log
|
||||
assert "optional_non_nullable_no_default VARCHAR NOT NULL," in create_table_log
|
||||
|
||||
# We can create a hero with `None` set for the optional non-nullable field
|
||||
hero = Hero(primary_key=123, optional_non_nullable_no_default=None)
|
||||
# But we cannot commit it.
|
||||
with Session(engine) as session:
|
||||
session.add(hero)
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
|
||||
|
||||
def test_nullable_primary_key(clear_sqlmodel, caplog):
|
||||
# Probably the weirdest corner case, it shouldn't happen anywhere, but let's test it
|
||||
class Hero(SQLModel, table=True):
|
||||
nullable_integer_primary_key: Optional[int] = Field(
|
||||
default=None,
|
||||
primary_key=True,
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
engine = create_engine("sqlite://", echo=True)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
create_table_log = [
|
||||
message for message in caplog.messages if "CREATE TABLE hero" in message
|
||||
][0]
|
||||
assert "nullable_integer_primary_key INTEGER," in create_table_log
|
||||
Reference in New Issue
Block a user