leoric 2.2.2 → 2.3.1

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,34 @@
1
+ 2.3.1 / 2022-03-22
2
+ ==================
3
+
4
+ ## What's Changed
5
+ * fix: mysql2 Invalid Date compatible by @JimmyDaddy in https://github.com/cyjake/leoric/pull/291
6
+ * fix: order by raw in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/292
7
+ * fix: bulk update query conditions duplicated in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/293
8
+ * fix: bulk destroy query conditions duplicated in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/295
9
+ * fix: drop column if not defined in attributes when alter table by @cyjake in https://github.com/cyjake/leoric/pull/296
10
+
11
+
12
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.3.0...v2.3.1
13
+
14
+ 2.3.0 / 2022-03-10
15
+ ==================
16
+
17
+ ## What's Changed
18
+ * feat: model declaration with decorators by @cyjake in https://github.com/cyjake/leoric/pull/287
19
+ * feat: add VIRTUAL data type by @JimmyDaddy in https://github.com/cyjake/leoric/pull/289
20
+ * fix: create instance dirty check rule fix by @JimmyDaddy in https://github.com/cyjake/leoric/pull/290
21
+
22
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.2.3...v2.3.0
23
+ 2.2.3 / 2022-03-01
24
+ ==================
25
+
26
+ ## What's Changed
27
+ * fix: normalize attribute defaultValue by @cyjake in https://github.com/cyjake/leoric/pull/285
28
+ * fix: instance beforeUpdate hooks should not modify any Raw if there are no Raw assignment in them by @JimmyDaddy in https://github.com/cyjake/leoric/pull/283
29
+
30
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.2.2...v2.2.3
31
+
1
32
  2.2.2 / 2022-02-28
2
33
  ==================
3
34
 
package/index.js CHANGED
@@ -11,6 +11,7 @@ const sequelize = require('./src/adapters/sequelize');
11
11
  const { heresql } = require('./src/utils/string');
12
12
  const Hint = require('./src/hint');
13
13
  const Realm = require('./src/realm');
14
+ const Decorators = require('./src/decorators');
14
15
 
