fix: defects found by the pre-production audit, including two regressions - #4
Merged
Conversation
JustGodWork
force-pushed
the
pr/adapters-promises-and-ci
branch
from
July 22, 2026 00:45
4d3b974 to
ddc08dd
Compare
JustGodWork
force-pushed
the
fix/audit-followup
branch
from
July 22, 2026 00:45
87f1943 to
510d900
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
fix/audit-followup
branch
from
July 22, 2026 01:00
510d900 to
591e71f
Compare
JustGodWork
force-pushed
the
pr/adapters-promises-and-ci
branch
from
July 22, 2026 01:04
bb86c42 to
9907d4d
Compare
Two of them were introduced by the previous batch of fixes. Regression, adapters: the pcall added around the driver call could invoke the callback twice. A driver that calls back synchronously runs the callback inside that pcall, so an error raised by the continuation came back as ok == false and was reported as a second call. With error(nil) the second call even took the success branch and re-entered the state machine of sync() and migrate(), re-emitting DDL and re-inserting the norm_migrations row. utils.protected now guarantees the single invocation the adapter contract requires, and logs a continuation error instead of turning it into a query failure. Regression, upsert: the soft-delete scope added to the read-back was wrong. ON DUPLICATE KEY / ON CONFLICT is not scoped, so the engine updates a trashed row happily, and filtering the read-back resolved nil for a write that had happened. A caller retrying on nil would loop. sync() only called _fail_queue from the error callback, so an adapter raising instead of calling back still stranded every queued operation, which is the exact failure the queue fix targeted. Marking index statements optional was too broad: on MySQL every CREATE INDEX failure was swallowed, including an unknown column on a UNIQUE index, which silently drops the guarantee upsert and find_or_create depend on. Only a duplicate-index error is skipped now. Two nil holes remained in the family the previous batch addressed: having(expr, nil) in its two-argument form emitted a placeholder with no parameter, and where_in(col, nil) compiled to the same `col IS NULL` as where_not_in(col, nil) despite meaning the opposite. Eager loading still shared one collection table between parents holding the same source key, which is legal with a non-unique localKey. Adds 16 regression tests. Suite: 357 passing.
JustGodWork
force-pushed
the
fix/audit-followup
branch
from
July 22, 2026 01:06
591e71f to
37030f3
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.
Follow-up to a full re-audit of the three stacked PRs. Six fixes, two of which are regressions introduced by the earlier batch.
Regressions from the previous fixes
The pcall around driver calls invoked the callback twice. A driver that calls back synchronously runs the callback inside that pcall, so an error raised by the continuation came back as
ok == falseand was reported as a second call. Witherror(nil)the second call took the success branch and re-entered the state machine ofsync()andmigrate(), re-emitting DDL and re-inserting thenorm_migrationsrow. It also produced anERROR statement failedlog and a_fail_queuefor a statement that had succeeded.utils.protectednow guarantees the single invocationadapter.luadocuments, and logs a continuation error rather than turning it into a query failure.The soft-delete scope added to the upsert read-back was wrong.
ON DUPLICATE KEY UPDATEandON CONFLICTare not scoped, so the engine updates a trashed row happily. Filtering the read-back withdeleted_at IS NULLthen resolved nil for a write that had actually happened, and a caller retrying on nil would loop forever. Removed.Incomplete fixes from the previous batch
sync() only purged the queue from the error callback. An adapter that raises instead of calling back, which is what a closed connection or a not-yet-started driver does, bypassed it and stranded every queued operation with its promise pending. That is the exact failure the queue fix was meant to remove.
Marking index statements optional was too broad. Since MySQL never supports
IF NOT EXISTSon an index, everyCREATE INDEXfailure was swallowed there, including an unknown column on a UNIQUE index. A missing unique index silently removes the race guarantee thatupsertandfind_or_createrely on. Only a duplicate-index error is skipped now, everything else failssync()as before.Two nil holes remained in the family the previous batch addressed.
having(expr, nil)in its two-argument form emitted a placeholder with no parameter, so the next HAVING value bound to the wrong slot.where_in(col, nil)compiled tocol IS NULL, exactly likewhere_not_in(col, nil), despite meaning the opposite: an authorisation filter built from a nil list stopped filtering silently.Eager loading still shared one collection table between parents holding the same source key, which is legal with a non-unique
localKey.Not addressed here
The audit also surfaced pre-existing defects outside the scope of these fixes, listed for the record:
with_countis lost when combined withinclude,paginateignoresincludeentirely,after_saveandafter_updatedo not fire whensave()has nothing to write,attach/detachignoreotherLocalKey, a duck-typed adapter withoutdefault_providercrashes at construction, soft delete mutates the record before the write is confirmed,update({})builds an empty SET, andonDelete/onUpdateare concatenated into the DDL without validation.Suite: 357 passing, 341 before this branch.