leoric 2.3.2 → 2.5.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.
@@ -34,8 +34,8 @@ class Sqlite_DATEONLY extends DataTypes.DATEONLY {
34
34
  }
35
35
 
36
36
  class Sqlite_INTEGER extends DataTypes.INTEGER {
37
- constructor(length) {
38
- super(length);
37
+ constructor(dataLength) {
38
+ super(dataLength);
39
39
  }
40
40
 
41
41
  uncast(value) {
@@ -54,24 +54,24 @@ class Sqlite_BIGINT extends DataTypes.BIGINT {
54
54
  }
55
55
  }
56
56
  class Sqlite_BINARY extends DataTypes {
57
- constructor(length = 255) {
58
- super(length);
59
- this.length = length;
57
+ constructor(dataLength = 255) {
58
+ super(dataLength);
59
+ this.dataLength = dataLength;
60
60
  this.dataType = 'binary';
61
61
  }
62
62
 
63
63
  toSqlString() {
64
- const { length } = this;
64
+ const { dataLength } = this;
65
65
  const dataType = this.dataType.toUpperCase();
66
66
  const chunks = [];
67
67
  chunks.push('VARCHAR');
68
- chunks.push(length > 0 ? `${dataType}(${length})` : dataType);
68
+ chunks.push(dataLength > 0 ? `${dataType}(${dataLength})` : dataType);
69
69
  return chunks.join(' ');
70
70
  }
71
71
  }
72
72
  class Sqlite_VARBINARY extends Sqlite_BINARY {
73
- constructor(length) {
74
- super(length);
73
+ constructor(dataLength) {
74
+ super(dataLength);
75
75
  this.dataType = 'varbinary';
76
76
  }
77
77
  }
@@ -6,17 +6,29 @@ const { performance } = require('perf_hooks');
6
6
  const AbstractDriver = require('../abstract');
7
7
  const Attribute = require('./attribute');
8
8
  const DataTypes = require('./data_types');
9
- const { escapeId, escape } = require('./sqlstring');
10
- const schema = require('./schema');
11
- const spellbook = require('./spellbook');
9
+ const { escapeId, escape, alterTableWithChangeColumn, parseDefaultValue } = require('./sqlstring');
10
+ const Spellbook = require('./spellbook');
12
11
  const Pool = require('./pool');
13
12
  const { calculateDuration } = require('../../utils');
13
+ const { heresql } = require('../../utils/string');
14
14
 
15
15
  class SqliteDriver extends AbstractDriver {
16
+
17
+ // define static properties as this way IDE will prompt
18
+ static Spellbook = Spellbook;
19
+ static Attribute = Attribute;
20
+ static DataTypes = DataTypes;
21
+
16
22
  constructor(opts = {}) {
17
23
  super(opts);
18
24
  this.type = 'sqlite';
19
25
  this.pool = this.createPool(opts);
26
+ this.Attribute = this.constructor.Attribute;
27
+ this.DataTypes = this.constructor.DataTypes;
28
+ this.spellbook = new this.constructor.Spellbook();
29
+
30
+ this.escape = escape;
31
+ this.escapeId = escapeId;
20
32
  }
21
33
 
22
34
  createPool(opts) {
@@ -58,17 +70,116 @@ class SqliteDriver extends AbstractDriver {
58
70
  return result;
59
71
  }
60
72
 
61
- format(spell) {
62
- return spellbook.format(spell);
73
+ async querySchemaInfo(database, tables) {
74
+ tables = [].concat(tables);
75
+
76
+ const queries = tables.map(table => {
77
+ return this.query(`PRAGMA table_info(${this.escapeId(table)})`);
78
+ });
79
+ const results = await Promise.all(queries);
80
+ const schemaInfo = {};
81
+ const rColumnType = /^(\w+)(?:\(([^)]+)\))?/i;
82
+ const rDateType = /(?:date|datetime|timestamp)/i;
83
+
84
+ for (let i = 0; i < tables.length; i++) {
85
+ const table = tables[i];
86
+ const { rows } = results[i];
87
+ const columns = rows.map(row => {
88
+ const { name, type, notnull, dflt_value, pk } = row;
89
+ const columnType = type.toLowerCase();
90
+ const [, dataType, precision ] = columnType.match(rColumnType);
91
+ const primaryKey = pk === 1;
92
+
93
+ const result = {
94
+ columnName: name,
95
+ columnType,
96
+ defaultValue: parseDefaultValue(dflt_value, type),
97
+ dataType: dataType,
98
+ allowNull: primaryKey ? false : notnull == 0,
99
+ primaryKey,
100
+ datetimePrecision: rDateType.test(dataType) ? parseInt(precision, 10) : null,
101
+ };
102
+ return result;
103
+ });
104
+ if (columns.length > 0) schemaInfo[table] = columns;
105
+ }
106
+
107
+ return schemaInfo;
63
108
  }
64
- };
65
109
 
66
- Object.assign(SqliteDriver.prototype, {
67
- ...schema,
68
- Attribute,
69
- DataTypes,
70
- escape,
71
- escapeId,
72
- });
110
+ async createTable(table, attributes, opts = {}) {
111
+ const chunks = [ `CREATE TABLE ${escapeId(table)}` ];
112
+ const columns = Object.keys(attributes).map(name => {
113
+ const attribute = new this.constructor.Attribute(name, attributes[name]);
114
+ return attribute.toSqlString();
115
+ });
116
+ chunks.push(`(${columns.join(', ')})`);
117
+ await this.query(chunks.join(' '), [], opts);
118
+ }
119
+
120
+ async alterTable(table, changes) {
121
+ const chunks = [ `ALTER TABLE ${escapeId(table)}` ];
122
+ const attributes = Object.keys(changes).map(name => {
123
+ const options = changes[name];
124
+ if (options.remove) return { columnName: name, remove: true };
125
+ return new this.constructor.Attribute(name, changes[name]);
126
+ });
127
+
128
+ // SQLite doesn't support altering column attributes with MODIFY COLUMN and adding a PRIMARY KEY column
129
+ if (attributes.some(entry => entry.modify || entry.primaryKey)) {
130
+ await alterTableWithChangeColumn(this, table, attributes);
131
+ return;
132
+ }
133
+
134
+ // SQLite can only add one column a time
135
+ // - https://www.sqlite.org/lang_altertable.html
136
+ for (const attribute of attributes) {
137
+ if (attribute.remove) {
138
+ const { columnName } = attribute;
139
+ await this.query(chunks.concat(`DROP COLUMN ${this.escapeId(columnName)}`).join(' '));
140
+ } else {
141
+ await this.query(chunks.concat(`ADD COLUMN ${attribute.toSqlString()}`).join(' '));
142
+ }
143
+ }
144
+ }
145
+
146
+ async addColumn(table, name, params) {
147
+ const attribute = new this.constructor.Attribute(name, params);
148
+ const sql = heresql(`
149
+ ALTER TABLE ${escapeId(table)}
150
+ ADD COLUMN ${attribute.toSqlString()}
151
+ `);
152
+ await this.query(sql);
153
+ }
154
+
155
+ async changeColumn(table, name, params) {
156
+ const attribute = new this.Attribute(name, params);
157
+ const schemaInfo = await this.querySchemaInfo(null, table);
158
+ const columns = schemaInfo[table];
159
+
160
+ for (const entry of columns) {
161
+ if (entry.columnName === attribute.columnName) {
162
+ Object.assign(entry, attribute, { modify: true });
163
+ }
164
+ }
165
+
166
+ await this.alterTable(table, columns);
167
+ }
168
+
169
+ async removeColumn(table, name) {
170
+ const attribute = new this.Attribute(name);
171
+ attribute.remove = true;
172
+ const changes = [ attribute ];
173
+ await alterTableWithChangeColumn(this, table, changes);
174
+ }
175
+
176
+ /**
177
+ * SQLite has only got implicit table truncation.
178
+ * - https://sqlite.org/lang_delete.html#the_truncate_optimization
179
+ */
180
+ async truncateTable(table) {
181
+ await this.query(`DELETE FROM ${escapeId(table)}`);
182
+ }
183
+ };
73
184
 
74
185
  module.exports = SqliteDriver;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const spellbook = require('../abstract/spellbook');
3
+ const Spellbook = require('../abstract/spellbook');
4
4
 
5
5
  function renameSelectExpr(spell) {
6
6
  const { Model, columns, joins, groups } = spell;
@@ -40,14 +40,14 @@ function renameSelectExpr(spell) {
40
40
  }
41
41
  }
42
42
 
43
- module.exports = {
44
- ...spellbook,
45
-
43
+ class SQLiteSpellBook extends Spellbook {
46
44
  formatSelect(spell) {
47
45
  if (Object.keys(spell.joins).length > 0) {
48
46
  spell = spell.dup;
49
47
  renameSelectExpr(spell);
50
48
  }
51
- return spellbook.formatSelect(spell);
49
+ return super.formatSelect(spell);
52
50
  }
53
- };
51
+ }
52
+
53
+ module.exports = SQLiteSpellBook;
@@ -1,6 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const SqlString = require('sqlstring');
4
+ const debug = require('debug')('leoric');
5
+
6
+ const { parseExpr } = require('../../expr');
7
+ const { heresql } = require('../../utils/string');
4
8
 
5
9
  exports.escape = function escape(value) {
6
10
  if (typeof value === 'boolean') return +value;
@@ -10,3 +14,87 @@ exports.escape = function escape(value) {
10
14
  exports.escapeId = function escapeId(identifier) {
11
15
  return `"${identifier.replace(/"/g, '""')}"`;
12
16
  };
17
+
18
+ /**
19
+ * Schema altering commands other than RENAME COLUMN or ADD COLUMN
20
+ * - https://www.sqlite.org/lang_altertable.html
21
+ * @param {string} table
22
+ * @param {Object} attributes the changed attributes
23
+ */
24
+ exports.alterTableWithChangeColumn = async function alterTableWithChangeColumn(driver, table, changes) {
25
+ const { escapeId } = driver;
26
+ const schemaInfo = await driver.querySchemaInfo(null, table);
27
+ const columns = schemaInfo[table];
28
+
29
+ const changeMap = changes.reduce((result, entry) => {
30
+ result[entry.columnName] = entry;
31
+ return result;
32
+ }, {});
33
+
34
+ const newAttributes = [];
35
+ for (const column of columns) {
36
+ const { columnName } = column;
37
+ const change = changeMap[columnName];
38
+ if (!change || !change.remove) {
39
+ newAttributes.push(Object.assign(column, change));
40
+ }
41
+ }
42
+
43
+ for (const attribute of changes) {
44
+ if (!attribute.modify && !attribute.remove) {
45
+ newAttributes.push(attribute);
46
+ }
47
+ }
48
+
49
+ const newColumns = [];
50
+ for (const attribute of newAttributes) {
51
+ const { columnName, defaultValue } = attribute;
52
+ const change = changeMap[columnName];
53
+ if (!change || change.modify) {
54
+ newColumns.push(escapeId(columnName));
55
+ } else {
56
+ newColumns.push(SqlString.escape(defaultValue));
57
+ }
58
+ }
59
+
60
+ const connection = await driver.getConnection();
61
+ await connection.query('BEGIN');
62
+ try {
63
+ const newTable = `new_${table}`;
64
+ await driver.createTable(newTable, newAttributes, { connection });
65
+ await connection.query(heresql(`
66
+ INSERT INTO ${escapeId(newTable)}
67
+ SELECT ${newColumns.join(', ')}
68
+ FROM ${escapeId(table)}
69
+ `));
70
+ await connection.query(`DROP TABLE ${escapeId(table)}`);
71
+ await connection.query(heresql(`
72
+ ALTER TABLE ${escapeId(newTable)}
73
+ RENAME TO ${escapeId(table)}
74
+ `));
75
+ await connection.query('COMMIT');
76
+ } catch (err) {
77
+ await connection.query('ROLLBACK');
78
+ throw err;
79
+ } finally {
80
+ await connection.release();
81
+ }
82
+ };
83
+
84
+ // eslint-disable-next-line no-unused-vars
85
+ exports.parseDefaultValue = function parseDefaultValue(text, type) {
86
+ if (typeof text !== 'string') return text;
87
+ if (type === 'boolean') return text === 'true';
88
+
89
+ try {
90
+ const ast = parseExpr(text);
91
+ if (ast.type === 'literal') {
92
+ return ast.value;
93
+ }
94
+ } catch (err) {
95
+ debug('[parseDefaultValue] [%s] %s', text, err);
96
+ }
97
+
98
+ return text;
99
+ };
100
+
package/src/hint.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { isDeepStrictEqual, format } = require('util');
3
+ const { format } = require('util');
4
+ const isDeepStrictEqual = require('deep-equal');
4
5
  const { isPlainObject } = require('./utils');
5
6
 
6
7
  /**
package/src/realm.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs').promises;
4
4
  const path = require('path');
5
5
 
6
6
  const Bone = require('./bone');
7
- const { findDriver } = require('./drivers');
7
+ const { findDriver, AbstractDriver } = require('./drivers');
8
8
  const { camelCase } = require('./utils/string');
9
9
  const sequelize = require('./adapters/sequelize');
10
10
  const Raw = require('./raw');
@@ -97,7 +97,7 @@ const rReplacementKey = /\s:(\w+)\b/g;
97
97
 
98
98
  class Realm {
99
99
  constructor(opts = {}) {
100
- const { client, dialect, database, ...restOpts } = {
100
+ let { client, dialect, database, driver: CustomDriver, ...restOpts } = {
101
101
  dialect: 'mysql',
102
102
  database: opts.db || opts.storage,
103
103
  ...opts
@@ -109,16 +109,18 @@ class Realm {
109
109
  for (const model of opts.models) models[model.name] = model;
110
110
  }
111
111
 
112
- const driver = new (findDriver(dialect))({
112
+ const DriverClass = CustomDriver && CustomDriver.prototype instanceof AbstractDriver? CustomDriver : findDriver(dialect);
113
+ const driver = new DriverClass({
113
114
  client,
114
115
  database,
115
- ...restOpts
116
+ ...restOpts,
116
117
  });
117
118
 
118
119
  const options = {
119
120
  client,
120
- dialect,
121
+ dialect: driver.dialect,
121
122
  database,
123
+ driver: CustomDriver,
122
124
  ...restOpts,
123
125
  define: { underscored: true, ...opts.define },
124
126
  };
@@ -158,6 +160,12 @@ class Realm {
158
160
  return this.Bone;
159
161
  }
160
162
 
163
+ async disconnect(callback) {
164
+ if (this.connected && this.driver) {
165
+ return await this.driver.disconnect(callback);
166
+ }
167
+ }
168
+
161
169
  async sync(options) {
162
170
  if (!this.connected) await this.connect();
163
171
  const { models } = this;
package/src/spell.js CHANGED
@@ -450,10 +450,8 @@ class Spell {
450
450
  }
451
451
 
452
452
  async ignite() {
453
- const { Model, command, laters } = this;
454
- const { sql, values } = Model.driver.format(this);
455
- const query = { sql, nestTables: command === 'select' };
456
- let result = await Model.driver.query(query, values, this);
453
+ const { Model, laters } = this;
454
+ let result = await Model.driver.cast(this);
457
455
  result = { ...result, spell: this };
458
456
  for (const later of laters) {
459
457
  result = await later(result);
@@ -18,6 +18,9 @@ function invokable(DataType) {
18
18
 
19
19
  // INTEGER.UNSIGNED
20
20
  get(target, p) {
21
+ // ref: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/length
22
+ // The length property indicates the number of parameters expected by the function.
23
+ // invokable INTEGER.toSqlString() will default to return "INTEGER(1)"
21
24
  return target.hasOwnProperty(p) ? target[p] : new target()[p];
22
25
  }
23
26
  });
@@ -1,65 +1,68 @@
1
1
  type LENGTH_VARIANTS = 'tiny' | '' | 'medium' | 'long';
2
2
 
3
- interface INVOKABLE<T> {
4
- (length: LENGTH_VARIANTS): T;
5
- (length: number): T;
3
+ interface INVOKABLE<T> extends DataType {
4
+ (dataLength?: LENGTH_VARIANTS): T;
5
+ (dataLength?: number): T;
6
6
  }
7
7
 
8
8
  export default class DataType {
9
9
  toSqlString(): string;
10
10
 
11
- static STRING: typeof STRING & INVOKABLE<STRING>;
12
- static INTEGER: typeof INTEGER & INVOKABLE<INTEGER>;
13
- static BIGINT: typeof BIGINT & INVOKABLE<BIGINT>;
14
- static DECIMAL: typeof DECIMAL & INVOKABLE<DECIMAL>;
15
- static TEXT: typeof TEXT & INVOKABLE<TEXT>;
16
- static BLOB: typeof BLOB & INVOKABLE<BLOB>;
17
- static JSON: typeof JSON & INVOKABLE<JSON>;
18
- static JSONB: typeof JSONB & INVOKABLE<JSONB>;
19
- static BINARY: typeof BINARY & INVOKABLE<BINARY>;
20
- static VARBINARY: typeof VARBINARY & INVOKABLE<VARBINARY>;
21
- static DATE: typeof DATE & INVOKABLE<DATE>;
22
- static DATEONLY: typeof DATEONLY & INVOKABLE<DATEONLY>;
23
- static BOOLEAN: typeof BOOLEAN & INVOKABLE<BOOLEAN>;
24
- static VIRTUAL: typeof VIRTUAL & INVOKABLE<VIRTUAL>;
11
+ static STRING: INVOKABLE<STRING>;
12
+ static INTEGER: INTEGER & INVOKABLE<INTEGER>;
13
+ static BIGINT: BIGINT & INVOKABLE<BIGINT>;
14
+ static DECIMAL: DECIMAL & INVOKABLE<DECIMAL>;
15
+ static TEXT: INVOKABLE<TEXT>;
16
+ static BLOB: INVOKABLE<BLOB>;
17
+ static JSON: JSON;
18
+ static JSONB: JSONB;
19
+ static BINARY: BINARY & INVOKABLE<BINARY>;
20
+ static VARBINARY: VARBINARY & INVOKABLE<VARBINARY>;
21
+ static DATE: DATE & INVOKABLE<DATE>;
22
+ static DATEONLY: DATEONLY;
23
+ static BOOLEAN: BOOLEAN;
24
+ static VIRTUAL: VIRTUAL;
25
25
 
26
26
  }
27
27
 
28
28
  declare class STRING extends DataType {
29
29
  dataType: 'varchar';
30
- length: number;
31
- constructor(length: number);
30
+ dataLength: number;
31
+ constructor(dataLength: number);
32
32
  }
33
33
 
34
34
  declare class INTEGER extends DataType {
35
35
  dataType: 'integer' | 'bigint' | 'decimal';
36
- length: number;
37
- constructor(length: number);
38
- get UNSIGNED(): this;
39
- get ZEROFILL(): this;
36
+ dataLength: number;
37
+ constructor(dataLength: number);
38
+ // avoid INTEGER.UNSIGNED.ZEROFILL.UNSIGNED.UNSIGNED
39
+ get UNSIGNED(): Omit<this, 'UNSIGNED' | 'ZEROFILL'>;
40
+ get ZEROFILL(): Omit<this, 'UNSIGNED' | 'ZEROFILL'>;
40
41
  }
41
42
 
42
43
  declare class BIGINT extends INTEGER {
43
44
  dataType: 'bigint';
44
45
  }
45
46
 
46
- declare class DECIMAL extends INTEGER {
47
+ declare class DECIMAL_INNER extends INTEGER {
47
48
  dataType: 'decimal';
48
49
  precision: number;
49
50
  scale: number;
50
51
  constructor(precision: number, scale: number);
51
52
  }
52
53
 
54
+ declare type DECIMAL = Omit<DECIMAL_INNER, 'dataLength'>;
55
+
53
56
  declare class TEXT extends DataType {
54
57
  dataType: 'text';
55
- length: LENGTH_VARIANTS;
56
- constructor(length: LENGTH_VARIANTS);
58
+ dataLength: LENGTH_VARIANTS;
59
+ constructor(dataLength: LENGTH_VARIANTS);
57
60
  }
58
61
 
59
62
  declare class BLOB extends DataType {
60
63
  dataType: 'blob';
61
- length: LENGTH_VARIANTS;
62
- constructor(length: LENGTH_VARIANTS)
64
+ dataLength: LENGTH_VARIANTS;
65
+ constructor(dataLength: LENGTH_VARIANTS)
63
66
  }
64
67
 
65
68
  declare class JSON extends DataType {
@@ -72,14 +75,14 @@ declare class JSONB extends JSON {
72
75
 
73
76
  declare class BINARY extends DataType {
74
77
  dataType: 'binary';
75
- length: number;
76
- constructor(length: number);
78
+ dataLength: number;
79
+ constructor(dataLength: number);
77
80
  }
78
81
 
79
82
  declare class VARBINARY extends DataType {
80
83
  dataType: 'varbinary';
81
- length: number;
82
- constructor(length: number);
84
+ dataLength: number;
85
+ constructor(dataLength: number);
83
86
  }
84
87
 
85
88
  declare class DATE extends DataType {
@@ -0,0 +1,96 @@
1
+ export class Hint {
2
+ static build(hint: Hint | { index: string } | string): typeof Hint;
3
+
4
+ constructor(text: string);
5
+
6
+ set text(value: string);
7
+
8
+ get text(): string;
9
+
10
+ /**
11
+ *
12
+ * @param {Hint} hint
13
+ * @returns {boolean}
14
+ * @memberof Hint
15
+ */
16
+ isEqual(hint: Hint): boolean;
17
+
18
+ toSqlString(): string;
19
+ }
20
+
21
+ /**
22
+ * @enum
23
+ */
24
+ export enum INDEX_HINT_TYPE {
25
+ use = 'use',
26
+ force = 'force',
27
+ ignore = 'ignore'
28
+ }
29
+
30
+ /**
31
+ * @enum
32
+ */
33
+ export enum INDEX_HINT_SCOPE {
34
+ join = 'join',
35
+ orderBy = 'order by',
36
+ groupBy = 'group by',
37
+ }
38
+
39
+ export class IndexHint {
40
+ /**
41
+ * build index hint
42
+ *
43
+ * @static
44
+ * @param {object | string} obj
45
+ * @param {string} indexHintType
46
+ * @returns {IndexHint}
47
+ * @example
48
+ * build('idx_title')
49
+ * build('idx_title', INDEX_HINT_TYPE.force, INDEX_HINT_SCOPE.groupBy)
50
+ * build({
51
+ * index: 'idx_title',
52
+ * type: INDEX_HINT_TYPE.ignore,
53
+ * scope: INDEX_HINT_SCOPE.groupBy,
54
+ * })
55
+ */
56
+ static build(hint: string | Array<string> | { index: string, type?: INDEX_HINT_TYPE, scope?: INDEX_HINT_SCOPE }, type?: INDEX_HINT_TYPE, scope?: INDEX_HINT_SCOPE): IndexHint;
57
+
58
+ /**
59
+ * Creates an instance of IndexHint.
60
+ * @param {Array<string> | string} index
61
+ * @param {INDEX_HINT_TYPE} type
62
+ * @param {INDEX_HINT_SCOPE?} scope
63
+ * @memberof IndexHint
64
+ */
65
+ constructor(index: string, type?: INDEX_HINT_TYPE, scope?: INDEX_HINT_SCOPE);
66
+
67
+ set index(values: string | Array<string>);
68
+
69
+ get index(): Array<string>;
70
+
71
+ set type(value: string);
72
+
73
+ get type(): string;
74
+
75
+ set scope(value: string);
76
+
77
+ get scope(): string;
78
+
79
+ toSqlString(): string;
80
+
81
+ /**
82
+ *
83
+ * @param {IndexHint} hint
84
+ * @returns {boolean}
85
+ * @memberof IndexHint
86
+ */
87
+ isEqual(hint: IndexHint): boolean;
88
+
89
+ /**
90
+ * @static
91
+ * @param {IndexHint} hints
92
+ * @returns {Array<IndexHint>}
93
+ * @memberof IndexHint
94
+ */
95
+ static merge(hints: Array<IndexHint>): Array<IndexHint>;
96
+ }