From 572055d5c49b261882de4a4653bf873b29f68b36 Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Thu, 1 Feb 2024 17:49:36 +0200 Subject: [PATCH 1/7] https://github.com/piccolo-orm/piccolo_api/discussions/265#discussioncomment-8270631 --- piccolo/columns/m2m.py | 103 ++++++++++++++++++++++++++++------------- piccolo/table.py | 4 +- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/piccolo/columns/m2m.py b/piccolo/columns/m2m.py index 0eefd22e7..7f092e1d4 100644 --- a/piccolo/columns/m2m.py +++ b/piccolo/columns/m2m.py @@ -72,20 +72,33 @@ def get_select_string(self, engine_type: str, with_alias=True) -> str: fk_2 = self.m2m._meta.secondary_foreign_key fk_2_name = fk_2._meta.db_column_name table_2 = fk_2._foreign_key_meta.resolved_references - table_2_name = table_2._meta.tablename - table_2_name_with_schema = table_2._meta.get_formatted_tablename() - table_2_pk_name = table_2._meta.primary_key._meta.db_column_name - - inner_select = f""" - {m2m_table_name_with_schema} - JOIN {table_1_name_with_schema} "inner_{table_1_name}" ON ( - {m2m_table_name_with_schema}."{fk_1_name}" = "inner_{table_1_name}"."{table_1_pk_name}" - ) - JOIN {table_2_name_with_schema} "inner_{table_2_name}" ON ( - {m2m_table_name_with_schema}."{fk_2_name}" = "inner_{table_2_name}"."{table_2_pk_name}" - ) - WHERE {m2m_table_name_with_schema}."{fk_1_name}" = "{table_1_name}"."{table_1_pk_name}" - """ # noqa: E501 + # if primary and secondary table are the same + if table_1 == table_2: + table_2_name = table_1._meta.tablename + table_2_name_with_schema = table_1._meta.get_formatted_tablename() + table_2_pk_name = table_1._meta.primary_key._meta.db_column_name + inner_select = f""" + {m2m_table_name_with_schema} + JOIN {table_1_name_with_schema} "inner_{table_1_name}" ON ( + {m2m_table_name_with_schema}."{fk_1_name}" = "inner_{table_1_name}"."{table_1_pk_name}" + ) + WHERE {m2m_table_name_with_schema}."{fk_1_name}" = "{table_1_name}"."{table_1_pk_name}" + """ # noqa: E501 + else: + table_2_name = table_2._meta.tablename + table_2_name_with_schema = table_2._meta.get_formatted_tablename() + table_2_pk_name = table_2._meta.primary_key._meta.db_column_name + + inner_select = f""" + {m2m_table_name_with_schema} + JOIN {table_1_name_with_schema} "inner_{table_1_name}" ON ( + {m2m_table_name_with_schema}."{fk_1_name}" = "inner_{table_1_name}"."{table_1_pk_name}" + ) + JOIN {table_2_name_with_schema} "inner_{table_2_name}" ON ( + {m2m_table_name_with_schema}."{fk_2_name}" = "inner_{table_2_name}"."{table_2_pk_name}" + ) + WHERE {m2m_table_name_with_schema}."{fk_1_name}" = "{table_1_name}"."{table_1_pk_name}" + """ # noqa: E501 if engine_type in ("postgres", "cockroach"): if self.as_list: @@ -233,10 +246,15 @@ def secondary_foreign_key(self) -> ForeignKey: """ See ``primary_foreign_key``. """ + # if primary and secondary table are the same for fk_column in self.foreign_key_columns: if fk_column._foreign_key_meta.resolved_references != self.table: return fk_column - + if ( + fk_column._foreign_key_meta.resolved_references + == self.primary_table + ): + return self.foreign_key_columns[-1] raise ValueError("No matching foreign key column found!") @property @@ -353,32 +371,51 @@ def __await__(self): @dataclass class M2MGetRelated: - row: Table m2m: M2M + reverse: t.Optional[bool] = False async def run(self): joining_table = self.m2m._meta.resolved_joining_table - secondary_table = self.m2m._meta.secondary_table - - # TODO - replace this with a subquery in the future. - ids = ( - await joining_table.select( - getattr( - self.m2m._meta.secondary_foreign_key, - secondary_table._meta.primary_key._meta.name, + if self.reverse: + try: + ids = ( + await joining_table.select( + getattr( + self.m2m._meta.primary_foreign_key, + secondary_table._meta.primary_key._meta.name, + ) + ) + .where(self.m2m._meta.secondary_foreign_key == self.row) + .output(as_list=True) + ) + results = await secondary_table.objects().where( + secondary_table._meta.primary_key.is_in(ids) + ) + except ValueError: + results = [] + return results + else: + try: + # TODO - replace this with a subquery in the future. + ids = ( + await joining_table.select( + getattr( + self.m2m._meta.secondary_foreign_key, + secondary_table._meta.primary_key._meta.name, + ) + ) + .where(self.m2m._meta.primary_foreign_key == self.row) + .output(as_list=True) ) - ) - .where(self.m2m._meta.primary_foreign_key == self.row) - .output(as_list=True) - ) - - results = await secondary_table.objects().where( - secondary_table._meta.primary_key.is_in(ids) - ) - return results + results = await secondary_table.objects().where( + secondary_table._meta.primary_key.is_in(ids) + ) + except ValueError: + results = [] + return results def run_sync(self): return run_sync(self.run()) diff --git a/piccolo/table.py b/piccolo/table.py index 64fb66ea9..d45d0d45c 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -610,7 +610,7 @@ def get_related( .first() ) - def get_m2m(self, m2m: M2M) -> M2MGetRelated: + def get_m2m(self, m2m: M2M, reverse: t.Optional[bool] = None) -> M2MGetRelated: """ Get all matching rows via the join table. @@ -621,7 +621,7 @@ def get_m2m(self, m2m: M2M) -> M2MGetRelated: [, ] """ - return M2MGetRelated(row=self, m2m=m2m) + return M2MGetRelated(row=self, m2m=m2m, reverse=reverse) def add_m2m( self, From e0430650f35d886bdc36bacff9634f6af1d6baff Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Tue, 28 May 2024 07:19:56 +0300 Subject: [PATCH 2/7] fix ValueError("No matching foreign key column found!") for SameSong --- piccolo/columns/m2m.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/piccolo/columns/m2m.py b/piccolo/columns/m2m.py index 7f092e1d4..18a825fed 100644 --- a/piccolo/columns/m2m.py +++ b/piccolo/columns/m2m.py @@ -255,6 +255,9 @@ def secondary_foreign_key(self) -> ForeignKey: == self.primary_table ): return self.foreign_key_columns[-1] + if self.table == self.primary_table: + return self.foreign_key_columns[-1] + raise ValueError("No matching foreign key column found!") @property From 1e7625fb17bf8a14fa1d306012a6268d93c9fc29 Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Tue, 28 May 2024 07:32:26 +0300 Subject: [PATCH 3/7] reverse param --- piccolo/columns/m2m.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/piccolo/columns/m2m.py b/piccolo/columns/m2m.py index 18a825fed..938cc391b 100644 --- a/piccolo/columns/m2m.py +++ b/piccolo/columns/m2m.py @@ -30,6 +30,7 @@ def __init__( m2m: M2M, as_list: bool = False, load_json: bool = False, + reverse: t.Optional[bool] = None, ): """ :param columns: @@ -39,12 +40,15 @@ def __init__( flattened list will be returned, rather than a list of objects. :param load_json: If ``True``, any JSON strings are loaded as Python objects. + :param reverse: + If ``True``, make reverse query to self reference tables. """ self.as_list = as_list self.columns = columns self.m2m = m2m self.load_json = load_json + self.reverse = reverse safe_types = (int, str) @@ -77,13 +81,23 @@ def get_select_string(self, engine_type: str, with_alias=True) -> str: table_2_name = table_1._meta.tablename table_2_name_with_schema = table_1._meta.get_formatted_tablename() table_2_pk_name = table_1._meta.primary_key._meta.db_column_name - inner_select = f""" - {m2m_table_name_with_schema} - JOIN {table_1_name_with_schema} "inner_{table_1_name}" ON ( - {m2m_table_name_with_schema}."{fk_1_name}" = "inner_{table_1_name}"."{table_1_pk_name}" - ) - WHERE {m2m_table_name_with_schema}."{fk_1_name}" = "{table_1_name}"."{table_1_pk_name}" - """ # noqa: E501 + # check reverse argument. If True change direction in query + if self.reverse: + inner_select = f""" + {m2m_table_name_with_schema} + JOIN {table_1_name_with_schema} "inner_{table_1_name}" ON ( + {m2m_table_name_with_schema}."{fk_1_name}" = "inner_{table_1_name}"."{table_1_pk_name}" + ) + WHERE {m2m_table_name_with_schema}."{fk_2_name}" = "{table_2_name}"."{table_2_pk_name}" + """ # noqa: E501 + else: + inner_select = f""" + {m2m_table_name_with_schema} + JOIN {table_2_name_with_schema} "inner_{table_2_name}" ON ( + {m2m_table_name_with_schema}."{fk_2_name}" = "inner_{table_2_name}"."{table_2_pk_name}" + ) + WHERE {m2m_table_name_with_schema}."{fk_1_name}" = "{table_1_name}"."{table_1_pk_name}" + """ # noqa: E501 else: table_2_name = table_2._meta.tablename table_2_name_with_schema = table_2._meta.get_formatted_tablename() @@ -379,7 +393,9 @@ class M2MGetRelated: reverse: t.Optional[bool] = False async def run(self): + joining_table = self.m2m._meta.resolved_joining_table + secondary_table = self.m2m._meta.secondary_table if self.reverse: try: @@ -393,11 +409,13 @@ async def run(self): .where(self.m2m._meta.secondary_foreign_key == self.row) .output(as_list=True) ) + results = await secondary_table.objects().where( secondary_table._meta.primary_key.is_in(ids) ) except ValueError: results = [] + return results else: try: @@ -418,6 +436,7 @@ async def run(self): ) except ValueError: results = [] + return results def run_sync(self): @@ -457,6 +476,7 @@ def __call__( *columns: t.Union[Column, t.List[Column]], as_list: bool = False, load_json: bool = False, + reverse: t.Optional[bool] = None, ) -> M2MSelect: """ :param columns: @@ -467,6 +487,7 @@ def __call__( flattened list will be returned, rather than a list of objects. :param load_json: If ``True``, any JSON strings are loaded as Python objects. + """ columns_ = flatten(columns) @@ -479,5 +500,5 @@ def __call__( ) return M2MSelect( - *columns_, m2m=self, as_list=as_list, load_json=load_json + *columns_, m2m=self, as_list=as_list, load_json=load_json, reverse=reverse ) From 66cda231b2c29ae937e951b57a5fcc5fdbd9b871 Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Tue, 28 May 2024 07:33:53 +0300 Subject: [PATCH 4/7] version --- piccolo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piccolo/__init__.py b/piccolo/__init__.py index f14dd8fd5..7344f431b 100644 --- a/piccolo/__init__.py +++ b/piccolo/__init__.py @@ -1 +1 @@ -__VERSION__ = "1.5.1" +__VERSION__ = "1.5.1.dev0" From cceb1a6680729e1a3535b25654e0f68ebd9135ad Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Tue, 20 Aug 2024 18:30:01 +0300 Subject: [PATCH 5/7] get_relateed with many --- piccolo/columns/column_types.py | 3 ++- piccolo/table.py | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index d16329b49..177c90d72 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -2052,7 +2052,8 @@ class Treasurer(Table): if not self._meta.unique or any( not i._meta.unique for i in self._meta.call_chain ): - raise ValueError("Only reverse unique foreign keys.") + pass + # raise ValueError("Only reverse unique foreign keys.") foreign_keys = [*self._meta.call_chain, self] diff --git a/piccolo/table.py b/piccolo/table.py index aa5fca7d5..1802d039e 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -581,7 +581,7 @@ def get_related( def get_related(self, foreign_key: str) -> First[Table]: ... def get_related( - self, foreign_key: t.Union[str, ForeignKey[ReferencedTable]] + self, foreign_key: t.Union[str, ForeignKey[ReferencedTable]], many: bool = False ) -> t.Union[First[Table], First[ReferencedTable]]: """ Used to fetch a ``Table`` instance, for the target of a foreign key. @@ -612,15 +612,15 @@ def get_related( references = foreign_key._foreign_key_meta.resolved_references - return ( - references.objects() - .where( + insts = references.objects().where( foreign_key._foreign_key_meta.resolved_target_column == getattr(self, column_name) ) - .first() - ) + if many: + return insts + return insts.first() + def get_m2m(self, m2m: M2M, reverse: t.Optional[bool] = None) -> M2MGetRelated: """ Get all matching rows via the join table. From 70a0845819267163e92c563b360c1120629e1ff2 Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Tue, 20 Aug 2024 18:30:49 +0300 Subject: [PATCH 6/7] get_relateed with many --- piccolo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piccolo/__init__.py b/piccolo/__init__.py index bca806d6a..705af3549 100644 --- a/piccolo/__init__.py +++ b/piccolo/__init__.py @@ -1 +1 @@ -__VERSION__ = "1.16.1.dev0" +__VERSION__ = "1.16.1.dev1" From 95420379fc78dc036994b45855076e9094117055 Mon Sep 17 00:00:00 2001 From: syalosovetskyi Date: Tue, 20 Aug 2024 18:32:40 +0300 Subject: [PATCH 7/7] get_relateed with many --- piccolo/__init__.py | 2 +- piccolo/table.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/piccolo/__init__.py b/piccolo/__init__.py index 705af3549..461d10894 100644 --- a/piccolo/__init__.py +++ b/piccolo/__init__.py @@ -1 +1 @@ -__VERSION__ = "1.16.1.dev1" +__VERSION__ = "1.16.1.dev2" diff --git a/piccolo/table.py b/piccolo/table.py index 1802d039e..b08d72b11 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -574,11 +574,11 @@ def refresh( @t.overload def get_related( - self, foreign_key: ForeignKey[ReferencedTable] + self, foreign_key: ForeignKey[ReferencedTable], many: bool = False ) -> First[ReferencedTable]: ... @t.overload - def get_related(self, foreign_key: str) -> First[Table]: ... + def get_related(self, foreign_key: str, many: bool = False) -> First[Table]: ... def get_related( self, foreign_key: t.Union[str, ForeignKey[ReferencedTable]], many: bool = False @@ -620,7 +620,7 @@ def get_related( return insts return insts.first() - + def get_m2m(self, m2m: M2M, reverse: t.Optional[bool] = None) -> M2MGetRelated: """ Get all matching rows via the join table.