Refactor in join in order to make possibility for nested duplicated relations (and it was a mess :D)
This commit is contained in:
@ -150,11 +150,11 @@ Process order_by causes for non m2m relations.
|
||||
- `fields (Optional[Union[Set, Dict]])`: fields to include
|
||||
- `exclude_fields (Optional[Union[Set, Dict]])`: fields to exclude
|
||||
|
||||
<a name="queryset.join.SqlJoin._switch_many_to_many_order_columns"></a>
|
||||
<a name="queryset.join.SqlJoin._replace_many_to_many_order_by_columns"></a>
|
||||
#### \_switch\_many\_to\_many\_order\_columns
|
||||
|
||||
```python
|
||||
| _switch_many_to_many_order_columns(part: str, new_part: str) -> None
|
||||
| _replace_many_to_many_order_by_columns(part: str, new_part: str) -> None
|
||||
```
|
||||
|
||||
Substitutes the name of the relation with actual model name in m2m order bys.
|
||||
|
||||
@ -76,7 +76,7 @@ Since it can be a function you can set `default=datetime.datetime.now` and get c
|
||||
response with `include`/`exclude` and `response_model_include`/`response_model_exclude` accordingly.
|
||||
|
||||
```python
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
class User(ormar.Model):
|
||||
class Meta:
|
||||
tablename: str = "users2"
|
||||
@ -93,14 +93,14 @@ class User(ormar.Model):
|
||||
pydantic_only=True, default=datetime.datetime.now
|
||||
)
|
||||
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
app =FastAPI()
|
||||
|
||||
@app.post("/users/")
|
||||
async def create_user(user: User):
|
||||
return await user.save()
|
||||
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
|
||||
def test_excluding_fields_in_endpoints():
|
||||
client = TestClient(app)
|
||||
@ -127,7 +127,7 @@ def test_excluding_fields_in_endpoints():
|
||||
assert response.json().get("timestamp") == str(timestamp).replace(" ", "T")
|
||||
|
||||
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
```
|
||||
|
||||
#### Property fields
|
||||
@ -190,7 +190,7 @@ in the response from `fastapi` and `dict()` and `json()` methods. You cannot pas
|
||||
```
|
||||
|
||||
```python
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
def gen_pass(): # note: NOT production ready
|
||||
choices = string.ascii_letters + string.digits + "!@#$%^&*()"
|
||||
return "".join(random.choice(choices) for _ in range(20))
|
||||
@ -215,7 +215,7 @@ class RandomModel(ormar.Model):
|
||||
def full_name(self) -> str:
|
||||
return " ".join([self.first_name, self.last_name])
|
||||
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
app =FastAPI()
|
||||
|
||||
# explicitly exclude property_field in this endpoint
|
||||
@ -223,7 +223,7 @@ app =FastAPI()
|
||||
async def create_user(user: RandomModel):
|
||||
return await user.save()
|
||||
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
|
||||
def test_excluding_property_field_in_endpoints2():
|
||||
client = TestClient(app)
|
||||
@ -241,7 +241,7 @@ def test_excluding_property_field_in_endpoints2():
|
||||
# despite being decorated with property_field if you explictly exclude it it will be gone
|
||||
assert response.json().get("full_name") is None
|
||||
|
||||
# <==part of code removed for clarity==>
|
||||
# <==related of code removed for clarity==>
|
||||
```
|
||||
|
||||
#### Fields names vs Column names
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||
import itertools
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
import ormar # noqa: I100
|
||||
from ormar.fields.foreign_key import ForeignKeyField
|
||||
@ -109,3 +110,32 @@ def validate_related_names_in_relations( # noqa CCR001
|
||||
f"\nTip: provide different related_name for FK and/or M2M fields"
|
||||
)
|
||||
previous_related_names.append(field.related_name)
|
||||
|
||||
|
||||
def group_related_list(list_: List) -> Dict:
|
||||
"""
|
||||
Translates the list of related strings into a dictionary.
|
||||
That way nested models are grouped to traverse them in a right order
|
||||
and to avoid repetition.
|
||||
|
||||
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
|
||||
will become:
|
||||
{'people': {'houses': [], 'cars': ['models', 'colors']}}
|
||||
|
||||
:param list_: list of related models used in select related
|
||||
:type list_: List[str]
|
||||
:return: list converted to dictionary to avoid repetition and group nested models
|
||||
:rtype: Dict[str, List]
|
||||
"""
|
||||
test_dict: Dict[str, Any] = dict()
|
||||
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
|
||||
for key, group in grouped:
|
||||
group_list = list(group)
|
||||
new = [
|
||||
"__".join(x.split("__")[1:]) for x in group_list if len(x.split("__")) > 1
|
||||
]
|
||||
if any("__" in x for x in new):
|
||||
test_dict[key] = group_related_list(new)
|
||||
else:
|
||||
test_dict[key] = new
|
||||
return test_dict
|
||||
|
||||
@ -221,7 +221,7 @@ def update_attrs_and_fields(
|
||||
|
||||
:param attrs: new namespace for class being constructed
|
||||
:type attrs: Dict
|
||||
:param new_attrs: part of the namespace extracted from parent class
|
||||
:param new_attrs: related of the namespace extracted from parent class
|
||||
:type new_attrs: Dict
|
||||
:param model_fields: ormar fields in defined in current class
|
||||
:type model_fields: Dict[str, BaseField]
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import itertools
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
@ -18,38 +17,9 @@ import ormar.queryset # noqa I100
|
||||
from ormar.exceptions import ModelPersistenceError, NoMatch
|
||||
from ormar.fields.many_to_many import ManyToManyField
|
||||
from ormar.models import NewBaseModel # noqa I100
|
||||
from ormar.models.helpers.models import group_related_list
|
||||
from ormar.models.metaclass import ModelMeta
|
||||
|
||||
|
||||
def group_related_list(list_: List) -> Dict:
|
||||
"""
|
||||
Translates the list of related strings into a dictionary.
|
||||
That way nested models are grouped to traverse them in a right order
|
||||
and to avoid repetition.
|
||||
|
||||
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
|
||||
will become:
|
||||
{'people': {'houses': [], 'cars': ['models', 'colors']}}
|
||||
|
||||
:param list_: list of related models used in select related
|
||||
:type list_: List[str]
|
||||
:return: list converted to dictionary to avoid repetition and group nested models
|
||||
:rtype: Dict[str, List]
|
||||
"""
|
||||
test_dict: Dict[str, Any] = dict()
|
||||
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
|
||||
for key, group in grouped:
|
||||
group_list = list(group)
|
||||
new = [
|
||||
"__".join(x.split("__")[1:]) for x in group_list if len(x.split("__")) > 1
|
||||
]
|
||||
if any("__" in x for x in new):
|
||||
test_dict[key] = group_related_list(new)
|
||||
else:
|
||||
test_dict[key] = new
|
||||
return test_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING: # pragma nocover
|
||||
from ormar import QuerySet
|
||||
|
||||
@ -73,9 +43,11 @@ class Model(NewBaseModel):
|
||||
select_related: List = None,
|
||||
related_models: Any = None,
|
||||
previous_model: Type[T] = None,
|
||||
source_model: Type[T] = None,
|
||||
related_name: str = None,
|
||||
fields: Optional[Union[Dict, Set]] = None,
|
||||
exclude_fields: Optional[Union[Dict, Set]] = None,
|
||||
current_relation_str: str = None,
|
||||
) -> Optional[T]:
|
||||
"""
|
||||
Model method to convert raw sql row from database into ormar.Model instance.
|
||||
@ -112,7 +84,10 @@ class Model(NewBaseModel):
|
||||
item: Dict[str, Any] = {}
|
||||
select_related = select_related or []
|
||||
related_models = related_models or []
|
||||
table_prefix = ""
|
||||
|
||||
if select_related:
|
||||
source_model = cls
|
||||
related_models = group_related_list(select_related)
|
||||
|
||||
rel_name2 = related_name
|
||||
@ -135,11 +110,15 @@ class Model(NewBaseModel):
|
||||
previous_model = through_field.through # type: ignore
|
||||
|
||||
if previous_model and rel_name2:
|
||||
# TODO finish duplicated nested relation or remove this
|
||||
if current_relation_str and "__" in current_relation_str and source_model:
|
||||
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
|
||||
previous_model, rel_name2
|
||||
from_model=source_model, relation_name=current_relation_str
|
||||
)
|
||||
if not table_prefix:
|
||||
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
|
||||
from_model=previous_model, relation_name=rel_name2
|
||||
)
|
||||
else:
|
||||
table_prefix = ""
|
||||
|
||||
item = cls.populate_nested_models_from_row(
|
||||
item=item,
|
||||
@ -147,6 +126,8 @@ class Model(NewBaseModel):
|
||||
related_models=related_models,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
current_relation_str=current_relation_str,
|
||||
source_model=source_model,
|
||||
)
|
||||
item = cls.extract_prefixed_table_columns(
|
||||
item=item,
|
||||
@ -163,8 +144,6 @@ class Model(NewBaseModel):
|
||||
)
|
||||
instance = cls(**item)
|
||||
instance.set_save_status(True)
|
||||
else:
|
||||
instance = None
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
@ -175,6 +154,8 @@ class Model(NewBaseModel):
|
||||
related_models: Any,
|
||||
fields: Optional[Union[Dict, Set]] = None,
|
||||
exclude_fields: Optional[Union[Dict, Set]] = None,
|
||||
current_relation_str: str = None,
|
||||
source_model: Type[T] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Traverses structure of related models and populates the nested models
|
||||
@ -202,14 +183,20 @@ class Model(NewBaseModel):
|
||||
and values are database values
|
||||
:rtype: Dict
|
||||
"""
|
||||
|
||||
for related in related_models:
|
||||
relation_str = (
|
||||
"__".join([current_relation_str, related])
|
||||
if current_relation_str
|
||||
else related
|
||||
)
|
||||
fields = cls.get_included(fields, related)
|
||||
exclude_fields = cls.get_excluded(exclude_fields, related)
|
||||
model_cls = cls.Meta.model_fields[related].to
|
||||
|
||||
remainder = None
|
||||
if isinstance(related_models, dict) and related_models[related]:
|
||||
first_part, remainder = related, related_models[related]
|
||||
model_cls = cls.Meta.model_fields[first_part].to
|
||||
|
||||
fields = cls.get_included(fields, first_part)
|
||||
exclude_fields = cls.get_excluded(exclude_fields, first_part)
|
||||
|
||||
remainder = related_models[related]
|
||||
child = model_cls.from_row(
|
||||
row,
|
||||
related_models=remainder,
|
||||
@ -217,18 +204,8 @@ class Model(NewBaseModel):
|
||||
related_name=related,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
)
|
||||
item[model_cls.get_column_name_from_alias(first_part)] = child
|
||||
else:
|
||||
model_cls = cls.Meta.model_fields[related].to
|
||||
fields = cls.get_included(fields, related)
|
||||
exclude_fields = cls.get_excluded(exclude_fields, related)
|
||||
child = model_cls.from_row(
|
||||
row,
|
||||
previous_model=cls,
|
||||
related_name=related,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
current_relation_str=relation_str,
|
||||
source_model=source_model,
|
||||
)
|
||||
item[model_cls.get_column_name_from_alias(related)] = child
|
||||
|
||||
@ -251,7 +228,7 @@ class Model(NewBaseModel):
|
||||
All joined tables have prefixes to allow duplicate column names,
|
||||
as well as duplicated joins to the same table from multiple different tables.
|
||||
|
||||
Extracted fields populates the item dict later used to construct a Model.
|
||||
Extracted fields populates the related dict later used to construct a Model.
|
||||
|
||||
Used in Model.from_row and PrefetchQuery._populate_rows methods.
|
||||
|
||||
|
||||
@ -194,7 +194,9 @@ class QueryClause:
|
||||
previous_model = through_field.through
|
||||
part2 = through_field.default_target_field_name() # type: ignore
|
||||
manager = model_cls.Meta.alias_manager
|
||||
table_prefix = manager.resolve_relation_alias(previous_model, part2)
|
||||
table_prefix = manager.resolve_relation_alias(
|
||||
from_model=previous_model, relation_name=part2
|
||||
)
|
||||
model_cls = model_cls.Meta.model_fields[part].to
|
||||
previous_model = model_cls
|
||||
return select_related, table_prefix, model_cls
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from collections import OrderedDict
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
@ -14,24 +14,13 @@ from typing import (
|
||||
import sqlalchemy
|
||||
from sqlalchemy import text
|
||||
|
||||
from ormar.fields import ManyToManyField # noqa I100
|
||||
from ormar.fields import BaseField, ManyToManyField # noqa I100
|
||||
from ormar.relations import AliasManager
|
||||
|
||||
if TYPE_CHECKING: # pragma no cover
|
||||
from ormar import Model
|
||||
|
||||
|
||||
class JoinParameters(NamedTuple):
|
||||
"""
|
||||
Named tuple that holds set of parameters passed during join construction.
|
||||
"""
|
||||
|
||||
prev_model: Type["Model"]
|
||||
previous_alias: str
|
||||
from_table: str
|
||||
model_cls: Type["Model"]
|
||||
|
||||
|
||||
class SqlJoin:
|
||||
def __init__( # noqa: CFQ002
|
||||
self,
|
||||
@ -42,7 +31,12 @@ class SqlJoin:
|
||||
exclude_fields: Optional[Union[Set, Dict]],
|
||||
order_columns: Optional[List],
|
||||
sorted_orders: OrderedDict,
|
||||
main_model: Type["Model"],
|
||||
related_models: Any = None,
|
||||
own_alias: str = "",
|
||||
) -> None:
|
||||
self.own_alias = own_alias
|
||||
self.related_models = related_models or []
|
||||
self.used_aliases = used_aliases
|
||||
self.select_from = select_from
|
||||
self.columns = columns
|
||||
@ -50,18 +44,17 @@ class SqlJoin:
|
||||
self.exclude_fields = exclude_fields
|
||||
self.order_columns = order_columns
|
||||
self.sorted_orders = sorted_orders
|
||||
self.main_model = main_model
|
||||
|
||||
@staticmethod
|
||||
def alias_manager(model_cls: Type["Model"]) -> AliasManager:
|
||||
@property
|
||||
def alias_manager(self) -> AliasManager:
|
||||
"""
|
||||
Shortcut for ormars model AliasManager stored on Meta.
|
||||
Shortcut for ormar's model AliasManager stored on Meta.
|
||||
|
||||
:param model_cls: ormar Model class
|
||||
:type model_cls: Type[Model]
|
||||
:return: alias manager from model's Meta
|
||||
:rtype: AliasManager
|
||||
"""
|
||||
return model_cls.Meta.alias_manager
|
||||
return self.main_model.Meta.alias_manager
|
||||
|
||||
@staticmethod
|
||||
def on_clause(
|
||||
@ -86,33 +79,32 @@ class SqlJoin:
|
||||
right_part = f"{previous_alias + '_' if previous_alias else ''}{from_clause}"
|
||||
return text(f"{left_part}={right_part}")
|
||||
|
||||
@staticmethod
|
||||
def update_inclusions(
|
||||
model_cls: Type["Model"],
|
||||
fields: Optional[Union[Set, Dict]],
|
||||
exclude_fields: Optional[Union[Set, Dict]],
|
||||
nested_name: str,
|
||||
) -> Tuple[Optional[Union[Dict, Set]], Optional[Union[Dict, Set]]]:
|
||||
"""
|
||||
Extract nested fields and exclude_fields if applicable.
|
||||
|
||||
:param model_cls: ormar model class
|
||||
:type model_cls: Type["Model"]
|
||||
:param fields: fields to include
|
||||
:type fields: Optional[Union[Set, Dict]]
|
||||
:param exclude_fields: fields to exclude
|
||||
:type exclude_fields: Optional[Union[Set, Dict]]
|
||||
:param nested_name: name of the nested field
|
||||
:type nested_name: str
|
||||
:return: updated exclude and include fields from nested objects
|
||||
:rtype: Tuple[Optional[Union[Dict, Set]], Optional[Union[Dict, Set]]]
|
||||
"""
|
||||
fields = model_cls.get_included(fields, nested_name)
|
||||
exclude_fields = model_cls.get_excluded(exclude_fields, nested_name)
|
||||
return fields, exclude_fields
|
||||
def process_deeper_join(
|
||||
self, related_name: str, model_cls: Type["Model"], remainder: Any, alias: str,
|
||||
) -> None:
|
||||
sql_join = SqlJoin(
|
||||
used_aliases=self.used_aliases,
|
||||
select_from=self.select_from,
|
||||
columns=self.columns,
|
||||
fields=self.main_model.get_excluded(self.fields, related_name),
|
||||
exclude_fields=self.main_model.get_excluded(
|
||||
self.exclude_fields, related_name
|
||||
),
|
||||
order_columns=self.order_columns,
|
||||
sorted_orders=self.sorted_orders,
|
||||
main_model=model_cls,
|
||||
related_models=remainder,
|
||||
own_alias=alias,
|
||||
)
|
||||
(
|
||||
self.used_aliases,
|
||||
self.select_from,
|
||||
self.columns,
|
||||
self.sorted_orders,
|
||||
) = sql_join.build_join(related_name)
|
||||
|
||||
def build_join( # noqa: CCR001
|
||||
self, item: str, join_parameters: JoinParameters
|
||||
self, related: str
|
||||
) -> Tuple[List, sqlalchemy.sql.select, List, OrderedDict]:
|
||||
"""
|
||||
Main external access point for building a join.
|
||||
@ -120,59 +112,61 @@ class SqlJoin:
|
||||
handles switching to through models for m2m relations, returns updated lists of
|
||||
used_aliases and sort_orders.
|
||||
|
||||
:param item: string with join definition
|
||||
:type item: str
|
||||
:param join_parameters: parameters from previous/ current join
|
||||
:type join_parameters: JoinParameters
|
||||
:param related: string with join definition
|
||||
:type related: str
|
||||
:return: list of used aliases, select from, list of aliased columns, sort orders
|
||||
:rtype: Tuple[List[str], Join, List[TextClause], collections.OrderedDict]
|
||||
"""
|
||||
fields = self.fields
|
||||
exclude_fields = self.exclude_fields
|
||||
target_field = self.main_model.Meta.model_fields[related]
|
||||
prev_model = self.main_model
|
||||
# TODO: Finish refactoring here
|
||||
if issubclass(target_field, ManyToManyField):
|
||||
new_part = self.process_m2m_related_name_change(
|
||||
target_field=target_field, related=related
|
||||
)
|
||||
self._replace_many_to_many_order_by_columns(related, new_part)
|
||||
|
||||
for index, part in enumerate(item.split("__")):
|
||||
if issubclass(
|
||||
join_parameters.model_cls.Meta.model_fields[part], ManyToManyField
|
||||
):
|
||||
_fields = join_parameters.model_cls.Meta.model_fields
|
||||
target_field = _fields[part]
|
||||
model_cls = target_field.through
|
||||
alias = self.alias_manager.resolve_relation_alias(
|
||||
from_model=prev_model, relation_name=related
|
||||
)
|
||||
if alias not in self.used_aliases:
|
||||
self._process_join(
|
||||
model_cls=model_cls,
|
||||
related=related,
|
||||
alias=alias,
|
||||
target_field=target_field,
|
||||
)
|
||||
related = new_part
|
||||
self.own_alias = alias
|
||||
prev_model = model_cls
|
||||
target_field = target_field.through.Meta.model_fields[related]
|
||||
|
||||
model_cls = target_field.to
|
||||
alias = model_cls.Meta.alias_manager.resolve_relation_alias(
|
||||
from_model=prev_model, relation_name=related
|
||||
)
|
||||
if alias not in self.used_aliases:
|
||||
self._process_join(
|
||||
model_cls=model_cls,
|
||||
prev_model=prev_model,
|
||||
related=related,
|
||||
alias=alias,
|
||||
target_field=target_field,
|
||||
)
|
||||
|
||||
for related_name in self.related_models:
|
||||
remainder = None
|
||||
if (
|
||||
target_field.self_reference
|
||||
and part == target_field.self_reference_primary
|
||||
isinstance(self.related_models, dict)
|
||||
and self.related_models[related_name]
|
||||
):
|
||||
new_part = target_field.default_source_field_name() # type: ignore
|
||||
else:
|
||||
new_part = target_field.default_target_field_name() # type: ignore
|
||||
self._switch_many_to_many_order_columns(part, new_part)
|
||||
if index > 0: # nested joins
|
||||
fields, exclude_fields = SqlJoin.update_inclusions(
|
||||
model_cls=join_parameters.model_cls,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
nested_name=part,
|
||||
)
|
||||
|
||||
join_parameters = self._build_join_parameters(
|
||||
part=part,
|
||||
join_params=join_parameters,
|
||||
is_multi=True,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
)
|
||||
part = new_part
|
||||
|
||||
if index > 0: # nested joins
|
||||
fields, exclude_fields = SqlJoin.update_inclusions(
|
||||
model_cls=join_parameters.model_cls,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
nested_name=part,
|
||||
)
|
||||
join_parameters = self._build_join_parameters(
|
||||
part=part,
|
||||
join_params=join_parameters,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
remainder = self.related_models[related_name]
|
||||
self.process_deeper_join(
|
||||
related_name=related_name,
|
||||
model_cls=model_cls,
|
||||
remainder=remainder,
|
||||
alias=alias,
|
||||
)
|
||||
|
||||
return (
|
||||
@ -182,65 +176,44 @@ class SqlJoin:
|
||||
self.sorted_orders,
|
||||
)
|
||||
|
||||
def _build_join_parameters(
|
||||
self,
|
||||
part: str,
|
||||
join_params: JoinParameters,
|
||||
fields: Optional[Union[Set, Dict]],
|
||||
exclude_fields: Optional[Union[Set, Dict]],
|
||||
is_multi: bool = False,
|
||||
) -> JoinParameters:
|
||||
@staticmethod
|
||||
def process_m2m_related_name_change(
|
||||
target_field: Type[ManyToManyField], related: str, reverse: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Updates used_aliases to not join multiple times to the same table.
|
||||
Updates join parameters with new values.
|
||||
Extracts relation name to link join through the Through model declared on
|
||||
relation field.
|
||||
|
||||
:param part: part of the join str definition
|
||||
:type part: str
|
||||
:param join_params: parameters from previous/ current join
|
||||
:type join_params: JoinParameters
|
||||
:param fields: fields to include
|
||||
:type fields: Optional[Union[Set, Dict]]
|
||||
:param exclude_fields: fields to exclude
|
||||
:type exclude_fields: Optional[Union[Set, Dict]]
|
||||
:param is_multi: flag if the relation is m2m
|
||||
:type is_multi: bool
|
||||
:return: updated join parameters
|
||||
:rtype: ormar.queryset.join.JoinParameters
|
||||
Changes the same names in order_by queries if they are present.
|
||||
|
||||
:param reverse: flag if it's on_clause lookup - use reverse fields
|
||||
:type reverse: bool
|
||||
:param target_field: relation field
|
||||
:type target_field: Type[ManyToManyField]
|
||||
:param related: name of the relation
|
||||
:type related: str
|
||||
:return: new relation name switched to through model field
|
||||
:rtype: str
|
||||
"""
|
||||
if is_multi:
|
||||
model_cls = join_params.model_cls.Meta.model_fields[part].through
|
||||
is_primary_self_ref = (
|
||||
target_field.self_reference
|
||||
and related == target_field.self_reference_primary
|
||||
)
|
||||
if (is_primary_self_ref and not reverse) or (
|
||||
not is_primary_self_ref and reverse
|
||||
):
|
||||
new_part = target_field.default_source_field_name() # type: ignore
|
||||
else:
|
||||
model_cls = join_params.model_cls.Meta.model_fields[part].to
|
||||
to_table = model_cls.Meta.table.name
|
||||
|
||||
alias = model_cls.Meta.alias_manager.resolve_relation_alias(
|
||||
join_params.prev_model, part
|
||||
)
|
||||
if alias not in self.used_aliases:
|
||||
self._process_join(
|
||||
join_params=join_params,
|
||||
is_multi=is_multi,
|
||||
model_cls=model_cls,
|
||||
part=part,
|
||||
alias=alias,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
)
|
||||
|
||||
previous_alias = alias
|
||||
from_table = to_table
|
||||
prev_model = model_cls
|
||||
return JoinParameters(prev_model, previous_alias, from_table, model_cls)
|
||||
new_part = target_field.default_target_field_name() # type: ignore
|
||||
return new_part
|
||||
|
||||
def _process_join( # noqa: CFQ002
|
||||
self,
|
||||
join_params: JoinParameters,
|
||||
is_multi: bool,
|
||||
model_cls: Type["Model"],
|
||||
part: str,
|
||||
related: str,
|
||||
alias: str,
|
||||
fields: Optional[Union[Set, Dict]],
|
||||
exclude_fields: Optional[Union[Set, Dict]],
|
||||
target_field: Type[BaseField],
|
||||
prev_model: Type["Model"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Resolves to and from column names and table names.
|
||||
@ -255,63 +228,53 @@ class SqlJoin:
|
||||
|
||||
Process order_by causes for non m2m relations.
|
||||
|
||||
:param join_params: parameters from previous/ current join
|
||||
:type join_params: JoinParameters
|
||||
:param is_multi: flag if it's m2m relation
|
||||
:type is_multi: bool
|
||||
:param model_cls:
|
||||
:type model_cls: ormar.models.metaclass.ModelMetaclass
|
||||
:param part: name of the field used in join
|
||||
:type part: str
|
||||
:param related: name of the field used in join
|
||||
:type related: str
|
||||
:param alias: alias of the current join
|
||||
:type alias: str
|
||||
:param fields: fields to include
|
||||
:type fields: Optional[Union[Set, Dict]]
|
||||
:param exclude_fields: fields to exclude
|
||||
:type exclude_fields: Optional[Union[Set, Dict]]
|
||||
"""
|
||||
to_table = model_cls.Meta.table.name
|
||||
to_key, from_key = self.get_to_and_from_keys(
|
||||
join_params, is_multi, model_cls, part
|
||||
)
|
||||
to_key, from_key = self.get_to_and_from_keys(related, target_field)
|
||||
|
||||
prev_model = prev_model or self.main_model
|
||||
|
||||
on_clause = self.on_clause(
|
||||
previous_alias=join_params.previous_alias,
|
||||
previous_alias=self.own_alias,
|
||||
alias=alias,
|
||||
from_clause=f"{join_params.from_table}.{from_key}",
|
||||
from_clause=f"{prev_model.Meta.tablename}.{from_key}",
|
||||
to_clause=f"{to_table}.{to_key}",
|
||||
)
|
||||
target_table = self.alias_manager(model_cls).prefixed_table_name(
|
||||
alias, to_table
|
||||
)
|
||||
target_table = self.alias_manager.prefixed_table_name(alias, to_table)
|
||||
self.select_from = sqlalchemy.sql.outerjoin(
|
||||
self.select_from, target_table, on_clause
|
||||
)
|
||||
|
||||
pkname_alias = model_cls.get_column_alias(model_cls.Meta.pkname)
|
||||
if not is_multi:
|
||||
if not issubclass(target_field, ManyToManyField):
|
||||
self.get_order_bys(
|
||||
alias=alias,
|
||||
to_table=to_table,
|
||||
pkname_alias=pkname_alias,
|
||||
part=part,
|
||||
part=related,
|
||||
model_cls=model_cls,
|
||||
)
|
||||
|
||||
self_related_fields = model_cls.own_table_columns(
|
||||
model=model_cls,
|
||||
fields=fields,
|
||||
exclude_fields=exclude_fields,
|
||||
fields=self.fields,
|
||||
exclude_fields=self.exclude_fields,
|
||||
use_alias=True,
|
||||
)
|
||||
self.columns.extend(
|
||||
self.alias_manager(model_cls).prefixed_columns(
|
||||
self.alias_manager.prefixed_columns(
|
||||
alias, model_cls.Meta.table, self_related_fields
|
||||
)
|
||||
)
|
||||
self.used_aliases.append(alias)
|
||||
|
||||
def _switch_many_to_many_order_columns(self, part: str, new_part: str) -> None:
|
||||
def _replace_many_to_many_order_by_columns(self, part: str, new_part: str) -> None:
|
||||
"""
|
||||
Substitutes the name of the relation with actual model name in m2m order bys.
|
||||
|
||||
@ -325,7 +288,7 @@ class SqlJoin:
|
||||
x.split("__") for x in self.order_columns if "__" in x
|
||||
]
|
||||
for condition in split_order_columns:
|
||||
if condition[-2] == part or condition[-2][1:] == part:
|
||||
if self._check_if_condition_apply(condition, part):
|
||||
condition[-2] = condition[-2].replace(part, new_part)
|
||||
self.order_columns = [x for x in self.order_columns if "__" not in x] + [
|
||||
"__".join(x) for x in split_order_columns
|
||||
@ -413,51 +376,34 @@ class SqlJoin:
|
||||
order = text(f"{alias}_{to_table}.{pkname_alias}")
|
||||
self.sorted_orders[f"{alias}.{pkname_alias}"] = order
|
||||
|
||||
@staticmethod
|
||||
def get_to_and_from_keys(
|
||||
join_params: JoinParameters,
|
||||
is_multi: bool,
|
||||
model_cls: Type["Model"],
|
||||
part: str,
|
||||
self, related: str, target_field: Type[BaseField]
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Based on the relation type, name of the relation and previous models and parts
|
||||
stored in JoinParameters it resolves the current to and from keys, which are
|
||||
different for ManyToMany relation, ForeignKey and reverse part of relations.
|
||||
different for ManyToMany relation, ForeignKey and reverse related of relations.
|
||||
|
||||
:param join_params: parameters from previous/ current join
|
||||
:type join_params: JoinParameters
|
||||
:param is_multi: flag if the relation is of m2m type
|
||||
:type is_multi: bool
|
||||
:param model_cls: ormar model class
|
||||
:type model_cls: Type[Model]
|
||||
:param part: name of the current relation join
|
||||
:type part: str
|
||||
:param target_field: relation field
|
||||
:type target_field: Type[ForeignKeyField]
|
||||
:param related: name of the current relation join
|
||||
:type related: str
|
||||
:return: to key and from key
|
||||
:rtype: Tuple[str, str]
|
||||
"""
|
||||
if is_multi:
|
||||
target_field = join_params.model_cls.Meta.model_fields[part]
|
||||
if (
|
||||
target_field.self_reference
|
||||
and part == target_field.self_reference_primary
|
||||
):
|
||||
to_key = target_field.default_target_field_name() # type: ignore
|
||||
else:
|
||||
to_key = target_field.default_source_field_name() # type: ignore
|
||||
from_key = join_params.prev_model.get_column_alias(
|
||||
join_params.prev_model.Meta.pkname
|
||||
if issubclass(target_field, ManyToManyField):
|
||||
to_key = self.process_m2m_related_name_change(
|
||||
target_field=target_field, related=related, reverse=True
|
||||
)
|
||||
from_key = self.main_model.get_column_alias(self.main_model.Meta.pkname)
|
||||
|
||||
elif join_params.prev_model.Meta.model_fields[part].virtual:
|
||||
to_field = join_params.prev_model.Meta.model_fields[part].get_related_name()
|
||||
to_key = model_cls.get_column_alias(to_field)
|
||||
from_key = join_params.prev_model.get_column_alias(
|
||||
join_params.prev_model.Meta.pkname
|
||||
)
|
||||
elif target_field.virtual:
|
||||
to_field = target_field.get_related_name()
|
||||
to_key = target_field.to.get_column_alias(to_field)
|
||||
from_key = self.main_model.get_column_alias(self.main_model.Meta.pkname)
|
||||
|
||||
else:
|
||||
to_key = model_cls.get_column_alias(model_cls.Meta.pkname)
|
||||
from_key = join_params.prev_model.get_column_alias(part)
|
||||
to_key = target_field.to.get_column_alias(target_field.to.Meta.pkname)
|
||||
from_key = self.main_model.get_column_alias(related)
|
||||
|
||||
return to_key, from_key
|
||||
|
||||
@ -526,7 +526,7 @@ class PrefetchQuery:
|
||||
query_target = target_field.through
|
||||
select_related = [target_name]
|
||||
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias(
|
||||
query_target, target_name
|
||||
from_model=query_target, relation_name=target_name
|
||||
)
|
||||
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
||||
|
||||
@ -551,14 +551,14 @@ class PrefetchQuery:
|
||||
@staticmethod
|
||||
def _get_select_related_if_apply(related: str, select_dict: Dict) -> Dict:
|
||||
"""
|
||||
Extract nested part of select_related dictionary to extract models nested
|
||||
Extract nested related of select_related dictionary to extract models nested
|
||||
deeper on related model and already loaded in select related query.
|
||||
|
||||
:param related: name of the relation
|
||||
:type related: str
|
||||
:param select_dict: dictionary of select related models in main query
|
||||
:type select_dict: Dict
|
||||
:return: dictionary with nested part of select related
|
||||
:return: dictionary with nested related of select related
|
||||
:rtype: Dict
|
||||
"""
|
||||
return (
|
||||
|
||||
@ -6,8 +6,9 @@ import sqlalchemy
|
||||
from sqlalchemy import text
|
||||
|
||||
import ormar # noqa I100
|
||||
from ormar.models.helpers.models import group_related_list
|
||||
from ormar.queryset import FilterQuery, LimitQuery, OffsetQuery, OrderQuery
|
||||
from ormar.queryset.join import JoinParameters, SqlJoin
|
||||
from ormar.queryset.join import SqlJoin
|
||||
|
||||
if TYPE_CHECKING: # pragma no cover
|
||||
from ormar import Model
|
||||
@ -140,14 +141,16 @@ class Query:
|
||||
else:
|
||||
self.select_from = self.table
|
||||
|
||||
# TODO: Refactor to convert to nested dict like in from_row in model
|
||||
self._select_related.sort(key=lambda item: (item, -len(item)))
|
||||
related_models = group_related_list(self._select_related)
|
||||
|
||||
for item in self._select_related:
|
||||
join_parameters = JoinParameters(
|
||||
self.model_cls, "", self.table.name, self.model_cls
|
||||
)
|
||||
fields = self.model_cls.get_included(self.fields, item)
|
||||
exclude_fields = self.model_cls.get_excluded(self.exclude_fields, item)
|
||||
for related in related_models:
|
||||
fields = self.model_cls.get_included(self.fields, related)
|
||||
exclude_fields = self.model_cls.get_excluded(self.exclude_fields, related)
|
||||
remainder = None
|
||||
if isinstance(related_models, dict) and related_models[related]:
|
||||
remainder = related_models[related]
|
||||
sql_join = SqlJoin(
|
||||
used_aliases=self.used_aliases,
|
||||
select_from=self.select_from,
|
||||
@ -156,6 +159,8 @@ class Query:
|
||||
exclude_fields=exclude_fields,
|
||||
order_columns=self.order_columns,
|
||||
sorted_orders=self.sorted_orders,
|
||||
main_model=self.model_cls,
|
||||
related_models=remainder,
|
||||
)
|
||||
|
||||
(
|
||||
@ -163,14 +168,14 @@ class Query:
|
||||
self.select_from,
|
||||
self.columns,
|
||||
self.sorted_orders,
|
||||
) = sql_join.build_join(item, join_parameters)
|
||||
) = sql_join.build_join(related)
|
||||
|
||||
expr = sqlalchemy.sql.select(self.columns)
|
||||
expr = expr.select_from(self.select_from)
|
||||
|
||||
expr = self._apply_expression_modifiers(expr)
|
||||
|
||||
# print(expr.compile(compile_kwargs={"literal_binds": True}))
|
||||
# print("\n", expr.compile(compile_kwargs={"literal_binds": True}))
|
||||
self._reset_query_parameters()
|
||||
|
||||
return expr
|
||||
|
||||
@ -113,6 +113,19 @@ class AliasManager:
|
||||
if child_key not in self._aliases_new:
|
||||
self._aliases_new[child_key] = get_table_alias()
|
||||
|
||||
def add_alias(self, alias_key: str) -> str:
|
||||
"""
|
||||
Adds alias to the dictionary of aliases under given key.
|
||||
|
||||
:param alias_key: key of relation to generate alias for
|
||||
:type alias_key: str
|
||||
:return: generated alias
|
||||
:rtype: str
|
||||
"""
|
||||
alias = get_table_alias()
|
||||
self._aliases_new[alias_key] = alias
|
||||
return alias
|
||||
|
||||
def resolve_relation_alias(
|
||||
self, from_model: Type["Model"], relation_name: str
|
||||
) -> str:
|
||||
|
||||
@ -127,7 +127,7 @@ class RelationProxy(list):
|
||||
self, item: "Model", keep_reversed: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Removes the item from relation with parent.
|
||||
Removes the related from relation with parent.
|
||||
|
||||
Through models are automatically deleted for m2m relations.
|
||||
|
||||
|
||||
@ -190,7 +190,7 @@ async def test_m2m_self_forwardref_relation(cleanup):
|
||||
# await steve.friends.add(billy)
|
||||
|
||||
billy_check = await Child.objects.select_related(
|
||||
["friends", "favourite_game", "least_favourite_game",]
|
||||
["friends", "favourite_game", "least_favourite_game"]
|
||||
).get(name="Billy")
|
||||
assert len(billy_check.friends) == 2
|
||||
assert billy_check.friends[0].name == "Kate"
|
||||
@ -200,5 +200,6 @@ async def test_m2m_self_forwardref_relation(cleanup):
|
||||
kate_check = await Child.objects.select_related(["also_friends",]).get(
|
||||
name="Kate"
|
||||
)
|
||||
|
||||
assert len(kate_check.also_friends) == 1
|
||||
assert kate_check.also_friends[0].name == "Billy"
|
||||
|
||||
@ -280,7 +280,7 @@ async def test_sort_order_on_many_to_many():
|
||||
assert users[1].cars[3].name == "Buggy"
|
||||
|
||||
users = (
|
||||
await User.objects.select_related(["cars", "cars__factory"])
|
||||
await User.objects.select_related(["cars__factory"])
|
||||
.order_by(["-cars__factory__name", "cars__name"])
|
||||
.all()
|
||||
)
|
||||
|
||||
@ -116,9 +116,7 @@ async def test_selecting_subset():
|
||||
)
|
||||
|
||||
all_cars = (
|
||||
await Car.objects.select_related(
|
||||
["manufacturer", "manufacturer__hq", "manufacturer__hq__nicks"]
|
||||
)
|
||||
await Car.objects.select_related(["manufacturer__hq__nicks"])
|
||||
.fields(
|
||||
[
|
||||
"id",
|
||||
@ -132,9 +130,7 @@ async def test_selecting_subset():
|
||||
)
|
||||
|
||||
all_cars2 = (
|
||||
await Car.objects.select_related(
|
||||
["manufacturer", "manufacturer__hq", "manufacturer__hq__nicks"]
|
||||
)
|
||||
await Car.objects.select_related(["manufacturer__hq__nicks"])
|
||||
.fields(
|
||||
{
|
||||
"id": ...,
|
||||
@ -149,9 +145,7 @@ async def test_selecting_subset():
|
||||
)
|
||||
|
||||
all_cars3 = (
|
||||
await Car.objects.select_related(
|
||||
["manufacturer", "manufacturer__hq", "manufacturer__hq__nicks"]
|
||||
)
|
||||
await Car.objects.select_related(["manufacturer__hq__nicks"])
|
||||
.fields(
|
||||
{
|
||||
"id": ...,
|
||||
|
||||
Reference in New Issue
Block a user