Skip to content

Chages - #3

Merged
ElectronSz merged 6 commits into
mainfrom
chages
Sep 16, 2026
Merged

ElectronSz merged 6 commits into
mainfrom
chages

Conversation

@ElectronSz

Copy link
Copy Markdown
Owner

No description provided.

- Microsoft SQL Server as a first-class dialect (mssql v12): T-SQL query
  generation (OUTPUT INSERTED.*, OFFSET/FETCH pagination, MERGE upserts),
  schema generation via OBJECT_ID and sys.indexes probes in place of the
  CREATE IF NOT EXISTS forms SQL Server lacks, and automatic rewriting of
  the library's ? placeholders to @param0.
- Model relationships: OneToOne, ManyToOne, OneToMany and ManyToMany,
  declared in the model config and eager-loaded in batched follow-up
  queries rather than per row. attach/detach/sync edit a join table
  without loading either side.
- Column encryption: marking a column encrypted encrypts on write and
  decrypts on read with AES-256-GCM, including for rows served from cache.
  The key is read from ORM_ENCRYPTION_KEY with no fallback, so a missing
  key fails loudly instead of silently protecting nothing.
- auto-migrate.ts: additive AutoMigrate that creates missing tables, adds
  missing columns and adds missing indexes. It never drops a column and
  never changes a column type.
- validateAll, findOrFail/firstOrFail, raw clause builders, and connection
  retry with exponential backoff and jitter for read-only statements.

Fixed:
- after* hooks and both delete hooks never ran: getHooks() resolved the
  model through proto.constructor, but reads return plain rows, so the
  prototype was Object and the metadata lookup found nothing.
- getRepository() opened a new, unclosed Redis connection on every call.
- Cached find() results were written without their relations, so a cache
  hit returned a different shape than a miss.

Adds test coverage for all of the above, plus integration suites for
sqlite, mysql, mariadb, postgres, mssql, models, relations and the write
paths, which skip themselves when no server answers.

The docs move to their own repository (ElectronSz/stabilize-docs) and are
now ignored here.
Brings in the validation metadata mapping fix from origin/chages. The only
conflict was the column mapping in repository.ts, where both sides had added
different fields to the same object: origin/chages added minLength, maxLength,
pattern and customValidator, and this branch added required, unique, encrypted,
softDelete and optimisticLock. Resolved as the union of both.

Two defects in the incoming tests/migrations.test.ts, both of which broke the
suite:

- The file was missing a closing brace. describe("generateMigration") opened at
  line 31 and never closed, so describe("runMigrations") was nested inside it and
  the file ended one brace short — 38 opening against 37 closing. It failed to
  parse, which took the whole file's tests out of the run.

- It called vi.mock("../client"). Under Bun's test runner a module mock is
  process-wide, not file-scoped, so the mock replaced DBClient for every other
  test file in the run. Those files then failed with "db.migrationQuery is not a
  function" against a mock that never had that method, and the suite went from
  262 passing to 239. The mock is gone; runMigrations is now driven against a
  real file-backed SQLite database and the result read back through a second
  connection, which covers the same ground and also asserts that a recorded
  migration is not applied twice.
…chema

MongoDB joins the four SQL dialects as a full backend rather than a
side-car. The work is staged M0-M4 of the approved plan, each milestone
green before the next began.

M0 - connection lifecycle. The driver is an optional dependency and is
imported lazily, so a project that only uses SQLite never needs it. A
connect-time probe warns when the server is a standalone, because the
ORM wraps every write in a transaction and a standalone rejects
startTransaction - so the failure would otherwise appear only on the
first create, with a driver message that names the rule but not the
remedy. The Mongo handles are structural interfaces, so no driver type
reaches an emitted .d.ts.

M1 - mongo-query.ts, the pure translation layer. Structured predicates
are recorded alongside the SQL fragments the existing methods already
render, so the four SQL backends are untouched. Clauses that have no
MongoDB equivalent (joins, CTEs, raw SQL) record a blocker and throw at
execute() naming every offending method.

M2 - the repository's ~24 raw where() calls became structured ones, so
no SQL parsing is needed on the Mongo path. This surfaced four bugs
where a lookup rendered the property key into SQL instead of the column
name; only a renamed column could expose them, which is why
tests/renamed-columns.test.ts builds models that have one and asserts
each fix, with a companion assertion that the property-named column is
genuinely absent so the test cannot pass for the wrong reason.

