Skip to content

django_subatomic.db

transaction

transaction(*, using: str | None = None) -> Generator[None]

Create a database transaction.

Can be used as a decorator or a context manager.

Nested calls are not allowed because SQL does not support nested transactions. Consider this like Django's atomic(durable=True), but with added after-commit callback support in tests.

Raises:

Type Description
RuntimeError

if we call this from inside another existing transaction.

Source code in django_subatomic/db.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@contextlib.contextmanager
def transaction(*, using: str | None = None) -> Generator[None]:
    """
    Create a database transaction.

    Can be used as a decorator or a context manager.

    Nested calls are not allowed because SQL does not support nested transactions.
    Consider this like Django's `atomic(durable=True)`, but with added after-commit callback support in tests.

    Raises:
        RuntimeError: if we call this from inside another existing transaction.
    """
    # Note that `savepoint=False` is not required here because
    # the `savepoint` flag is ignored when `durable` is `True`.
    with (
        _execute_on_commit_callbacks_in_tests(using),
        django_transaction.atomic(using=using, durable=True),
    ):
        yield

transaction_if_not_already

transaction_if_not_already(*, using: str | None = None) -> Generator[None]

Create a transaction if one isn't already open.

Can be used as a decorator or a context manager.

Use of this hints at code which lacks control over the state it's called in.

Note

This has a bit of a clunky name. This is a deliberate attempt to discourage its use. It acts as a code-smell to highlight that places which use it may need further work to achieve full control over how transactions are managed.

Warning

If this function is called when a transaction is already open, errors raised through it will invalidate the current transaction, regardless of where it was opened.

Suggested alternatives
  • In functions which should not control transactions, use transaction_required. This ensures they are handled by the caller.

  • In functions which can unambiguously control transactions, use transaction.

Source code in django_subatomic/db.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@contextlib.contextmanager
def transaction_if_not_already(*, using: str | None = None) -> Generator[None]:
    """
    Create a transaction if one isn't already open.

    Can be used as a decorator or a context manager.

    Use of this hints at code which lacks control over the state it's called in.

    Note:
        This has a bit of a clunky name. This is a deliberate attempt to
        discourage its use. It acts as a code-smell to highlight that places
        which use it may need further work to achieve full control over how
        transactions are managed.

    Warning:
        If this function is called when a transaction is already open, errors raised
        through it will invalidate the current transaction, regardless of where
        it was opened.

    Tip: Suggested alternatives
        - In functions which should not control transactions,
          use [`transaction_required`][django_subatomic.db.transaction_required].
          This ensures they are handled by the caller.

        - In functions which can unambiguously control transactions,
          use [`transaction`][django_subatomic.db.transaction].
    """
    # If the innermost atomic block is from a test case, we should create a SAVEPOINT here.
    # This allows for a rollback when an exception propagates out of this block, and so
    # better simulates a production transaction behaviour in tests.
    savepoint = _innermost_atomic_block_wraps_testcase(using=using)

    with (
        _execute_on_commit_callbacks_in_tests(using),
        django_transaction.atomic(using=using, savepoint=savepoint),
    ):
        yield

savepoint

savepoint(*, using: str | None = None) -> Generator[None]

Create a database savepoint.

Can be used as a context manager, but not as a decorator.

Must be called inside an active transaction.

Recommended usage
  • You should only create a savepoint if you may roll back to it before continuing with your transaction. If your intention is to ensure that your code is committed atomically, consider using transaction_required instead.
  • We believe savepoint rollback should be handled where the savepoint is created. That locality is not possible with a decorator, so this function deliberately does not work as one.

Raises:

Type Description
_MissingRequiredTransaction

if we are not in a transaction

