Skip to content

fix(model): falsy values and soft-deleted rows leaking through the model layer - #6

Merged
JustGodWork merged 4 commits into
developfrom
pr/model-and-relations
Jul 22, 2026
Merged

fix(model): falsy values and soft-deleted rows leaking through the model layer#6
JustGodWork merged 4 commits into
developfrom
pr/model-and-relations

Conversation

@JustGodWork

Copy link
Copy Markdown
Owner

Four fixes in the model and relation layer. Follow-up to #1, already merged.

Booleans were never decoded. NormRecord:__init used persisted and model:parse(col, value) or value. parse() returns false for a boolean column, which that idiom swaps back to the raw driver value, so admin = 0 stayed the number 0. That is truthy in Lua, so if user.admin then was true for every non-admin. reload() cast it correctly, so the same record changed type after reloading. The existing test only covered 1 -> true, which passes by chance.

Eager loading returned an empty table instead of nil. The same idiom, twice: first() combined with include() resolved {} on no match, and an empty has_one became {}. Both are truthy, so if post then post:save() end passed the guard and failed on the call. The lazy load() path returned nil correctly, and so did belongs_to.

Related, in the same function: when no parent had a usable key, one single empty table was assigned to every parent, so inserting into one parent's collection showed up on all the others.

find_or_ ignored the soft-delete scope.* _find_by_attrs never applied it, unlike find, find_by, load and every relation loader. On a soft-deleting model, find_or_create revived a trashed row instead of creating a new one, and update_or_create wrote into it.

upsert resolved nil after a successful write. The row is read back by the conflict columns, which default to the primary key. With an auto-increment key the caller does not supply it, so the read-back compiled to WHERE id IS NULL. The existing assertion only checked that the conflict list was non-empty. The columns are now required in the payload, and the read-back applies the soft-delete scope like every other lookup.

Suite: 315 passing.

…alue

NormRecord:__init used `persisted and model:parse(col, value) or value`.
When parse() returns false for a boolean column, that idiom falls through
to the raw driver value, so `admin = 0` stayed the number 0, which is
truthy in Lua. Every `if record.admin then` was true for non-admins.

Replaced with an explicit branch and added regression tests covering
0/1 decoding and reload().
…thing

Three defects in the include() path, two of them the same and/or idiom:

  resolve(single and nil or records)  -- first() resolved {} on no match
  (kind == "has_one") and (g[1] or nil) or g  -- empty has_one became {}

Both produced a truthy empty table where the contract, and the lazy load
path, return nil. `if post then post:save() end` passed the guard and
then failed on the method call.

The third: when no parent had a usable key, one single empty table was
assigned to every parent, so inserting into one parent's collection was
visible on all the others. Each parent now gets its own table.
_find_by_attrs built its state without utils.soft_scope, unlike find,
find_by and every relation loader. On a soft-deleting model,
find_or_create, find_or_new and update_or_create therefore matched rows
that had been deleted: instead of creating a new row they revived a
trashed one, and update_or_create wrote into it.
upsert() reads the canonical row back by its conflict columns, which
default to the primary key. With an auto-increment key the caller does
not supply it, so the read-back compiled to `WHERE id IS NULL` and the
promise resolved nil even though the row had been written. The existing
assertion only checked that the conflict list was non-empty.

The columns are now required in the data, and the read-back applies the
soft-delete scope like every other lookup.
@JustGodWork
JustGodWork merged commit 22e2c7b into develop Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant