in the past few years. To simplify the dialect, version 2.7, released
in March, 2017 is now the minimum version required.
+.. _change_5941:
+
+psycopg2 dialect no longer has limitations regarding bound parameter names
+--------------------------------------------------------------------------
+
+SQLAlchemy 1.3 was not able to accommodate bound parameter names that included
+percent signs or parenthesis under the psycopg2 dialect, which meant that
+column names which included these characters were also problematic as
+INSERT and other DML statements would generate parameter names that matched
+that of the column, unless the :paramref:`_schema.Column.key` parameter
+were used to provide an alternate name that would be used to generate
+the parameter, or otherwise the parameter style of the dialect had to be
+changed. As of SQLAlchemy 1.4.0beta3 all naming limitations have been removed
+and parameters are fully escaped in all scenarios.
+
+
+:ticket:`5941`
+
+:ticket:`5653`
+
+
.. _change_5401:
psycopg2 dialect features "execute_values" with RETURNING for INSERT statements by default
--- /dev/null
+.. change::
+ :tags: bug, engine, postgresql
+ :tickets: 5941
+
+ Continued with the improvement made as part of :ticket:`5653` to further
+ support bound parameter names, including those generated against column
+ names, for names that include colons, parenthesis, and question marks, as
+ well as improved test support, so that bound parameter names even if they
+ are auto-derived from column names should have no problem including for
+ parenthesis in psycopg2's "pyformat" style.
+
+ As part of this change, the format used by the asyncpg DBAPI adapter (which
+ is local to SQLAlchemy's asyncpg diaelct) has been changed from using
+ "qmark" paramstyle to "format", as there is a standard and internally
+ supported SQL string escaping style for names that use percent signs with
+ "format" style (i.e. to double percent signs), as opposed to names that use
+ question marks with "qmark" style (where an escaping system is not defined
+ by pep-249 or Python).
+
+ .. seealso::
+
+ :ref:`change_5941`
\ No newline at end of file
import collections
import decimal
-import itertools
import json as _py_json
import re
import time
def _handle_exception(self, error):
self._adapt_connection._handle_exception(error)
- def _parameters(self):
+ def _parameter_placeholders(self, params):
if not self._inputsizes:
- return ("$%d" % idx for idx in itertools.count(1))
+ return tuple("$%d" % idx for idx, _ in enumerate(params, 1))
else:
- return (
+ return tuple(
"$%d::%s" % (idx, typ) if typ else "$%d" % idx
for idx, typ in enumerate(
(_pg_types.get(typ) for typ in self._inputsizes), 1
if not self._adapt_connection._started:
await self._adapt_connection._start_transaction()
- params = self._parameters()
-
- # TODO: would be nice to support the dollar numeric thing
- # directly, this is much easier for now
- operation = re.sub(r"\?", lambda m: next(params), operation)
+ if parameters is not None:
+ operation = operation % self._parameter_placeholders(parameters)
+ else:
+ parameters = ()
try:
prepared_stmt, attributes = await self._adapt_connection._prepare(
except Exception as error:
self._handle_exception(error)
- def execute(self, operation, parameters=()):
+ def execute(self, operation, parameters=None):
try:
self._adapt_connection.await_(
self._prepare_and_execute(operation, parameters)
if not adapt_connection._started:
adapt_connection.await_(adapt_connection._start_transaction())
- params = self._parameters()
- operation = re.sub(r"\?", lambda m: next(params), operation)
+ operation = operation % self._parameter_placeholders(
+ seq_of_parameters[0]
+ )
+
try:
return adapt_connection.await_(
self._connection.executemany(operation, seq_of_parameters)
class AsyncAdapt_asyncpg_dbapi:
def __init__(self, asyncpg):
self.asyncpg = asyncpg
- self.paramstyle = "qmark"
+ self.paramstyle = "format"
def connect(self, *arg, **kw):
async_fallback = kw.pop("async_fallback", False)
supports_unicode_binds = True
- default_paramstyle = "qmark"
+ default_paramstyle = "format"
supports_sane_multi_rowcount = False
execution_ctx_cls = PGExecutionContext_asyncpg
statement_compiler = PGCompiler_asyncpg
SQLAlchemy's own unicode encode/decode functionality is steadily becoming
obsolete as most DBAPIs now support unicode fully.
-Bound Parameter Styles
-----------------------
-
-The default parameter style for the psycopg2 dialect is "pyformat", where
-SQL is rendered using ``%(paramname)s`` style. This format has the limitation
-that it does not accommodate the unusual case of parameter names that
-actually contain percent or parenthesis symbols; as SQLAlchemy in many cases
-generates bound parameter names based on the name of a column, the presence
-of these characters in a column name can lead to problems.
-
-There are two solutions to the issue of a :class:`_schema.Column`
-that contains
-one of these characters in its name. One is to specify the
-:paramref:`.schema.Column.key` for columns that have such names::
-
- measurement = Table('measurement', metadata,
- Column('Size (meters)', Integer, key='size_meters')
- )
-
-Above, an INSERT statement such as ``measurement.insert()`` will use
-``size_meters`` as the parameter name, and a SQL expression such as
-``measurement.c.size_meters > 10`` will derive the bound parameter name
-from the ``size_meters`` key as well.
-
-.. versionchanged:: 1.0.0 - SQL expressions will use
- :attr:`_schema.Column.key`
- as the source of naming when anonymous bound parameters are created
- in SQL expressions; previously, this behavior only applied to
- :meth:`_schema.Table.insert` and :meth:`_schema.Table.update`
- parameter names.
-
-The other solution is to use a positional format; psycopg2 allows use of the
-"format" paramstyle, which can be passed to
-:paramref:`_sa.create_engine.paramstyle`::
-
- engine = create_engine(
- 'postgresql://scott:tiger@localhost:5432/test', paramstyle='format')
-
-With the above engine, instead of a statement like::
-
- INSERT INTO measurement ("Size (meters)") VALUES (%(Size (meters))s)
- {'Size (meters)': 1}
-
-we instead see::
-
- INSERT INTO measurement ("Size (meters)") VALUES (%s)
- (1, )
-
-Where above, the dictionary style is converted into a tuple with positional
-style.
-
Transactions
------------
class PGCompiler_psycopg2(PGCompiler):
- def bindparam_string(self, name, **kw):
- if "%" in name and not kw.get("post_compile", False):
- # psycopg2 will not allow a percent sign in a
- # pyformat parameter name even if it is doubled
- kw["escaped_from"] = name
- name = name.replace("%", "P")
-
- return PGCompiler.bindparam_string(self, name, **kw)
+ pass
class PGIdentifierPreparer_psycopg2(PGIdentifierPreparer):
"named": ":%(name)s",
}
+BIND_TRANSLATE = {
+ "pyformat": re.compile(r"[%\(\)]"),
+ "named": re.compile(r"[\:]"),
+}
+_BIND_TRANSLATE_CHARS = {"%": "P", "(": "A", ")": "Z", ":": "C"}
OPERATORS = {
# binary
self.positiontup = []
self._numeric_binds = dialect.paramstyle == "numeric"
self.bindtemplate = BIND_TEMPLATES[dialect.paramstyle]
+ self._bind_translate = BIND_TRANSLATE.get(dialect.paramstyle, None)
self.ctes = None
escaped_from=None,
**kw
):
+
if self.positional:
if positional_names is not None:
positional_names.append(name)
else:
self.positiontup.append(name)
+ elif not post_compile and not escaped_from:
+ tr_reg = self._bind_translate
+ if tr_reg.search(name):
+ # i'd rather use translate() here but I can't get it to work
+ # in all cases under Python 2, not worth it right now
+ new_name = tr_reg.sub(
+ lambda m: _BIND_TRANSLATE_CHARS[m.group(0)],
+ name,
+ )
+ escaped_from = name
+ name = new_name
if escaped_from:
if not self.escaped_bind_names:
#! coding: utf-8
+from . import testing
from .. import assert_raises
from .. import config
from .. import engines
from ..provision import set_default_schema_on_connection
from ..schema import Column
from ..schema import Table
+from ... import bindparam
from ... import event
from ... import exc
from ... import Integer
fixtures.FutureEngineMixin, WeCanSetDefaultSchemaWEventsTest
):
pass
+
+
+class DifficultParametersTest(fixtures.TestBase):
+ __backend__ = True
+
+ @testing.combinations(
+ ("boring",),
+ ("per cent",),
+ ("per % cent",),
+ ("%percent",),
+ ("par(ens)",),
+ ("percent%(ens)yah",),
+ ("col:ons",),
+ ("more :: %colons%",),
+ ("/slashes/",),
+ ("more/slashes",),
+ ("q?marks",),
+ ("1param",),
+ ("1col:on",),
+ argnames="name",
+ )
+ def test_round_trip(self, name, connection, metadata):
+ t = Table(
+ "t",
+ metadata,
+ Column("id", Integer, primary_key=True),
+ Column(name, String(50), nullable=False),
+ )
+
+ # table is created
+ t.create(connection)
+
+ # automatic param generated by insert
+ connection.execute(t.insert().values({"id": 1, name: "some name"}))
+
+ # automatic param generated by criteria, plus selecting the column
+ stmt = select(t.c[name]).where(t.c[name] == "some name")
+
+ eq_(connection.scalar(stmt), "some name")
+
+ # use the name in a param explicitly
+ stmt = select(t.c[name]).where(t.c[name] == bindparam(name))
+
+ row = connection.execute(stmt, {name: "some name"}).first()
+
+ # name works as the key from cursor.description
+ eq_(row._mapping[name], "some name")
# TEST: test.aaa_profiling.test_compiler.CompileTest.test_insert
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_mysqldb_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_mysqldb_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_pymysql_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_pymysql_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mssql_pyodbc_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mssql_pyodbc_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_cextensions 62
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_nocextensions 62
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 64
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_mysqldb_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_mysqldb_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_pymysql_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_pymysql_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mssql_pyodbc_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mssql_pyodbc_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_mysqldb_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_mysqldb_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_pymysql_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_pymysql_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_oracle_cx_oracle_dbapiunicode_cextensions 67
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_oracle_cx_oracle_dbapiunicode_nocextensions 67
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_cextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_nocextensions 69
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_mysqldb_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_mysqldb_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_pymysql_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mariadb_pymysql_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mssql_pyodbc_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mssql_pyodbc_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_cextensions 66
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_nocextensions 66
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 68
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_mysqldb_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_mysqldb_dbapiunicode_nocextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_pymysql_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mariadb_pymysql_dbapiunicode_nocextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mssql_pyodbc_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mssql_pyodbc_dbapiunicode_nocextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_mysqldb_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_mysqldb_dbapiunicode_nocextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_pymysql_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_mysql_pymysql_dbapiunicode_nocextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_oracle_cx_oracle_dbapiunicode_cextensions 71
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_oracle_cx_oracle_dbapiunicode_nocextensions 71
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_nocextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_cextensions 73
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_nocextensions 73
# TEST: test.aaa_profiling.test_compiler.CompileTest.test_select
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_nocextensions 69
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_cextensions 69
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_nocextensions 69
-test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_cextensions 67
-test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_nocextensions 67
+test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_cextensions 79
+test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_nocextensions 79
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_cextensions 69
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_nocextensions 69
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 69
# TEST: test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_cextensions 159
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_nocextensions 159
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 159
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 159
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_cextensions 165
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_nocextensions 165
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_cextensions 165
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_nocextensions 165
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_cextensions 169
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_nocextensions 169
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 169
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 169
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_cextensions 175
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_nocextensions 175
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_cextensions 175
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_nocextensions 175
# TEST: test.aaa_profiling.test_misc.CacheKeyTest.test_statement_key_is_cached