conceptadev / conceptadev/nestjs-modules

SQLite: concurrent writes to versioned entities fail with 500 instead of waiting — worth fixing?

Closed
#476 0 comments 0 reactions 0 assignees View on GitHub
discussion
Dominant language
TypeScript
Stars
95
Forks
12
Avg merge
35m
Merged PRs (30d)
1

Description

## Summary

Opening this as a discussion rather than a bug report: on TypeORM + SQLite,
concurrent writes to entities with a version column fail with a `500` instead
of waiting. We are not sure it is worth fixing upstream, since SQLite is mostly
a test / local-dev database — raising it so the trade-off is decided on
purpose.

## What happens

When two requests update versioned entities at the same time, one of them fails
with `SQLITE_ERROR: cannot start a transaction within a transaction`, surfacing
as `500 REPOSITORY_QUERY_ERROR`.

- **No update is lost** — the failing write never lands.
- **But the version check never runs** for that write, so even a real conflict
cannot come back as `409 OPTIMISTIC_LOCK_CONFLICT`.

## Measured

Rockets sample app, in-memory SQLite, two concurrent CRUD `PATCH` requests:

| Scenario | Runs | Result |
| --- | --- | --- |
| Same row, entity **with** `@VersionColumn` | 60 | one `200`, one `500` every time |
| Different rows, entity **with** `@VersionColumn` | 30 | one `200`, one `500` every time |
| Same row, entity **without** a version column | 20 | `200` / `200` every time |
| Different rows, entity **without** a version column | 20 | `200` / `200` every time |

The different-rows case is the important one: there is no conflict there at
all, so translating this error into a `409` downstream would be wrong.

## Why (from reading the code)

- `TypeOrmRepository.withVersionGuard` runs the compare-and-swap inside
`transactionScope.run(ctx ?? {}, …)`. `update` and `replace` resolve the
version guard in `'guard'` mode, so every write to a versioned entity opens a
transaction; `delete` / `softDelete` / `restore` do so only when an
`expectedVersion` is passed.
- TypeORM's `SqliteDriver.createQueryRunner()` returns **one shared**
`SqliteQueryRunner` for the whole data source. Two guarded writes that
overlap issue `BEGIN` on the same connection, and SQLite rejects the second.
- Unversioned writes do not open that transaction, which matches the table
above.
- `PostgresDriver.createQueryRunner()` creates a new runner per call on pooled
connections, so this should not reproduce on Postgres. **Not verified by
running it.**

## Repro

1. An entity with `@VersionColumn()`.
2. TypeORM data source with `type: 'sqlite'`, `database: ':memory:'`, and a
`TransactionScope` registered (`RepositoryModule.forRoot()`).
3. Fire two concurrent CRUD `PATCH` requests at two different rows of that
entity.

## Versions

- `@concepta/nestjs-repository-typeorm` 8.0.0-alpha.12
- `@concepta/nestjs-repository` 8.0.0-alpha.12
- `typeorm` 0.3.31, `sqlite3` 5.1.7
- Node 22.22.3

## The question

Is this worth fixing upstream? Options we can see:

1. **Leave it and document it.** This is what Rockets does today (known
limitation in its changelog).
2. **Serialize transactions for single-connection drivers** — e.g. queue
`TransactionScope.run` (or the TypeORM transaction factory) when the driver
is SQLite, so the second write waits for the first and the version check
then answers `409` only on a real conflict.
3. **Handle it at the TypeORM driver level**, if there is a supported way to do
that.

Surfaced while upgrading Rockets to 8.0.0-alpha.12:
conceptadev/rockets#117. Related: #472 (client-supplied version for the
optimistic lock).

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reproducing the concurrent PATCH case with TypeORM, an in-memory SQLite data source, @VersionColumn, and a registered TransactionScope. Read TypeOrmRepository.withVersionGuard and transactionScope.run, then compare the SQLite and Postgres driver transaction behavior described in the issue. Done means the project has a decided upstream behavior, verified concurrency coverage, and either a documented limitation or an implemented supported fix.

Written by the indexing model from the issue text.

Assessment

Tech stack
sqlite, typescript
Domain
backend, databases
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.