]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Allow escaped quotes in Postgres quoted identifier
authorAustin Graham <austingraham731@gmail.com>
Wed, 18 Mar 2026 14:57:13 +0000 (10:57 -0400)
committerFederico Caselli <cfederico87@gmail.com>
Wed, 18 Mar 2026 20:05:42 +0000 (21:05 +0100)
<!-- Provide a general summary of your proposed changes in the Title field above -->

### Description
Issue: https://github.com/sqlalchemy/sqlalchemy/issues/10902

Hello! This is my first PR here, so please let me know what I may have missed in terms of having a valuable contribution. I was looking through issues to grab an easy first one, and found this. Looks like someone else was going to have a go at it, but never did.

I simply added a small change to the FK regex in for Postgres that allows anything not quotes alongside escaped double quotes. Test is included for the scenario mentioned in the issue. Alongside that, I didn't see a test for general quoted strings, so I added another one that includes spaces and dashes, in my experience common things to be used inside quoted identifiers.

A manual test as well:
DB setup:
```
austin_test_bug=# CREATE TABLE """test_parent_table-quoted""" (id SERIAL PRIMARY KEY, val INTEGER);
CREATE TABLE
austin_test_bug=# CREATE TABLE test_child_table_ref_quoted (id SERIAL, parent INTEGER, CONSTRAINT fk_parent FOREIGN KEY (parent) REFERENCES """test_parent_table-quoted"""(id));
CREATE TABLE
austin_test_bug=# \d+
                                                     List of relations
 Schema |                Name                |   Type   |  Owner   | Persistence | Access method |    Size    | Description
--------+------------------------------------+----------+----------+-------------+---------------+------------+-------------
 public | "test_parent_table-quoted"         | table    | postgres | permanent   | heap          | 0 bytes    |
 public | "test_parent_table-quoted"_id_seq  | sequence | postgres | permanent   |               | 8192 bytes |
 public | test_child_table_ref_quoted        | table    | postgres | permanent   | heap          | 0 bytes    |
 public | test_child_table_ref_quoted_id_seq | sequence | postgres | permanent   |               | 8192 bytes |
(4 rows)

```

And the python:
```
>>> from sqlalchemy import create_engine, inspect
>>> engine = create_engine('postgresql://scott:tiger@localhost:5432/austin_test_bug')
>>> connection = engine.connect()
>>> inspect(connection).get_multi_foreign_keys()
{(None, '"test_parent_table-quoted"'): [], (None, 'test_child_table_ref_quoted'): [{'name': 'fk_parent', 'constrained_columns': ['parent'], 'referred_schema': None, 'referred_table': '"test_parent_table-quoted"', 'referred_columns': ['id'], 'options': {}, 'comment': None}]}
```

### Checklist
<!-- go over following points. check them with an `x` if they do apply, (they turn into clickable checkboxes once the PR is submitted, so no need to do everything at once)

-->

This pull request is:

- [ ] A documentation / typographical / small typing error fix
- Good to go, no issue or tests are needed
- [x] A short code fix
- please include the issue number, and create an issue if none exists, which
  must include a complete example of the issue.  one line code fixes without an
  issue and demonstration will not be accepted.
- Please include: `Fixes: #<issue number>` in the commit message
- please include tests.   one line code fixes without tests will not be accepted.
- [ ] A new feature implementation
- please include the issue number, and create an issue if none exists, which must
  include a complete example of how the feature would look.
- Please include: `Fixes: #<issue number>` in the commit message
- please include tests.

**Have a nice day!**

Fixes: #10902
Closes: #13179
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13179
Pull-request-sha: 8890dc3250a30fc62b8f15cd1da1353b81524c00

Change-Id: I185c2cb062740551ab931368de602054eb5a4acd
(cherry picked from commit c7412ac909125db873b9ddff0dafb8d92034d0a7)

doc/build/changelog/unreleased_20/10902.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
test/dialect/postgresql/test_reflection.py

diff --git a/doc/build/changelog/unreleased_20/10902.rst b/doc/build/changelog/unreleased_20/10902.rst
new file mode 100644 (file)
index 0000000..7ef60ab
--- /dev/null
@@ -0,0 +1,7 @@
+.. change::
+    :tags: postgresql, bug
+    :tickets: 10902
+
+    Fixed regular expression used when reflecting foreign keys in PostgreSQL to
+    support escaped quotes in table names.
+    Pull request courtesy of Austin Graham
index a5a15bb361c9553c89a85a97cc3243f2934b24ff..e2b6f257e9c6cb448b77352fa21705c898bf727f 100644 (file)
@@ -4507,7 +4507,7 @@ class PGDialect(default.DefaultDialect):
     @util.memoized_property
     def _fk_regex_pattern(self):
         # optionally quoted token
-        qtoken = r'(?:"[^"]+"|[\w]+?)'
+        qtoken = r'(?:"(?:[^"]|"")+"|[\w]+?)'
 
         # https://www.postgresql.org/docs/current/static/sql-createtable.html
         return re.compile(
index 32a7b6a0a31de6a98b37ef75f4b233d8fdd6e81d..df7247dd7aa48b7b16738d2fde4d8424e9bd7f18 100644 (file)
@@ -1180,6 +1180,29 @@ class RegexTest(fixtures.TestBase):
                 referred_schema='"schema_スキーマ"',
             ),
         ),
+        # Tests quoted identifiers containing characters
+        # not valid in unquoted SQL identifiers
+        (
+            'FOREIGN KEY ("tid 1", "tid-2") '
+            'REFERENCES some_schema."some table"(id1, id2)',
+            _fk_match(
+                '"tid 1", "tid-2"',
+                '"some table"',
+                "id1, id2",
+                referred_schema="some_schema",
+            ),
+        ),
+        # Tests more quotes escaped within a quote string
+        (
+            "FOREIGN KEY (tid_1, tid_2) "
+            'REFERENCES some_schema."""some_table"""(id1, id2)',
+            _fk_match(
+                "tid_1, tid_2",
+                '"""some_table"""',
+                "id1, id2",
+                referred_schema="some_schema",
+            ),
+        ),
     )
     def test_fk_parsing(self, condef, expected):