diff --git a/ormar/fields/base.py b/ormar/fields/base.py index 7b86277..2bd1209 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -18,6 +18,7 @@ class BaseField(FieldInfo): column_type: sqlalchemy.Column constraints: List = [] name: str + alias: str primary_key: bool autoincrement: bool @@ -33,10 +34,14 @@ class BaseField(FieldInfo): default: Any server_default: Any + @classmethod + def get_alias(cls) -> str: + return cls.alias if cls.alias else cls.name + @classmethod def is_valid_field_info_field(cls, field_name: str) -> bool: return ( - field_name not in ["default", "default_factory"] + field_name not in ["default", "default_factory", "alias"] and not field_name.startswith("__") and hasattr(cls, field_name) ) @@ -93,7 +98,7 @@ class BaseField(FieldInfo): @classmethod def get_column(cls, name: str) -> sqlalchemy.Column: return sqlalchemy.Column( - cls.name or name, + cls.alias or name, cls.column_type, *cls.constraints, primary_key=cls.primary_key, diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index f6ad4db..72fd485 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -50,6 +50,7 @@ def ForeignKey( # noqa CFQ002 virtual: bool = False, onupdate: str = None, ondelete: str = None, + **kwargs: Any, ) -> Any: fk_string = to.Meta.tablename + "." + to.get_column_alias(to.Meta.pkname) to_field = to.Meta.model_fields[to.Meta.pkname] @@ -62,7 +63,8 @@ def ForeignKey( # noqa CFQ002 namespace = dict( __type__=__type__, to=to, - name=name, + alias=name, + name=kwargs.pop("real_name", None), nullable=nullable, constraints=[ sqlalchemy.schema.ForeignKey( diff --git a/ormar/fields/many_to_many.py b/ormar/fields/many_to_many.py index d53106a..582d318 100644 --- a/ormar/fields/many_to_many.py +++ b/ormar/fields/many_to_many.py @@ -31,6 +31,7 @@ def ManyToMany( __type__=__type__, to=to, through=through, + alias=name, name=name, nullable=True, unique=unique, diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index f32b5d4..4e1c0cd 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -32,7 +32,8 @@ class ModelFieldFactory: namespace = dict( __type__=cls._type, - name=kwargs.pop("name", None), + alias=kwargs.pop("name", None), + name=None, primary_key=kwargs.pop("primary_key", False), default=default, server_default=server_default, diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 0190a6b..72b159e 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -88,7 +88,7 @@ def register_reverse_model_fields( adjust_through_many_to_many_model(model, child, model_field) else: model.Meta.model_fields[child_model_name] = ForeignKey( - child, name=child_model_name, virtual=True + child, real_name=child_model_name, virtual=True ) @@ -96,10 +96,10 @@ def adjust_through_many_to_many_model( model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField] ) -> None: model_field.through.Meta.model_fields[model.get_name()] = ForeignKey( - model, name=model.get_name(), ondelete="CASCADE" + model, real_name=model.get_name(), ondelete="CASCADE" ) model_field.through.Meta.model_fields[child.get_name()] = ForeignKey( - child, name=child.get_name(), ondelete="CASCADE" + child, real_name=child.get_name(), ondelete="CASCADE" ) create_and_append_m2m_fk(model, model_field) @@ -166,7 +166,7 @@ def sqlalchemy_columns_from_model_fields( and not field.virtual and not issubclass(field, ManyToManyField) ): - columns.append(field.get_column(field_name)) + columns.append(field.get_column(field.get_alias())) register_relation_in_alias_manager(table_name, field) return pkname, columns @@ -211,8 +211,7 @@ def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]: {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)} ) for field_name, field in potential_fields.items(): - if field.name is None: - field.name = field_name + field.name = field_name attrs = populate_default_pydantic_field_value(field, field_name, attrs) model_fields[field_name] = field attrs["__annotations__"][field_name] = field.__type__ diff --git a/ormar/models/model.py b/ormar/models/model.py index bbd78cc..7cef4ee 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -164,8 +164,11 @@ class Model(NewBaseModel): if not self.pk and self.Meta.model_fields[self.Meta.pkname].autoincrement: self_fields.pop(self.Meta.pkname, None) self_fields = self.populate_default_values(self_fields) + + self_fields = self.translate_columns_to_aliases(self_fields) expr = self.Meta.table.insert() expr = expr.values(**self_fields) + item_id = await self.Meta.database.execute(expr) if item_id: # postgress does not return id if it's already there setattr(self, self.Meta.pkname, item_id) diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 98e0ad6..c39f112 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -65,14 +65,14 @@ class ModelTableProxy: @classmethod def get_column_alias(cls, field_name: str) -> str: field = cls.Meta.model_fields.get(field_name) - if field and field.name is not None and field.name != field_name: - return field.name + if field 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 and field.name == alias: + if field and field.alias == alias: return field_name return alias # if not found it's not an alias but actual name @@ -164,19 +164,15 @@ class ModelTableProxy: @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 - and field.name is not None - and field.name != field_name - ): - new_kwargs[field.name] = new_kwargs.pop(field_name) + 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.name in new_kwargs and field.name != field_name: - new_kwargs[field_name] = new_kwargs.pop(field.name) + if field.alias and field.alias in new_kwargs: + new_kwargs[field_name] = new_kwargs.pop(field.alias) return new_kwargs @classmethod diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 4cb67fa..2d6ac22 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -138,9 +138,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass def _extract_related_model_instead_of_field( self, item: str ) -> Optional[Union["T", Sequence["T"]]]: - alias = self.get_column_alias(item) - if alias in self._orm: - return self._orm.get(alias) + # alias = self.get_column_alias(item) + if item in self._orm: + return self._orm.get(item) return None # pragma no cover def __eq__(self, other: object) -> bool: diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py index 0cc6953..496020e 100644 --- a/ormar/queryset/join.py +++ b/ormar/queryset/join.py @@ -120,7 +120,13 @@ class SqlJoin: pkname_alias = model_cls.get_column_alias(model_cls.Meta.pkname) if not is_multi: - self.get_order_bys(alias, to_table, pkname_alias, part) + self.get_order_bys( + alias=alias, + to_table=to_table, + pkname_alias=pkname_alias, + part=part, + model_cls=model_cls, + ) self_related_fields = model_cls.own_table_columns( model_cls, self.fields, self.exclude_fields, nested=True, @@ -150,8 +156,21 @@ class SqlJoin: condition[-2] == part or condition[-2][1:] == part ) + def set_aliased_order_by( + self, condition: List[str], alias: str, to_table: str, model_cls: Type["Model"], + ) -> None: + direction = f"{'desc' if condition[0][0] == '-' else ''}" + column_alias = model_cls.get_column_alias(condition[-1]) + order = text(f"{alias}_{to_table}.{column_alias} {direction}") + self.sorted_orders["__".join(condition)] = order + def get_order_bys( # noqa: CCR001 - self, alias: str, to_table: str, pkname_alias: str, part: str + self, + alias: str, + to_table: str, + pkname_alias: str, + part: str, + model_cls: Type["Model"], ) -> None: if self.order_columns: split_order_columns = [ @@ -159,9 +178,12 @@ class SqlJoin: ] for condition in split_order_columns: if self._check_if_condition_apply(condition, part): - direction = f"{'desc' if condition[0][0] == '-' else ''}" - order = text(f"{alias}_{to_table}.{condition[-1]} {direction}") - self.sorted_orders["__".join(condition)] = order + self.set_aliased_order_by( + condition=condition, + alias=alias, + to_table=to_table, + model_cls=model_cls, + ) else: order = text(f"{alias}_{to_table}.{pkname_alias}") self.sorted_orders[f"{to_table}.{pkname_alias}"] = order diff --git a/ormar/queryset/query.py b/ormar/queryset/query.py index ebe8925..1bdf829 100644 --- a/ormar/queryset/query.py +++ b/ormar/queryset/query.py @@ -54,14 +54,17 @@ class Query: pkname_alias = self.model_cls.get_column_alias(self.model_cls.Meta.pkname) return f"{self.table.name}.{pkname_alias}" + def alias(self, name: str) -> str: + return self.model_cls.get_column_alias(name) + def apply_order_bys_for_primary_model(self) -> None: # noqa: CCR001 if self.order_columns: for clause in self.order_columns: if "__" not in clause: clause = ( - text(f"{clause[1:]} desc") + text(f"{self.alias(clause[1:])} desc") if clause.startswith("-") - else text(clause) + else text(self.alias(clause)) ) self.sorted_orders[clause] = clause else: diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index d45908e..e202a99 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -337,15 +337,15 @@ class QuerySet: expr = self.table.insert() expr = expr.values(**new_kwargs) - instance = self.model(**kwargs) pk = await self.database.execute(expr) pk_name = self.model.get_column_alias(self.model_meta.pkname) if pk_name not in kwargs and pk_name in new_kwargs: - instance.pk = new_kwargs[self.model_meta.pkname] + kwargs["pk"] = new_kwargs[self.model_meta.pkname] if pk and isinstance(pk, self.model.pk_type()): - setattr(instance, self.model_meta.pkname, pk) + kwargs[self.model_meta.pkname] = pk + instance = self.model(**kwargs) # refresh server side defaults instance = await instance.load() diff --git a/tests/test_excluding_subset_of_columns.py b/tests/test_excluding_subset_of_columns.py index 6679c12..6b7a6a5 100644 --- a/tests/test_excluding_subset_of_columns.py +++ b/tests/test_excluding_subset_of_columns.py @@ -34,7 +34,7 @@ class Car(ormar.Model): name: str = ormar.String(max_length=100) year: int = ormar.Integer(nullable=True) gearbox_type: str = ormar.String(max_length=20, nullable=True) - gears: int = ormar.Integer(nullable=True) + gears: int = ormar.Integer(nullable=True, name="gears_number") aircon_type: str = ormar.String(max_length=20, nullable=True) @@ -79,7 +79,9 @@ async def test_selecting_subset(): all_cars = ( await Car.objects.select_related("manufacturer") - .exclude_fields(["gearbox_type", "gears", "aircon_type", "year", "company__founded"]) + .exclude_fields( + ["gearbox_type", "gears", "aircon_type", "year", "company__founded"] + ) .all() ) for car in all_cars: @@ -114,6 +116,20 @@ async def test_selecting_subset(): assert car.manufacturer.name == "Toyota" assert car.manufacturer.founded == 1937 + all_cars_check2 = ( + await Car.objects.select_related("manufacturer") + .fields(["id", "name", "manufacturer"]) + .exclude_fields("company__founded") + .all() + ) + for car in all_cars_check2: + assert all( + getattr(car, x) is None + for x in ["year", "gearbox_type", "gears", "aircon_type"] + ) + assert car.manufacturer.name == "Toyota" + assert car.manufacturer.founded is None + with pytest.raises(pydantic.error_wrappers.ValidationError): # cannot exclude mandatory model columns - company__name in this example await Car.objects.select_related("manufacturer").exclude_fields( diff --git a/tests/test_order_by.py b/tests/test_order_by.py index dbe5cd2..8bd50c6 100644 --- a/tests/test_order_by.py +++ b/tests/test_order_by.py @@ -32,6 +32,27 @@ class Owner(ormar.Model): name: str = ormar.String(max_length=100) +class AliasNested(ormar.Model): + class Meta: + tablename = "aliases_nested" + metadata = metadata + database = database + + id: int = ormar.Integer(name="alias_id", primary_key=True) + name: str = ormar.String(name="alias_name", max_length=100) + + +class AliasTest(ormar.Model): + class Meta: + tablename = "aliases" + metadata = metadata + database = database + + id: int = ormar.Integer(name="alias_id", primary_key=True) + name: str = ormar.String(name="alias_name", max_length=100) + nested: str = ormar.ForeignKey(AliasNested, name="nested_alias") + + class Toy(ormar.Model): class Meta: tablename = "toys" @@ -275,3 +296,34 @@ async def test_sort_order_on_many_to_many(): assert users[1].cars[1].name == "Buggy" assert users[1].cars[2].name == "Ferrari" assert users[1].cars[3].name == "Skoda" + + +@pytest.mark.asyncio +async def test_sort_order_with_aliases(): + async with database: + al1 = await AliasTest.objects.create(name="Test4") + al2 = await AliasTest.objects.create(name="Test2") + await AliasTest.objects.create(name="Test1") + await AliasTest.objects.create(name="Test3") + + aliases = await AliasTest.objects.order_by("-name").all() + assert [alias.name[-1] for alias in aliases] == ["4", "3", "2", "1"] + + nest1 = await AliasNested.objects.create(name="Try1") + nest2 = await AliasNested.objects.create(name="Try2") + + al1.nested = nest1 + await al1.update() + + al2.nested = nest2 + await al2.update() + + aliases = ( + await AliasTest.objects.select_related("nested") + .order_by("-nested__name") + .all() + ) + assert aliases[0].nested.name == "Try2" + assert aliases[1].nested.name == "Try1" + assert aliases[2].nested is None + assert aliases[3].nested is None diff --git a/tests/test_properties.py b/tests/test_properties.py index 72398b3..b89db67 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -50,17 +50,19 @@ async def test_sort_order_on_main_model(): songs = await Song.objects.all() song_dict = [song.dict() for song in songs] - assert all('sorted_name' in x for x in song_dict) - assert all(x['sorted_name'] == f"{x['sort_order']}: {x['name']}" for x in song_dict) + assert all("sorted_name" in x for x in song_dict) + assert all( + x["sorted_name"] == f"{x['sort_order']}: {x['name']}" for x in song_dict + ) song_json = [song.json() for song in songs] - assert all('sorted_name' in x for x in song_json) + assert all("sorted_name" in x for x in song_json) check_include = songs[0].dict(include={"sample"}) - assert 'sample' in check_include - assert 'sample2' not in check_include - assert 'sorted_name' not in check_include + assert "sample" in check_include + assert "sample2" not in check_include + assert "sorted_name" not in check_include check_include = songs[0].dict(exclude={"sample"}) - assert 'sample' not in check_include - assert 'sample2' in check_include - assert 'sorted_name' in check_include + assert "sample" not in check_include + assert "sample2" in check_include + assert "sorted_name" in check_include diff --git a/tests/test_selecting_subset_of_columns.py b/tests/test_selecting_subset_of_columns.py index 389648b..0f76f1a 100644 --- a/tests/test_selecting_subset_of_columns.py +++ b/tests/test_selecting_subset_of_columns.py @@ -19,7 +19,7 @@ class Company(ormar.Model): database = database id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=100, nullable=False) + name: str = ormar.String(max_length=100, nullable=False, name="company_name") founded: int = ormar.Integer(nullable=True)