fix(adapters): transaction outcome, silent driver errors, await, and CI running the tests - #3
Merged
Merged
Conversation
JustGodWork
force-pushed
the
pr/model-and-relations
branch
from
July 22, 2026 00:45
7772186 to
21dda5a
Compare
JustGodWork
force-pushed
the
pr/adapters-promises-and-ci
branch
from
July 22, 2026 00:45
4d3b974 to
ddc08dd
Compare
JustGodWork
force-pushed
the
pr/model-and-relations
branch
from
July 22, 2026 01:00
21dda5a to
fc22761
Compare
JustGodWork
force-pushed
the
pr/adapters-promises-and-ci
branch
from
July 22, 2026 01:00
ddc08dd to
bb86c42
Compare
JustGodWork
force-pushed
the
pr/model-and-relations
branch
from
July 22, 2026 01:03
fc22761 to
89ae994
Compare
transaction() ended with `finish(committed and nil or "...")`. Since `true and nil` is nil, the expression always fell through to the error string, so every successful COMMIT was reported to the caller as a rollback. The data was written, the caller saw a failure, and a retry duplicated the whole transaction. Replaced with an explicit branch and added regression tests exercising the real adapter for both outcomes.
Neither adapter could report a failed query, so the three outcomes of a
broken statement were an empty result set, total silence, or a coroutine
suspended for good.
nanos passed nil as the error unconditionally on the async path, so a
callback carrying a driver error read as zero rows: find() on a missing
table resolved nil ("not found") instead of rejecting. The sync path
already handled it, which is what gave the omission away.
oxmysql now catches a raising export, and inside a transaction treats a
nil return from query() as the failure oxmysql documents it to be rather
than an empty result the body would keep working from.
A callback the driver never invokes at all still hangs; that needs a
timeout the adapters cannot provide on their own.
await() yielded exactly once and stored the waiting coroutine in a single
slot. Two consequences on hosts that run their own scheduler, which is
the case for both FiveM and nanos:
- any foreign resume made await() return while the promise was still
pending, handing back a nil value;
- a second coroutine awaiting the same promise overwrote the first,
which was then never resumed at all.
await() now yields until the promise actually settles, and _settle wakes
every registered waiter that is still suspended.
A driver error only ever reached the rejected promise, and the most common way to call Norm discards it: no :catch, no :await means a failed create() or delete() produced not one line of output. Combined with the adapters, which could not report an error at all, a broken statement was invisible on a live server. Every data operation now reports the error and the statement through the orm's logger before handing it to the callback. Rejection handling is unchanged, so an application that does catch it sees no difference.
_flush_ready only ran on the success path, so a failing sync() left every operation held by queue_until_ready with its callback never invoked. The promises stayed pending forever: no resolution, no rejection, no timeout, and any coroutine awaiting one hung for good. That is precisely the scenario the option exists for, a database not yet reachable at boot. Queued operations now receive the schema error and the ORM stays not ready, so a later successful sync() still works.
engine_to_dialect mapped PostgreSQL to the mysql dialect as "close enough for our SQL". It is not: that dialect quotes identifiers with backticks and appends ENGINE=InnoDB DEFAULT CHARSET=utf8mb4, so the very first CREATE TABLE was rejected at the opening backtick, while supports_returning still advertised PostgreSQL as supported. The engine now raises with a message pointing at the alternatives. An explicit `dialect` option still bypasses the mapping.
The workflow rebuilt dist/ and pushed it straight to master without ever running tests/run.py or tests/run_nanos.py, so nothing stood between a source regression and the published bundle. Both suites now run against the freshly built dist/ and a failure stops the job before the commit step.
JustGodWork
force-pushed
the
pr/adapters-promises-and-ci
branch
from
July 22, 2026 01:04
bb86c42 to
9907d4d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seven changes covering the async error path, the built-in promise and the CI. Based on #2, review #1 and #2 first.
Every successful transaction was reported as a rollback.
finish(committed and nil or "..."):true and nilis nil, which falls through to the error string, so the callback always received it. The COMMIT had happened, the caller saw a failure, and a naive retry ran the whole transaction a second time. oxmysql is the only adapter advertising transaction support, so the feature was unusable. The self-test covers transactions with a mock that short-circuits that exact line.No driver error ever reached the caller. nanos passed nil as the error unconditionally on the async path, so a failed query read as an empty result set:
find()on a missing table resolved nil, meaning "not found", instead of rejecting. The synchronous path handled it correctly, which is what gave the omission away. oxmysql now catches a raising export, and inside a transaction treats a nil return fromquery()as the failure oxmysql documents it to be, rather than an empty result the transaction body keeps working from.Failed statements are now logged through the configured logger. A driver error previously only reached the rejected promise, and the most common call style, no
:catchand no:await, discards it. A failed write produced no output at all on a live server.await() could return before the promise settled. One yield, no loop, so any host scheduler resuming the coroutine made it return a nil value. FiveM and nanos both run their own scheduler, and the built-in provider is the default. The waiting coroutine was also stored in a single slot, so a second waiter on the same promise replaced the first, which was then never resumed at all.
queue_until_ready leaked its callbacks.
_flush_readyonly ran on the success path, so a failingsync()left every queued operation with its callback never invoked: no resolution, no rejection, no timeout, and any awaiting coroutine suspended for good. That is exactly the scenario the option exists for, a database not reachable at boot.PostgreSQL was mapped to the mysql dialect as "close enough for our SQL". It is not: that dialect quotes with backticks and appends
ENGINE=InnoDB, both rejected at the first token, whilesupports_returningstill advertised the engine as supported. It now raises with a message pointing at the alternatives.CI ran build.lua and pushed dist/ to master without ever running the test suites, so nothing stood between a source regression and the published bundle. Both suites now run against the freshly built dist/ and a failure stops the job before the commit step.
Known limit, not addressed here: a callback the driver never invokes at all still hangs. That needs a timeout the adapters cannot provide on their own.
Suite: 341 passing, 260 before this series.