leoric 1.14.3 → 2.0.0

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/History.md CHANGED
@@ -1,3 +1,51 @@
1
+ 2.0.0 / 2021-12-28
2
+ ==================
3
+
4
+ ## What's Changed
5
+ * breaking: model.sync add force/alter option by @SmartOrange in https://github.com/cyjake/leoric/pull/224
6
+ * breaking: logQueryError(err, sql, duration, options) by @cyjake in https://github.com/cyjake/leoric/pull/237
7
+ * test: add utf8mb4 test cases by @fengmk2 in https://github.com/cyjake/leoric/pull/239
8
+ * Merge 1.x changes by @cyjake in https://github.com/cyjake/leoric/pull/249
9
+
10
+ ## New Contributors
11
+ * @SmartOrange made their first contribution in https://github.com/cyjake/leoric/pull/222
12
+
13
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.15.1...v2.0.0
14
+
15
+ 1.15.1 / 2021-12-28
16
+ ===================
17
+
18
+ ## What's Changed
19
+ * fix: fix #242 date string format by @JimmyDaddy in https://github.com/cyjake/leoric/pull/243
20
+ * fix: update with empty conditions by @JimmyDaddy in https://github.com/cyjake/leoric/pull/241
21
+ * fix: silent option's priority should be lower than valueSet by @JimmyDaddy in https://github.com/cyjake/leoric/pull/244
22
+ * fix: information_schema.columns.datetime_precision by @cyjake in https://github.com/cyjake/leoric/pull/246
23
+ * fix: should not hoist subquery if query is ordered by external columns by @cyjake in https://github.com/cyjake/leoric/pull/247
24
+
25
+
26
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.15.0...v1.15.1
27
+
28
+ 1.15.0 / 2021-11-22
29
+ ===================
30
+
31
+ ## What's Changed
32
+ * feat: make duration in precise milliseconds by @fengmk2 in https://github.com/cyjake/leoric/pull/236
33
+ * fix: spell.increment() & spell.decrement() @cyjake https://github.com/cyjake/leoric/pull/234
34
+ * fix: bulkCreate should adapte empty data @JimmyDaddy https://github.com/cyjake/leoric/pull/232
35
+
36
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.14.4...v1.14.5
37
+
38
+ 1.14.4 / 2021-11-15
39
+ ===================
40
+
41
+ ## What's Changed
42
+
43
+ * test: PostgreSQL v14 test case compatibility by @cyjake https://github.com/cyjake/leoric/pull/230
44
+ * fix: turn off subquery optimization if query criteria contains other column by @cyjake https://github.com/cyjake/leoric/pull/229
45
+ * fix: bone.changed() return `false | string[]` type by @fengmk2 https://github.com/cyjake/leoric/pull/231
46
+
47
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.14.3...v1.14.4
48
+
1
49
  1.14.3 / 2021-11-12
2
50
  ===================
3
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leoric",
3
- "version": "1.14.3",
3
+ "version": "2.0.0",
4
4
  "description": "JavaScript Object-relational mapping alchemy",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
package/src/bone.js CHANGED
@@ -11,6 +11,7 @@ const { executeValidator, LeoricValidateError } = require('./validator');
11
11
  const DataTypes = require('./data_types');
12
12
  const Collection = require('./collection');
13
13
  const Spell = require('./spell');
14
+ const Raw = require('./raw');
14
15
  const { capitalize, camelCase, snakeCase } = require('./utils/string');
15
16
  const { hookNames, setupSingleHook } = require('./setup_hooks');
16
17
  const { logger } = require('./utils/index');
@@ -27,12 +28,8 @@ function looseReadonly(props) {
27
28
  }, {});
28
29
  }
29
30
 
