From 089d133708ecb2227bb777dcdace505659ca15ac Mon Sep 17 00:00:00 2001 From: amyfranz Date: Mon, 22 Sep 2025 14:13:58 +0100 Subject: [PATCH 1/3] add tables to mkdocs --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 557be92..0431eaa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,5 +9,7 @@ extra: alias: true default: - latest +markdown_extensions: + - tables plugins: - mike From 39ebff9541627b6a3c3228282b85619707242434 Mon Sep 17 00:00:00 2001 From: amyfranz Date: Mon, 22 Sep 2025 14:13:42 +0100 Subject: [PATCH 2/3] add explanation for the problem with atomic --- docs/why.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/why.md diff --git a/docs/why.md b/docs/why.md new file mode 100644 index 0000000..8887608 --- /dev/null +++ b/docs/why.md @@ -0,0 +1,35 @@ +## Django's Atomic +Django's `atomic` ensures database changes are committed together-or-not-at-all. It creates a savepoint or a transaction depending on two factors: + +- The arguments passed to it (`durable` and `savepoint`). +- If a database transaction is already open. + +Specifically, the **Behaviours** which `atomic` exhibits are: + +| | `durable=False` (default) | `durable=True` | +| --- | --- | --- | +| `savepoint=True` (default) | **A**. Begin a transaction if needed. Creates a savepoint if already in a transaction. | **B**. Begin a transaction, or throw an error if one is already open. Never creates a savepoint. (The `savepoint` flag is ignored.) | +| `savepoint=False` | **C**. Begin a transaction if needed. Never creates a savepoint. | **D**. Same as **B**. | + +Uses of `atomic` fall into three broad **Categories**: + +1. Create a *transaction* to wrap multiple changes. +2. Create a *savepoint* so we can roll back to in order to continue with a transaction after failure. +3. Changes to be committed *atomically*, but not specific about where the transaction is created, as long as there is one. + +## Problems +1. Django's atomic creates many savepoints that are never used. There are a couple of main causes: + 1. We create savepoints with decorators. *Linting for this is possible, but each existing case requires investigation.* + 2. `atomic` creates savepoints by default. The default arguments (*Behaviour* **A**) are an [attractive nuisance](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) because they make us create savepoints when we don't need them. + > … if you have two ways to accomplish a task and one is a simple way that *looks* like the right thing but is subtly wrong, and the other is correct but + more complicated, the majority of people will end up doing the wrong + thing. + — [**Attractive nuisances in software design](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) -** [Paul Ganssle](https://blog.ganssle.io/author/paul-ganssle.html) + > +3. We have no easy way to indicate the creation of a savepoint that doesn't have the potential to create a transaction instead. The only tool we have to create a savepoint is *Behaviour* **A**, which can create a transaction. + +## What Subatomic implements +- `transaction()`. Begin a transaction, or throw an error if a transaction is already open. Like atomic(durable=True), but with added after-commit callback support in tests. +- `savepoint()`. Create a savepoint, or throw an error if we're not already in a transaction. This is not in the table of *Behaviours* (the closest we have is *Behaviour* **A**, but that can create transactions). +- `transaction_if_not_already()`. Begin a transaction if we're not already in one. Just like *Behaviour* **C**. This has a bit of a clunky name. This is deliberate, and reflects that it's a bit of a clunky thing to do. To be used with caution because the creation of a transaction is implicit. For a stricter alternative, see `transaction_required()` below. +- `transaction_required()`. Throw an error if we're not already in a transaction. Does not create savepoints *or* transactions. *Most likely to be useful in the domain layer, which should not be responsible for the creation of transactions.* From 751a4f8a266482bd5a01490a4de541e98665ae20 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 23 Sep 2025 17:07:03 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Samuel Searles-Bryant Co-authored-by: Lily Acorn --- docs/why.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/why.md b/docs/why.md index 8887608..ea5f13c 100644 --- a/docs/why.md +++ b/docs/why.md @@ -18,18 +18,19 @@ Uses of `atomic` fall into three broad **Categories**: 3. Changes to be committed *atomically*, but not specific about where the transaction is created, as long as there is one. ## Problems -1. Django's atomic creates many savepoints that are never used. There are a couple of main causes: - 1. We create savepoints with decorators. *Linting for this is possible, but each existing case requires investigation.* - 2. `atomic` creates savepoints by default. The default arguments (*Behaviour* **A**) are an [attractive nuisance](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) because they make us create savepoints when we don't need them. - > … if you have two ways to accomplish a task and one is a simple way that *looks* like the right thing but is subtly wrong, and the other is correct but - more complicated, the majority of people will end up doing the wrong - thing. - — [**Attractive nuisances in software design](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) -** [Paul Ganssle](https://blog.ganssle.io/author/paul-ganssle.html) - > + +Django's atomic creates many savepoints that are never used. There are a couple of main causes: + +1. Savepoints are created with decorators (`@atomic`). +2. `atomic` creates savepoints by default. The default arguments (*Behaviour* **A**) are an [attractive nuisance](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) because they make us create savepoints when we don't need them. + > … if you have two ways to accomplish a task and one is a simple way that *looks* like the right thing but is subtly wrong, and the other is correct but + > more complicated, the majority of people will end up doing the wrong + > thing. + > — [**Attractive nuisances in software design**](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) - [Paul Ganssle](https://blog.ganssle.io/author/paul-ganssle.html) 3. We have no easy way to indicate the creation of a savepoint that doesn't have the potential to create a transaction instead. The only tool we have to create a savepoint is *Behaviour* **A**, which can create a transaction. ## What Subatomic implements -- `transaction()`. Begin a transaction, or throw an error if a transaction is already open. Like atomic(durable=True), but with added after-commit callback support in tests. +- `transaction()`. Begin a transaction, or throw an error if a transaction is already open. Like `atomic(durable=True)`, but with added after-commit callback support in tests. - `savepoint()`. Create a savepoint, or throw an error if we're not already in a transaction. This is not in the table of *Behaviours* (the closest we have is *Behaviour* **A**, but that can create transactions). - `transaction_if_not_already()`. Begin a transaction if we're not already in one. Just like *Behaviour* **C**. This has a bit of a clunky name. This is deliberate, and reflects that it's a bit of a clunky thing to do. To be used with caution because the creation of a transaction is implicit. For a stricter alternative, see `transaction_required()` below. -- `transaction_required()`. Throw an error if we're not already in a transaction. Does not create savepoints *or* transactions. *Most likely to be useful in the domain layer, which should not be responsible for the creation of transactions.* +- `transaction_required()`. Throw an error if we're not already in a transaction. Does not create savepoints *or* transactions.