Source code in django_subatomic/db.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
@_utils.contextmanager
def savepoint(*, using: str | None = None) -> Generator[None]:
    """
    Create a database savepoint.

    Can be used as a context manager, but _not_ as a decorator.

    Must be called inside an active transaction.

    Tip: Recommended usage
        - You should only create a savepoint if you may roll back to it before
          continuing with your transaction. If your intention is to ensure that
          your code is committed atomically, consider using [`transaction_required`][django_subatomic.db.transaction_required]
          instead.
        - We believe savepoint rollback should be handled where the savepoint is created.
          That locality is not possible with a decorator, so this function
          deliberately does not work as one.

    Raises:
        _MissingRequiredTransaction: if we are not in a transaction
    """
    with (
        transaction_required(using=using),
        django_transaction.atomic(using=using),
    ):
        yield

transaction_required

transaction_required(*, using: str | None = None) -> Generator[None]

Make sure that code is always executed in a transaction.

Can be used as a decorator or a context manager.

We ignore test-suite transactions when checking for a transaction because we don't want to run the risk of allowing code to pass tests but fail in production.

Raises:

Type Description
_MissingRequiredTransaction

if we are not in a transaction.

Source code in django_subatomic/db.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
@contextlib.contextmanager
def transaction_required(*, using: str | None = None) -> Generator[None]:
    """
    Make sure that code is always executed in a transaction.

    Can be used as a decorator or a context manager.

    We ignore test-suite transactions when checking for a transaction
    because we don't want to run the risk of allowing code to pass tests
    but fail in production.

    Raises:
        _MissingRequiredTransaction: if we are not in a transaction.
    """
    if using is None:
        using = django_db.DEFAULT_DB_ALIAS

    if not in_transaction(using=using):
        raise _MissingRequiredTransaction(database=using)
    yield

durable

durable[**P, R](func: Callable[P, R]) -> Callable[P, R]

Enforce durability with this decorator.

Can be used as a decorator, but not as a context manager.

"Durability" means that the function's work cannot be rolled back after it completes, and is not to be confused with "atomicity" (which is about ensuring that the function either completes all its work or none of it).

We enforce this by ensuring that the function is not called within a transaction, and that no transaction is left open when the function completes.

Raises:

Type Description
_UnexpectedOpenTransaction

if a transaction is already open when this is called.

_UnexpectedDanglingTransaction

if a transaction remains open after the decorated function exits. Before raising this exeption, we roll back and end the transaction.

Source code in django_subatomic/db.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def durable[**P, R](func: Callable[P, R]) -> Callable[P, R]:
    """
    Enforce durability with this decorator.

    Can be used as a decorator, but _not_ as a context manager.

    "Durability" means that the function's work cannot be rolled back after it completes,
    and is not to be confused with "atomicity" (which is about ensuring that the function
    either completes all its work or none of it).

    We enforce this by ensuring that the function is not called within a transaction,
    and that no transaction is left open when the function completes.

    Raises:
        _UnexpectedOpenTransaction: if a transaction is already open when this is called.
        _UnexpectedDanglingTransaction: if a transaction remains open after the decorated
            function exits. Before raising this exeption, we roll back and end the
            transaction.
    """

    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        if open_dbs := dbs_with_open_transactions():
            raise _UnexpectedOpenTransaction(open_dbs=open_dbs)

        try:
            return func(*args, **kwargs)
        finally:
            if open_dbs := dbs_with_open_transactions():
                # Clean up first, otherwise we may see errors later that will mask this one.
                # This can only happen if the function manually opens a transaction,
                # so we need to manually roll it back and close it.
                for db_alias in open_dbs:
                    django_transaction.rollback(using=db_alias)
                    django_transaction.set_autocommit(True, using=db_alias)
                raise _UnexpectedDanglingTransaction(open_dbs=open_dbs)

    return wrapper

run_after_commit

run_after_commit(callback: Callable[[], object], *, using: str | None = None) -> None

Register a callback to be called after the current transaction is committed.

(Transactions created by the test suite are deliberately ignored.)

If the current transaction is rolled back, the callback will not be called.

