]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
accomodate schema translate keys present or not present
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 27 Jun 2023 14:17:36 +0000 (10:17 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 27 Jun 2023 16:59:05 +0000 (12:59 -0400)
Adjusted the :paramref:`_sa.create_engine.schema_translate_map` feature
such that **all** schema names in the statement are now tokenized,
regardless of whether or not a specific name is in the immediate schema
translate map given, and to fallback to substituting the original name when
the key is not in the actual schema translate map at execution time.  These
two changes allow for repeated use of a compiled object with schema
schema_translate_maps that include or dont include various keys on each
run, allowing cached SQL constructs to continue to function at runtime when
schema translate maps with different sets of keys are used each time. In
addition, added detection of schema_translate_map dictionaries which gain
or lose a ``None`` key across calls for the same statement, which affects
compilation of the statement and is not compatible with caching; an
exception is raised for these scenarios.

Fixes: #10025
Change-Id: I6f5e0c8e067d1702a3647b6251af483669ad854b

doc/build/changelog/unreleased_20/10025.rst [new file with mode: 0644]
lib/sqlalchemy/sql/compiler.py
test/engine/test_execute.py

diff --git a/doc/build/changelog/unreleased_20/10025.rst b/doc/build/changelog/unreleased_20/10025.rst
new file mode 100644 (file)
index 0000000..ee7fad1
--- /dev/null
@@ -0,0 +1,17 @@
+.. change::
+    :tags: bug, engine
+    :tickets: 10025
+
+    Adjusted the :paramref:`_sa.create_engine.schema_translate_map` feature
+    such that **all** schema names in the statement are now tokenized,
+    regardless of whether or not a specific name is in the immediate schema
+    translate map given, and to fallback to substituting the original name when
+    the key is not in the actual schema translate map at execution time.  These
+    two changes allow for repeated use of a compiled object with schema
+    schema_translate_maps that include or dont include various keys on each
+    run, allowing cached SQL constructs to continue to function at runtime when
+    schema translate maps with different sets of keys are used each time. In
+    addition, added detection of schema_translate_map dictionaries which gain
+    or lose a ``None`` key across calls for the same statement, which affects
+    compilation of the statement and is not compatible with caching; an
+    exception is raised for these scenarios.
index 79092ec6619507fbffde4543c709477d006eec29..6623d2062fcd7bd789bca03b3e5e762e47ba4dd5 100644 (file)
@@ -7156,6 +7156,8 @@ class IdentifierPreparer:
 
     """
 
+    _includes_none_schema_translate: bool = False
+
     def __init__(
         self,
         dialect,
@@ -7196,9 +7198,11 @@ class IdentifierPreparer:
         prep = self.__class__.__new__(self.__class__)
         prep.__dict__.update(self.__dict__)
 
+        includes_none = None in schema_translate_map
+
         def symbol_getter(obj):
             name = obj.schema
-            if name in schema_translate_map and obj._use_schema_map:
+            if obj._use_schema_map and (name is not None or includes_none):
                 if name is not None and ("[" in name or "]" in name):
                     raise exc.CompileError(
                         "Square bracket characters ([]) not supported "
@@ -7211,16 +7215,38 @@ class IdentifierPreparer:
                 return obj.schema
 
         prep.schema_for_object = symbol_getter
+        prep._includes_none_schema_translate = includes_none
         return prep
 
     def _render_schema_translates(self, statement, schema_translate_map):
         d = schema_translate_map
         if None in d:
+            if not self._includes_none_schema_translate:
+                raise exc.InvalidRequestError(
+                    "schema translate map which previously did not have "
+                    "`None` present as a key now has `None` present; compiled "
+                    "statement may lack adequate placeholders.  Please use "
+                    "consistent keys in successive "
+                    "schema_translate_map dictionaries."
+                )
+
             d["_none"] = d[None]
 
         def replace(m):
             name = m.group(2)
-            effective_schema = d[name]
+            if name in d:
+                effective_schema = d[name]
+            else:
+                if name in (None, "_none"):
+                    raise exc.InvalidRequestError(
+                        "schema translate map which previously had `None` "
+                        "present as a key now no longer has it present; don't "
+                        "know how to apply schema for compiled statement. "
+                        "Please use consistent keys in successive "
+                        "schema_translate_map dictionaries."
+                    )
+                effective_schema = name
+
             if not effective_schema:
                 effective_schema = self.dialect.default_schema_name
                 if not effective_schema:
index 692a51b91098ab72a4435715dcc8f3c5fbaa3a69..6080f3dc6d045aede97cc6ff738f4909ce621454 100644 (file)
@@ -1250,6 +1250,19 @@ class SchemaTranslateTest(fixtures.TestBase, testing.AssertsExecutionResults):
 
         return t1, t2, t3
 
+    @testing.fixture
+    def same_named_tables(self, metadata, connection):
+        ts1 = Table(
+            "t1", metadata, Column("x", String(10)), schema=config.test_schema
+        )
+        tsnone = Table("t1", metadata, Column("x", String(10)), schema=None)
+
+        metadata.create_all(connection)
+
+        connection.execute(ts1.insert().values(x="ts1"))
+        connection.execute(tsnone.insert().values(x="tsnone"))
+        return ts1, tsnone
+
     def test_create_table(self, plain_tables, connection):
         map_ = {
             None: config.test_schema,
@@ -1403,6 +1416,133 @@ class SchemaTranslateTest(fixtures.TestBase, testing.AssertsExecutionResults):
             CompiledSQL("DELETE FROM __[SCHEMA_bar].t3"),
         )
 
+    def test_schema_translate_map_keys_change_name_added(
+        self, same_named_tables, connection
+    ):
+        """test #10024"""
+
+        metadata = MetaData()
+
+        translate_table = Table(
+            "t1", metadata, Column("x", String(10)), schema=config.test_schema
+        )
+
+        eq_(
+            connection.scalar(
+                select(translate_table),
+                execution_options={"schema_translate_map": {"foo": "bar"}},
+            ),
+            "ts1",
+        )
+
+        eq_(
+            connection.scalar(
+                select(translate_table),
+                execution_options={
+                    "schema_translate_map": {
+                        "foo": "bar",
+                        config.test_schema: None,
+                    }
+                },
+            ),
+            "tsnone",
+        )
+
+    def test_schema_translate_map_keys_change_name_removed(
+        self, same_named_tables, connection
+    ):
+        """test #10024"""
+
+        metadata = MetaData()
+
+        translate_table = Table(
+            "t1", metadata, Column("x", String(10)), schema=config.test_schema
+        )
+
+        eq_(
+            connection.scalar(
+                select(translate_table),
+                execution_options={
+                    "schema_translate_map": {
+                        "foo": "bar",
+                        config.test_schema: None,
+                    }
+                },
+            ),
+            "tsnone",
+        )
+
+        eq_(
+            connection.scalar(
+                select(translate_table),
+                execution_options={"schema_translate_map": {"foo": "bar"}},
+            ),
+            "ts1",
+        )
+
+    def test_schema_translate_map_keys_change_none_removed(
+        self, same_named_tables, connection
+    ):
+        """test #10024"""
+
+        connection.engine.clear_compiled_cache()
+
+        metadata = MetaData()
+
+        translate_table = Table("t1", metadata, Column("x", String(10)))
+
+        eq_(
+            connection.scalar(
+                select(translate_table),
+                execution_options={
+                    "schema_translate_map": {None: config.test_schema}
+                },
+            ),
+            "ts1",
+        )
+
+        with expect_raises_message(
+            tsa.exc.StatementError,
+            "schema translate map which previously had `None` "
+            "present as a key now no longer has it present",
+        ):
+            connection.scalar(
+                select(translate_table),
+                execution_options={"schema_translate_map": {"foo": "bar"}},
+            ),
+
+    def test_schema_translate_map_keys_change_none_added(
+        self, same_named_tables, connection
+    ):
+        """test #10024"""
+
+        connection.engine.clear_compiled_cache()
+
+        metadata = MetaData()
+
+        translate_table = Table("t1", metadata, Column("x", String(10)))
+
+        eq_(
+            connection.scalar(
+                select(translate_table),
+                execution_options={"schema_translate_map": {"foo": "bar"}},
+            ),
+            "tsnone",
+        )
+
+        with expect_raises_message(
+            tsa.exc.StatementError,
+            "schema translate map which previously did not have `None` "
+            "present as a key now has `None` present; compiled statement may "
+            "lack adequate placeholders.",
+        ):
+            connection.scalar(
+                select(translate_table),
+                execution_options={
+                    "schema_translate_map": {None: config.test_schema}
+                },
+            ),
+
     def test_crud(self, plain_tables, connection):
         # provided by metadata fixture provided by plain_tables fixture
         self.metadata.create_all(connection)