M3 - the read path and integer auto-increment keys. Ids come from a
stabilize_counters collection; one $inc reserves a whole bulkCreate
batch, and a caller-supplied id advances the counter with $max so a
later generated id cannot collide. Documents are keyed by _id.

M4 - schema and migrations. Collections are created with a $jsonSchema
validator and indexes. Two rules are load-bearing and are asserted
against a real server: validationLevel "moderate", without which an
update to a document lacking a newly declared required field is
rejected, and sparse: true on unique indexes, without which the second
document that omits a unique column collides where SQL would allow it.
Migrations are data rather than closures, so a generated one is
assertable without a server.

Also fixes a bug the M4 tests found in shared code: the transaction
error classifier rewrote every error as TX_ERROR, including errors the
ORM raised itself, so a validation failure pointed the reader at the
server's replica-set configuration. The four SQL branches all let their
original error through; MongoDB now does too, while still recognising a
standalone's rejection when the executor has already wrapped it.

380 pass / 0 fail, typecheck clean, build clean, no mongodb reference in
dist/*.d.ts.
…atches

Completes the backend begun in 681e341. The four SQL backends are untouched:
every MongoDB body sits behind an early return in repository.ts, and the shared
code above it — validation, hooks, timestamps, optimistic locking, cache
invalidation, relation algorithms, encryption — is reused, not duplicated.

Write path: create/bulkCreate, update/bulkUpdate, upsert, delete/bulkDelete,
recover, plus updateBy/deleteBy/restoreBy, increment/decrement/toggle,
aggregate, countDistinct, paginate, random and the findMany cursor.

Relations: attach/fetch/sync for many-to-many, with the link collection keyed
by a compound {parent, child} _id so attach is idempotent at the storage layer
rather than by pre-read. Link reads are explicitly sorted, because MongoDB's
to-many order is unspecified and not stable between two reads of unchanged data.

Versioning: asOf/history/rollback/writeHistory against a history collection with
native Date validity windows, so the range comparisons stay index-backed. A
delete now records max(sent, newestRecorded + 1): on MongoDB the version is half
the compound _id, so recording the row's current version collided with the row
it was journalling and failed the delete it was meant to record.

Escape hatches: the 22 clauses with no MongoDB equivalent throw
MONGO_UNSUPPORTED naming every offending method, and an inverse table proves an
allowed clause is not refused. lock()/forUpdate() is a no-op that
Repository.lockForUpdate now warns about, since the read it returns is
indistinguishable from a locked one.

Also fixes aggregate() on an empty collection, where $group over no input
produces no document at all and the count came back undefined rather than 0.

Flagged, not fixed: processForLoad reads processed[key] while rows arrive keyed
by column name, so an encrypted column that declares name: is not decrypted on
read. This affects all five backends and is left for a separate change.
Major version for the MongoDB backend, which is a new supported database
rather than a change to the existing four.

The version number is the only purely mechanical part: no SQL behaviour
changed in this release, so a 2.x consumer that does not opt into MongoDB
has no migration to perform. The bump signals that defineModel and the
Repository API now carry a backend whose guarantees differ in documented
ways — raw SQL is refused, transactions require a replica set, and DECIMAL
is stored as a double.

The description now names SQL Server and MongoDB, neither of which it
mentioned despite both being supported.
It was recorded as a bare gitlink with no .gitmodules, so a fresh clone
produced an empty stabilize-cli directory with nothing to tell git where the
contents should come from.

The recorded pointer was also stale: it named 6a92aa13 while the nested repo's
HEAD was ac5e810. That commit is a descendant, so the pointer was simply
behind rather than pointing at something that never existed — and ac5e810 is
already on origin/main, so nothing needed pushing to make it reachable.

Adds the missing .gitmodules and advances the pointer. The nested repo's
working tree is left as it is: it has uncommitted changes of its own, which
belong to its own history rather than to this pointer.
@ElectronSz
ElectronSz merged commit e941b8f into main Sep 16, 2026
1 of 2 checks passed
@ElectronSz
ElectronSz deleted the chages branch September 16, 2026 12:10
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