refactor and cleanup - drop of resolving relation names as not fully proper, extract mixins from modelproxy to be more maintainable, add some docstrings
This commit is contained in:
@ -50,3 +50,7 @@ def ManyToMany(
|
|||||||
|
|
||||||
class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol):
|
class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol):
|
||||||
through: Type["Model"]
|
through: Type["Model"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_target_field_name(cls) -> str:
|
||||||
|
return cls.to.get_name()
|
||||||
|
|||||||
@ -1,12 +1,56 @@
|
|||||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type
|
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
from ormar import ModelDefinitionError
|
import ormar
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
|
from ormar.models.helpers.pydantic import populate_pydantic_default_values
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
|
def populate_default_options_values(
|
||||||
|
new_model: Type["Model"], model_fields: Dict
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Sets all optional Meta values to it's defaults
|
||||||
|
and set model_fields that were already previously extracted.
|
||||||
|
|
||||||
|
Here should live all options that are not overwritten/set for all models.
|
||||||
|
|
||||||
|
Current options are:
|
||||||
|
* constraints = []
|
||||||
|
* abstract = False
|
||||||
|
|
||||||
|
:param new_model: newly constructed Model
|
||||||
|
:type new_model: Model class
|
||||||
|
:param model_fields:
|
||||||
|
:type model_fields: Union[Dict[str, type], Dict]
|
||||||
|
"""
|
||||||
|
if not hasattr(new_model.Meta, "constraints"):
|
||||||
|
new_model.Meta.constraints = []
|
||||||
|
if not hasattr(new_model.Meta, "model_fields"):
|
||||||
|
new_model.Meta.model_fields = model_fields
|
||||||
|
if not hasattr(new_model.Meta, "abstract"):
|
||||||
|
new_model.Meta.abstract = False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_annotations_and_default_vals(attrs: Dict) -> Tuple[Dict, Dict]:
|
||||||
|
"""
|
||||||
|
Extracts annotations from class namespace dict and triggers
|
||||||
|
extraction of ormar model_fields.
|
||||||
|
|
||||||
|
:param attrs: namespace of the class created
|
||||||
|
:type attrs: Dict
|
||||||
|
:return: namespace of the class updated, dict of extracted model_fields
|
||||||
|
:rtype: Tuple[Dict, Dict]
|
||||||
|
"""
|
||||||
|
key = "__annotations__"
|
||||||
|
attrs[key] = attrs.get(key, {})
|
||||||
|
attrs, model_fields = populate_pydantic_default_values(attrs)
|
||||||
|
return attrs, model_fields
|
||||||
|
|
||||||
|
|
||||||
|
# cannot be in relations helpers due to cyclical import
|
||||||
def validate_related_names_in_relations(
|
def validate_related_names_in_relations(
|
||||||
model_fields: Dict, new_model: Type["Model"]
|
model_fields: Dict, new_model: Type["Model"]
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -28,7 +72,7 @@ def validate_related_names_in_relations(
|
|||||||
if issubclass(field, ForeignKeyField):
|
if issubclass(field, ForeignKeyField):
|
||||||
previous_related_names = already_registered.setdefault(field.to, [])
|
previous_related_names = already_registered.setdefault(field.to, [])
|
||||||
if field.related_name in previous_related_names:
|
if field.related_name in previous_related_names:
|
||||||
raise ModelDefinitionError(
|
raise ormar.ModelDefinitionError(
|
||||||
f"Multiple fields declared on {new_model.get_name(lower=False)} "
|
f"Multiple fields declared on {new_model.get_name(lower=False)} "
|
||||||
f"model leading to {field.to.get_name(lower=False)} model without "
|
f"model leading to {field.to.get_name(lower=False)} model without "
|
||||||
f"related_name property set. \nThere can be only one relation with "
|
f"related_name property set. \nThere can be only one relation with "
|
||||||
|
|||||||
@ -12,74 +12,6 @@ if TYPE_CHECKING: # pragma no cover
|
|||||||
from ormar import Model
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
def verify_related_name_dont_duplicate(
|
|
||||||
child: Type["Model"], parent_model: Type["Model"], related_name: str,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Verifies whether the used related_name (regardless of the fact if user defined or
|
|
||||||
auto generated) is already used on related model, but is connected with other model
|
|
||||||
than the one that we connect right now.
|
|
||||||
|
|
||||||
:raises: ModelDefinitionError if name is already used but lead to different related
|
|
||||||
model
|
|
||||||
:param child: related Model class
|
|
||||||
:type child: ormar.models.metaclass.ModelMetaclass
|
|
||||||
:param parent_model: parent Model class
|
|
||||||
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
|
||||||
:param related_name:
|
|
||||||
:type related_name:
|
|
||||||
:return: None
|
|
||||||
:rtype: None
|
|
||||||
"""
|
|
||||||
if parent_model.Meta.model_fields.get(related_name):
|
|
||||||
fk_field = parent_model.Meta.model_fields.get(related_name)
|
|
||||||
if not fk_field: # pragma: no cover
|
|
||||||
return
|
|
||||||
if fk_field.to != child and fk_field.to.Meta != child.Meta:
|
|
||||||
raise ormar.ModelDefinitionError(
|
|
||||||
f"Relation with related_name "
|
|
||||||
f"'{related_name}' "
|
|
||||||
f"leading to model "
|
|
||||||
f"{parent_model.get_name(lower=False)} "
|
|
||||||
f"cannot be used on model "
|
|
||||||
f"{child.get_name(lower=False)} "
|
|
||||||
f"because it's already used by model "
|
|
||||||
f"{fk_field.to.get_name(lower=False)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_field_not_already_registered(
|
|
||||||
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if child is already registered in parents pydantic fields.
|
|
||||||
|
|
||||||
:raises: ModelDefinitionError if related name is already used but lead to different
|
|
||||||
related model
|
|
||||||
:param child: related Model class
|
|
||||||
:type child: ormar.models.metaclass.ModelMetaclass
|
|
||||||
:param child_model_name: related_name of the child if provided
|
|
||||||
:type child_model_name: str
|
|
||||||
:param parent_model: parent Model class
|
|
||||||
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
|
||||||
:return: result of the check
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
check_result = child_model_name not in parent_model.Meta.model_fields
|
|
||||||
check_result2 = child.get_name() not in parent_model.Meta.model_fields
|
|
||||||
|
|
||||||
if not check_result:
|
|
||||||
verify_related_name_dont_duplicate(
|
|
||||||
child=child, parent_model=parent_model, related_name=child_model_name
|
|
||||||
)
|
|
||||||
if not check_result2:
|
|
||||||
verify_related_name_dont_duplicate(
|
|
||||||
child=child, parent_model=parent_model, related_name=child.get_name()
|
|
||||||
)
|
|
||||||
|
|
||||||
return check_result and check_result2
|
|
||||||
|
|
||||||
|
|
||||||
def create_pydantic_field(
|
def create_pydantic_field(
|
||||||
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
|
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -214,32 +146,6 @@ def get_pydantic_base_orm_config() -> Type[BaseConfig]:
|
|||||||
return Config
|
return Config
|
||||||
|
|
||||||
|
|
||||||
def populate_default_options_values(
|
|
||||||
new_model: Type["Model"], model_fields: Dict
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Sets all optional Meta values to it's defaults
|
|
||||||
and set model_fields that were already previously extracted.
|
|
||||||
|
|
||||||
Here should live all options that are not overwritten/set for all models.
|
|
||||||
|
|
||||||
Current options are:
|
|
||||||
* constraints = []
|
|
||||||
* abstract = False
|
|
||||||
|
|
||||||
:param new_model: newly constructed Model
|
|
||||||
:type new_model: Model class
|
|
||||||
:param model_fields:
|
|
||||||
:type model_fields: Union[Dict[str, type], Dict]
|
|
||||||
"""
|
|
||||||
if not hasattr(new_model.Meta, "constraints"):
|
|
||||||
new_model.Meta.constraints = []
|
|
||||||
if not hasattr(new_model.Meta, "model_fields"):
|
|
||||||
new_model.Meta.model_fields = model_fields
|
|
||||||
if not hasattr(new_model.Meta, "abstract"):
|
|
||||||
new_model.Meta.abstract = False
|
|
||||||
|
|
||||||
|
|
||||||
def get_potential_fields(attrs: Dict) -> Dict:
|
def get_potential_fields(attrs: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
Gets all the fields in current class namespace that are Fields.
|
Gets all the fields in current class namespace that are Fields.
|
||||||
@ -250,19 +156,3 @@ def get_potential_fields(attrs: Dict) -> Dict:
|
|||||||
:rtype: Dict
|
:rtype: Dict
|
||||||
"""
|
"""
|
||||||
return {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
|
return {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
|
||||||
|
|
||||||
|
|
||||||
def extract_annotations_and_default_vals(attrs: Dict) -> Tuple[Dict, Dict]:
|
|
||||||
"""
|
|
||||||
Extracts annotations from class namespace dict and triggers
|
|
||||||
extraction of ormar model_fields.
|
|
||||||
|
|
||||||
:param attrs: namespace of the class created
|
|
||||||
:type attrs: Dict
|
|
||||||
:return: namespace of the class updated, dict of extracted model_fields
|
|
||||||
:rtype: Tuple[Dict, Dict]
|
|
||||||
"""
|
|
||||||
key = "__annotations__"
|
|
||||||
attrs[key] = attrs.get(key, {})
|
|
||||||
attrs, model_fields = populate_pydantic_default_values(attrs)
|
|
||||||
return attrs, model_fields
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
import ormar
|
||||||
from ormar import ForeignKey, ManyToMany
|
from ormar import ForeignKey, ManyToMany
|
||||||
from ormar.fields import ManyToManyField
|
from ormar.fields import ManyToManyField
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
from ormar.models.helpers.pydantic import reverse_field_not_already_registered
|
|
||||||
from ormar.models.helpers.sqlalchemy import adjust_through_many_to_many_model
|
from ormar.models.helpers.sqlalchemy import adjust_through_many_to_many_model
|
||||||
from ormar.relations import AliasManager
|
from ormar.relations import AliasManager
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ def register_relation_on_build(new_model: Type["Model"], field_name: str) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def register_many_to_many_relation_on_build(
|
def register_many_to_many_relation_on_build(
|
||||||
new_model: Type["Model"], field: Type[ManyToManyField]
|
new_model: Type["Model"], field: Type[ManyToManyField], field_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Registers connection between through model and both sides of the m2m relation.
|
Registers connection between through model and both sides of the m2m relation.
|
||||||
@ -43,13 +43,22 @@ def register_many_to_many_relation_on_build(
|
|||||||
|
|
||||||
By default relation name is a model.name.lower().
|
By default relation name is a model.name.lower().
|
||||||
|
|
||||||
|
:param field_name: name of the relation key
|
||||||
|
:type field_name: str
|
||||||
:param new_model: model on which m2m field is declared
|
:param new_model: model on which m2m field is declared
|
||||||
:type new_model: Model class
|
:type new_model: Model class
|
||||||
:param field: relation field
|
:param field: relation field
|
||||||
:type field: ManyToManyField class
|
:type field: ManyToManyField class
|
||||||
"""
|
"""
|
||||||
alias_manager.add_relation_type(field.through, new_model.get_name())
|
alias_manager.add_relation_type(
|
||||||
alias_manager.add_relation_type(field.through, field.to.get_name())
|
field.through, new_model.get_name(), is_multi=True, reverse_name=field_name
|
||||||
|
)
|
||||||
|
alias_manager.add_relation_type(
|
||||||
|
field.through,
|
||||||
|
field.to.get_name(),
|
||||||
|
is_multi=True,
|
||||||
|
reverse_name=field.related_name or new_model.get_name() + "s",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
||||||
@ -133,6 +142,76 @@ def register_relation_in_alias_manager(
|
|||||||
:type field_name: str
|
:type field_name: str
|
||||||
"""
|
"""
|
||||||
if issubclass(field, ManyToManyField):
|
if issubclass(field, ManyToManyField):
|
||||||
register_many_to_many_relation_on_build(new_model=new_model, field=field)
|
register_many_to_many_relation_on_build(
|
||||||
|
new_model=new_model, field=field, field_name=field_name
|
||||||
|
)
|
||||||
elif issubclass(field, ForeignKeyField):
|
elif issubclass(field, ForeignKeyField):
|
||||||
register_relation_on_build(new_model=new_model, field_name=field_name)
|
register_relation_on_build(new_model=new_model, field_name=field_name)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_related_name_dont_duplicate(
|
||||||
|
child: Type["Model"], parent_model: Type["Model"], related_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Verifies whether the used related_name (regardless of the fact if user defined or
|
||||||
|
auto generated) is already used on related model, but is connected with other model
|
||||||
|
than the one that we connect right now.
|
||||||
|
|
||||||
|
:raises: ModelDefinitionError if name is already used but lead to different related
|
||||||
|
model
|
||||||
|
:param child: related Model class
|
||||||
|
:type child: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:param parent_model: parent Model class
|
||||||
|
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:param related_name:
|
||||||
|
:type related_name:
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
if parent_model.Meta.model_fields.get(related_name):
|
||||||
|
fk_field = parent_model.Meta.model_fields.get(related_name)
|
||||||
|
if not fk_field: # pragma: no cover
|
||||||
|
return
|
||||||
|
if fk_field.to != child and fk_field.to.Meta != child.Meta:
|
||||||
|
raise ormar.ModelDefinitionError(
|
||||||
|
f"Relation with related_name "
|
||||||
|
f"'{related_name}' "
|
||||||
|
f"leading to model "
|
||||||
|
f"{parent_model.get_name(lower=False)} "
|
||||||
|
f"cannot be used on model "
|
||||||
|
f"{child.get_name(lower=False)} "
|
||||||
|
f"because it's already used by model "
|
||||||
|
f"{fk_field.to.get_name(lower=False)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_field_not_already_registered(
|
||||||
|
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if child is already registered in parents pydantic fields.
|
||||||
|
|
||||||
|
:raises: ModelDefinitionError if related name is already used but lead to different
|
||||||
|
related model
|
||||||
|
:param child: related Model class
|
||||||
|
:type child: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:param child_model_name: related_name of the child if provided
|
||||||
|
:type child_model_name: str
|
||||||
|
:param parent_model: parent Model class
|
||||||
|
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:return: result of the check
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
check_result = child_model_name not in parent_model.Meta.model_fields
|
||||||
|
check_result2 = child.get_name() not in parent_model.Meta.model_fields
|
||||||
|
|
||||||
|
if not check_result:
|
||||||
|
verify_related_name_dont_duplicate(
|
||||||
|
child=child, parent_model=parent_model, related_name=child_model_name
|
||||||
|
)
|
||||||
|
if not check_result2:
|
||||||
|
verify_related_name_dont_duplicate(
|
||||||
|
child=child, parent_model=parent_model, related_name=child.get_name()
|
||||||
|
)
|
||||||
|
|
||||||
|
return check_result and check_result2
|
||||||
|
|||||||
@ -21,12 +21,14 @@ from ormar import ForeignKey, Integer, ModelDefinitionError # noqa I100
|
|||||||
from ormar.fields import BaseField
|
from ormar.fields import BaseField
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
from ormar.fields.many_to_many import ManyToManyField
|
from ormar.fields.many_to_many import ManyToManyField
|
||||||
from ormar.models.helpers.pydantic import (
|
from ormar.models.helpers.models import (
|
||||||
extract_annotations_and_default_vals,
|
extract_annotations_and_default_vals,
|
||||||
|
populate_default_options_values,
|
||||||
|
)
|
||||||
|
from ormar.models.helpers.pydantic import (
|
||||||
get_potential_fields,
|
get_potential_fields,
|
||||||
get_pydantic_base_orm_config,
|
get_pydantic_base_orm_config,
|
||||||
get_pydantic_field,
|
get_pydantic_field,
|
||||||
populate_default_options_values,
|
|
||||||
)
|
)
|
||||||
from ormar.models.helpers.relations import (
|
from ormar.models.helpers.relations import (
|
||||||
alias_manager,
|
alias_manager,
|
||||||
@ -50,6 +52,12 @@ CONFIG_KEY = "Config"
|
|||||||
|
|
||||||
|
|
||||||
class ModelMeta:
|
class ModelMeta:
|
||||||
|
"""
|
||||||
|
Class used for type hinting.
|
||||||
|
Users can subclass this one for conveniance but it's not required.
|
||||||
|
The only requirement is that ormar.Model has to have inner class with name Meta.
|
||||||
|
"""
|
||||||
|
|
||||||
tablename: str
|
tablename: str
|
||||||
table: sqlalchemy.Table
|
table: sqlalchemy.Table
|
||||||
metadata: sqlalchemy.MetaData
|
metadata: sqlalchemy.MetaData
|
||||||
|
|||||||
5
ormar/models/mixins/__init__.py
Normal file
5
ormar/models/mixins/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from ormar.models.mixins.alias_mixin import AliasMixin
|
||||||
|
from ormar.models.mixins.merge_mixin import MergeModelMixin
|
||||||
|
from ormar.models.mixins.prefetch_mixin import PrefetchQueryMixin
|
||||||
|
|
||||||
|
__all__ = ["MergeModelMixin", "AliasMixin", "PrefetchQueryMixin"]
|
||||||
83
ormar/models/mixins/alias_mixin.py
Normal file
83
ormar/models/mixins/alias_mixin.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from typing import Dict, List, Optional, Set, TYPE_CHECKING, Type, Union
|
||||||
|
|
||||||
|
|
||||||
|
class AliasMixin:
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from ormar import Model, ModelMeta
|
||||||
|
|
||||||
|
Meta: ModelMeta
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_column_alias(cls, field_name: str) -> str:
|
||||||
|
field = cls.Meta.model_fields.get(field_name)
|
||||||
|
return field.get_alias() if field is not None else field_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_column_name_from_alias(cls, alias: str) -> str:
|
||||||
|
for field_name, field in cls.Meta.model_fields.items():
|
||||||
|
if field.get_alias() == alias:
|
||||||
|
return field_name
|
||||||
|
return alias # if not found it's not an alias but actual name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict:
|
||||||
|
for field_name, field in cls.Meta.model_fields.items():
|
||||||
|
if field_name in new_kwargs:
|
||||||
|
new_kwargs[field.get_alias()] = new_kwargs.pop(field_name)
|
||||||
|
return new_kwargs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def translate_aliases_to_columns(cls, new_kwargs: Dict) -> Dict:
|
||||||
|
for field_name, field in cls.Meta.model_fields.items():
|
||||||
|
if field.alias and field.alias in new_kwargs:
|
||||||
|
new_kwargs[field_name] = new_kwargs.pop(field.alias)
|
||||||
|
return new_kwargs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _populate_pk_column(
|
||||||
|
model: Type["Model"], columns: List[str], use_alias: bool = False,
|
||||||
|
) -> List[str]:
|
||||||
|
pk_alias = (
|
||||||
|
model.get_column_alias(model.Meta.pkname)
|
||||||
|
if use_alias
|
||||||
|
else model.Meta.pkname
|
||||||
|
)
|
||||||
|
if pk_alias not in columns:
|
||||||
|
columns.append(pk_alias)
|
||||||
|
return columns
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def own_table_columns(
|
||||||
|
cls,
|
||||||
|
model: Type["Model"],
|
||||||
|
fields: Optional[Union[Set, Dict]],
|
||||||
|
exclude_fields: Optional[Union[Set, Dict]],
|
||||||
|
use_alias: bool = False,
|
||||||
|
) -> List[str]:
|
||||||
|
columns = [
|
||||||
|
model.get_column_name_from_alias(col.name) if not use_alias else col.name
|
||||||
|
for col in model.Meta.table.columns
|
||||||
|
]
|
||||||
|
field_names = [
|
||||||
|
model.get_column_name_from_alias(col.name)
|
||||||
|
for col in model.Meta.table.columns
|
||||||
|
]
|
||||||
|
if fields:
|
||||||
|
columns = [
|
||||||
|
col
|
||||||
|
for col, name in zip(columns, field_names)
|
||||||
|
if model.is_included(fields, name)
|
||||||
|
]
|
||||||
|
if exclude_fields:
|
||||||
|
columns = [
|
||||||
|
col
|
||||||
|
for col, name in zip(columns, field_names)
|
||||||
|
if not model.is_excluded(exclude_fields, name)
|
||||||
|
]
|
||||||
|
|
||||||
|
# always has to return pk column for ormar to work
|
||||||
|
columns = cls._populate_pk_column(
|
||||||
|
model=model, columns=columns, use_alias=use_alias
|
||||||
|
)
|
||||||
|
|
||||||
|
return columns
|
||||||
46
ormar/models/mixins/merge_mixin.py
Normal file
46
ormar/models/mixins/merge_mixin.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
from typing import List, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma no cover
|
||||||
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
|
class MergeModelMixin:
|
||||||
|
@classmethod
|
||||||
|
def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
|
||||||
|
merged_rows: List["Model"] = []
|
||||||
|
grouped_instances: OrderedDict = OrderedDict()
|
||||||
|
|
||||||
|
for model in result_rows:
|
||||||
|
grouped_instances.setdefault(model.pk, []).append(model)
|
||||||
|
|
||||||
|
for group in grouped_instances.values():
|
||||||
|
model = group.pop(0)
|
||||||
|
if group:
|
||||||
|
for next_model in group:
|
||||||
|
model = cls.merge_two_instances(next_model, model)
|
||||||
|
merged_rows.append(model)
|
||||||
|
|
||||||
|
return merged_rows
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def merge_two_instances(cls, one: "Model", other: "Model") -> "Model":
|
||||||
|
for field in one.Meta.model_fields.keys():
|
||||||
|
current_field = getattr(one, field)
|
||||||
|
if isinstance(current_field, list) and not isinstance(
|
||||||
|
current_field, ormar.Model
|
||||||
|
):
|
||||||
|
setattr(other, field, current_field + getattr(other, field))
|
||||||
|
elif (
|
||||||
|
isinstance(current_field, ormar.Model)
|
||||||
|
and current_field.pk == getattr(other, field).pk
|
||||||
|
):
|
||||||
|
setattr(
|
||||||
|
other,
|
||||||
|
field,
|
||||||
|
cls.merge_two_instances(current_field, getattr(other, field)),
|
||||||
|
)
|
||||||
|
other.set_save_status(True)
|
||||||
|
return other
|
||||||
64
ormar/models/mixins/prefetch_mixin.py
Normal file
64
ormar/models/mixins/prefetch_mixin.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from typing import Callable, Dict, List, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
from ormar.fields import BaseField
|
||||||
|
|
||||||
|
|
||||||
|
class PrefetchQueryMixin:
|
||||||
|
if TYPE_CHECKING: # pragma no cover
|
||||||
|
from ormar import Model
|
||||||
|
|
||||||
|
get_name: Callable # defined in NewBaseModel
|
||||||
|
extract_related_names: Callable # defined in ModelTableProxy
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_clause_target_and_filter_column_name(
|
||||||
|
parent_model: Type["Model"],
|
||||||
|
target_model: Type["Model"],
|
||||||
|
reverse: bool,
|
||||||
|
related: str,
|
||||||
|
) -> Tuple[Type["Model"], str]:
|
||||||
|
if reverse:
|
||||||
|
field_name = (
|
||||||
|
parent_model.Meta.model_fields[related].related_name
|
||||||
|
or parent_model.get_name() + "s"
|
||||||
|
)
|
||||||
|
field = target_model.Meta.model_fields[field_name]
|
||||||
|
if issubclass(field, ormar.fields.ManyToManyField):
|
||||||
|
field_name = field.default_target_field_name()
|
||||||
|
sub_field = field.through.Meta.model_fields[field_name]
|
||||||
|
return field.through, sub_field.get_alias()
|
||||||
|
return target_model, field.get_alias()
|
||||||
|
target_field = target_model.get_column_alias(target_model.Meta.pkname)
|
||||||
|
return target_model, target_field
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_column_name_for_id_extraction(
|
||||||
|
parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool,
|
||||||
|
) -> str:
|
||||||
|
if reverse:
|
||||||
|
column_name = parent_model.Meta.pkname
|
||||||
|
return (
|
||||||
|
parent_model.get_column_alias(column_name) if use_raw else column_name
|
||||||
|
)
|
||||||
|
column = parent_model.Meta.model_fields[related]
|
||||||
|
return column.get_alias() if use_raw else column.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_related_field_name(cls, target_field: Type["BaseField"]) -> str:
|
||||||
|
if issubclass(target_field, ormar.fields.ManyToManyField):
|
||||||
|
return cls.get_name()
|
||||||
|
if target_field.virtual:
|
||||||
|
return target_field.related_name or cls.get_name() + "s"
|
||||||
|
return target_field.to.Meta.pkname
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List:
|
||||||
|
related_to_extract = []
|
||||||
|
if prefetch_dict and prefetch_dict is not Ellipsis:
|
||||||
|
related_to_extract = [
|
||||||
|
related
|
||||||
|
for related in cls.extract_related_names()
|
||||||
|
if related in prefetch_dict
|
||||||
|
]
|
||||||
|
return related_to_extract
|
||||||
@ -125,13 +125,11 @@ class Model(NewBaseModel):
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
through_field = previous_model.Meta.model_fields[related_name]
|
through_field = previous_model.Meta.model_fields[related_name]
|
||||||
rel_name2 = previous_model.resolve_relation_name(
|
rel_name2 = through_field.default_target_field_name() # type: ignore
|
||||||
through_field.through, through_field.to, explicit_multi=True
|
|
||||||
)
|
|
||||||
previous_model = through_field.through # type: ignore
|
previous_model = through_field.through # type: ignore
|
||||||
|
|
||||||
if previous_model and rel_name2:
|
if previous_model and rel_name2:
|
||||||
table_prefix = cls.Meta.alias_manager.resolve_relation_join(
|
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
|
||||||
previous_model, rel_name2
|
previous_model, rel_name2
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import inspect
|
import inspect
|
||||||
from collections import OrderedDict
|
|
||||||
from typing import (
|
from typing import (
|
||||||
AbstractSet,
|
AbstractSet,
|
||||||
Any,
|
Any,
|
||||||
@ -8,25 +7,22 @@ from typing import (
|
|||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
|
||||||
Set,
|
Set,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
|
||||||
Type,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
import ormar # noqa: I100
|
import ormar # noqa: I100
|
||||||
from ormar.exceptions import ModelPersistenceError
|
from ormar.exceptions import ModelPersistenceError
|
||||||
from ormar.fields import BaseField, ManyToManyField
|
from ormar.fields import BaseField
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
from ormar.models.metaclass import ModelMeta
|
from ormar.models.metaclass import ModelMeta
|
||||||
|
from ormar.models.mixins import AliasMixin, MergeModelMixin, PrefetchQueryMixin
|
||||||
from ormar.queryset.utils import translate_list_to_dict, update
|
from ormar.queryset.utils import translate_list_to_dict, update
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
from ormar.models import NewBaseModel
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=Model)
|
T = TypeVar("T", bound=Model)
|
||||||
IntStr = Union[int, str]
|
IntStr = Union[int, str]
|
||||||
@ -36,7 +32,7 @@ if TYPE_CHECKING: # pragma no cover
|
|||||||
Field = TypeVar("Field", bound=BaseField)
|
Field = TypeVar("Field", bound=BaseField)
|
||||||
|
|
||||||
|
|
||||||
class ModelTableProxy:
|
class ModelTableProxy(PrefetchQueryMixin, MergeModelMixin, AliasMixin):
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
Meta: ModelMeta
|
Meta: ModelMeta
|
||||||
_related_names: Optional[Set]
|
_related_names: Optional[Set]
|
||||||
@ -46,76 +42,6 @@ class ModelTableProxy:
|
|||||||
_props: Set
|
_props: Set
|
||||||
dict: Callable # noqa: A001, VNE003
|
dict: Callable # noqa: A001, VNE003
|
||||||
|
|
||||||
def _extract_own_model_fields(self) -> Dict:
|
|
||||||
related_names = self.extract_related_names()
|
|
||||||
self_fields = self.dict(exclude=related_names)
|
|
||||||
return self_fields
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_related_field_name(cls, target_field: Type["BaseField"]) -> str:
|
|
||||||
if issubclass(target_field, ormar.fields.ManyToManyField):
|
|
||||||
return cls.resolve_relation_name(
|
|
||||||
target_field.through, cls, explicit_multi=True
|
|
||||||
)
|
|
||||||
if target_field.virtual:
|
|
||||||
return target_field.related_name or cls.get_name() + "s"
|
|
||||||
return target_field.to.Meta.pkname
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_clause_target_and_filter_column_name(
|
|
||||||
parent_model: Type["Model"],
|
|
||||||
target_model: Type["Model"],
|
|
||||||
reverse: bool,
|
|
||||||
related: str,
|
|
||||||
) -> Tuple[Type["Model"], str]:
|
|
||||||
if reverse:
|
|
||||||
field_name = (
|
|
||||||
parent_model.Meta.model_fields[related].related_name
|
|
||||||
or parent_model.get_name() + "s"
|
|
||||||
)
|
|
||||||
field = target_model.Meta.model_fields[field_name]
|
|
||||||
if issubclass(field, ormar.fields.ManyToManyField):
|
|
||||||
field_name = parent_model.resolve_relation_name(
|
|
||||||
field.through, field.to, explicit_multi=True
|
|
||||||
)
|
|
||||||
sub_field = field.through.Meta.model_fields[field_name]
|
|
||||||
return field.through, sub_field.get_alias()
|
|
||||||
return target_model, field.get_alias()
|
|
||||||
target_field = target_model.get_column_alias(target_model.Meta.pkname)
|
|
||||||
return target_model, target_field
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_column_name_for_id_extraction(
|
|
||||||
parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool,
|
|
||||||
) -> str:
|
|
||||||
if reverse:
|
|
||||||
column_name = parent_model.Meta.pkname
|
|
||||||
return (
|
|
||||||
parent_model.get_column_alias(column_name) if use_raw else column_name
|
|
||||||
)
|
|
||||||
column = parent_model.Meta.model_fields[related]
|
|
||||||
return column.get_alias() if use_raw else column.name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List:
|
|
||||||
related_to_extract = []
|
|
||||||
if prefetch_dict and prefetch_dict is not Ellipsis:
|
|
||||||
related_to_extract = [
|
|
||||||
related
|
|
||||||
for related in cls.extract_related_names()
|
|
||||||
if related in prefetch_dict
|
|
||||||
]
|
|
||||||
return related_to_extract
|
|
||||||
|
|
||||||
def get_relation_model_id(self, target_field: Type["BaseField"]) -> Optional[int]:
|
|
||||||
if target_field.virtual or issubclass(
|
|
||||||
target_field, ormar.fields.ManyToManyField
|
|
||||||
):
|
|
||||||
return self.pk
|
|
||||||
related_name = target_field.name
|
|
||||||
related_model = getattr(self, related_name)
|
|
||||||
return None if not related_model else related_model.pk
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def extract_db_own_fields(cls) -> Set:
|
def extract_db_own_fields(cls) -> Set:
|
||||||
related_names = cls.extract_related_names()
|
related_names = cls.extract_related_names()
|
||||||
@ -124,36 +50,6 @@ class ModelTableProxy:
|
|||||||
}
|
}
|
||||||
return self_fields
|
return self_fields
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_names_to_exclude(
|
|
||||||
cls,
|
|
||||||
fields: Optional[Union[Dict, Set]] = None,
|
|
||||||
exclude_fields: Optional[Union[Dict, Set]] = None,
|
|
||||||
) -> Set:
|
|
||||||
fields_names = cls.extract_db_own_fields()
|
|
||||||
if fields and fields is not Ellipsis:
|
|
||||||
fields_to_keep = {name for name in fields if name in fields_names}
|
|
||||||
else:
|
|
||||||
fields_to_keep = fields_names
|
|
||||||
|
|
||||||
fields_to_exclude = fields_names - fields_to_keep
|
|
||||||
|
|
||||||
if isinstance(exclude_fields, Set):
|
|
||||||
fields_to_exclude = fields_to_exclude.union(
|
|
||||||
{name for name in exclude_fields if name in fields_names}
|
|
||||||
)
|
|
||||||
elif isinstance(exclude_fields, Dict):
|
|
||||||
new_to_exclude = {
|
|
||||||
name
|
|
||||||
for name in exclude_fields
|
|
||||||
if name in fields_names and exclude_fields[name] is Ellipsis
|
|
||||||
}
|
|
||||||
fields_to_exclude = fields_to_exclude.union(new_to_exclude)
|
|
||||||
|
|
||||||
fields_to_exclude = fields_to_exclude - {cls.Meta.pkname}
|
|
||||||
|
|
||||||
return fields_to_exclude
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001
|
def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001
|
||||||
for field in cls.extract_related_names():
|
for field in cls.extract_related_names():
|
||||||
@ -194,20 +90,6 @@ class ModelTableProxy:
|
|||||||
new_kwargs.pop(field_name, None)
|
new_kwargs.pop(field_name, None)
|
||||||
return new_kwargs
|
return new_kwargs
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_column_alias(cls, field_name: str) -> str:
|
|
||||||
field = cls.Meta.model_fields.get(field_name)
|
|
||||||
if field is not None and field.alias is not None:
|
|
||||||
return field.alias
|
|
||||||
return field_name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_column_name_from_alias(cls, alias: str) -> str:
|
|
||||||
for field_name, field in cls.Meta.model_fields.items():
|
|
||||||
if field is not None and field.alias == alias:
|
|
||||||
return field_name
|
|
||||||
return alias # if not found it's not an alias but actual name
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def extract_related_fields(cls) -> List:
|
def extract_related_fields(cls) -> List:
|
||||||
|
|
||||||
@ -270,150 +152,32 @@ class ModelTableProxy:
|
|||||||
exclude = update(related_dict, exclude)
|
exclude = update(related_dict, exclude)
|
||||||
return exclude
|
return exclude
|
||||||
|
|
||||||
def _extract_model_db_fields(self) -> Dict:
|
@classmethod
|
||||||
self_fields = self._extract_own_model_fields()
|
def get_names_to_exclude(
|
||||||
self_fields = {
|
cls,
|
||||||
k: v
|
fields: Optional[Union[Dict, Set]] = None,
|
||||||
for k, v in self_fields.items()
|
exclude_fields: Optional[Union[Dict, Set]] = None,
|
||||||
if self.get_column_alias(k) in self.Meta.table.columns
|
) -> Set:
|
||||||
|
fields_names = cls.extract_db_own_fields()
|
||||||
|
if fields and fields is not Ellipsis:
|
||||||
|
fields_to_keep = {name for name in fields if name in fields_names}
|
||||||
|
else:
|
||||||
|
fields_to_keep = fields_names
|
||||||
|
|
||||||
|
fields_to_exclude = fields_names - fields_to_keep
|
||||||
|
|
||||||
|
if isinstance(exclude_fields, Set):
|
||||||
|
fields_to_exclude = fields_to_exclude.union(
|
||||||
|
{name for name in exclude_fields if name in fields_names}
|
||||||
|
)
|
||||||
|
elif isinstance(exclude_fields, Dict):
|
||||||
|
new_to_exclude = {
|
||||||
|
name
|
||||||
|
for name in exclude_fields
|
||||||
|
if name in fields_names and exclude_fields[name] is Ellipsis
|
||||||
}
|
}
|
||||||
for field in self._extract_db_related_names():
|
fields_to_exclude = fields_to_exclude.union(new_to_exclude)
|
||||||
target_pk_name = self.Meta.model_fields[field].to.Meta.pkname
|
|
||||||
target_field = getattr(self, field)
|
|
||||||
self_fields[field] = getattr(target_field, target_pk_name, None)
|
|
||||||
return self_fields
|
|
||||||
|
|
||||||
@staticmethod
|
fields_to_exclude = fields_to_exclude - {cls.Meta.pkname}
|
||||||
def resolve_relation_name( # noqa CCR001
|
|
||||||
item: Union[
|
|
||||||
"NewBaseModel",
|
|
||||||
Type["NewBaseModel"],
|
|
||||||
"ModelTableProxy",
|
|
||||||
Type["ModelTableProxy"],
|
|
||||||
],
|
|
||||||
related: Union[
|
|
||||||
"NewBaseModel",
|
|
||||||
Type["NewBaseModel"],
|
|
||||||
"ModelTableProxy",
|
|
||||||
Type["ModelTableProxy"],
|
|
||||||
],
|
|
||||||
explicit_multi: bool = False,
|
|
||||||
) -> str:
|
|
||||||
for name, field in item.Meta.model_fields.items():
|
|
||||||
# fastapi is creating clones of response model
|
|
||||||
# that's why it can be a subclass of the original model
|
|
||||||
# so we need to compare Meta too as this one is copied as is
|
|
||||||
if issubclass(field, ManyToManyField):
|
|
||||||
attrib = "to" if not explicit_multi else "through"
|
|
||||||
if (
|
|
||||||
getattr(field, attrib) == related.__class__
|
|
||||||
or getattr(field, attrib).Meta == related.Meta
|
|
||||||
):
|
|
||||||
return name
|
|
||||||
|
|
||||||
elif issubclass(field, ForeignKeyField):
|
return fields_to_exclude
|
||||||
if field.to == related.__class__ or field.to.Meta == related.Meta:
|
|
||||||
return name
|
|
||||||
|
|
||||||
raise ValueError(
|
|
||||||
f"No relation between {item.get_name()} and {related.get_name()}"
|
|
||||||
) # pragma nocover
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict:
|
|
||||||
for field_name, field in cls.Meta.model_fields.items():
|
|
||||||
if field_name in new_kwargs:
|
|
||||||
new_kwargs[field.get_alias()] = new_kwargs.pop(field_name)
|
|
||||||
return new_kwargs
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def translate_aliases_to_columns(cls, new_kwargs: Dict) -> Dict:
|
|
||||||
for field_name, field in cls.Meta.model_fields.items():
|
|
||||||
if field.alias and field.alias in new_kwargs:
|
|
||||||
new_kwargs[field_name] = new_kwargs.pop(field.alias)
|
|
||||||
return new_kwargs
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
|
|
||||||
merged_rows: List["Model"] = []
|
|
||||||
grouped_instances: OrderedDict = OrderedDict()
|
|
||||||
|
|
||||||
for model in result_rows:
|
|
||||||
grouped_instances.setdefault(model.pk, []).append(model)
|
|
||||||
|
|
||||||
for group in grouped_instances.values():
|
|
||||||
model = group.pop(0)
|
|
||||||
if group:
|
|
||||||
for next_model in group:
|
|
||||||
model = cls.merge_two_instances(next_model, model)
|
|
||||||
merged_rows.append(model)
|
|
||||||
|
|
||||||
return merged_rows
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def merge_two_instances(cls, one: "Model", other: "Model") -> "Model":
|
|
||||||
for field in one.Meta.model_fields.keys():
|
|
||||||
current_field = getattr(one, field)
|
|
||||||
if isinstance(current_field, list) and not isinstance(
|
|
||||||
current_field, ormar.Model
|
|
||||||
):
|
|
||||||
setattr(other, field, current_field + getattr(other, field))
|
|
||||||
elif (
|
|
||||||
isinstance(current_field, ormar.Model)
|
|
||||||
and current_field.pk == getattr(other, field).pk
|
|
||||||
):
|
|
||||||
setattr(
|
|
||||||
other,
|
|
||||||
field,
|
|
||||||
cls.merge_two_instances(current_field, getattr(other, field)),
|
|
||||||
)
|
|
||||||
other.set_save_status(True)
|
|
||||||
return other
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _populate_pk_column(
|
|
||||||
model: Type["Model"], columns: List[str], use_alias: bool = False,
|
|
||||||
) -> List[str]:
|
|
||||||
pk_alias = (
|
|
||||||
model.get_column_alias(model.Meta.pkname)
|
|
||||||
if use_alias
|
|
||||||
else model.Meta.pkname
|
|
||||||
)
|
|
||||||
if pk_alias not in columns:
|
|
||||||
columns.append(pk_alias)
|
|
||||||
return columns
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def own_table_columns(
|
|
||||||
model: Type["Model"],
|
|
||||||
fields: Optional[Union[Set, Dict]],
|
|
||||||
exclude_fields: Optional[Union[Set, Dict]],
|
|
||||||
use_alias: bool = False,
|
|
||||||
) -> List[str]:
|
|
||||||
columns = [
|
|
||||||
model.get_column_name_from_alias(col.name) if not use_alias else col.name
|
|
||||||
for col in model.Meta.table.columns
|
|
||||||
]
|
|
||||||
field_names = [
|
|
||||||
model.get_column_name_from_alias(col.name)
|
|
||||||
for col in model.Meta.table.columns
|
|
||||||
]
|
|
||||||
if fields:
|
|
||||||
columns = [
|
|
||||||
col
|
|
||||||
for col, name in zip(columns, field_names)
|
|
||||||
if model.is_included(fields, name)
|
|
||||||
]
|
|
||||||
if exclude_fields:
|
|
||||||
columns = [
|
|
||||||
col
|
|
||||||
for col, name in zip(columns, field_names)
|
|
||||||
if not model.is_excluded(exclude_fields, name)
|
|
||||||
]
|
|
||||||
|
|
||||||
# always has to return pk column
|
|
||||||
columns = ModelTableProxy._populate_pk_column(
|
|
||||||
model=model, columns=columns, use_alias=use_alias
|
|
||||||
)
|
|
||||||
|
|
||||||
return columns
|
|
||||||
|
|||||||
@ -379,3 +379,30 @@ class NewBaseModel(
|
|||||||
column_name in self.Meta.model_fields
|
column_name in self.Meta.model_fields
|
||||||
and self.Meta.model_fields[column_name].__type__ == pydantic.Json
|
and self.Meta.model_fields[column_name].__type__ == pydantic.Json
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _extract_own_model_fields(self) -> Dict:
|
||||||
|
related_names = self.extract_related_names()
|
||||||
|
self_fields = self.dict(exclude=related_names)
|
||||||
|
return self_fields
|
||||||
|
|
||||||
|
def _extract_model_db_fields(self) -> Dict:
|
||||||
|
self_fields = self._extract_own_model_fields()
|
||||||
|
self_fields = {
|
||||||
|
k: v
|
||||||
|
for k, v in self_fields.items()
|
||||||
|
if self.get_column_alias(k) in self.Meta.table.columns
|
||||||
|
}
|
||||||
|
for field in self._extract_db_related_names():
|
||||||
|
target_pk_name = self.Meta.model_fields[field].to.Meta.pkname
|
||||||
|
target_field = getattr(self, field)
|
||||||
|
self_fields[field] = getattr(target_field, target_pk_name, None)
|
||||||
|
return self_fields
|
||||||
|
|
||||||
|
def get_relation_model_id(self, target_field: Type["BaseField"]) -> Optional[int]:
|
||||||
|
if target_field.virtual or issubclass(
|
||||||
|
target_field, ormar.fields.ManyToManyField
|
||||||
|
):
|
||||||
|
return self.pk
|
||||||
|
related_name = target_field.name
|
||||||
|
related_model = getattr(self, related_name)
|
||||||
|
return None if not related_model else related_model.pk
|
||||||
|
|||||||
@ -137,11 +137,9 @@ class QueryClause:
|
|||||||
if issubclass(model_cls.Meta.model_fields[part], ManyToManyField):
|
if issubclass(model_cls.Meta.model_fields[part], ManyToManyField):
|
||||||
through_field = model_cls.Meta.model_fields[part]
|
through_field = model_cls.Meta.model_fields[part]
|
||||||
previous_model = through_field.through
|
previous_model = through_field.through
|
||||||
part2 = model_cls.resolve_relation_name(
|
part2 = through_field.default_target_field_name() # type: ignore
|
||||||
previous_model, through_field.to, explicit_multi=True
|
|
||||||
)
|
|
||||||
manager = model_cls.Meta.alias_manager
|
manager = model_cls.Meta.alias_manager
|
||||||
table_prefix = manager.resolve_relation_join(previous_model, part2)
|
table_prefix = manager.resolve_relation_alias(previous_model, part2)
|
||||||
model_cls = model_cls.Meta.model_fields[part].to
|
model_cls = model_cls.Meta.model_fields[part].to
|
||||||
previous_model = model_cls
|
previous_model = model_cls
|
||||||
return select_related, table_prefix, model_cls
|
return select_related, table_prefix, model_cls
|
||||||
|
|||||||
@ -135,7 +135,7 @@ class SqlJoin:
|
|||||||
model_cls = join_params.model_cls.Meta.model_fields[part].to
|
model_cls = join_params.model_cls.Meta.model_fields[part].to
|
||||||
to_table = model_cls.Meta.table.name
|
to_table = model_cls.Meta.table.name
|
||||||
|
|
||||||
alias = model_cls.Meta.alias_manager.resolve_relation_join(
|
alias = model_cls.Meta.alias_manager.resolve_relation_alias(
|
||||||
join_params.prev_model, part
|
join_params.prev_model, part
|
||||||
)
|
)
|
||||||
if alias not in self.used_aliases:
|
if alias not in self.used_aliases:
|
||||||
|
|||||||
@ -328,7 +328,7 @@ class PrefetchQuery:
|
|||||||
if issubclass(target_field, ManyToManyField):
|
if issubclass(target_field, ManyToManyField):
|
||||||
query_target = target_field.through
|
query_target = target_field.through
|
||||||
select_related = [target_name]
|
select_related = [target_name]
|
||||||
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_join(
|
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias(
|
||||||
query_target, target_name
|
query_target, target_name
|
||||||
)
|
)
|
||||||
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
||||||
|
|||||||
@ -11,11 +11,25 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
def get_table_alias() -> str:
|
def get_table_alias() -> str:
|
||||||
|
"""
|
||||||
|
Creates a random string that is used to alias tables in joins.
|
||||||
|
It's necessary that each relation has it's own aliases cause you can link
|
||||||
|
to the same target tables from multiple fields on one model as well as from
|
||||||
|
multiple different models in one join.
|
||||||
|
|
||||||
|
:return: randomly generated alias
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
alias = "".join(choices(string.ascii_uppercase, k=2)) + uuid.uuid4().hex[:4]
|
alias = "".join(choices(string.ascii_uppercase, k=2)) + uuid.uuid4().hex[:4]
|
||||||
return alias.lower()
|
return alias.lower()
|
||||||
|
|
||||||
|
|
||||||
class AliasManager:
|
class AliasManager:
|
||||||
|
"""
|
||||||
|
Keep all aliases of relations between different tables.
|
||||||
|
One global instance is shared between all models.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._aliases: Dict[str, str] = dict()
|
self._aliases: Dict[str, str] = dict()
|
||||||
self._aliases_new: Dict[str, str] = dict()
|
self._aliases_new: Dict[str, str] = dict()
|
||||||
@ -24,6 +38,22 @@ class AliasManager:
|
|||||||
def prefixed_columns(
|
def prefixed_columns(
|
||||||
alias: str, table: sqlalchemy.Table, fields: List = None
|
alias: str, table: sqlalchemy.Table, fields: List = None
|
||||||
) -> List[text]:
|
) -> List[text]:
|
||||||
|
"""
|
||||||
|
Creates a list of aliases sqlalchemy text clauses from
|
||||||
|
string alias and sqlalchemy.Table.
|
||||||
|
|
||||||
|
Optional list of fields to include can be passed to extract only those columns.
|
||||||
|
List has to have sqlalchemy names of columns (ormar aliases) not the ormar ones.
|
||||||
|
|
||||||
|
:param alias: alias of given table
|
||||||
|
:type alias: str
|
||||||
|
:param table: table from which fields should be aliased
|
||||||
|
:type table: sqlalchemy.Table
|
||||||
|
:param fields: fields to include
|
||||||
|
:type fields: Optional[List[str]]
|
||||||
|
:return: list of sqlalchemy text clauses with "column name as aliased name"
|
||||||
|
:rtype: List[text]
|
||||||
|
"""
|
||||||
alias = f"{alias}_" if alias else ""
|
alias = f"{alias}_" if alias else ""
|
||||||
all_columns = (
|
all_columns = (
|
||||||
table.columns
|
table.columns
|
||||||
@ -37,11 +67,49 @@ class AliasManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prefixed_table_name(alias: str, name: str) -> text:
|
def prefixed_table_name(alias: str, name: str) -> text:
|
||||||
|
"""
|
||||||
|
Creates text clause with table name with aliased name.
|
||||||
|
|
||||||
|
:param alias: alias of given table
|
||||||
|
:type alias: str
|
||||||
|
:param name: table name
|
||||||
|
:type name: str
|
||||||
|
:return: sqlalchemy text clause as "table_name aliased_name"
|
||||||
|
:rtype: sqlalchemy text clause
|
||||||
|
"""
|
||||||
return text(f"{name} {alias}_{name}")
|
return text(f"{name} {alias}_{name}")
|
||||||
|
|
||||||
def add_relation_type(
|
def add_relation_type(
|
||||||
self, source_model: Type["Model"], relation_name: str
|
self,
|
||||||
|
source_model: Type["Model"],
|
||||||
|
relation_name: str,
|
||||||
|
reverse_name: str = None,
|
||||||
|
is_multi: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers the relations defined in ormar models.
|
||||||
|
Given the relation it registers also the reverse side of this relation.
|
||||||
|
|
||||||
|
Used by both ForeignKey and ManyToMany relations.
|
||||||
|
|
||||||
|
Each relation is registered as Model name and relation name.
|
||||||
|
Each alias registered has to be unique.
|
||||||
|
|
||||||
|
Aliases are used to construct joins to assure proper links between tables.
|
||||||
|
That way you can link to the same target tables from multiple fields
|
||||||
|
on one model as well as from multiple different models in one join.
|
||||||
|
|
||||||
|
:param source_model: model with relation defined
|
||||||
|
:type source_model: source Model
|
||||||
|
:param relation_name: name of the relation to define
|
||||||
|
:type relation_name: str
|
||||||
|
:param reverse_name: name of related_name fo given relation for m2m relations
|
||||||
|
:type reverse_name: Optional[str]
|
||||||
|
:param is_multi: flag if relation being registered is a through m2m model
|
||||||
|
:type is_multi: bool
|
||||||
|
:return: none
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
parent_key = f"{source_model.get_name()}_{relation_name}"
|
parent_key = f"{source_model.get_name()}_{relation_name}"
|
||||||
if parent_key not in self._aliases_new:
|
if parent_key not in self._aliases_new:
|
||||||
self._aliases_new[parent_key] = get_table_alias()
|
self._aliases_new[parent_key] = get_table_alias()
|
||||||
@ -49,15 +117,24 @@ class AliasManager:
|
|||||||
child_model = to_field.to
|
child_model = to_field.to
|
||||||
related_name = to_field.related_name
|
related_name = to_field.related_name
|
||||||
if not related_name:
|
if not related_name:
|
||||||
related_name = child_model.resolve_relation_name(
|
related_name = reverse_name if is_multi else source_model.get_name() + "s"
|
||||||
child_model, source_model, explicit_multi=True
|
|
||||||
)
|
|
||||||
child_key = f"{child_model.get_name()}_{related_name}"
|
child_key = f"{child_model.get_name()}_{related_name}"
|
||||||
if child_key not in self._aliases_new:
|
if child_key not in self._aliases_new:
|
||||||
self._aliases_new[child_key] = get_table_alias()
|
self._aliases_new[child_key] = get_table_alias()
|
||||||
|
|
||||||
def resolve_relation_join(
|
def resolve_relation_alias(
|
||||||
self, from_model: Type["Model"], relation_name: str
|
self, from_model: Type["Model"], relation_name: str
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""
|
||||||
|
Given model and relation name returns the alias for this relation.
|
||||||
|
|
||||||
|
:param from_model: model with relation defined
|
||||||
|
:type from_model: source Model
|
||||||
|
:param relation_name: name of the relation field
|
||||||
|
:type relation_name: str
|
||||||
|
:return: alias of the relation
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "")
|
alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "")
|
||||||
return alias
|
return alias
|
||||||
|
|||||||
@ -67,7 +67,7 @@ class RelationsManager:
|
|||||||
to_field: Type[BaseField] = child.Meta.model_fields[relation_name]
|
to_field: Type[BaseField] = child.Meta.model_fields[relation_name]
|
||||||
# print('comming', child_name, relation_name)
|
# print('comming', child_name, relation_name)
|
||||||
(parent, child, child_name, to_name,) = get_relations_sides_and_names(
|
(parent, child, child_name, to_name,) = get_relations_sides_and_names(
|
||||||
to_field, parent, child, child_name, virtual
|
to_field, parent, child, child_name, virtual, relation_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# print('adding', parent.get_name(), child.get_name(), child_name)
|
# print('adding', parent.get_name(), child.get_name(), child_name)
|
||||||
|
|||||||
@ -14,16 +14,11 @@ def get_relations_sides_and_names(
|
|||||||
child: "Model",
|
child: "Model",
|
||||||
child_name: str,
|
child_name: str,
|
||||||
virtual: bool,
|
virtual: bool,
|
||||||
|
relation_name: str,
|
||||||
) -> Tuple["Model", "Model", str, str]:
|
) -> Tuple["Model", "Model", str, str]:
|
||||||
to_name = to_field.name
|
to_name = to_field.name
|
||||||
if issubclass(to_field, ManyToManyField):
|
if issubclass(to_field, ManyToManyField):
|
||||||
child_name, to_name = (
|
child_name = to_field.related_name or child.get_name() + "s"
|
||||||
to_field.related_name
|
|
||||||
or child.resolve_relation_name(
|
|
||||||
parent, to_field.through, explicit_multi=True
|
|
||||||
),
|
|
||||||
to_name,
|
|
||||||
)
|
|
||||||
child = proxy(child)
|
child = proxy(child)
|
||||||
elif virtual:
|
elif virtual:
|
||||||
child_name, to_name = to_name, child_name or child.get_name()
|
child_name, to_name = to_name, child_name or child.get_name()
|
||||||
|
|||||||
Reference in New Issue
Block a user