From: Javid Khan Date: Wed, 8 Jul 2026 14:20:43 +0000 (-0400) Subject: fix catastrophic backtracking in sqlite inline unique reflection regex X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6c35a3f6433c52c0c6878b2348caec34e669edc6;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git fix catastrophic backtracking in sqlite inline unique reflection regex Fixes: #13419 ### Description While reflecting a SQLite table, `get_unique_constraints` scans the stored `CREATE TABLE` text (taken verbatim from `sqlite_master.sql`) with an `INLINE_UNIQUE_PATTERN` to spot inline `UNIQUE` columns. The tail of that pattern is `[\t ]+[a-z0-9_ ]+?[\t ]+UNIQUE`, where the lazy middle class itself contains a space, so all three quantifiers can lay claim to the same space character. When a column definition contains a run of whitespace that isn't followed by `UNIQUE`, the engine tries every way of splitting that run across the three quantifiers, which is cubic in the length of the run. SQLite keeps the original whitespace of a `CREATE TABLE` statement in `sqlite_master`, so any account that can create a table can leave a long gap in a column definition and make later reflection of that schema hang. A small reproduction: ```python from sqlalchemy import create_engine, inspect e = create_engine("sqlite://") with e.begin() as c: c.exec_driver_sql( "CREATE TABLE t (x INTEGER" + " " * 1000 + "NOT NULL, " "y INTEGER NOT NULL UNIQUE)" ) inspect(e).get_unique_constraints("t") # ~11s before, instant after ``` The fix tokenises the inter-keyword whitespace with disjoint classes, `[\t ]+[a-z0-9_]+(?:[\t ]+[a-z0-9_]+)*?[\t ]+UNIQUE`, so a space only ever belongs to a separator and never to a token. Valid inline `UNIQUE` columns reflect exactly as before; I have added a regression test that reflects a table carrying a long whitespace run and runs in linear time on the new pattern. ### Checklist This pull request is: - [x] A short code fix Closes: #13398 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13398 Pull-request-sha: e398806f1243897f25c6ef0697200a49df464243 Change-Id: I7e394357162c9d3aad6c7a72d9276321d404ca64 (cherry picked from commit 121a17bb32046b980f120abca4793aad116aa23c) --- diff --git a/doc/build/changelog/unreleased_20/13419.rst b/doc/build/changelog/unreleased_20/13419.rst new file mode 100644 index 0000000000..8fba8c2440 --- /dev/null +++ b/doc/build/changelog/unreleased_20/13419.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: bug, sqlite + :tickets: 13419 + + Reworked the regular expression that detects inline ``UNIQUE`` column + constraints during SQLite ``CREATE TABLE`` reflection so that the + whitespace separating a column's type from a following clause is matched + unambiguously. The previous pattern had three overlapping quantifiers + that could each consume a space character, so a column definition + carrying a long run of whitespace in the stored schema made + :meth:`_reflection.Inspector.get_unique_constraints` spend cubic time + backtracking before returning. Fix courtesy of Javid Khan. diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 11c69ef218..abd9366480 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -2774,7 +2774,7 @@ class SQLiteDialect(default.DefaultDialect): ) INLINE_UNIQUE_PATTERN = ( r'(?:(".+?")|(?:[\[`])?([a-z0-9_]+)(?:[\]`])?)[\t ]' - r"+[a-z0-9_ ]+?[\t ]+UNIQUE" + r"+[a-z0-9_]+(?:[\t ]+[a-z0-9_]+)*?[\t ]+UNIQUE" ) for match in re.finditer(UNIQUE_PATTERN, table_data, re.I): diff --git a/test/dialect/sqlite/test_reflection.py b/test/dialect/sqlite/test_reflection.py index 804116f3fe..53d8772ddd 100644 --- a/test/dialect/sqlite/test_reflection.py +++ b/test/dialect/sqlite/test_reflection.py @@ -872,6 +872,23 @@ class ConstraintReflectionTest(fixtures.TestBase): [{"column_names": ["some STUPID n,ame"], "name": None}], ) + def test_unique_constraint_inline_whitespace_run( + self, connection, metadata + ): + # a long run of whitespace between a column type and a following + # clause used to make the inline-UNIQUE regex backtrack + # catastrophically while scanning the stored CREATE TABLE text; + # the ambiguous pattern was cubic on the whitespace length + Table("t", metadata, Column("x", Integer), Column("y", Integer)) + connection.exec_driver_sql( + "CREATE TABLE t (x INTEGER" + (" " * 1000) + "NOT NULL, " + "y INTEGER NOT NULL UNIQUE)" + ) + eq_( + inspect(connection).get_unique_constraints("t"), + [{"column_names": ["y"], "name": None}], + ) + def test_unique_constraint_unnamed_normal(self): inspector = inspect(testing.db) eq_(