]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add option to disable from linting for table valued function
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 23 Mar 2022 14:07:13 +0000 (10:07 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 23 Mar 2022 14:26:54 +0000 (10:26 -0400)
Added new parameter
:paramref:`.FunctionElement.table_valued.joins_implicitly`, for the
:meth:`.FunctionElement.table_valued` construct. This parameter indicates
that the given table-valued function implicitly joins to the table it
refers towards, essentially disabling the "from linting" feature, i.e. the
"cartesian product" warning, from taking effect due to the presence of this
parameter. May be used for functions such as ``func.json_each()``.

Fixes: #7845
Change-Id: I80edcb74efbd4417172132c0db4d9c756fdd5eae
(cherry picked from commit 04dcc5c704dbf0b22705523e263e512c24936175)

doc/build/changelog/unreleased_14/7845.rst [new file with mode: 0644]
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/functions.py
lib/sqlalchemy/sql/selectable.py
test/sql/test_from_linter.py

diff --git a/doc/build/changelog/unreleased_14/7845.rst b/doc/build/changelog/unreleased_14/7845.rst
new file mode 100644 (file)
index 0000000..1cfa9cd
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: usecase, sql
+    :tickets: 7845
+
+    Added new parameter
+    :paramref:`.FunctionElement.table_valued.joins_implicitly`, for the
+    :meth:`.FunctionElement.table_valued` construct. This parameter indicates
+    that the given table-valued function implicitly joins to the table it
+    refers towards, essentially disabling the "from linting" feature, i.e. the
+    "cartesian product" warning, from taking effect due to the presence of this
+    parameter. May be used for functions such as ``func.json_each()``.
index 7780d3782a46e1818d738ec9e39873d6f800f968..671ca6749242234729e5a054ac9b189baff5e6e7 100644 (file)
@@ -2805,6 +2805,8 @@ class SQLCompiler(Compiled):
                 return self.preparer.format_alias(cte, cte_name)
 
     def visit_table_valued_alias(self, element, **kw):
+        if element.joins_implicitly:
+            kw["from_linter"] = None
         if element._is_lateral:
             return self.visit_lateral(element, **kw)
         else:
index 8c07bc066999d89fade89b8d0fc3ef4ab4e97316..e0ff1655f9f0e8ec6ed52f185881a3fbeae14738 100644 (file)
@@ -212,8 +212,16 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative):
          string name will be added as a column to the .c collection
          of the resulting :class:`_sql.TableValuedAlias`.
 
+        :param joins_implicitly: when True, the table valued function may be
+         used in the FROM clause without any explicit JOIN to other tables
+         in the SQL query, and no "cartesian product" warning will be generated.
+         May be useful for SQL functions such as ``func.json_each()``.
+
+         .. versionadded:: 1.4.33
+
         .. versionadded:: 1.4.0b2
 
+
         .. seealso::
 
             :ref:`tutorial_functions_table_valued` - in the :ref:`unified_tutorial`
@@ -234,6 +242,7 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative):
         new_func = self._generate()
 
         with_ordinality = kw.pop("with_ordinality", None)
+        joins_implicitly = kw.pop("joins_implicitly", None)
         name = kw.pop("name", None)
 
         if with_ordinality:
@@ -244,7 +253,7 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative):
             *expr
         )
 
-        return new_func.alias(name=name)
+        return new_func.alias(name=name, joins_implicitly=joins_implicitly)
 
     def column_valued(self, name=None):
         """Return this :class:`_functions.FunctionElement` as a column expression that
@@ -497,7 +506,7 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative):
 
         return None
 
-    def alias(self, name=None):
+    def alias(self, name=None, joins_implicitly=False):
         r"""Produce a :class:`_expression.Alias` construct against this
         :class:`.FunctionElement`.
 
@@ -539,6 +548,17 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative):
 
         .. versionadded:: 1.4.0b2  Added the ``.column`` accessor
 
+        :param name: alias name, will be rendered as ``AS <name>`` in the
+         FROM clause
+
+        :param joins_implicitly: when True, the table valued function may be
+         used in the FROM clause without any explicit JOIN to other tables
+         in the SQL query, and no "cartesian product" warning will be
+         generated.  May be useful for SQL functions such as
+         ``func.json_each()``.
+
+         .. versionadded:: 1.4.33
+
         .. seealso::
 
             :ref:`tutorial_functions_table_valued` -
@@ -554,7 +574,10 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative):
         """
 
         return TableValuedAlias._construct(
-            self, name, table_value_type=self.type
+            self,
+            name,
+            table_value_type=self.type,
+            joins_implicitly=joins_implicitly,
         )
 
     def select(self):
index 5516898a83ffb2d6b0c40a4f5195144e81598cb9..125c3724b82c642c436471069a325ce15a923ee5 100644 (file)
@@ -1764,6 +1764,7 @@ class TableValuedAlias(Alias):
     _supports_derived_columns = True
     _render_derived = False
     _render_derived_w_types = False
+    joins_implicitly = False
 
     _traverse_internals = [
         ("element", InternalTraversal.dp_clauseelement),
@@ -1773,9 +1774,16 @@ class TableValuedAlias(Alias):
         ("_render_derived_w_types", InternalTraversal.dp_boolean),
     ]
 
-    def _init(self, selectable, name=None, table_value_type=None):
+    def _init(
+        self,
+        selectable,
+        name=None,
+        table_value_type=None,
+        joins_implicitly=False,
+    ):
         super(TableValuedAlias, self)._init(selectable, name=name)
 
+        self.joins_implicitly = joins_implicitly
         self._tableval_type = (
             type_api.TABLEVALUE
             if table_value_type is None
index a22913868525a9d0d6fd5a2eff0269a895e3b3b3..4a4d907f965dd0cf7132c09ed674f7ee4de06092 100644 (file)
@@ -1,6 +1,10 @@
+from sqlalchemy import column
+from sqlalchemy import func
 from sqlalchemy import Integer
+from sqlalchemy import JSON
 from sqlalchemy import select
 from sqlalchemy import sql
+from sqlalchemy import table
 from sqlalchemy import testing
 from sqlalchemy import true
 from sqlalchemy.testing import config
@@ -161,6 +165,30 @@ class TestFindUnmatchingFroms(fixtures.TablesTest):
         assert start is p3
         assert froms == {p1}
 
+    @testing.combinations(True, False, argnames="joins_implicitly")
+    def test_table_valued(self, joins_implicitly):
+        """test #7845"""
+        my_table = table(
+            "tbl",
+            column("id", Integer),
+            column("data", JSON()),
+        )
+
+        sub_dict = my_table.c.data["d"]
+        tv = func.json_each(sub_dict).table_valued(
+            "key", joins_implicitly=joins_implicitly
+        )
+        has_key = tv.c.key == "f"
+        stmt = select(my_table.c.id).where(has_key)
+        froms, start = find_unmatching_froms(stmt, my_table)
+
+        if joins_implicitly:
+            is_(start, None)
+            is_(froms, None)
+        else:
+            assert start == my_table
+            assert froms == {tv}
+
     def test_count_non_eq_comparison_operators(self):
         query = select(self.a).where(self.a.c.col_a > self.b.c.col_b)
         froms, start = find_unmatching_froms(query, self.a)