added unique columns constraints to Meta options

This commit is contained in:
collerek
2020-10-01 11:42:20 +02:00
parent c4d1d00ad3
commit d0b6e75470
7 changed files with 107 additions and 42 deletions

BIN
.coverage

Binary file not shown.

View File

@ -394,8 +394,8 @@ All fields are required unless one of the following is set:
* `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.
Autoincrement is set by default on int primary keys. Autoincrement is set by default on int primary keys.
Available Model Fields: Available Model Fields (with required args - optional ones in docs):
* `String(length)` * `String(max_length)`
* `Text()` * `Text()`
* `Boolean()` * `Boolean()`
* `Integer()` * `Integer()`

View File

@ -14,6 +14,7 @@ from ormar.fields import (
Text, Text,
Time, Time,
UUID, UUID,
UniqueColumns,
) )
from ormar.models import Model from ormar.models import Model
from ormar.queryset import QuerySet from ormar.queryset import QuerySet
@ -51,4 +52,5 @@ __all__ = [
"RelationType", "RelationType",
"Undefined", "Undefined",
"UUID", "UUID",
"UniqueColumns",
] ]

View File

@ -1,5 +1,5 @@
from ormar.fields.base import BaseField from ormar.fields.base import BaseField
from ormar.fields.foreign_key import ForeignKey from ormar.fields.foreign_key import ForeignKey, UniqueColumns
from ormar.fields.many_to_many import ManyToMany, ManyToManyField from ormar.fields.many_to_many import ManyToMany, ManyToManyField
from ormar.fields.model_fields import ( from ormar.fields.model_fields import (
BigInteger, BigInteger,
@ -33,4 +33,5 @@ __all__ = [
"ManyToMany", "ManyToMany",
"ManyToManyField", "ManyToManyField",
"BaseField", "BaseField",
"UniqueColumns",
] ]

View File

@ -1,6 +1,7 @@
from typing import Any, Generator, List, Optional, TYPE_CHECKING, Type, Union from typing import Any, Generator, List, Optional, TYPE_CHECKING, Type, Union
import sqlalchemy import sqlalchemy
from sqlalchemy import UniqueConstraint
import ormar # noqa I101 import ormar # noqa I101
from ormar.exceptions import RelationshipInstanceError from ormar.exceptions import RelationshipInstanceError
@ -22,6 +23,10 @@ def create_dummy_instance(fk: Type["Model"], pk: Any = None) -> "Model":
return fk(**init_dict) return fk(**init_dict)
class UniqueColumns(UniqueConstraint):
pass
def ForeignKey( # noqa CFQ002 def ForeignKey( # noqa CFQ002
to: Type["Model"], to: Type["Model"],
*, *,

View File

@ -6,6 +6,7 @@ import pydantic
import sqlalchemy import sqlalchemy
from pydantic import BaseConfig from pydantic import BaseConfig
from pydantic.fields import FieldInfo, ModelField from pydantic.fields import FieldInfo, ModelField
from sqlalchemy.sql.schema import ColumnCollectionConstraint
import ormar # noqa I100 import ormar # noqa I100
from ormar import ForeignKey, ModelDefinitionError, Integer # noqa I100 from ormar import ForeignKey, ModelDefinitionError, Integer # noqa I100
@ -27,6 +28,7 @@ class ModelMeta:
metadata: sqlalchemy.MetaData metadata: sqlalchemy.MetaData
database: databases.Database database: databases.Database
columns: List[sqlalchemy.Column] columns: List[sqlalchemy.Column]
constraints: List[ColumnCollectionConstraint]
pkname: str pkname: str
model_fields: Dict[ model_fields: Dict[
str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]] str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]]
@ -246,7 +248,7 @@ def populate_meta_sqlalchemy_table_if_required(
) -> Type["Model"]: ) -> Type["Model"]:
if not hasattr(new_model.Meta, "table"): if not hasattr(new_model.Meta, "table"):
new_model.Meta.table = sqlalchemy.Table( new_model.Meta.table = sqlalchemy.Table(
new_model.Meta.tablename, new_model.Meta.metadata, *new_model.Meta.columns new_model.Meta.tablename, new_model.Meta.metadata, *new_model.Meta.columns, *new_model.Meta.constraints
) )
return new_model return new_model
@ -304,6 +306,8 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
) )
if hasattr(new_model, "Meta"): if hasattr(new_model, "Meta"):
if not hasattr(new_model.Meta, 'constraints'):
new_model.Meta.constraints = []
new_model = populate_meta_orm_model_fields(attrs, new_model) new_model = populate_meta_orm_model_fields(attrs, new_model)
new_model = populate_meta_tablename_columns_and_pk(name, new_model) new_model = populate_meta_tablename_columns_and_pk(name, new_model)
new_model = populate_meta_sqlalchemy_table_if_required(new_model) new_model = populate_meta_sqlalchemy_table_if_required(new_model)

View File

@ -0,0 +1,53 @@
import asyncio
import sqlite3
import databases
import pytest
import sqlalchemy
from sqlalchemy.exc import IntegrityError
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Product(ormar.Model):
class Meta:
tablename = "products"
metadata = metadata
database = database
constraints = [ormar.UniqueColumns('name', 'company')]
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
company: ormar.String(max_length=200)
@pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
@pytest.fixture(autouse=True, scope="module")
async def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_unique_columns():
async with database:
async with database.transaction(force_rollback=True):
await Product.objects.create(name='Cookies', company='Nestle')
await Product.objects.create(name='Mars', company='Mars')
await Product.objects.create(name='Mars', company='Nestle')
with pytest.raises((IntegrityError, sqlite3.IntegrityError)):
await Product.objects.create(name='Mars', company='Mars')