From 95adc7146a212ab867bcbca310d6cae01d16ee8f Mon Sep 17 00:00:00 2001 From: collerek Date: Wed, 8 Sep 2021 09:22:29 +0200 Subject: [PATCH] add sql_nullable param --- README.md | 3 +- docs/fields/common-parameters.md | 29 ++++++++- docs/index.md | 3 +- ormar/fields/base.py | 3 +- ormar/fields/foreign_key.py | 4 ++ ormar/fields/model_fields.py | 12 +++- .../test_overwriting_sql_nullable.py | 62 +++++++++++++++++++ 7 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 tests/test_model_definition/test_overwriting_sql_nullable.py diff --git a/README.md b/README.md index f89e954..dbd82c8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/fields/common-parameters.md b/docs/fields/common-parameters.md index 044dfe9..79ce920 100644 --- a/docs/fields/common-parameters.md +++ b/docs/fields/common-parameters.md @@ -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 diff --git a/docs/index.md b/docs/index.md index a7807f9..7621fc1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/ormar/fields/base.py b/ormar/fields/base.py index 8eb4188..8d05cdf 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -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, diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index 2326a85..3151ba6 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -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, diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index 8478718..ac1d534 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -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, diff --git a/tests/test_model_definition/test_overwriting_sql_nullable.py b/tests/test_model_definition/test_overwriting_sql_nullable.py new file mode 100644 index 0000000..9c2bbdf --- /dev/null +++ b/tests/test_model_definition/test_overwriting_sql_nullable.py @@ -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()