30
- function compare(attributes, columns) {
31
+ function compare(attributes, columnMap) {
31
32
  const diff = {};
32
- const columnMap = columns.reduce((result, entry) => {
33
- result[entry.columnName] = entry;
34
- return result;
35
- }, {});
36
33
 
37
34
  for (const name in attributes) {
38
35
  const attribute = attributes[name];
@@ -71,7 +68,7 @@ function copyValues(values) {
71
68
  for (const key in values) {
72
69
  if (Object.hasOwnProperty.call(values, key)) {
73
70
  const v = values[key];
74
- if (v && (v.__raw || v.__expr || (v instanceof Spell))) continue;
71
+ if (v && ((v instanceof Raw) || v.__expr || (v instanceof Spell))) continue;
75
72
  copyValue[key] = v;
76
73
  }
77
74
  }
@@ -162,14 +159,15 @@ class Bone {
162
159
  attribute(...args) {
163
160
  const [ name, value ] = args;
164
161
  const { attributes } = this.constructor;
162
+ const attribute = attributes[name];
165
163
 
166
- if (!attributes.hasOwnProperty(name)) {
164
+ if (!attribute) {
167
165
  throw new Error(`${this.constructor.name} has no attribute "${name}"`);
168
166
  }
169
167
 
170
168
  if (args.length > 1) {
171
169
  // execute validators
172
- this.#raw[name] = value;
170
+ this.#raw[name] = value instanceof Raw ? value : attribute.cast(value);
173
171
  this.#rawUnset.delete(name);
174
172
  return this;
175
173
  }
@@ -682,7 +680,7 @@ class Bone {
682
680
  if (shardingKey) where[shardingKey] = this[shardingKey];
683
681
 
684
682
  const { updatedAt, deletedAt } = Model.timestamps;
685
- if (attributes[updatedAt] && !changes[updatedAt] && !changes[deletedAt]) {
683
+ if (attributes[updatedAt] && !changes[updatedAt] && !changes[deletedAt] && !options.silent) {
686
684
  changes[updatedAt] = new Date();
687
685
  }
688
686
  if (options.validate !== false ) {
@@ -933,10 +931,20 @@ class Bone {
933
931
  };
934
932
  }
935
933
 
934
+ const columnMap = columns.reduce((result, entry) => {
935
+ result[entry.columnName] = entry;
936
+ return result;
937
+ }, {});
938
+
936
939
  for (const name of Object.keys(attributes)) {
937
940
  const attribute = new Attribute(name, attributes[name], options.define);
938
941
  attributeMap[attribute.columnName] = attribute;
939
942
  attributes[name] = attribute;
943
+ const columnInfo = columnMap[attribute.columnName];
944
+ // if datetime or timestamp precision not defined, default to column info
945
+ if (columnInfo && attribute.type instanceof DataTypes.DATE && attribute.type.precision == null) {
946
+ attribute.type.precision = columnInfo.datetimePrecision;
947
+ }
940
948
  }
941
949
 
942
950
  const primaryKey = Object.keys(attributes).find(key => attributes[key].primaryKey);
@@ -945,8 +953,7 @@ class Bone {
945
953
  const name = attributes.hasOwnProperty(key) ? key : snakeCase(key);
946
954
  const attribute = attributes[name];
947
955
 
948
- if (!attribute) continue;
949
- if (columns.some(column => column.columnName === attribute.columnName)) {
956
+ if (attribute && columnMap[attribute.columnName]) {
950
957
  timestamps[key] = name;
951
958
  }
952
959
  }
@@ -960,7 +967,7 @@ class Bone {
960
967
  attributeMap,
961
968
  associations,
962
969
  tableAlias,
963
- synchronized: Object.keys(compare(attributes, columns)).length === 0,
970
+ synchronized: Object.keys(compare(attributes, columnMap)).length === 0,
964
971
  }));
965
972
 
966
973
  for (const hookName of hookNames) {
@@ -1322,6 +1329,7 @@ class Bone {
1322
1329
  }
1323
1330
 
1324
1331
  static async bulkCreate(records, options = {}) {
1332
+ if (!records || !records.length) return records;
1325
1333
  const { driver, attributes, primaryKey, primaryColumn } = this;
1326
1334
 
1327
1335
  const { createdAt, updatedAt } = this.timestamps;
@@ -1415,7 +1423,7 @@ class Bone {
1415
1423
  // values should be immutable
1416
1424
  const data = Object.assign({}, values);
1417
1425
  const { updatedAt, deletedAt } = this.timestamps;
1418
- if (attributes[updatedAt] && !data[updatedAt] && !data[deletedAt]) {
1426
+ if (attributes[updatedAt] && !data[updatedAt] && !data[deletedAt] && !options.silent) {
1419
1427
  data[updatedAt] = new Date();
1420
1428
  }
1421
1429
 
@@ -1536,7 +1544,7 @@ class Bone {
1536
1544
  Object.defineProperties(this, looseReadonly({ ...hookMethods, attributes, table }));
1537
1545
  }
1538
1546
 
1539
- static async sync() {
1547
+ static async sync({ force = false, alter = false } = {}) {
1540
1548
  const { driver, physicTable: table } = this;
1541
1549
  const { database } = this.options;
1542
1550
 
@@ -1553,11 +1561,22 @@ class Bone {
1553
1561
  }
1554
1562
 
1555
1563
  const { attributes, columns } = this;
1564
+ const columnMap = columns.reduce((result, entry) => {
1565
+ result[entry.columnName] = entry;
1566
+ return result;
1567
+ }, {});
1556
1568
 
1557
1569
  if (columns.length === 0) {
1558
1570
  await driver.createTable(table, attributes);
1559
1571
  } else {
1560
- await driver.alterTable(table, compare(attributes, columns));
1572
+ if (force) {
1573
+ await driver.dropTable(table);
1574
+ await driver.createTable(table, attributes);
1575
+ } else if (alter){
1576
+ await driver.alterTable(table, compare(attributes, columnMap));
1577
+ } else {
1578
+ console.warn('[synchronize_fail] %s couldn\'t be synchronized, please use force or alter to specify execution', this.name);
1579
+ }
1561
1580
  }
1562
1581
 
1563
1582
  const schemaInfo = await driver.querySchemaInfo(database, table);
package/src/collection.js CHANGED
@@ -11,7 +11,8 @@ class Collection extends Array {
11
11
  * @param {Array} fields
12
12
  * @returns {Collection|Array}
13
13
  */
14
- static init({ spell, rows, fields }) {
14
+ static init({ spell, rows, fields, insertId, affectedRows}) {
15
+ if (spell.command !== 'select') return { insertId, affectedRows };
15
16
  return dispatch(spell, rows, fields);
16
17
  }
17
18
 
package/src/data_types.js CHANGED
@@ -214,7 +214,7 @@ class BIGINT extends INTEGER {
214
214
  }
215
215
  }
216
216
 
217
- const rDateFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d{3,6})?$/;
217
+ const rDateFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d{3,6})$/;
218
218
 
219
219
  class DATE extends DataType {
220
220
  constructor(precision, timezone = true) {
@@ -229,14 +229,23 @@ class DATE extends DataType {
229
229
  toSqlString() {
230
230
  const { precision } = this;
231
231
  const dataType = this.dataType.toUpperCase();
232
- if (precision > 0) return `${dataType}(${precision})`;
232
+ if (precision != null && precision >= 0) return `${dataType}(${precision})`;
233
233
  return dataType;
234
234
  }
235
235
 
236
+ _round(value) {
237
+ const { precision } = this;
238
+ if (precision != null && precision < 3 && value instanceof Date) {
239
+ const divider = 10 ** (3 - precision);
240
+ return new Date(Math.round(value.getTime() / divider) * divider);
241
+ }
242
+ return value;
243
+ }
244
+
236
245
  cast(value) {
237
246
  if (value == null) return value;
238
- if (value instanceof Date) return value;
239
- return new Date(value);
247
+ if (!(value instanceof Date)) value = new Date(value);
248
+ return this._round(value);
240
249
  }
241
250
 
242
251
  uncast(value) {
@@ -258,13 +267,7 @@ class DATE extends DataType {
258
267
  if (!(value instanceof Date)) value = new Date(value);
259
268
  if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));
260
269
 
261
- const { precision } = this;
262
- if (precision < 3) {
263
- const result = new Date(value);
264
- result.setMilliseconds(result.getMilliseconds() % (10 ** precision));
265
- return result;
266
- }
267
- return value;
270
+ return this._round(value);
268
271
  }
269
272
  }
270
273
 
@@ -278,10 +281,17 @@ class DATEONLY extends DataType {
278
281
  return this.dataType.toUpperCase();
279
282
  }
280
283
 
284
+ _round(value) {
285
+ if (value instanceof Date) {
286
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate());
287
+ }
288
+ return value;
289
+ }
290
+
281
291
  cast(value) {
282
292
  if (value == null) return value;
283
- if (value instanceof Date) return value;
284
- return new Date(value);
293
+ if (!(value instanceof Date)) value = new Date(value);
294
+ return this._round(value);
285
295
  }
286
296
 
287
297
  uncast(value) {
@@ -301,7 +311,7 @@ class DATEONLY extends DataType {
301
311
  if (!(value instanceof Date)) value = new Date(value);
302
312
  if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));;
303
313
 
304
- return new Date(value.getFullYear(), value.getMonth(), value.getDate());
314
+ return this._round(value);
305
315
  }
306
316
  }
307
317
 
@@ -374,7 +384,12 @@ class JSON extends DataType {
374
384
  if (!value) return value;
375
385
  // type === JSONB
376
386
  if (typeof value === 'object') return value;
377
- return global.JSON.parse(value);
387
+ try {
388
+ return global.JSON.parse(value);
389
+ } catch (err) {
390
+ console.error(new Error(`unable to cast ${value} to JSON`));
391
+ return value;
392
+ }
378
393
  }
379
394
 
380
395
  uncast(value) {
@@ -4,6 +4,7 @@ const SqlString = require('sqlstring');
4
4
 
5
5
  const { copyExpr, findExpr, walkExpr } = require('../../expr');
6
6
  const { formatExpr, formatConditions, collectLiteral } = require('../../expr_formatter');
7
+ const Raw = require('../../raw');
7
8
 
8
9
  /**
9
10
  * Format orders into ORDER BY clause in SQL
@@ -117,21 +118,12 @@ function createSubspell(spell) {
117
118
  subspell.groups = [];
118
119
 
119
120
  subspell.whereConditions = [];
120
- for (let i = whereConditions.length - 1; i >= 0; i--) {
121
- const condition = whereConditions[i];
122
- let internal = true;
123
- walkExpr(condition, ({ type, qualifiers }) => {
124
- if (type == 'id' && qualifiers[0] != baseName) {
125
- internal = false;
126
- }
121
+ while (whereConditions.length > 0) {
122
+ const condition = whereConditions.shift();
123
+ const token = copyExpr(condition, ({ type, value }) => {
124
+ if (type === 'id') return { type, value };
127
125
  });
128
- if (internal) {
129
- const token = copyExpr(condition, ({ type, value }) => {
130
- if (type === 'id') return { type, value };
131
- });
132
- subspell.whereConditions.unshift(token);
133
- whereConditions.splice(i, 1);
134
- }
126
+ subspell.whereConditions.push(token);
135
127
  }
136
128
 
137
129
  subspell.orders = [];
@@ -226,7 +218,18 @@ function formatSelectWithJoin(spell) {
226
218
  }
227
219
  chunks.push(selects.join(', '));
228
220
 
229
- if (skip > 0 || rowCount > 0) {
221
+ let hoistable = skip > 0 || rowCount > 0;
222
+ if (hoistable) {
223
+ function checkQualifier({ type, qualifiers }) {
224
+ if (type === 'id' && qualifiers.length> 0 && !qualifiers.includes(baseName)) {
225
+ hoistable = false;
226
+ }
227
+ }
228
+ for (const condition of whereConditions) walkExpr(condition, checkQualifier);
229
+ for (const orderExpr of orders) walkExpr(orderExpr[0], checkQualifier);
230
+ }
231
+
232
+ if (hoistable) {
230
233
  const subspell = createSubspell(spell);
231
234
  const subquery = this.formatSelectWithoutJoin(subspell);
232
235
  values.push(...subquery.values);
@@ -262,6 +265,10 @@ function formatSelectWithJoin(spell) {
262
265
  }
263
266
 
264
267
  if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(spell, orders).join(', ')}`);
268
+ if (!hoistable) {
269
+ if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`);
270
+ if (skip > 0) chunks.push(`OFFSET ${skip}`);
271
+ }
265
272
  return { sql: chunks.join(' '), values };
266
273
  }
267
274
 
@@ -380,7 +387,7 @@ function formatInsert(spell) {
380
387
  const value = sets[name];
381
388
  // upsert should not update createdAt
382
389
  columns.push(Model.unalias(name));
383
- if (value && value.__raw) {
390
+ if (value instanceof Raw) {
384
391
  values.push(SqlString.raw(value.value));
385
392
  } else {
386
393
  values.push(value);
@@ -443,7 +450,7 @@ function formatUpdate(spell) {
443
450
  if (value && value.__expr) {
444
451
  assigns.push(`${escapeId(Model.unalias(name))} = ${formatExpr(spell, value)}`);
445
452
  collectLiteral(spell, value, values);
446
- } else if (value && value.__raw) {
453
+ } else if (value instanceof Raw) {
447
454
  assigns.push(`${escapeId(Model.unalias(name))} = ${value.value}`);
448
455
  } else {
449
456
  assigns.push(`${escapeId(Model.unalias(name))} = ?`);
@@ -451,7 +458,6 @@ function formatUpdate(spell) {
451
458
  }
452
459
  }
453
460
 
454
- for (const condition of whereConditions) collectLiteral(spell, condition, values);
455
461
  // see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
456
462
  const hintStr = this.formatOptimizerHints(spell);
457
463
  // see https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
@@ -466,7 +472,10 @@ function formatUpdate(spell) {
466
472
  }
467
473
 
468
474
  chunks.push(`SET ${assigns.join(', ')}`);
469
- chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
475
+ if (whereConditions.length > 0) {
476
+ for (const condition of whereConditions) collectLiteral(spell, condition, values);
477
+ chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
478
+ }
470
479
  return {
471
480
  sql: chunks.join(' '),
472
481
  values,
@@ -1,10 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ const { performance } = require('perf_hooks');
4
+
3
5
  const AbstractDriver = require('../abstract');
4
6
  const Attribute = require('./attribute');
5
7
  const DataTypes = require('./data_types');
6
8
  const spellbook = require('./spellbook');
7
9
  const schema = require('./schema');
10
+ const { calculateDuration } = require('../../utils');
8
11
 
9
12
  class MysqlDriver extends AbstractDriver {
10
13
  /**
@@ -84,19 +87,19 @@ class MysqlDriver extends AbstractDriver {
84
87
  });
85
88
  });
86
89
  const sql = logger.format(query, values, opts);
87
- const start = Date.now();
90
+ const start = performance.now();
88
91
  let result;
89
92
 
90
93
  try {
91
94
  result = await promise;
92
95
  } catch (err) {
93
- logger.logQueryError(sql, err, Date.now() - start, opts);
96
+ logger.logQueryError(err, sql, calculateDuration(start), opts);
94
97
  throw err;
95
98
  } finally {
96
99
  if (!opts.connection) connection.release();
97
100
  }
98
101
 
99
- logger.tryLogQuery(sql, Date.now() - start, opts);
102
+ logger.tryLogQuery(sql, calculateDuration(start), opts);
100
103
  const [ results, fields ] = result;
101
104
  if (fields) return { rows: results, fields };
102
105
  return results;
@@ -17,7 +17,8 @@ module.exports = {
17
17
  tables = [].concat(tables);
18
18
  const sql = heresql(`
19
19
  SELECT table_name, column_name, column_type, data_type, is_nullable,
20
- column_default, column_key, column_comment
20
+ column_default, column_key, column_comment,
21
+ datetime_precision
21
22
  FROM information_schema.columns
22
23
  WHERE table_schema = ? AND table_name in (?)
23
24
  ORDER BY table_name, column_name
@@ -43,6 +44,7 @@ module.exports = {
43
44
  allowNull: row.is_nullable === 'YES',
44
45
  primaryKey: row.column_key == 'PRI',
45
46
  unique: row.column_key == 'PRI' || row.column_key == 'UNI',
47
+ datetimePrecision: row.datetime_precision,
46
48
  });
47
49
  }
48
50
 
@@ -35,7 +35,6 @@ class PostgresAttribute extends Attribute {
35
35
  return chunks.join(' ');
36
36
  }
37
37
 
38
-
39
38
  equals(columnInfo) {
40
39
  if (!columnInfo) return false;
41
40
  if (this.type.toSqlString() !== columnInfo.columnType.toUpperCase()) {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { Pool } = require('pg');
4
+ const { performance } = require('perf_hooks');
4
5
 
5
6
  const AbstractDriver = require('../abstract');
6
7
  const Attribute = require('./attribute');
@@ -8,6 +9,7 @@ const DataTypes = require('./data_types');
8
9
  const { escape, escapeId } = require('./sqlstring');
9
10
  const spellbook = require('./spellbook');
10
11
  const schema = require('./schema');
12
+ const { calculateDuration } = require('../../utils');
11
13
 
12
14
  /**
13
15
  * The actual column type can be found by mapping the `oid` (which is called `dataTypeID`) in the `RowDescription`.
@@ -129,19 +131,19 @@ class PostgresDriver extends AbstractDriver {
129
131
 
130
132
  async function tryQuery(...args) {
131
133
  const formatted = logger.format(sql, values, spell);
132
- const start = new Date();
134
+ const start = performance.now();
133
135
  let result;
134
136
 
135
137
  try {
136
138
  result = await connection.query(...args);
137
139
  } catch (err) {
138
- logger.logQueryError(formatted, err, Date.now() - start, spell);
140
+ logger.logQueryError(err, formatted, calculateDuration(start), spell);
139
141
  throw err;
140
142
  } finally {
141
143
  if (!spell.connection) connection.release();
142
144
  }
143
145
 
144
- logger.tryLogQuery(formatted, Date.now() - start, spell);
146
+ logger.tryLogQuery(formatted, calculateDuration(start), spell);
145
147
  return result;
146
148
  }
147
149
 
@@ -58,6 +58,8 @@ module.exports = {
58
58
  if (dataType === 'character varying') dataType = 'varchar';
59
59
  if (dataType === 'timestamp without time zone') dataType = 'timestamp';
60
60
  const primaryKey = row.constraint_type === 'PRIMARY KEY';
61
+ const precision = row.datetime_precision;
62
+
61
63
  columns.push({
62
64
  columnName: row.column_name,
63
65
  columnType: length > 0 ? `${dataType}(${length})` : dataType,
@@ -67,6 +69,7 @@ module.exports = {
67
69
  // https://www.postgresql.org/docs/9.5/infoschema-table-constraints.html
68
70
  primaryKey,
69
71
  unique: row.constraint_type === 'UNIQUE',
72
+ datetimePrecision: precision === 6 ? null : precision,
70
73
  });
71
74
  }
72
75
 
@@ -3,14 +3,10 @@
3
3
  const DataTypes = require('../../data_types');
4
4
 
5
5
  class Sqlite_DATE extends DataTypes.DATE {
6
- constructor(precision) {
7
- super(precision);
6
+ constructor(precision, timezone) {
7
+ super(precision, timezone);
8
8
  this.dataType = 'datetime';
9
9
  }
10
-
11
- toSqlString() {
12
- return this.dataType.toUpperCase();
13
- }
14
10
  }
15
11
 
16
12
  class Sqlite_BIGINT extends DataTypes.BIGINT {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const strftime = require('strftime');
4
+ const { performance } = require('perf_hooks');
4
5
 
5
6
  const AbstractDriver = require('../abstract');
6
7
  const Attribute = require('./attribute');
@@ -9,6 +10,7 @@ const { escapeId, escape } = require('./sqlstring');
9
10
  const schema = require('./schema');
10
11
  const spellbook = require('./spellbook');
11
12
  const Pool = require('./pool');
13
+ const { calculateDuration } = require('../../utils');
12
14
 
13
15
  class SqliteDriver extends AbstractDriver {
14
16
  constructor(opts = {}) {
@@ -40,19 +42,19 @@ class SqliteDriver extends AbstractDriver {
40
42
 
41
43
  const { logger } = this;
42
44
  const sql = logger.format(query, values, opts);
43
- const start = Date.now();
45
+ const start = performance.now();
44
46
  let result;
45
47
 
46
48
  try {
47
49
  result = await connection.query(query, values, opts);
48
50
  } catch (err) {
49
- logger.logQueryError(sql, err, Date.now() - start, opts);
51
+ logger.logQueryError(err, sql, calculateDuration(start), opts);
50
52
  throw err;
51
53
  } finally {
52
54
  if (!opts.connection) connection.release();
53
55
  }
54
56
 
55
- logger.tryLogQuery(sql, Date.now() - start, opts);
57
+ logger.tryLogQuery(sql, calculateDuration(start), opts);
56
58
  return result;
57
59
  }
58
60
 
@@ -101,19 +101,24 @@ module.exports = {
101
101
  });
102
102
  const results = await Promise.all(queries);
103
103
  const schemaInfo = {};
104
+ const rColumnType = /^(\w+)(?:\(([^)]+)\))?/i;
105
+ const rDateType = /(?:date|datetime|timestamp)/i;
106
+
104
107
  for (let i = 0; i < tables.length; i++) {
105
108
  const table = tables[i];
106
109
  const { rows } = results[i];
107
110
  const columns = rows.map(({ name, type, notnull, dflt_value, pk }) => {
108
111
  const columnType = type.toLowerCase();
112
+ const [, dataType, precision ] = columnType.match(rColumnType);
109
113
  const primaryKey = pk === 1;
110
114
  const result = {
111
115
  columnName: name,
112
116
  columnType,
113
117
  defaultValue: parseDefaultValue(dflt_value),
114
- dataType: columnType.split('(')[0],
118
+ dataType: dataType,
115
119
  allowNull: primaryKey ? false : notnull == 0,
116
120
  primaryKey,
121
+ datetimePrecision: rDateType.test(dataType) ? parseInt(precision, 10) : null,
117
122
  };
118
123
  return result;
119
124
  });
@@ -3,6 +3,7 @@
3
3
  const util = require('util');
4
4
  const { isPlainObject } = require('./utils');
5
5
  const { parseExpr } = require('./expr');
6
+ const Raw = require('./raw');
6
7
  // deferred to break cyclic dependencies
7
8
  let Spell;
8
9
 
@@ -29,7 +30,7 @@ const OPERATOR_MAP = {
29
30
  */
30
31
  function parseValue(value) {
31
32
  if (value instanceof Spell) return { type: 'subquery', value };
32
- if (value && value.__raw) return { type: 'raw', value: value.value };
33
+ if (value instanceof Raw) return { type: 'raw', value: value.value };
33
34
  return parseExpr('?', value);
34
35
  }
35
36
 
package/src/raw.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ module.exports = class Raw {
4
+ constructor(value) {
5
+ this.value = value;
6
+ // consumed in expr_formatter.js
7
+ this.type = 'raw';
8
+ }
9
+ };
package/src/realm.js CHANGED
@@ -7,6 +7,7 @@ const Bone = require('./bone');
7
7
  const { findDriver } = require('./drivers');
8
8
  const { camelCase } = require('./utils/string');
9
9
  const sequelize = require('./adapters/sequelize');
10
+ const Raw = require('./raw');
10
11
 
11
12
  /**
12
13
  *
@@ -163,12 +164,12 @@ class Realm {
163
164
  return this.Bone;
164
165
  }
165
166
 
166
- async sync() {
167
+ async sync(options) {
167
168
  if (!this.connected) await this.connect();
168
169
  const { models } = this;
169
170
 
170
171
  for (const model of Object.values(models)) {
171
- await model.sync();
172
+ await model.sync(options);
172
173
  }
173
174
  }
174
175
 
@@ -239,11 +240,7 @@ class Realm {
239
240
  if (typeof sql !== 'string') {
240
241
  throw new TypeError('sql must be a string');
241
242
  }
242
- return {
243
- __raw: true,
244
- value: sql,
245
- type: 'raw',
246
- };
243
+ return new Raw(sql);
247
244
  }
248
245
 
249
246
  // instance.raw
package/src/spell.js CHANGED
@@ -10,6 +10,7 @@ const { parseExprList, parseExpr, walkExpr } = require('./expr');
10
10
  const { isPlainObject } = require('./utils');
11
11
  const { IndexHint, INDEX_HINT_TYPE, Hint } = require('./hint');
12
12
  const { parseObject } = require('./query_object');
13
+ const Raw = require('./raw');
13
14
 
14
15
  /**
15
16
  * Parse condition expressions
@@ -21,7 +22,7 @@ const { parseObject } = require('./query_object');
21
22
  * @returns {Array}
22
23
  */
23
24
  function parseConditions(conditions, ...values) {
24
- if (conditions.__raw) return [ conditions ];
25
+ if (conditions instanceof Raw) return [ conditions ];
25
26
  if (isPlainObject(conditions)) {
26
27
  return parseObject(conditions);
27
28
  } else if (typeof conditions == 'string') {
@@ -41,7 +42,7 @@ function parseSelect(spell, ...names) {
41
42
 
42
43
  const columns = [];
43
44
  for (const name of names) {
44
- if (name.__raw) columns.push(name);
45
+ if (name instanceof Raw) columns.push(name);
45
46
  else columns.push(...parseExprList(name));
46
47
  }
47
48
 
@@ -68,8 +69,7 @@ function parseSelect(spell, ...names) {
68
69
  * @returns {Object}
69
70
  */
70
71
  function formatValueSet(spell, obj, strict = true) {
71
- const { Model, silent = false, command } = spell;
72
- const { timestamps } = Model;
72
+ const { Model } = spell;
73
73
  const sets = {};
74
74
  for (const name in obj) {
75
75
  const attribute = Model.attributes[name];
@@ -79,12 +79,8 @@ function formatValueSet(spell, obj, strict = true) {
79
79
  throw new Error(`Undefined attribute "${name}"`);
80
80
  }
81
81
 
82
- if (silent && timestamps.updatedAt && name === timestamps.updatedAt && command === 'update') {
83
- continue;
84
- }
85
-
86
82
  // raw sql don't need to uncast
87
- if (value && value.__raw) {
83
+ if (value instanceof Raw) {
88
84
  sets[name] = value;
89
85
  } else {
90
86
  sets[name] = attribute.uncast(value);
@@ -402,7 +398,6 @@ class Spell {
402
398
  hints: [...this.hints],
403
399
  // used by transaction
404
400
  connection: this.connection,
405
- silent: this.silent,
406
401
  });
407
402
  }
408
403
 
@@ -530,8 +525,8 @@ class Spell {
530
525
  }
531
526
 
532
527
  $increment(name, by = 1, opts = {}) {
533
- let { Model, silent = false } = this;
534
- if (opts.silent != null) silent = opts.silent;
528
+ const { Model } = this;
529
+ const silent = opts.silent;
535
530
  const { timestamps } = Model;
536
531
  this.command = 'update';
537
532
  if (!Number.isFinite(by)) throw new Error(`unexpected increment value ${by}`);
@@ -652,7 +647,7 @@ class Spell {
652
647
  */
653
648
  $order(name, direction) {
654
649
  if (isPlainObject(name)) {
655
- if (name.__raw) {
650
+ if (name instanceof Raw) {
656
651
  this.orders.push([
657
652
  name,
658
653
  ]);
@@ -717,7 +712,7 @@ class Spell {
717
712
  for (const condition of parseConditions(conditions, ...values)) {
718
713
  // Postgres can't have alias in HAVING caluse
719
714
  // https://stackoverflow.com/questions/32730296/referring-to-a-select-aggregate-column-alias-in-the-having-clause-in-postgres
720
- if (this.Model.driver.type === 'postgres' && !condition.__raw) {
715
+ if (this.Model.driver.type === 'postgres' && !(condition instanceof Raw)) {
721
716
  const { value } = condition.args[0];
722
717
  for (const column of this.columns) {
723
718
  if (column.value === value && column.type === 'alias') {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { performance } = require('perf_hooks');
4
+
3
5
  function isPlainObject(value) {
4
6
  return Object.prototype.toString.call(value) === '[object Object]';
5
7
  }
@@ -28,6 +30,11 @@ function getPropertyNames(obj) {
28
30
  return Array.from(propertyNamesSet);
29
31
  }
30
32
 
33
+ // microseconds to millisecond, 10.456
34
+ function calculateDuration(starttime) {
35
+ return Math.floor((performance.now() - starttime) * 1000) / 1000;
36
+ }
37
+
31
38
  const logger = {};
32
39
 
33
40
  [ 'log', 'warn', 'debug', 'info', 'error' ].forEach(key => {
@@ -40,5 +47,6 @@ module.exports = {
40
47
  isPlainObject,
41
48
  compose,
42
49
  getPropertyNames,
50
+ calculateDuration,
43
51
  logger,
44
52
  };
package/types/index.d.ts CHANGED
@@ -64,10 +64,10 @@ export class Spell<T extends typeof Bone, U = InstanceType<T> | Collection<Insta
64
64
  constructor(Model: T, opts: SpellOptions);
65
65
 
66
66
  select(...names: Array<string | RawSql> | Array<(name: string) => boolean>): Spell<T, U>;
67
- insert(opts: SetOptions): Spell<T, number>;
68
- update(opts: SetOptions): Spell<T, number>;
69
- upsert(opts: SetOptions): Spell<T, number>;
70
- delete(): Spell<T, number>;
67
+ insert(opts: SetOptions): Spell<T, QueryResult>;
68
+ update(opts: SetOptions): Spell<T, QueryResult>;
69
+ upsert(opts: SetOptions): Spell<T, QueryResult>;
70
+ delete(): Spell<T, QueryResult>;
71
71
 
72
72
  from(table: string | Spell<T>): Spell<T, U>;
73
73
 
@@ -107,6 +107,9 @@ export class Spell<T extends typeof Bone, U = InstanceType<T> | Collection<Insta
107
107
 
108
108
  batch(size?: number): AsyncIterable<T>;
109
109
 
110
+ increment(name: string, by?: number, options?: QueryOptions): Spell<T, QueryResult>;
111
+ decrement(name: string, by?: number, options?: QueryOptions): Spell<T, QueryResult>;
112
+
110
113
  toSqlString(): string;
111
114
  toString(): string;
112
115
  }
@@ -158,6 +161,7 @@ interface QueryOptions {
158
161
  individualHooks?: boolean;
159
162
  hooks?: boolean;
160
163
  paranoid?: boolean;
164
+ silent?: boolean;
161
165
  }
162
166
 
163
167
  interface QueryResult {
@@ -494,7 +498,7 @@ export class Bone {
494
498
  * Get changed attributes or check if given attribute is changed or not
495
499
  */
496
500
  changed(name: string): boolean;
497
- changed(): Array<string>;
501
+ changed(): Array<string> | false;
498
502
 
499
503
  /**
500
504
  * Get attribute changes