add sql_nullable param

This commit is contained in:
collerek
2021-09-08 09:22:29 +02:00
parent cd87303b5c
commit 95adc7146a
7 changed files with 108 additions and 8 deletions

View File

@ -657,7 +657,8 @@ The following keyword arguments are supported on all field types.
All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `None`.
* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details.
* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details.
* `default` - Set a default value for the field. **Not available for relation fields**
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.

View File

@ -22,18 +22,41 @@ Used both in sql and pydantic (changes pk field to optional for autoincrement).
## nullable
`nullable`: `bool` = `not primary_key` -> defaults to False for primary key column, and True for all other.
`nullable`: `bool` = `False` -> defaults to False for all fields except relation fields.
Automatically changed to True if user provide one of the following:
* `default` value or function is provided
* `server_default` value or function is provided
* `autoincrement` is set on `Integer` `primary_key` field
* **[DEPRECATED]**`pydantic_only=True` is set
Specifies if field is optional or required, used both with sql and pydantic.
By default, used for both `pydantic` and `sqlalchemy` as those are the most common settings:
* `nullable=False` - means database column is not null and field is required in pydantic
* `nullable=True` - means database column is null and field is optional in pydantic
If you want to set different setting for pydantic and the database see `sql_nullable` below.
!!!note
By default all `ForeignKeys` are also nullable, meaning the related `Model` is not required.
If you change the `ForeignKey` column to `nullable=False`, it becomes required.
!!!info
If you want to know more about how you can preload related models during queries and how the relations work read the [queries][queries] and [relations][relations] sections.
## sql_nullable
`sql_nullable`: `bool` = `nullable` -> defaults to the value of nullable (described above).
Specifies if field is not null or allows nulls in the database only.
Use this setting in combination with `nullable` only if you want to set different options on pydantic model and in the database.
A sample usage might be i.e. making field not null in the database, but allow this field to be nullable in pydantic (i.e. with `server_default` value).
That will prevent the updates of the field to null (as with `server_default` set you cannot insert null values already as the default value would be used)
## default

View File

@ -660,7 +660,8 @@ The following keyword arguments are supported on all field types.
All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `None`.
* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details.
* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details.
* `default` - Set a default value for the field. **Not available for relation fields**
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.

View File

@ -43,6 +43,7 @@ class BaseField(FieldInfo):
self.primary_key: bool = kwargs.pop("primary_key", False)
self.autoincrement: bool = kwargs.pop("autoincrement", False)
self.nullable: bool = kwargs.pop("nullable", False)
self.sql_nullable: bool = kwargs.pop("sql_nullable", False)
self.index: bool = kwargs.pop("index", False)
self.unique: bool = kwargs.pop("unique", False)
self.pydantic_only: bool = kwargs.pop("pydantic_only", False)
@ -265,7 +266,7 @@ class BaseField(FieldInfo):
self.column_type,
*self.construct_constraints(),
primary_key=self.primary_key,
nullable=self.nullable and not self.primary_key,
nullable=self.sql_nullable,
index=self.index,
unique=self.unique,
default=self.ormar_default,

View File

@ -234,6 +234,9 @@ def ForeignKey( # noqa CFQ002
skip_reverse = kwargs.pop("skip_reverse", False)
skip_field = kwargs.pop("skip_field", False)
sql_nullable = kwargs.pop("sql_nullable", None)
sql_nullable = nullable if sql_nullable is None else sql_nullable
validate_not_allowed_fields(kwargs)
if to.__class__ == ForwardRef:
@ -255,6 +258,7 @@ def ForeignKey( # noqa CFQ002
alias=name,
name=kwargs.pop("real_name", None),
nullable=nullable,
sql_nullable=sql_nullable,
constraints=constraints,
unique=unique,
column_type=column_type,

View File

@ -75,6 +75,7 @@ class ModelFieldFactory:
default = kwargs.pop("default", None)
server_default = kwargs.pop("server_default", None)
nullable = kwargs.pop("nullable", None)
sql_nullable = kwargs.pop("sql_nullable", None)
pydantic_only = kwargs.pop("pydantic_only", False)
primary_key = kwargs.pop("primary_key", False)
@ -86,6 +87,13 @@ class ModelFieldFactory:
overwrite_pydantic_type = kwargs.pop("overwrite_pydantic_type", None)
nullable = is_field_nullable(
nullable, default, server_default, pydantic_only
) or is_auto_primary_key(primary_key, autoincrement)
sql_nullable = (
nullable if sql_nullable is None else (sql_nullable and not primary_key)
)
namespace = dict(
__type__=cls._type,
__pydantic_type__=overwrite_pydantic_type
@ -97,8 +105,8 @@ class ModelFieldFactory:
primary_key=primary_key,
default=default,
server_default=server_default,
nullable=is_field_nullable(nullable, default, server_default, pydantic_only)
or is_auto_primary_key(primary_key, autoincrement),
nullable=nullable,
sql_nullable=sql_nullable,
index=kwargs.pop("index", False),
unique=kwargs.pop("unique", False),
pydantic_only=pydantic_only,

View File

@ -0,0 +1,62 @@
import sqlite3
from typing import Optional
import asyncpg
import databases
import pymysql
import sqlalchemy
from sqlalchemy import create_engine, text
import ormar
import pytest
from tests.settings import DATABASE_URL
db = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = db
class PrimaryModel(ormar.Model):
class Meta(BaseMeta):
tablename = "primary_models"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=255, index=True)
some_text: Optional[str] = ormar.Text(nullable=True, sql_nullable=False)
some_other_text: Optional[str] = ormar.Text(
nullable=True, sql_nullable=False, server_default=text("''")
)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = create_engine(DATABASE_URL)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_create_models():
async with db:
primary = await PrimaryModel(
name="Foo", some_text="Bar", some_other_text="Baz"
).save()
assert primary.id == 1
primary2 = await PrimaryModel(name="Foo2", some_text="Bar2").save()
assert primary2.id == 2
with pytest.raises(
(
sqlite3.IntegrityError,
pymysql.IntegrityError,
asyncpg.exceptions.NotNullViolationError,
)
):
await PrimaryModel(name="Foo3").save()