15
16
  /**
16
17
  * @typedef {Object} RawSql
@@ -38,6 +39,7 @@ const connect = async function connect(opts) {
38
39
 
39
40
  Object.assign(Realm.prototype, migrations, { DataTypes });
40
41
  Object.assign(Realm, {
42
+ default: Realm,
41
43
  connect,
42
44
  Bone,
43
45
  Collection,
@@ -47,6 +49,7 @@ Object.assign(Realm, {
47
49
  sequelize,
48
50
  heresql,
49
51
  ...Hint,
52
+ ...Decorators,
50
53
  });
51
54
 
52
55
  module.exports = Realm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leoric",
3
- "version": "2.2.2",
3
+ "version": "2.3.1",
4
4
  "description": "JavaScript Object-relational mapping alchemy",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -11,7 +11,8 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "jsdoc": "rm -rf docs/api && jsdoc -c .jsdoc.json -d docs/api -t node_modules/@cara/minami",
14
- "pretest": "./test/prepare.sh",
14
+ "prepack": "tsc",
15
+ "pretest": "tsc && ./test/prepare.sh",
15
16
  "test": "./test/start.sh",
16
17
  "test:local": "./test/start.sh",
17
18
  "test:unit": "./test/start.sh unit",
@@ -50,6 +51,7 @@
50
51
  "debug": "^3.1.0",
51
52
  "heredoc": "^1.3.1",
52
53
  "pluralize": "^7.0.0",
54
+ "reflect-metadata": "^0.1.13",
53
55
  "sqlstring": "^2.3.0",
54
56
  "strftime": "^0.10.0",
55
57
  "validator": "^13.5.2"
@@ -72,6 +74,6 @@
72
74
  "pg": "^8.5.1",
73
75
  "sinon": "^10.0.0",
74
76
  "sqlite3": "^5.0.2",
75
- "typescript": "^4.4.4"
77
+ "typescript": "^4.6.2"
76
78
  }
77
79
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { setupSingleHook } = require('../setup_hooks');
4
4
  const { compose, isPlainObject } = require('../utils');
5
+ const Raw = require('../raw');
5
6
 
6
7
  function translateOptions(spell, options) {
7
8
  const { attributes, where, group, order, offset, limit, include, having } = options;
@@ -18,7 +19,7 @@ function translateOptions(spell, options) {
18
19
  if (having) spell.$having(having);
19
20
 
20
21
  if (order) {
21
- if (typeof order === 'string') {
22
+ if (typeof order === 'string' || order instanceof Raw) {
22
23
  spell.$order(order);
23
24
  } else if (Array.isArray(order) && order.length) {
24
25
  if (order.some(item => Array.isArray(item))) {
@@ -317,10 +318,12 @@ module.exports = Bone => {
317
318
  }
318
319
 
319
320
  // proxy to class.destroy({ individualHooks=false }) see https://github.com/sequelize/sequelize/blob/4063c2ab627ad57919d5b45cc7755f077a69fa5e/lib/model.js#L2895 before(after)BulkDestroy
320
- static async bulkDestroy(options = {}) {
321
+ static bulkDestroy(options = {}) {
321
322
  const { where, force } = options;
322
323
  const spell = this._remove(where || {}, force, { ...options });
323
- translateOptions(spell, options);
324
+ const transOptions = { ...options };
325
+ delete transOptions.where;
326
+ translateOptions(spell, transOptions);
324
327
  return spell;
325
328
  }
326
329
 
@@ -496,7 +499,9 @@ module.exports = Bone => {
496
499
  const { where, paranoid = false, validate } = options;
497
500
  const whereConditions = where || {};
498
501
  const spell = super.update(whereConditions, values, { validate, hooks: false, ...options });
499
- translateOptions(spell, options);
502
+ const transOptions = { ...options };
503
+ delete transOptions.where;
504
+ translateOptions(spell, transOptions);
500
505
  if (!paranoid) return spell.unparanoid;
501
506
  return spell;
502
507
  }
package/src/bone.js CHANGED
@@ -7,6 +7,7 @@
7
7
  const util = require('util');
8
8
  const pluralize = require('pluralize');
9
9
  const { executeValidator, LeoricValidateError } = require('./validator');
10
+ require('reflect-metadata');
10
11
 
11
12
  const DataTypes = require('./data_types');
12
13
  const Collection = require('./collection');
@@ -14,8 +15,13 @@ const Spell = require('./spell');
14
15
  const Raw = require('./raw');
15
16
  const { capitalize, camelCase, snakeCase } = require('./utils/string');
16
17
  const { hookNames, setupSingleHook } = require('./setup_hooks');
17
- const { logger } = require('./utils/index');
18
- const { TIMESTAMP_NAMES, LEGACY_TIMESTAMP_COLUMN_MAP } = require('./constants');
18
+ const {
19
+ TIMESTAMP_NAMES,
20
+ LEGACY_TIMESTAMP_COLUMN_MAP,
21
+ ASSOCIATE_METADATA_MAP,
22
+ } = require('./constants');
23
+
24
+ const columnAttributesKey = Symbol('leoric#columns');
19
25
 
20
26
  function looseReadonly(props) {
21
27
  return Object.keys(props).reduce((result, name) => {
@@ -31,10 +37,12 @@ function looseReadonly(props) {
31
37
 
32
38
  function compare(attributes, columnMap) {
33
39
  const diff = {};
40
+ const columnNames = new Set();
34
41
 
35
42
  for (const name in attributes) {
36
43
  const attribute = attributes[name];
37
44
  const { columnName } = attribute;
45
+ columnNames.add(columnName);
38
46
 
39
47
  if (!attribute.equals(columnMap[columnName])) {
40
48
  diff[name] = {
@@ -44,6 +52,12 @@ function compare(attributes, columnMap) {
44
52
  }
45
53
  }
46
54
 
55
+ for (const columnName in columnMap) {
56
+ if (!columnNames.has(columnName)) {
57
+ diff[columnName] = { remove: true };
58
+ }
59
+ }
60
+
47
61
  return diff;
48
62
  }
49
63
 
@@ -156,7 +170,6 @@ class Bone {
156
170
  * Get or set attribute value by name. This method is quite similiar to `jQuery.attr()`. If the attribute isn't selected when queried from database, an error will be thrown when accessing it.
157
171
  *
158
172
  * const post = Post.select('title').first
159
- * post.content // throw Error('Unset attribute "content"')
160
173
  *
161
174
  * This is the underlying method of attribute getter/setters:
162
175
  *
@@ -189,7 +202,6 @@ class Bone {
189
202
  }
190
203
 
191
204
  if (this.#rawUnset.has(name)) {
192
- logger.warn(`unset attribute "${name}"`);
193
205
  return;
194
206
  }
195
207
 
@@ -199,14 +211,14 @@ class Bone {
199
211
  }
200
212
 
201
213
  /**
202
- *
203
- * clone instance
214
+ * @protected clone instance
204
215
  * @param {Bone} target
205
216
  * @memberof Bone
206
217
  */
