Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 3.8.3 (2025-09-23)

#### Fixes
- Ignore context tracking on VACUUM and other SQL statements that are either irrelevant or cause issues by [@wesleykendall](https://github.com/wesleykendall) in [#232](https://github.com/AmbitionEng/django-pghistory/pull/232).

## 3.8.2 (2025-09-12)

#### Fixes
Expand Down
41 changes: 25 additions & 16 deletions pghistory/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,34 @@


Context = collections.namedtuple("Context", ["id", "metadata"])


def _is_concurrent_statement(sql: Union[str, bytes]):
IGNORED_SQL_PREFIXES = (
"select",
"vacuum",
"analyze",
"checkpoint",
"discard",
"load",
"cluster",
"reindex",
"create",
"alter",
"drop",
)


def _is_ignored_statement(sql: Union[str, bytes]):
"""
True if the sql statement is concurrent and cannot be ran in a transaction
"""
sql = sql.strip().lower() if sql else ""
sql = sql.decode() if isinstance(sql, bytes) else sql
return sql.startswith("create") and "concurrently" in sql

True if the sql statement is ignored for context tracking.
This includes select statements and other statements like vacuum.

def _is_dml_statement(sql: Union[str, bytes]):
"""
True if the sql statement is a dml statement (insert, update, delete)
Note: SQL is very complex. We may still inject context variables into
SQL that has CTEs, for example. Generally this should handle most cases
where it's impossible to even put a variable in the SQL, such as statements
that cannot be ran in a transaction (vacuum, etc).
"""
sql = sql.strip().lower() if sql else ""
sql = sql.decode() if isinstance(sql, bytes) else sql
return not sql.startswith("select")
return sql.startswith(IGNORED_SQL_PREFIXES)


def _is_transaction_errored(cursor):
Expand Down Expand Up @@ -72,10 +82,9 @@ def _can_inject_variable(cursor, sql):
setting. Ignore these cases for now.
"""
return (
not getattr(cursor, "name", None)
and not _is_concurrent_statement(sql)
not _is_ignored_statement(sql)
and not getattr(cursor, "name", None)
and not _is_transaction_errored(cursor)
and _is_dml_statement(sql)
)


Expand Down
17 changes: 13 additions & 4 deletions pghistory/tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@
"statement, expected",
[
("create index concurrently", True),
("create index", False),
("create index", True),
("select * from auth_user", True),
("vacuum table", True),
("analyze table", True),
("checkpoint table", True),
("discard all", True),
("load extension", True),
("cluster", True),
("update auth_user set id= %s where id = %s", False),
(b"create index concurrently", True),
(b"create index", False),
(b"select * from auth_user", True),
(b"update auth_user set id= %s where id = %s", False),
],
)
def test_is_concurrent_statement(statement, expected):
assert pghistory.runtime._is_concurrent_statement(statement) == expected
def test_is_ignored_statement(statement, expected):
assert pghistory.runtime._is_ignored_statement(statement) == expected


@pytest.mark.skipif(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ packages = [
exclude = [
"*/tests/"
]
version = "3.8.2"
version = "3.8.3"
description = "History tracking for Django and Postgres"
authors = ["Wes Kendall"]
classifiers = [
Expand Down