diff options
-rw-r--r-- | doc/build/changelog/changelog_08.rst | 9 | ||||
-rw-r--r-- | doc/build/changelog/changelog_09.rst | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/hstore.py | 6 | ||||
-rw-r--r-- | test/dialect/test_postgresql.py | 37 |
4 files changed, 59 insertions, 3 deletions
diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index ef87fea45..625b72c0b 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -8,6 +8,15 @@ .. change:: :tags: bug, postgresql + :tickets: 2766 + + Fixed bug in HSTORE type where keys/values that contained + backslashed quotes would not be escaped correctly when + using the "non native" (i.e. non-psycopg2) means + of translating HSTORE data. Patch courtesy Ryan Kelly. + + .. change:: + :tags: bug, postgresql :tickets: 2767 Fixed bug where the order of columns in a multi-column diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst index 57cf0da31..85fd69d8d 100644 --- a/doc/build/changelog/changelog_09.rst +++ b/doc/build/changelog/changelog_09.rst @@ -8,6 +8,16 @@ .. change:: :tags: bug, postgresql + :tickets: 2766 + + Fixed bug in HSTORE type where keys/values that contained + backslashed quotes would not be escaped correctly when + using the "non native" (i.e. non-psycopg2) means + of translating HSTORE data. Patch courtesy Ryan Kelly. + Also in 0.8.2. + + .. change:: + :tags: bug, postgresql :tickets: 2767 Fixed bug where the order of columns in a multi-column diff --git a/lib/sqlalchemy/dialects/postgresql/hstore.py b/lib/sqlalchemy/dialects/postgresql/hstore.py index d7368ff42..c645e25d2 100644 --- a/lib/sqlalchemy/dialects/postgresql/hstore.py +++ b/lib/sqlalchemy/dialects/postgresql/hstore.py @@ -68,11 +68,11 @@ def _parse_hstore(hstore_str): pair_match = HSTORE_PAIR_RE.match(hstore_str) while pair_match is not None: - key = pair_match.group('key') + key = pair_match.group('key').replace(r'\"', '"').replace("\\\\", "\\") if pair_match.group('value_null'): value = None else: - value = pair_match.group('value').replace(r'\"', '"') + value = pair_match.group('value').replace(r'\"', '"').replace("\\\\", "\\") result[key] = value pos += pair_match.end() @@ -98,7 +98,7 @@ def _serialize_hstore(val): if position == 'value' and s is None: return 'NULL' elif isinstance(s, util.string_types): - return '"%s"' % s.replace('"', r'\"') + return '"%s"' % s.replace("\\", "\\\\").replace('"', r'\"') else: raise ValueError("%r in %s position is not a string." % (s, position)) diff --git a/test/dialect/test_postgresql.py b/test/dialect/test_postgresql.py index 46a7b316b..ba42015e8 100644 --- a/test/dialect/test_postgresql.py +++ b/test/dialect/test_postgresql.py @@ -2948,6 +2948,16 @@ class HStoreTest(fixtures.TestBase): '"key1"=>"value1", "key2"=>"value2"' ) + def test_bind_serialize_with_slashes_and_quotes(self): + from sqlalchemy.engine import default + + dialect = default.DefaultDialect() + proc = self.test_table.c.hash.type._cached_bind_processor(dialect) + eq_( + proc({'\\"a': '\\"1'}), + '"\\\\\\"a"=>"\\\\\\"1"' + ) + def test_parse_error(self): from sqlalchemy.engine import default @@ -2974,6 +2984,17 @@ class HStoreTest(fixtures.TestBase): {"key1": "value1", "key2": "value2"} ) + def test_result_deserialize_with_slashes_and_quotes(self): + from sqlalchemy.engine import default + + dialect = default.DefaultDialect() + proc = self.test_table.c.hash.type._cached_result_processor( + dialect, None) + eq_( + proc('"\\\\\\"a"=>"\\\\\\"1"'), + {'\\"a': '\\"1'} + ) + def test_bind_serialize_psycopg2(self): from sqlalchemy.dialects.postgresql import psycopg2 @@ -3288,6 +3309,22 @@ class HStoreRoundTripTest(fixtures.TablesTest): engine = testing.db self._test_unicode_round_trip(engine) + def test_escaped_quotes_round_trip_python(self): + engine = self._non_native_engine() + self._test_escaped_quotes_round_trip(engine) + + @testing.only_on("postgresql+psycopg2") + def test_escaped_quotes_round_trip_native(self): + engine = testing.db + self._test_escaped_quotes_round_trip(engine) + + def _test_escaped_quotes_round_trip(self, engine): + engine.execute( + self.tables.data_table.insert(), + {'name': 'r1', 'data': {r'key \"foo\"': r'value \"bar"\ xyz'}} + ) + self._assert_data([{r'key \"foo\"': r'value \"bar"\ xyz'}]) + class _RangeTypeMixin(object): __requires__ = 'range_types', __dialect__ = 'postgresql+psycopg2' |