--- /dev/null
+.. change::
+ :tags: bug, sql
+ :tickets: 4837
+
+ Characters that interfere with "pyformat" or "named" formats in bound
+ parameters, namely ``%, (, )`` and the space character, as well as a few
+ other typically undesirable characters, are stripped early for a
+ :func:`.bindparam` that is using an anonymized name, which is typically
+ generated automatically from a named column which itself includes these
+ characters in its name and does not use a ``.key``, so that they do not
+ interfere either with the SQLAlchemy compiler's use of string formatting or
+ with the driver-level parsing of the parameter, both of which could be
+ demonstrated before the fix. The change only applies to anonymized
+ parameter names that are generated and consumed internally, not end-user
+ defined names, so the change should have no impact on any existing code.
+ Applies in particular to the psycopg2 driver which does not otherwise quote
+ special parameter names, but also strips leading underscores to suit Oracle
+ (but not yet leading numbers, as some anon parameters are currently
+ entirely numeric/underscore based); Oracle in any case continues to quote
+ parameter names that include special characters.
eq_(len(set(pp)), total_params, "%s %s" % (len(set(pp)), len(pp)))
eq_(len(set(pp.values())), total_params)
+ def test_bind_anon_name_no_special_chars(self):
+ for paramstyle in "named", "pyformat":
+ dialect = default.DefaultDialect()
+ dialect.paramstyle = paramstyle
+
+ for name, named, pyformat in [
+ ("%(my name)s", ":my_name_s_1", "%(my_name_s_1)s"),
+ ("myname(foo)", ":myname_foo_1", "%(myname_foo_1)s"),
+ (
+ "this is a name",
+ ":this_is_a_name_1",
+ "%(this_is_a_name_1)s",
+ ),
+ ("_leading_one", ":leading_one_1", "%(leading_one_1)s"),
+ ("3leading_two", ":3leading_two_1", "%(3leading_two_1)s"),
+ ("$leading_three", ":leading_three_1", "%(leading_three_1)s"),
+ ("%(tricky", ":tricky_1", "%(tricky_1)s"),
+ ("5(tricky", ":5_tricky_1", "%(5_tricky_1)s"),
+ ]:
+ t = table("t", column(name, String))
+ expr = t.c[name] == "foo"
+
+ self.assert_compile(
+ expr,
+ "t.%s = %s"
+ % (
+ dialect.identifier_preparer.quote(name),
+ named if paramstyle == "named" else pyformat,
+ ),
+ dialect=dialect,
+ checkparams={named[1:]: "foo"},
+ )
+
+ def test_bind_anon_name_special_chars_uniqueify_one(self):
+ # test that the chars are escaped before doing the counter,
+ # otherwise these become the same name and bind params will conflict
+ t = table("t", column("_3foo"), column("4%foo"))
+
+ self.assert_compile(
+ (t.c["_3foo"] == "foo") & (t.c["4%foo"] == "bar"),
+ 't._3foo = :3foo_1 AND t."4%foo" = :4_foo_1',
+ checkparams={"3foo_1": "foo", "4_foo_1": "bar"},
+ )
+
+ def test_bind_anon_name_special_chars_uniqueify_two(self):
+
+ t = table("t", column("_3foo"), column("4(foo"))
+
+ self.assert_compile(
+ (t.c["_3foo"] == "foo") & (t.c["4(foo"] == "bar"),
+ 't._3foo = :3foo_1 AND t."4(foo" = :4_foo_1',
+ checkparams={"3foo_1": "foo", "4_foo_1": "bar"},
+ )
+
def test_bind_as_col(self):
t = table("foo", column("id"))