@axiosleo/orm-mysql 0.15.1 → 0.15.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## 0.15.2 (2026-06-12)
4
+
5
+ ### Fixed
6
+
7
+ - **Data correctness: `IN` conditions silently matched zero rows on the transaction path.**
8
+
9
+ In 0.15.0 / 0.15.1, `whereIn()` / `whereNotIn()` / `where(field, 'IN', array)` executed
10
+ through `TransactionHandler` silently returned an empty result set (SELECT) or affected
11
+ 0 rows (UPDATE / DELETE), without any error. The same query worked fine through
12
+ `QueryHandler`.
13
+
14
+ Root cause: the SQL builder bound the whole array as a single value, producing
15
+ `WHERE \`id\` IN (?)` with values `[[1, 2]]`. The non-transaction path uses
16
+ `conn.query()` (client-side interpolation), which happens to expand array parameters
17
+ into `IN (1, 2)`. The transaction path uses `conn.execute()` (server-side prepared
18
+ statement), which never expands arrays, so the condition matched nothing.
19
+
20
+ Fix: `Builder` now emits one placeholder per array element (`IN (?,?)`) and binds flat
21
+ scalar values, so `query` and `execute` behave identically. The transaction path keeps
22
+ using prepared statements (`conn.execute`).
23
+
24
+ - The same array-as-single-bind pattern was fixed in the JSON branches: `key->'$.path'`
25
+ with `IN` / `NOT IN`, `CONTAIN` / `NOT CONTAIN` and `OVERLAPS` / `NOT OVERLAPS` now
26
+ expand arrays into `JSON_ARRAY(?,?,...)` with scalar bindings.
27
+
28
+ - Comma-separated string values for `IN` (e.g. `whereIn('id', '1,2,3')`, documented in
29
+ `index.d.ts`) previously threw `Value must be an array or sub-query for "IN" condition`;
30
+ they are now split, trimmed and expanded like arrays.
31
+
32
+ ### Unchanged behavior
33
+
34
+ - Empty arrays (and now empty strings) for `IN` still throw
35
+ `Value must not be empty for "IN" condition`.
36
+ - `Query` sub-queries for `IN` still render the sub-select SQL.
package/bin/orm-mysql.js CHANGED
@@ -9,7 +9,7 @@ const app = new App({
9
9
  name: 'MySQL ORM CLI',
10
10
  desc: 'migrate, model, seed, etc.',
11
11
  bin: 'orm-mysql',
12
- version: '0.15.1',
12
+ version: '0.15.2',
13
13
  commands_dir: path.join(__dirname, '../commands'),
14
14
  });
15
15
 
