]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Do the CompoundSelect check for number of columns in the compile phase
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 16 Dec 2019 21:38:06 +0000 (16:38 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 16 Dec 2019 21:38:06 +0000 (16:38 -0500)
Starting to go forward with the general idea of moving more
of Core / ORM construction into the compile phase.   Bigger
initiatives like the refactor of Query will follow onto this.

Change-Id: I0f364d3182e21e32ed85ef34cfd11fd9d11cf653

doc/build/changelog/migration_14.rst
doc/build/changelog/unreleased_14/checks_deferred_to_compile.rst [new file with mode: 0644]
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/selectable.py
test/sql/test_compiler.py

index f2dd2745eaa792d31bc36c23c04a97b9a6c92984..6c91c40f4fd8f3d940cb8ccb1dfc5c08fb939f54 100644 (file)
@@ -18,6 +18,37 @@ What's New in SQLAlchemy 1.4?
     implicit behaviors and rarely-used API flags that complicate the internals
     and hinder performance will be removed.
 
+Behavioral Changes - General
+============================
+
+.. _change_change_deferred_construction:
+
+
+Many Core and ORM statement objects now perform much of their validation in the compile phase
+---------------------------------------------------------------------------------------------
+
+A major initiative in the 1.4 series is to approach the model of both Core SQL
+statements as well as the ORM Query to allow for an efficient, cacheable model
+of statement creation and compilation, where the compilation step would be
+cached, based on a cache key generated by the created statement object, which
+itself is newly created for each use.  Towards this goal, much of the Python
+computation which occurs within the construction of statements, particularly
+the ORM :class:`.Query`, is being moved to occur only when the statement is
+invoked.   This means that some of the error messages which can arise based on
+arguments passed to the object will no longer be raised immediately, and
+instead will occur only when the statement is invoked.
+
+Error conditions which fall under this category include:
+
+* when a :class:`.CompoundSelect` is constructed (e.g. a UNION, EXCEPT, etc.)
+  and the SELECT statements passed do not have the same number of columns, a
+  :class:`.CompileError` is now raised to this effect; previously, a
+  :class:`.ArgumentError` would be raised immediately upon statement
+  construction.
+
+* To be continued...
+
+
 API Changes - Core
 ==================
 
diff --git a/doc/build/changelog/unreleased_14/checks_deferred_to_compile.rst b/doc/build/changelog/unreleased_14/checks_deferred_to_compile.rst
new file mode 100644 (file)
index 0000000..bbaeac2
--- /dev/null
@@ -0,0 +1,19 @@
+.. change::
+    :tags: change, orm, sql
+
+    A selection of Core and ORM query objects now perform much more of their
+    Python computational tasks within the compile step, rather than at
+    construction time.  This is to support an upcoming caching model that will
+    provide for caching of the compiled statement structure based on a cache
+    key that is derived from the statement construct, which itself is expected
+    to be newly constructed in Python code each time it is used.    This means
+    that the internal state of these objects may not be the same as it used to
+    be, as well as that some but not all error raise scenarios for various
+    kinds of argument validation will occur within the compilation / execution
+    phase, rather than at statement construction time.   See the migration
+    notes linked below for complete details.
+
+    .. seealso::
+
+        :ref:`change_deferred_construction`
+
index a28ce465a911e905902a04e25b45bdb8544f3aeb..4ec3b93ea7e189b4f4275fe7e08c88a812c9dbfb 100644 (file)
@@ -2149,7 +2149,9 @@ class SQLCompiler(Compiled):
         if not populate_result_map and "add_to_result_map" in kwargs:
             del kwargs["add_to_result_map"]
 
-        froms = self._setup_select_stack(select, entry, asfrom, lateral)
+        froms = self._setup_select_stack(
+            select, entry, asfrom, lateral, compound_index
+        )
 
         column_clause_args = kwargs.copy()
         column_clause_args.update(
@@ -2254,10 +2256,33 @@ class SQLCompiler(Compiled):
         hint_text = self.get_select_hint_text(byfrom)
         return hint_text, byfrom
 
-    def _setup_select_stack(self, select, entry, asfrom, lateral):
+    def _setup_select_stack(
+        self, select, entry, asfrom, lateral, compound_index
+    ):
         correlate_froms = entry["correlate_froms"]
         asfrom_froms = entry["asfrom_froms"]
 
+        if compound_index > 0:
+            # note this is cached
+            select_0 = entry["selectable"].selects[0]
+            if select_0._is_select_container:
+                select_0 = select_0.element
+            numcols = len(select_0.selected_columns)
+            # numcols = len(select_0._columns_plus_names)
+            if len(select._columns_plus_names) != numcols:
+                raise exc.CompileError(
+                    "All selectables passed to "
+                    "CompoundSelect must have identical numbers of "
+                    "columns; select #%d has %d columns, select "
+                    "#%d has %d"
+                    % (
+                        1,
+                        numcols,
+                        compound_index + 1,
+                        len(select.selected_columns),
+                    )
+                )
+
         if asfrom and not lateral:
             froms = select._get_display_froms(
                 explicit_correlate_froms=correlate_froms.difference(
index 5f609f8fdcfb861ba6807bc1356948f7273e691f..bece0b3c58bc370fd80fafcf44cbc3c52458c8bf 100644 (file)
@@ -2782,31 +2782,12 @@ class CompoundSelect(GenerativeSelect):
     def __init__(self, keyword, *selects, **kwargs):
         self._auto_correlate = kwargs.pop("correlate", False)
         self.keyword = keyword
-        self.selects = []
-
-        numcols = None
-
-        # some DBs do not like ORDER BY in the inner queries of a UNION, etc.
-        for n, s in enumerate(selects):
-            s = coercions.expect(roles.CompoundElementRole, s)
-
-            if not numcols:
-                numcols = len(s.selected_columns)
-            elif len(s.selected_columns) != numcols:
-                raise exc.ArgumentError(
-                    "All selectables passed to "
-                    "CompoundSelect must have identical numbers of "
-                    "columns; select #%d has %d columns, select "
-                    "#%d has %d"
-                    % (
-                        1,
-                        len(self.selects[0].selected_columns),
-                        n + 1,
-                        len(s.selected_columns),
-                    )
-                )
-
-            self.selects.append(s.self_group(against=self))
+        self.selects = [
+            coercions.expect(roles.CompoundElementRole, s).self_group(
+                against=self
+            )
+            for s in selects
+        ]
 
         GenerativeSelect.__init__(self, **kwargs)
 
index 486b546828024ee0bc3077c1c8c70b70fc729c4f..54c3e2f6edaed20e008b9f2498d526249ccb2863 100644 (file)
@@ -1995,13 +1995,11 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_compound_selects(self):
         assert_raises_message(
-            exc.ArgumentError,
+            exc.CompileError,
             "All selectables passed to CompoundSelect "
             "must have identical numbers of columns; "
             "select #1 has 2 columns, select #2 has 3",
-            union,
-            table3.select(),
-            table1.select(),
+            union(table3.select(), table1.select()).compile,
         )
 
         x = union(