add sql_nullable param
This commit is contained in:
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
62
tests/test_model_definition/test_overwriting_sql_nullable.py
Normal file
62
tests/test_model_definition/test_overwriting_sql_nullable.py
Normal 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()
|
||||
Reference in New Issue
Block a user