From: Mike Bayer Date: Tue, 27 Jun 2023 14:17:36 +0000 (-0400) Subject: accomodate schema translate keys present or not present X-Git-Tag: rel_2_0_18~22^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=51b65467e1c4150c4b594ac490eafd6d721d7459;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git accomodate schema translate keys present or not present 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 --- diff --git a/doc/build/changelog/unreleased_20/10025.rst b/doc/build/changelog/unreleased_20/10025.rst new file mode 100644 index 0000000000..ee7fad1d6e --- /dev/null +++ b/doc/build/changelog/unreleased_20/10025.rst @@ -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. diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 79092ec661..6623d2062f 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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: diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 692a51b910..6080f3dc6d 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -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)