By default, an error will be raised if there is no transaction open. While you are transitioning your codebase to stricter transaction handling, you may disable this with settings.SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION.

Note

Django's on_commit has a robust parameter, which allows a callback to fail silently. Kraken has a convention to "not allow code to fail silently" so this behaviour is not available from this function.

Source code in django_subatomic/db.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def run_after_commit(
    callback: Callable[[], object],
    *,
    using: str | None = None,
) -> None:
    """
    Register a callback to be called after the current transaction is committed.

    (Transactions created by the test suite are deliberately ignored.)

    If the current transaction is rolled back, the callback will not be called.

    By default, an error will be raised if there is no transaction open.
    While you are transitioning your codebase to stricter transaction handling,
    you may disable this with [`settings.SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION`][subatomic_after_commit_needs_transaction].

    Note:
        Django's `on_commit` has a `robust` parameter, which allows a callback
        to fail silently. Kraken has a convention to "not allow code to fail
        silently" so this behaviour is not available from this function.
    """
    if using is None:
        using = django_db.DEFAULT_DB_ALIAS

    # See Note [After-commit callbacks require a transaction]
    needs_transaction = getattr(
        settings, "SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION", True
    )
    only_in_testcase_transaction = _innermost_atomic_block_wraps_testcase(using=using)

    # Fail if a transaction is required, but none exists.
    # Ignore test-suite transactions when checking for a transaction.
    # See Note [After-commit callbacks require a transaction]
    if needs_transaction and not in_transaction(using=using):
        raise _MissingRequiredTransaction(database=using)

    if (
        # See Note [Running after-commit callbacks in tests]
        getattr(settings, "SUBATOMIC_RUN_AFTER_COMMIT_CALLBACKS_IN_TESTS", True)
        and only_in_testcase_transaction
    ):
        callback()
    else:
        django_transaction.on_commit(callback, using=using)

in_transaction

in_transaction(*, using: str | None = None) -> bool

Return True if the database connection has a transaction active.

A transaction is active if the connection is no longer in autocommit mode.

So that code doesn't need to handle how testcase transactions work, testcase transactions are not considered a transaction.

Source code in django_subatomic/db.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def in_transaction(*, using: str | None = None) -> bool:
    """
    Return `True` if the database connection has a transaction active.

    A transaction is active if the connection is no longer in autocommit mode.

    So that code doesn't need to handle how testcase transactions work,
    testcase transactions are not considered a transaction.
    """
    if using is None:
        using = django_db.DEFAULT_DB_ALIAS

    connection = django_db.connections[using]
    if connection.connection is None:
        # If there is no database connection, we can't be in a transaction.
        # We need to check this before checking for an open transaction,
        # because `get_autocommit()` would open a database connection
        # which we might not use and would consume resources unnecessarily.
        return False

    in_transaction = not django_transaction.get_autocommit(using=using)
    if not in_transaction:
        return False

    only_in_testcase_transaction = _innermost_atomic_block_wraps_testcase(using=using)

    # To make this as clear as possible I've spelled out the boolean logic here,
    # and have told ruff to ignore that this could have been simply:
    #
    #     return not only_in_testcase_transaction
    if only_in_testcase_transaction:  # noqa: SIM103
        return False
    else:
        return True

dbs_with_open_transactions

dbs_with_open_transactions() -> frozenset[str]

Get the names of databases with open transactions.

Source code in django_subatomic/db.py
414
415
416
417
418
419
420
421
422
423
424
425
def dbs_with_open_transactions() -> frozenset[str]:
    """
    Get the names of databases with open transactions.
    """
    dbs_with_open_transaction = set()
    # Note: django_db.connections is a special class which implements __iter__,
    # and should not be confused with a list or dict.
    for db_alias in django_db.connections:
        if in_transaction(using=db_alias):
            dbs_with_open_transaction.add(db_alias)

    return frozenset(dbs_with_open_transaction)