207
218
  _clone(target) {
208
- Object.assign(this.#raw, target.getRaw());
209
- Object.assign(this.#rawSaved, target.getRawSaved());
219
+ this.#raw = Object.assign({}, this.getRaw(), target.getRaw());
220
+ this.#rawSaved = Object.assign({}, this.getRawSaved(), target.getRawSaved());
221
+ this.#rawPrevious = Object.assign({}, this.getRawPrevious(), target.getRawPrevious());
210
222
  }
211
223
 
212
224
  /**
@@ -235,6 +247,34 @@ class Bone {
235
247
  return attributes.hasOwnProperty(name);
236
248
  }
237
249
 
250
+ /**
251
+ * get attributes except virtuals
252
+ */
253
+ static get columnAttributes() {
254
+ if (this[columnAttributesKey]) return this[columnAttributesKey];
255
+ const { attributes } = this;
256
+ this[columnAttributesKey] = {};
257
+ for (const key in this.attributes) {
258
+ if (!attributes[key].virtual) this[columnAttributesKey][key] = attributes[key];
259
+ }
260
+ return this[columnAttributesKey];
261
+ }
262
+
263
+ /**
264
+ * get actual update/insert columns to avoid empty insert or update
265
+ * @param {Object} data
266
+ * @returns
267
+ */
268
+ static _getColumns(data) {
269
+ if (!Object.keys(data).length) return data;
270
+ const attributes = this.columnAttributes;
271
+ const res = {};
272
+ for (const key in data) {
273
+ if (attributes[key]) res[key] = data[key];
274
+ }
275
+ return res;
276
+ }
277
+
238
278
  getRaw(key) {
239
279
  if (key) return this.#raw[key];
240
280
  return this.#raw;
@@ -320,7 +360,6 @@ class Bone {
320
360
  * post.attributeWas('title') // => 'Leah'
321
361
  */
322
362
  attributeWas(name) {
323
- if (this.#rawUnset.has(name)) throw new Error(`unset attribute "${name}"`);
324
363
  const value = this.#rawSaved[name];
325
364
  return value == null ? null : value;
326
365
  }
@@ -536,18 +575,25 @@ class Bone {
536
575
  * Sync changes made in {@link Bone.raw} back to {@link Bone.rawSaved}.
537
576
  * @private
538
577
  */
539
- syncRaw(changes) {
578
+ syncRaw() {
540
579
  const { attributes } = this.constructor;
541
580
  this.isNewRecord = false;
542
- for (const name of Object.keys(changes || attributes)) {
581
+ for (const name of Object.keys(attributes)) {
543
582
  const attribute = attributes[name];
544
583
  // Take advantage of uncast/cast to create new copy of value
545
- const value = attribute.uncast(this.#raw[name]);
584
+ let value;
585
+ try {
586
+ value = attribute.uncast(this.#raw[name]);
587
+ } catch (error) {
588
+ console.error(error);
589
+ // do not interrupt sync raw
590
+ value = this.#raw[name];
591
+ }
546
592
  if (this.#rawSaved[name] !== undefined) {
547
593
  this.#rawPrevious[name] = this.#rawSaved[name];
548
- } else if (!changes && this.#rawPrevious[name] === undefined) {
594
+ } else if (this.#rawPrevious[name] === undefined && this.#raw[name] != null) {
549
595
  // first persisting
550
- this.#rawPrevious[name] = attribute.cast(value);
596
+ this.#rawPrevious[name] = null;
551
597
  }
552
598
  this.#rawSaved[name] = attribute.cast(value);
553
599
  }
@@ -579,7 +625,10 @@ class Bone {
579
625
  if (this.changed(name)) data[name] = this.attribute(name);
580
626
  }
581
627
 
582
- if (Object.keys(data).length === 0) return Promise.resolve(0);
628
+ if (!Object.keys(Model._getColumns(data)).length) {
629
+ this.syncRaw();
630
+ return Promise.resolve(0);
631
+ }
583
632
 
584
633
  const { createdAt, updatedAt } = Model.timestamps;
585
634
 
@@ -659,7 +708,10 @@ class Bone {
659
708
  }
660
709
  }
661
710
 
662
- if (Object.keys(changes).length === 0) return Promise.resolve(0);
711
+ if (!Object.keys(Model._getColumns(changes)).length) {
712
+ this.syncRaw();
713
+ return Promise.resolve(0);
714
+ }
663
715
  if (this[primaryKey] == null) {
664
716
  throw new Error(`unset primary key ${primaryKey}`);
665
717
  }
@@ -680,7 +732,7 @@ class Bone {
680
732
  for (const key in changes) {
681
733
  this.attribute(key, changes[key]);
682
734
  }
683
- this.syncRaw(changes);
735
+ this.syncRaw();
684
736
  return result.affectedRows;
685
737
  });
686
738
  }
@@ -730,9 +782,15 @@ class Bone {
730
782
  this._validateAttributes(validateValues);
731
783
  }
732
784
 
785
+ if (!Object.keys(Model._getColumns(data)).length) {
786
+ this.syncRaw();
787
+ return this;
788
+ }
789
+
733
790
  const spell = new Spell(Model, opts).$insert(data);
734
791
  return spell.later(result => {
735
792
  this[primaryKey] = result.insertId;
793
+ // this.#rawSaved[primaryKey] = null;
736
794
  this.syncRaw();
737
795
  return this;
738
796
  });
@@ -856,7 +914,10 @@ class Bone {
856
914
  }
857
915
  }
858
916
 
859
- if (Object.keys(data).length === 0) return Promise.resolve(0);
917
+ if (!Object.keys(Model._getColumns(data)).length) {
918
+ this.syncRaw();
919
+ return Promise.resolve(0);
920
+ }
860
921
 
861
922
  const { createdAt, updatedAt } = Model.timestamps;
862
923
 
@@ -977,6 +1038,7 @@ class Bone {
977
1038
  for (const hookName of hookNames) {
978
1039
  if (this[hookName]) setupSingleHook(this, hookName, this[hookName]);
979
1040
  }
1041
+ this[columnAttributesKey] = null;
980
1042
  }
981
1043
 
982
1044
  /**
@@ -989,7 +1051,12 @@ class Bone {
989
1051
  * }
990
1052
  * }
991
1053
  */
992
- static initialize() {}
1054
+ static initialize() {
1055
+ for (const [key, metadataKey] of Object.entries(ASSOCIATE_METADATA_MAP)) {
1056
+ const result = Reflect.getMetadata(metadataKey, this);
1057
+ for (const property in result) this[key].call(this, property, result[property]);
1058
+ }
1059
+ }
993
1060
 
994
1061
  /**
995
1062
  * The primary key of the model, in camelCase.
@@ -1107,6 +1174,7 @@ class Bone {
1107
1174
  Reflect.deleteProperty(this.prototype, originalName);
1108
1175
  this.loadAttribute(newName);
1109
1176
  }
1177
+ this[columnAttributesKey] = null;
1110
1178
  }
1111
1179
 
1112
1180
  /**
@@ -1116,15 +1184,16 @@ class Bone {
1116
1184
  * @param {string} [opts.className]
1117
1185
  * @param {string} [opts.foreignKey]
1118
1186
  */
1119
- static hasOne(name, opts) {
1120
- opts = Object.assign({
1187
+ static hasOne(name, options) {
1188
+ options = ({
1121
1189
  className: capitalize(name),
1122
- foreignKey: this.table + 'Id',
1123
- }, opts);
1190
+ foreignKey: camelCase(`${this.name}Id`),
1191
+ ...options,
1192
+ });
1124
1193
 
1125
- if (opts.through) opts.foreignKey = '';
1194
+ if (options.through) options.foreignKey = '';
1126
1195
 
1127
- this.associate(name, opts);
1196
+ this.associate(name, options);
1128
1197
  }
1129
1198
 
1130
1199
  /**
@@ -1134,16 +1203,17 @@ class Bone {
1134
1203
  * @param {string} [opts.className]
1135
1204
  * @param {string} [opts.foreignKey]
1136
1205
  */
1137
- static hasMany(name, opts) {
1138
- opts = Object.assign({
1206
+ static hasMany(name, options) {
1207
+ options = {
1139
1208
  className: capitalize(pluralize(name, 1)),
1140
- }, opts, {
1209
+ foreignKey: camelCase(`${this.name}Id`),
1210
+ ...options,
1141
1211
  hasMany: true,
1142
- });
1212
+ };
1143
1213
 
1144
- if (opts.through) opts.foreignKey = '';
1214
+ if (options.through) options.foreignKey = '';
1145
1215
 
1146
- this.associate(name, opts);
1216
+ this.associate(name, options);
1147
1217
  }
1148
1218
 
1149
1219
  /**
@@ -1153,15 +1223,15 @@ class Bone {
1153
1223
  * @param {string} [opts.className]
1154
1224
  * @param {string} [opts.foreignKey]
1155
1225
  */
1156
- static belongsTo(name, opts) {
1157
- opts = Object.assign({
1158
- className: capitalize(name),
1159
- }, opts);
1160
-
1161
- let { className, foreignKey } = opts;
1162
- if (!foreignKey) foreignKey = camelCase(className) + 'Id';
1226
+ static belongsTo(name, options = {}) {
1227
+ const { className = capitalize(name) } = options;
1228
+ options = {
1229
+ className,
1230
+ foreignKey: camelCase(`${className}Id`),
1231
+ ...options,
1232
+ };
1163
1233
 
1164
- this.associate(name, Object.assign(opts, { foreignKey, belongsTo: true }));
1234
+ this.associate(name, { ...options, belongsTo: true });
1165
1235
  }
1166
1236
 
1167
1237
  /**
@@ -1181,6 +1251,9 @@ class Bone {
1181
1251
  const { className } = opts;
1182
1252
  const Model = this.models[className];
1183
1253
  if (!Model) throw new Error(`unable to find model "${className}"`);
1254
+ if (opts.foreignKey && Model.attributes[opts.foreignKey] && Model.attributes[opts.foreignKey].virtual) {
1255
+ throw new Error(`unable to use virtual attribute ${opts.foreignKey} as foreign key in model ${Model.name}`);
1256
+ }
1184
1257
 
1185
1258
  const { deletedAt } = this.timestamps;
1186
1259
  if (Model.attributes[deletedAt] && !opts.where) {
@@ -1559,6 +1632,7 @@ class Bone {
1559
1632
  return result;
1560
1633
  }, {});
1561
1634
 
1635
+ this[columnAttributesKey] = null;
1562
1636
  Object.defineProperties(this, looseReadonly({ ...hookMethods, attributes, table }));
1563
1637
  }
1564
1638
 
@@ -1578,7 +1652,7 @@ class Bone {
1578
1652
  throw new Error('unable to sync model with custom physic tables');
1579
1653
  }
1580
1654
 
1581
- const { attributes, columns } = this;
1655
+ const { columnAttributes: attributes, columns } = this;
1582
1656
  const columnMap = columns.reduce((result, entry) => {
1583
1657
  result[entry.columnName] = entry;
1584
1658
  return result;
package/src/collection.js CHANGED
@@ -63,14 +63,14 @@ class Collection extends Array {
63
63
  */
64
64
  function instantiatable(spell) {
65
65
  const { columns, groups, Model } = spell;
66
- const { attributes, tableAlias } = Model;
66
+ const { columnAttributes, tableAlias } = Model;
67
67
 
68
68
  if (groups.length > 0) return false;
69
69
  if (columns.length === 0) return true;
70
70
 
71
71
  return columns
72
72
  .filter(({ qualifiers }) => (!qualifiers || qualifiers.includes(tableAlias)))
73
- .every(({ value }) => attributes[value]);
73
+ .every(({ value }) => columnAttributes[value]);
74
74
  }
75
75
 
76
76
  /**
package/src/constants.js CHANGED
@@ -22,9 +22,16 @@ const LEGACY_TIMESTAMP_COLUMN_MAP = {
22
22
 
23
23
  const TIMESTAMP_NAMES = [ 'createdAt', 'updatedAt', 'deletedAt' ];
24
24
 
25
+ const ASSOCIATE_METADATA_MAP = {
26
+ hasMany: Symbol('hasMany'),
27
+ hasOne: Symbol('hasOne'),
28
+ belongsTo: Symbol('belongsTo'),
29
+ };
30
+
25
31
  module.exports = {
26
32
  AGGREGATOR_MAP,
27
33
  LEGACY_TIMESTAMP_MAP,
28
34
  TIMESTAMP_NAMES,
29
- LEGACY_TIMESTAMP_COLUMN_MAP
35
+ LEGACY_TIMESTAMP_COLUMN_MAP,
36
+ ASSOCIATE_METADATA_MAP
30
37
  };
package/src/data_types.js CHANGED
@@ -17,17 +17,20 @@ class DataType {
17
17
  const {
18
18
  STRING, TEXT,
19
19
  DATE, DATEONLY,
20
- TINYINT, SMALLINT, MEDIUMINT, INTEGER, BIGINT,
20
+ TINYINT, SMALLINT, MEDIUMINT, INTEGER, BIGINT, DECIMAL,
21
21
  BOOLEAN,
22
22
  BINARY, VARBINARY, BLOB,
23
23
  } = this;
24
- const [ , dataType, appendix ] = columnType.match(/(\w+)(?:\((\d+)\))?/);
25
- const length = appendix && parseInt(appendix, 10);
24
+ const [ , dataType, ...matches ] = columnType.match(/(\w+)(?:\((\d+)(?:,(\d+))?\))?/);
25
+ const params = [];
26
+ for (let i = 0; i < matches.length; i++) {
27
+ if (matches[i] != null) params[i] = parseInt(matches[i], 10);
28
+ }
26
29
 
27
30
  switch (dataType) {
28
31
  case 'varchar':
29
32
  case 'char':
30
- return new STRING(length);
33
+ return new STRING(...params);
31
34
  // longtext is only for MySQL
32
35
  case 'longtext':
33
36
  return new TEXT('long');
@@ -40,30 +43,31 @@ class DataType {
40
43
  case 'datetime':
41
44
  case 'timestamp':
42
45
  // new DATE(precision)
43
- return new DATE(length);
46
+ return new DATE(...params);
44
47
  case 'decimal':
48
+ return new DECIMAL(...params);
45
49
  case 'int':
46
50
  case 'integer':
47
51
  case 'numeric':
48
- return new INTEGER(length);
52
+ return new INTEGER(...params);
49
53
  case 'mediumint':
50
- return new MEDIUMINT(length);
54
+ return new MEDIUMINT(...params);
51
55
  case 'smallint':
52
- return new SMALLINT(length);
56
+ return new SMALLINT(...params);
53
57
  case 'tinyint':
54
- return new TINYINT(length);
58
+ return new TINYINT(...params);
55
59
  case 'bigint':
56
- return new BIGINT(length);
60
+ return new BIGINT(...params);
57
61
  case 'boolean':
58
62
  return new BOOLEAN();
59
63
  // mysql only
60
64
  case 'binary':
61
65
  // postgres only
62
66
  case 'bytea':
63
- return new BINARY(length);
67
+ return new BINARY(...params);
64
68
  // mysql only
65
69
  case 'varbinary':
66
- return new VARBINARY(length);
70
+ return new VARBINARY(...params);
67
71
  case 'longblob':
68
72
  return new BLOB('long');
69
73
  case 'mediumblob':
@@ -273,6 +277,41 @@ class BIGINT extends INTEGER {
273
277
  }
274
278
  }
275
279
 
280
+ /**
281
+ * fixed-point decimal types
282
+ * @example
283
+ * DECIMAL
284
+ * DECIMAL.UNSIGNED
285
+ * DECIMAL(5, 2)
286
+ * @param {number} precision
287
+ * @param {number} scale
288
+ * - https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html
289
+ */
290
+ class DECIMAL extends INTEGER {
291
+ constructor(precision, scale) {
292
+ super();
293
+ this.dataType = 'decimal';
294
+ this.precision = precision;
295
+ this.scale = scale;
296
+ }
297
+
298
+ toSqlString() {
299
+ const { precision, scale, unsigned, zerofill } = this;
300
+ const dataType = this.dataType.toUpperCase();
301
+ const chunks = [];
302
+ if (precision > 0 && scale >= 0) {
303
+ chunks.push(`${dataType}(${precision},${scale})`);
304
+ } else if (precision > 0) {
305
+ chunks.push(`${dataType}(${precision})`);
306
+ } else {
307
+ chunks.push(dataType);
308
+ }
309
+ if (unsigned) chunks.push('UNSIGNED');
310
+ if (zerofill) chunks.push('ZEROFILL');
311
+ return chunks.join(' ');
312
+ }
313
+ }
314
+
276
315
  const rDateFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d{3,6})$/;
277
316
 
278
317
  class DATE extends DataType {
@@ -475,6 +514,18 @@ class JSONB extends JSON {
475
514
  }
476
515
  }
477
516
 
517
+ class VIRTUAL extends DataType {
518
+ constructor() {
519
+ super();
520
+ this.dataType = 'virtual';
521
+ this.virtual = true;
522
+ }
523
+
524
+ toSqlString() {
525
+ return 'VIRTUAL';
526
+ }
527
+ }
528
+
478
529
  const DataTypes = {
479
530
  STRING,
480
531
  TINYINT,
@@ -482,6 +533,7 @@ const DataTypes = {
482
533
  MEDIUMINT,
483
534
  INTEGER,
484
535
  BIGINT,
536
+ DECIMAL,
485
537
  DATE,
486
538
  DATEONLY,
487
539
  BOOLEAN,
@@ -491,6 +543,7 @@ const DataTypes = {
491
543
  JSONB,
492
544
  BINARY,
493
545
  VARBINARY,
546
+ VIRTUAL,
494
547
  };
495
548
 
496
549
  Object.assign(DataType, DataTypes);