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: 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** * `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** * `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. * `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
`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. 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 !!!note
By default all `ForeignKeys` are also nullable, meaning the related `Model` is not required. 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. If you change the `ForeignKey` column to `nullable=False`, it becomes required.
!!!info ## sql_nullable
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`: `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 ## 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: 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** * `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** * `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. * `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.primary_key: bool = kwargs.pop("primary_key", False)
self.autoincrement: bool = kwargs.pop("autoincrement", False) self.autoincrement: bool = kwargs.pop("autoincrement", False)
self.nullable: bool = kwargs.pop("nullable", 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.index: bool = kwargs.pop("index", False)
self.unique: bool = kwargs.pop("unique", False) self.unique: bool = kwargs.pop("unique", False)
self.pydantic_only: bool = kwargs.pop("pydantic_only", False) self.pydantic_only: bool = kwargs.pop("pydantic_only", False)
@ -265,7 +266,7 @@ class BaseField(FieldInfo):
self.column_type, self.column_type,
*self.construct_constraints(), *self.construct_constraints(),
primary_key=self.primary_key, primary_key=self.primary_key,
nullable=self.nullable and not self.primary_key, nullable=self.sql_nullable,
index=self.index, index=self.index,
unique=self.unique, unique=self.unique,
default=self.ormar_default, default=self.ormar_default,

View File

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

View File

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