w-orm-reladb 1.1.13 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -149,6 +149,29 @@ Checked by running the previous release (1.1.10) on sqlite.
149
149
 
150
150
  `inDateRange` and `genDateChecker` are new exports of `src/cvFind.mjs`; the condition side (`select` / `delAll`) and the write side use the same range test.
151
151
 
152
+ ### `defaultScope` of a model: always combined with AND
153
+
154
+ A model may carry a `defaultScope` (`{ defaultScope: { where: … } }` in its definition). The `where` of the scope is combined with AND with every condition this package sends to the database: the `find` of `select` and `delAll` (`scope AND find`), and the primary key condition of `selectByPk`, `del` and `save`. Only the `where` is covered: `insert`, `insertBulk` and `save` do not check the rows you write against the scope, and the other scope options (`limit`, `offset`, `group`, `having`, `include`) are applied by `select` and `selectByPk` only, while `del` and `delAll` ignore them (`delAll` refuses such a model, see below).
155
+
156
+ sequelize merges the two `where` objects by `Object.assign` by default, so a key of your `find` replaced the same key of the scope, and a scope written as `Sequelize.literal(…)` or `Sequelize.where(…)` was **dropped completely**: `select` returned rows outside the scope and `delAll` deleted them. This package now creates its Sequelize instance with `define: { whereMergeStrategy: 'and' }` and sets the same strategy on the model it imports (a model that declares `whereMergeStrategy: 'overwrite'` in its own options would otherwise override the instance default; it is switched to `'and'` after the import), so the scope and the condition are always joined with AND, for every kind of scope `where`: plain keys, `Op.or`, `Op.and`, `Sequelize.literal` and `Sequelize.where`. `delAll` stays refused for a model whose `defaultScope` has `limit`, `offset`, `group`, `having` or a non-empty `include` (`select` and the delete would not touch the same rows), and, kept as it was, for a `where` that uses `Op.and` (with the AND merge described here `select` and the delete now do touch the same rows for that case; the refusal is kept for compatibility, and its message still carries the older wording).
157
+
158
+ | Model `defaultScope.where` | before (1.1.13) | now |
159
+ |---|---|---|
160
+ | plain keys (`{ g: 'A' }`), `Op.or` | combined with AND (the condition built by this package has `Op.and` on top, so no key of the scope was replaced) | unchanged |
161
+ | `Sequelize.literal(…)`, `Sequelize.where(…)` | the scope was ignored: `select` returned every row matching the `find`, `delAll` deleted rows outside the scope (also `selectByPk` and `del`) | combined with AND |
162
+ | `Op.and` | `select` ignored the scope (`delAll` was refused) | `select` applies the scope (`delAll` is still refused) |
163
+ | any scope, `save` of a primary key that already exists **outside** the scope | with `Sequelize.literal` / `Sequelize.where` the scope was ignored and the row was updated (`nModified: 1`); with the other scopes it failed | fails for every kind of scope: the pre-read does not see the row, `save` turns to an insert, the insert hits the existing key and the call ends with `ok: 0` and `can not save data[i].<pk> in 3 tries`; the row is not touched (with `autoInsert: false` it reports `nModified: 0`) |
164
+
165
+ ### Date conditions without a time zone: `ctx.dateNoTz` (`'local'`, `'floating'`) and `toFloating`
166
+
167
+ This is for packages that sit on top of `cvFind` (`w-orm-mdb` does); `select` and `delAll` of this package never set it, so they keep rejecting a date string without a time zone. `cvFind(find, { attrs, dateNoTz })` accepts `undefined` (default), `'local'` or `'floating'`; anything else throws `invalid ctx: dateNoTz must be 'local', 'floating' or undefined, got [x]`.
168
+
169
+ - `'local'` reads a string without a time zone (`2026-01-01T00:00:00`, or with a single space) in the **host time zone** and rejects the years 1 to 99 (JavaScript's `new Date(y, …)` maps them to 1900 to 1999). It is unchanged and has a known defect for backends that keep wall clock times without any time zone (such as Jet): in a time zone behind UTC (the Americas, for instance) the sentinel `9999-12-31 23:59:59` becomes year 10000 in UTC, sorts before `2026-…` as text and is **matched by `$lt` today and deleted by `delAll`**, and using the sentinel as a condition value is rejected; in a time zone with daylight saving time (Berlin, Lord Howe) a wall clock time inside the skipped hour is normalised. Use `'floating'` for such backends.
170
+ - `'floating'` turns every `DATE` condition value (strings without a time zone, a `Date`, an epoch number, a string with a time zone; every element of `$in` / `$nin` too) into a **wall clock time** and sends that wall clock time as if it were UTC: a string without a time zone is taken as it is; a `Date`, an epoch number or a string with a time zone is first an instant and then read through its **local** fields (so `new Date(2026, 0, 1)` is always `2026-01-01T00:00:00Z`, whatever the host time zone). Years 1 to 9999 of the wall clock are accepted (`'local'` refuses 1 to 99); a time inside a skipped hour is valid; an ambiguous hour is compared by its wall clock (no "earlier one wins"). A date without a time (`'2026-01-01'`) is still rejected. Result: conditions that name the same wall clock time (a string without a time zone, or a `Date` / epoch built from the same local fields) give the same rows in every time zone (checked on 12 time zones, sqlite and mssql, 108 conditions each). A fixed instant such as `new Date('2026-01-01T00:00:00Z')` or an epoch constant is read through the host's local fields, so it names a different wall clock time in each time zone.
171
+ - `toFloating(v)` (exported by `src/cvFind.mjs`) applies the same rule to a `Date`, an epoch number or a string and returns the floating `Date`, or `null` when the value is not acceptable (it never throws). The caller builds the `DATE` values of its in-memory table with it, so that conditions and data live in the same floating space.
172
+
173
+ Three obligations of the caller, because breaking them silently returns or deletes the wrong rows: (1) **a floating `Date` is the result of the encoding, not an input**: `toFloating(toFloating(x))` and a `Date` read back from the table used as a `'floating'` condition shift the time a second time (not a no-op in any time zone but UTC); (2) **when `toFloating` returns `null`, reject the whole batch**; never store the original value or `NULL` instead (a `NULL` row is skipped by `$lt` / `$gt` and matched by `{ col: null }`, the very mistakes `'floating'` removes); (3) **use `'floating'` and `toFloating` together**: a table filled with ordinary dates and queried with `'floating'` (or the other way round) matches the wrong rows without any error.
174
+
152
175
  ### Well formed strings: primary keys, written data and names
153
176
 
154
177
  A string with a **lone surrogate** (half of a UTF-16 surrogate pair; `s.isWellFormed()` is `false`) cannot be stored as it is: the database turns the lone half into U+FFFD (sqlite stores text as UTF-8), so different keys become one key. On sqlite `'a\uD800'`, `'a\uDC00'` and `'a\uFFFD'` are the same primary key: `insert` skipped the "duplicate", `save` overwrote the other row, `selectByPk` and `del` hit a row that was never meant (checked on the previous release, 1.1.12). The conditions of `select` and `delAll` have refused such strings for a long time; the write side and the primary key functions now apply the same standard, series wide (shared with the other `w-orm-*` packages through `chkOrmStr` and `chkOrmData` of `wsemi`). Everything below happens **before the model files are loaded, before a connection is made and before anything is read, written or deleted**, on the copy of your data that is used for writing (made when you call the function), and it does not depend on `opt.autoGenPk`: a bad key you supplied is never replaced by a generated one. U+FFFD itself and valid pairs (emoji, rare CJK characters) are well formed and accepted as before.