conceptadev / conceptadev/nestjs-modules
SQLite: concurrent writes to versioned entities fail with 500 instead of waiting — worth fixing?
- 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