@@ -118,7 +118,7 @@ class SkillsCommand extends Command {
118
118
  printer.info(`Found ${PKG_NAME}@${source.version} in node_modules`);
119
119
  } else if (source.outdated) {
120
120
  printer.warning(`${PKG_NAME}@${source.localVersion} is installed locally but does not include skills files.`);
121
- printer.warning('Skills files are available since v0.15.1. Please update:');
121
+ printer.warning('Skills files are available since v0.15.2. Please update:');
122
122
  printer.warning(` npm install ${PKG_NAME}@latest`);
123
123
  printer.println();
124
124
  printer.info(`Using skills from npx ${PKG_NAME}@${source.version} instead.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiosleo/orm-mysql",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "MySQL ORM tool",
5
5
  "keywords": [
6
6
  "mysql",
package/src/builder.js CHANGED
@@ -334,6 +334,20 @@ class Builder {
334
334
  return null;
335
335
  }
336
336
 
337
+ /**
338
+ * Bind each array element as a single scalar value and return the matching
339
+ * placeholder list (e.g. "?,?,?"). Binding the whole array as one value only
340
+ * works with conn.query (client-side interpolation); conn.execute (prepared
341
+ * statement, used on the transaction path) does not expand arrays and would
342
+ * silently match nothing.
343
+ * @param {Array} values
344
+ * @returns {string}
345
+ */
346
+ _buildArrayPlaceholders(values) {
347
+ values.forEach((item) => this.values.push(item));
348
+ return values.map(() => '?').join(',');
349
+ }
350
+
337
351
  _buildConditionBetween(condition, isNot = false) {
338
352
  if (!Array.isArray(condition.value) || condition.value.length !== 2) {
339
353
  throw new Error('Value must be an array with two elements for "BETWEEN" condition');
@@ -353,22 +367,33 @@ class Builder {
353
367
  }
354
368
 
355
369
  _buildConditionIn(condition, isNot = false) {
356
- if (Array.isArray(condition.value) && !condition.value.length) {
370
+ // "1,2,3" is split into an array, then goes through the same expansion path as arrays
371
+ let v = is.string(condition.value)
372
+ ? condition.value.split(',').map(s => s.trim()).filter(s => s.length)
373
+ : condition.value;
374
+ if (Array.isArray(v) && !v.length) {
357
375
  throw new Error('Value must not be empty for "IN" condition');
358
- } else if (!Array.isArray(condition.value) && !(condition.value instanceof Query)) {
376
+ } else if (!Array.isArray(v) && !(v instanceof Query)) {
359
377
  throw new Error('Value must be an array or sub-query for "IN" condition');
360
378
  }
361
379
  if (condition.key.indexOf('->') !== -1) {
362
380
  let keys = condition.key.split('->');
363
381
  let k = `${this._buildFieldKey(keys[0])}`;
364
- let res = this._buildConditionValues(condition.value);
365
- let sql = res ? `JSON_CONTAINS(JSON_ARRAY(${res}), JSON_EXTRACT(${k}, '${keys[1]}'))` :
366
- `JSON_CONTAINS(JSON_ARRAY(?), JSON_EXTRACT(${k}, '${keys[1]}'))`;
382
+ let sql;
383
+ if (Array.isArray(v)) {
384
+ sql = `JSON_CONTAINS(JSON_ARRAY(${this._buildArrayPlaceholders(v)}), JSON_EXTRACT(${k}, '${keys[1]}'))`;
385
+ } else {
386
+ let res = this._buildConditionValues(v);
387
+ sql = res ? `JSON_CONTAINS(JSON_ARRAY(${res}), JSON_EXTRACT(${k}, '${keys[1]}'))` :
388
+ `JSON_CONTAINS(JSON_ARRAY(?), JSON_EXTRACT(${k}, '${keys[1]}'))`;
389
+ }
367
390
  return isNot ? `${sql}=0` : sql;
368
391
  }
369
- let v = is.string(condition.value) ? condition.value.split(',').map(v => v.trim()) : condition.value;
370
- let res = this._buildConditionValues(v);
371
392
  const opt = isNot ? 'NOT IN' : 'IN';
393
+ if (Array.isArray(v)) {
394
+ return `${this._buildFieldKey(condition.key)} ${opt} (${this._buildArrayPlaceholders(v)})`;
395
+ }
396
+ let res = this._buildConditionValues(v);
372
397
  return res ? `${this._buildFieldKey(condition.key)} ${opt} (${res})` : `${this._buildFieldKey(condition.key)} ${opt} (?)`;
373
398
  }
374
399
 
@@ -376,9 +401,14 @@ class Builder {
376
401
  if (condition.key.indexOf('->') !== -1) {
377
402
  let keys = condition.key.split('->');
378
403
  let k = `${this._buildFieldKey(keys[0])}`;
379
- let res = this._buildConditionValues(condition.value);
380
- let sql = res ? `JSON_CONTAINS(${k}, JSON_ARRAY(${res}), '${keys[1]}')` :
381
- `JSON_CONTAINS(${k}, JSON_ARRAY(?), '${keys[1]}')`;
404
+ let sql;
405
+ if (Array.isArray(condition.value)) {
406
+ sql = `JSON_CONTAINS(${k}, JSON_ARRAY(${this._buildArrayPlaceholders(condition.value)}), '${keys[1]}')`;
407
+ } else {
408
+ let res = this._buildConditionValues(condition.value);
409
+ sql = res ? `JSON_CONTAINS(${k}, JSON_ARRAY(${res}), '${keys[1]}')` :
410
+ `JSON_CONTAINS(${k}, JSON_ARRAY(?), '${keys[1]}')`;
411
+ }
382
412
  return isNot ? `${sql}=0` : sql;
383
413
  }
384
414
  let res = this._buildConditionValues(condition.value);
@@ -390,9 +420,14 @@ class Builder {
390
420
  if (condition.key.indexOf('->') !== -1) {
391
421
  let keys = condition.key.split('->');
392
422
  let k = `${this._buildFieldKey(keys[0])}`;
393
- let res = this._buildConditionValues(condition.value);
394
- let sql = res ? `JSON_OVERLAPS(JSON_EXTRACT(${k}, '${keys[1]}'), JSON_ARRAY(${res}))` :
395
- `JSON_OVERLAPS(JSON_EXTRACT(${k}, '${keys[1]}'), JSON_ARRAY(?))`;
423
+ let sql;
424
+ if (Array.isArray(condition.value)) {
425
+ sql = `JSON_OVERLAPS(JSON_EXTRACT(${k}, '${keys[1]}'), JSON_ARRAY(${this._buildArrayPlaceholders(condition.value)}))`;
426
+ } else {
427
+ let res = this._buildConditionValues(condition.value);
428
+ sql = res ? `JSON_OVERLAPS(JSON_EXTRACT(${k}, '${keys[1]}'), JSON_ARRAY(${res}))` :
429
+ `JSON_OVERLAPS(JSON_EXTRACT(${k}, '${keys[1]}'), JSON_ARRAY(?))`;
430
+ }
396
431
  return isNot ? `${sql}=0` : sql;
397
432
  }
398
433
  let res = this._buildConditionValues(condition.value);