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.
- package/History.md +31 -0
- package/Readme.md +1 -1
- package/index.js +14 -0
- package/package.json +3 -1
- package/src/bone.js +11 -11
- package/src/constants.js +8 -1
- package/src/data_types.js +32 -32
- package/src/drivers/abstract/attribute.js +1 -1
- package/src/drivers/abstract/index.js +201 -1
- package/src/drivers/abstract/spellbook.js +403 -412
- package/src/drivers/index.js +15 -4
- package/src/drivers/mysql/index.js +101 -10
- package/src/drivers/mysql/spellbook.js +13 -11
- package/src/drivers/postgres/data_types.js +2 -2
- package/src/drivers/postgres/index.js +103 -109
- package/src/drivers/postgres/spellbook.js +9 -9
- package/src/drivers/postgres/sqlstring.js +124 -0
- package/src/drivers/sqlite/data_types.js +9 -9
- package/src/drivers/sqlite/index.js +124 -13
- package/src/drivers/sqlite/spellbook.js +6 -6
- package/src/drivers/sqlite/sqlstring.js +88 -0
- package/src/hint.js +2 -1
- package/src/realm.js +13 -5
- package/src/spell.js +2 -4
- package/src/utils/invokable.js +3 -0
- package/types/data_types.d.ts +35 -32
- package/types/hint.d.ts +96 -0
- package/types/index.d.ts +266 -26
- package/src/drivers/abstract/schema.js +0 -143
- package/src/drivers/mysql/schema.js +0 -98
- package/src/drivers/postgres/schema.js +0 -125
- package/src/drivers/sqlite/schema.js +0 -211
package/src/drivers/index.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const MysqlDriver = require('./mysql');
|
|
4
|
+
const PostgresDriver = require('./postgres');
|
|
5
|
+
const SqliteDriver = require('./sqlite');
|
|
6
|
+
const AbstractDriver = require('./abstract');
|
|
7
|
+
|
|
3
8
|
function findDriver(dialect) {
|
|
4
9
|
switch (dialect) {
|
|
5
10
|
case 'mysql':
|
|
6
|
-
return
|
|
11
|
+
return MysqlDriver;
|
|
7
12
|
case 'pg':
|
|
8
13
|
case 'postgres':
|
|
9
|
-
return
|
|
14
|
+
return PostgresDriver;
|
|
10
15
|
case 'sqlite':
|
|
11
16
|
case 'sqlite3':
|
|
12
|
-
return
|
|
17
|
+
return SqliteDriver;
|
|
13
18
|
default:
|
|
14
19
|
throw new Error(`Unsupported database ${dialect}`);
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
module.exports = {
|
|
23
|
+
module.exports = {
|
|
24
|
+
findDriver,
|
|
25
|
+
MysqlDriver,
|
|
26
|
+
PostgresDriver,
|
|
27
|
+
SqliteDriver,
|
|
28
|
+
AbstractDriver,
|
|
29
|
+
};
|
|
@@ -5,11 +5,17 @@ const { performance } = require('perf_hooks');
|
|
|
5
5
|
const AbstractDriver = require('../abstract');
|
|
6
6
|
const Attribute = require('./attribute');
|
|
7
7
|
const DataTypes = require('./data_types');
|
|
8
|
-
const
|
|
9
|
-
const schema = require('./schema');
|
|
8
|
+
const Spellbook = require('./spellbook');
|
|
10
9
|
const { calculateDuration } = require('../../utils');
|
|
10
|
+
const { heresql } = require('../../utils/string');
|
|
11
11
|
|
|
12
12
|
class MysqlDriver extends AbstractDriver {
|
|
13
|
+
|
|
14
|
+
// define static properties as this way IDE will prompt
|
|
15
|
+
static Spellbook = Spellbook;
|
|
16
|
+
static Attribute = Attribute;
|
|
17
|
+
static DataTypes = DataTypes;
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Create a connection pool
|
|
15
21
|
* @param {string} clientName
|
|
@@ -29,11 +35,12 @@ class MysqlDriver extends AbstractDriver {
|
|
|
29
35
|
super(opts);
|
|
30
36
|
this.type = 'mysql';
|
|
31
37
|
this.pool = this.createPool(opts);
|
|
32
|
-
this.
|
|
33
|
-
|
|
38
|
+
this.Attribute = this.constructor.Attribute;
|
|
39
|
+
this.DataTypes = this.constructor.DataTypes;
|
|
40
|
+
this.spellbook = new this.constructor.Spellbook();
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
this.escape = this.pool.escape.bind(this.pool);
|
|
43
|
+
this.escapeId = this.pool.escapeId;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
createPool(opts) {
|
|
@@ -105,11 +112,95 @@ class MysqlDriver extends AbstractDriver {
|
|
|
105
112
|
return results;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Fetch columns of give tables from database
|
|
118
|
+
* - https://dev.mysql.com/doc/mysql-infoschema-excerpt/5.6/en/columns-table.html
|
|
119
|
+
* @param {string} database
|
|
120
|
+
* @param {string|string[]} tables
|
|
121
|
+
*/
|
|
122
|
+
async querySchemaInfo(database, tables) {
|
|
123
|
+
tables = [].concat(tables);
|
|
124
|
+
const sql = heresql(`
|
|
125
|
+
SELECT table_name, column_name, column_type, data_type, is_nullable,
|
|
126
|
+
column_default, column_key, column_comment,
|
|
127
|
+
datetime_precision
|
|
128
|
+
FROM information_schema.columns
|
|
129
|
+
WHERE table_schema = ? AND table_name in (?)
|
|
130
|
+
ORDER BY table_name, column_name
|
|
131
|
+
`);
|
|
132
|
+
|
|
133
|
+
const { rows } = await this.query(sql, [ database, tables ]);
|
|
134
|
+
const schemaInfo = {};
|
|
135
|
+
|
|
136
|
+
for (const entry of rows) {
|
|
137
|
+
// make sure the column names are in lower case
|
|
138
|
+
const row = Object.keys(entry).reduce((obj, name) => {
|
|
139
|
+
obj[name.toLowerCase()] = entry[name];
|
|
140
|
+
return obj;
|
|
141
|
+
}, {});
|
|
142
|
+
const tabelName = row.table_name;
|
|
143
|
+
const columns = schemaInfo[tabelName] || (schemaInfo[tabelName] = []);
|
|
144
|
+
columns.push({
|
|
145
|
+
columnName: row.column_name,
|
|
146
|
+
columnType: row.column_type,
|
|
147
|
+
comment: row.column_comment,
|
|
148
|
+
defaultValue: row.column_default,
|
|
149
|
+
dataType: row.data_type,
|
|
150
|
+
allowNull: row.is_nullable === 'YES',
|
|
151
|
+
primaryKey: row.column_key == 'PRI',
|
|
152
|
+
unique: row.column_key == 'PRI' || row.column_key == 'UNI',
|
|
153
|
+
datetimePrecision: row.datetime_precision,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return schemaInfo;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Rename column with SQL that works on older versions of MySQL
|
|
162
|
+
* - https://dev.mysql.com/doc/refman/5.7/en/alter-table.html
|
|
163
|
+
* - https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
|
|
164
|
+
* @param {string} table
|
|
165
|
+
* @param {string} column the old column name
|
|
166
|
+
* @param {string} newColumn the new column name
|
|
167
|
+
*/
|
|
168
|
+
async renameColumn(table, name, newName) {
|
|
169
|
+
const { escapeId } = this;
|
|
170
|
+
const { database } = this.options;
|
|
171
|
+
const { columnName } = new this.Attribute(name);
|
|
172
|
+
const schemaInfo = await this.querySchemaInfo(database, table);
|
|
173
|
+
const { columnName: _, ...columnInfo } = schemaInfo[table].find(entry => {
|
|
174
|
+
return entry.columnName == columnName;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!columnInfo) {
|
|
178
|
+
throw new Error(`Unable to find column ${table}.${columnName}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const attribute = new this.Attribute(newName, columnInfo);
|
|
182
|
+
const sql = heresql(`
|
|
183
|
+
ALTER TABLE ${escapeId(table)}
|
|
184
|
+
CHANGE COLUMN ${escapeId(columnName)} ${attribute.toSqlString()}
|
|
185
|
+
`);
|
|
186
|
+
await this.query(sql);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async describeTable(table) {
|
|
190
|
+
const { escapeId } = this;
|
|
191
|
+
const { rows } = await this.query(`DESCRIBE ${escapeId(table)}`);
|
|
192
|
+
const result = {};
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
result[row.Field] = {
|
|
195
|
+
columnName: row.Field,
|
|
196
|
+
columnType: row.Type,
|
|
197
|
+
allowNull: row.Null === 'YES',
|
|
198
|
+
defaultValue: row.Default,
|
|
199
|
+
autoIncrement: row.Extra === 'auto_increment',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
110
203
|
}
|
|
111
204
|
}
|
|
112
205
|
|
|
113
|
-
Object.assign(MysqlDriver.prototype, { ...schema, Attribute, DataTypes });
|
|
114
|
-
|
|
115
206
|
module.exports = MysqlDriver;
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const { Hint, IndexHint } = require('../../hint');
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const Spellbook = require('../abstract/spellbook');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
...spellbook,
|
|
9
|
+
class MySQLSpellBook extends Spellbook {
|
|
11
10
|
|
|
12
11
|
formatOptimizerHints(spell) {
|
|
13
12
|
const optimizerHints = spell.hints.filter(hint => hint instanceof Hint);
|
|
@@ -16,7 +15,7 @@ module.exports = {
|
|
|
16
15
|
return `/*+ ${hints} */`;
|
|
17
16
|
}
|
|
18
17
|
return '';
|
|
19
|
-
}
|
|
18
|
+
}
|
|
20
19
|
|
|
21
20
|
formatIndexHints(spell) {
|
|
22
21
|
const indexHints = spell.hints.filter(hint => hint instanceof IndexHint);
|
|
@@ -25,7 +24,7 @@ module.exports = {
|
|
|
25
24
|
return hints.map(hint => hint.toSqlString()).join(' ');
|
|
26
25
|
}
|
|
27
26
|
return '';
|
|
28
|
-
}
|
|
27
|
+
}
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* INSERT ... ON DUPLICATE KEY UPDATE
|
|
@@ -56,11 +55,11 @@ module.exports = {
|
|
|
56
55
|
sets.push(...columns.map(column => `${escapeId(column)}=VALUES(${escapeId(column)})`));
|
|
57
56
|
|
|
58
57
|
return `ON DUPLICATE KEY UPDATE ${sets.join(', ')}`;
|
|
59
|
-
}
|
|
58
|
+
}
|
|
60
59
|
|
|
61
60
|
formatReturning() {
|
|
62
61
|
return '';
|
|
63
|
-
}
|
|
62
|
+
}
|
|
64
63
|
|
|
65
64
|
/**
|
|
66
65
|
* UPDATE ... ORDER BY ... LIMIT ${rowCount}
|
|
@@ -68,7 +67,7 @@ module.exports = {
|
|
|
68
67
|
* @param {Spell} spell
|
|
69
68
|
*/
|
|
70
69
|
formatUpdate(spell) {
|
|
71
|
-
const result =
|
|
70
|
+
const result = super.formatUpdate(spell);
|
|
72
71
|
const { rowCount, orders } = spell;
|
|
73
72
|
const chunks = [];
|
|
74
73
|
|
|
@@ -77,13 +76,14 @@ module.exports = {
|
|
|
77
76
|
if (chunks.length > 0) result.sql += ` ${chunks.join(' ')}`;
|
|
78
77
|
|
|
79
78
|
return result;
|
|
80
|
-
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
81
|
/**
|
|
82
82
|
* DELETE ... ORDER BY ...LIMIT
|
|
83
83
|
* @param {Spell} spell
|
|
84
84
|
*/
|
|
85
85
|
formatDelete(spell) {
|
|
86
|
-
const result =
|
|
86
|
+
const result = super.formatDelete(spell);
|
|
87
87
|
const { rowCount, orders } = spell;
|
|
88
88
|
const chunks = [];
|
|
89
89
|
|
|
@@ -93,4 +93,6 @@ module.exports = {
|
|
|
93
93
|
|
|
94
94
|
return result;
|
|
95
95
|
}
|
|
96
|
-
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = MySQLSpellBook;
|
|
@@ -1,121 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { Pool } = require('pg');
|
|
4
3
|
const { performance } = require('perf_hooks');
|
|
5
4
|
|
|
6
5
|
const AbstractDriver = require('../abstract');
|
|
7
6
|
const Attribute = require('./attribute');
|
|
8
7
|
const DataTypes = require('./data_types');
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The actual column type can be found by mapping the `oid` (which is called `dataTypeID`) in the `RowDescription`.
|
|
16
|
-
* - https://stackoverflow.com/questions/11829368/how-can-i-see-the-postgresql-column-type-from-a-rowdescription-message
|
|
17
|
-
* - https://www.postgresql.org/docs/8.4/static/catalog-pg-type.html
|
|
18
|
-
* - https://www.postgresql.org/docs/9.1/static/protocol-message-formats.html
|
|
19
|
-
* - https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
|
|
20
|
-
*/
|
|
21
|
-
const pgType = {
|
|
22
|
-
20: { oid: 20, typname: 'int8', type: Number }
|
|
23
|
-
};
|
|
8
|
+
const {
|
|
9
|
+
escape, escapeId, formatAddColumn,
|
|
10
|
+
formatAlterColumns, formatDropColumn,
|
|
11
|
+
cast, nest, parameterize,
|
|
12
|
+
} = require('./sqlstring');
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* @param {Object} field
|
|
29
|
-
*/
|
|
30
|
-
function cast(value, field) {
|
|
31
|
-
const opts = pgType[field.dataTypeID];
|
|
32
|
-
|
|
33
|
-
if (opts) {
|
|
34
|
-
try {
|
|
35
|
-
return value == null ? null : opts.type(value);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
throw new Error('unable to cast %s to type %s', value, opts);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Postgres returns a nested array when `rowMode` is `array`:
|
|
45
|
-
*
|
|
46
|
-
* [ [ 1, 'New Post' ] ]
|
|
47
|
-
*
|
|
48
|
-
* the corresponding fields would be:
|
|
49
|
-
*
|
|
50
|
-
* [ { tableID: 15629, dataTypeID: 20, name: 'id' },
|
|
51
|
-
* { tableID: 15629, dataTypeID: 20, name: 'title' } ]
|
|
52
|
-
*
|
|
53
|
-
* This function is to turn above rows into nested objects with qualifers as keys:
|
|
54
|
-
*
|
|
55
|
-
* [ { articles: { id: 1, title: 'New Post' } } ]
|
|
56
|
-
*
|
|
57
|
-
* @param {Array} rows
|
|
58
|
-
* @param {Array} fields
|
|
59
|
-
* @param {Spell} spell
|
|
60
|
-
*/
|
|
61
|
-
function nest(rows, fields, spell) {
|
|
62
|
-
const results = [];
|
|
63
|
-
const qualifiers = [ spell.Model.tableAlias, ...Object.keys(spell.joins) ];
|
|
64
|
-
let defaultTableIndex = 0;
|
|
65
|
-
|
|
66
|
-
if (spell.groups.length > 0 && Object.keys(spell.joins).length > 0) {
|
|
67
|
-
defaultTableIndex = Infinity;
|
|
68
|
-
for (const token of spell.columns) {
|
|
69
|
-
if (token.type !== 'id') continue;
|
|
70
|
-
const index = qualifiers.indexOf(token.qualifiers[0]);
|
|
71
|
-
if (index >= 0 && index < defaultTableIndex) defaultTableIndex = index;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
14
|
+
const Spellbook = require('./spellbook');
|
|
15
|
+
const { calculateDuration } = require('../../utils');
|
|
16
|
+
const { heresql } = require('../../utils/string');
|
|
74
17
|
|
|
75
|
-
|
|
76
|
-
const result = {};
|
|
77
|
-
let tableIndex = defaultTableIndex;
|
|
78
|
-
let tableIDWas;
|
|
79
|
-
let qualifier;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < fields.length; i++) {
|
|
82
|
-
const { name, tableID } = fields[i];
|
|
83
|
-
if (tableID !== tableIDWas) {
|
|
84
|
-
qualifier = tableID === 0 ? '' : qualifiers[tableIndex++];
|
|
85
|
-
tableIDWas = tableID;
|
|
86
|
-
}
|
|
87
|
-
const obj = result[qualifier] || (result[qualifier] = {});
|
|
88
|
-
obj[name] = cast(row[i], fields[i]);
|
|
89
|
-
}
|
|
90
|
-
results.push(result);
|
|
91
|
-
}
|
|
18
|
+
class PostgresDriver extends AbstractDriver {
|
|
92
19
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* Postgres supports parameterized queries with `$i` slots.
|
|
98
|
-
* - https://node-postgres.com/features/queries#Parameterized%20query
|
|
99
|
-
* @param {string} sql
|
|
100
|
-
* @param {Array} values
|
|
101
|
-
*/
|
|
102
|
-
function parameterize(sql, values) {
|
|
103
|
-
let i = 0;
|
|
104
|
-
// starts with 1
|
|
105
|
-
const text = sql.replace(/\?/g, () => `$${++i}`);
|
|
106
|
-
return { text, values };
|
|
107
|
-
}
|
|
20
|
+
// define static properties as this way IDE will prompt
|
|
21
|
+
static Spellbook = Spellbook;
|
|
22
|
+
static Attribute = Attribute;
|
|
23
|
+
static DataTypes = DataTypes;
|
|
108
24
|
|
|
109
|
-
class PostgresDriver extends AbstractDriver {
|
|
110
25
|
constructor(opts = {}) {
|
|
111
26
|
super(opts);
|
|
112
27
|
this.type = 'postgres';
|
|
113
28
|
this.pool = this.createPool(opts);
|
|
29
|
+
this.Attribute = this.constructor.Attribute;
|
|
30
|
+
this.DataTypes = this.constructor.DataTypes;
|
|
31
|
+
this.spellbook = new this.constructor.Spellbook();
|
|
32
|
+
|
|
33
|
+
this.escape = escape;
|
|
34
|
+
this.escapeId = escapeId;
|
|
114
35
|
}
|
|
115
36
|
|
|
116
37
|
createPool(opts) {
|
|
117
38
|
const { host, port, user, password, database } = opts;
|
|
118
|
-
return new Pool({ host, port, user, password, database });
|
|
39
|
+
return new (require('pg')).Pool({ host, port, user, password, database });
|
|
119
40
|
}
|
|
120
41
|
|
|
121
42
|
async getConnection() {
|
|
@@ -180,17 +101,90 @@ class PostgresDriver extends AbstractDriver {
|
|
|
180
101
|
}
|
|
181
102
|
}
|
|
182
103
|
|
|
183
|
-
|
|
184
|
-
|
|
104
|
+
async querySchemaInfo(database, tables) {
|
|
105
|
+
tables = [].concat(tables);
|
|
106
|
+
const text = heresql(`
|
|
107
|
+
SELECT columns.*,
|
|
108
|
+
constraints.constraint_type
|
|
109
|
+
FROM information_schema.columns AS columns
|
|
110
|
+
LEFT JOIN information_schema.key_column_usage AS usage
|
|
111
|
+
ON columns.table_catalog = usage.table_catalog
|
|
112
|
+
AND columns.table_name = usage.table_name
|
|
113
|
+
AND columns.column_name = usage.column_name
|
|
114
|
+
LEFT JOIN information_schema.table_constraints AS constraints
|
|
115
|
+
ON usage.constraint_name = constraints.constraint_name
|
|
116
|
+
WHERE columns.table_catalog = $1 AND columns.table_name = ANY($2)
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
const { pool } = this;
|
|
120
|
+
const { rows } = await pool.query(text, [database, tables]);
|
|
121
|
+
const schemaInfo = {};
|
|
122
|
+
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
const tableName = row.table_name;
|
|
125
|
+
const columns = schemaInfo[tableName] || (schemaInfo[tableName] = []);
|
|
126
|
+
let { data_type: dataType, character_maximum_length: length } = row;
|
|
127
|
+
|
|
128
|
+
if (dataType === 'character varying') dataType = 'varchar';
|
|
129
|
+
if (dataType === 'timestamp without time zone') dataType = 'timestamp';
|
|
130
|
+
|
|
131
|
+
let columnDefault = row.column_default;
|
|
132
|
+
if (/^NULL::/i.test(columnDefault)) columnDefault = null;
|
|
133
|
+
if (dataType === 'boolean') columnDefault = columnDefault === 'true';
|
|
134
|
+
|
|
135
|
+
const primaryKey = row.constraint_type === 'PRIMARY KEY';
|
|
136
|
+
const precision = row.datetime_precision;
|
|
137
|
+
|
|
138
|
+
columns.push({
|
|
139
|
+
columnName: row.column_name,
|
|
140
|
+
columnType: length > 0 ? `${dataType}(${length})` : dataType,
|
|
141
|
+
defaultValue: primaryKey ? null : columnDefault,
|
|
142
|
+
dataType,
|
|
143
|
+
allowNull: row.is_nullable !== 'NO',
|
|
144
|
+
// https://www.postgresql.org/docs/9.5/infoschema-table-constraints.html
|
|
145
|
+
primaryKey,
|
|
146
|
+
unique: row.constraint_type === 'UNIQUE',
|
|
147
|
+
datetimePrecision: precision === 6 ? null : precision,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return schemaInfo;
|
|
185
152
|
}
|
|
186
|
-
};
|
|
187
153
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
154
|
+
async alterTable(table, changes) {
|
|
155
|
+
const chunks = [ `ALTER TABLE ${escapeId(table)}` ];
|
|
156
|
+
const actions = Object.keys(changes).map(name => {
|
|
157
|
+
const options = changes[name];
|
|
158
|
+
if (options.remove) return formatDropColumn(this, name);
|
|
159
|
+
const attribute = new this.Attribute(name, options);
|
|
160
|
+
const { columnName } = attribute;;
|
|
161
|
+
return attribute.modify
|
|
162
|
+
? formatAlterColumns(this, columnName, attribute).join(', ')
|
|
163
|
+
: formatAddColumn(this, columnName, attribute);
|
|
164
|
+
});
|
|
165
|
+
chunks.push(actions.join(', '));
|
|
166
|
+
await this.query(chunks.join(' '));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async changeColumn(table, column, params) {
|
|
170
|
+
const attribute = new this.Attribute(column, params);
|
|
171
|
+
const { columnName } = attribute;
|
|
172
|
+
const alterColumns = formatAlterColumns(this, columnName, attribute);
|
|
173
|
+
const sql = heresql(`ALTER TABLE ${escapeId(table)} ${alterColumns.join(', ')}`);
|
|
174
|
+
await this.query(sql);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Truncate table
|
|
179
|
+
* @param {string} table the name of the table to truncate
|
|
180
|
+
* @param {Object} [opts={}] extra truncation options
|
|
181
|
+
* @param {object} [opts.restartIdentity] restart sequences owned by the table
|
|
182
|
+
*/
|
|
183
|
+
async truncateTable(table, opts = {}) {
|
|
184
|
+
const chunks = [ `TRUNCATE TABLE ${escapeId(table)}` ];
|
|
185
|
+
if (opts.restartIdentity) chunks.push('RESTART IDENTITY');
|
|
186
|
+
await this.query(chunks.join(' '));
|
|
187
|
+
}
|
|
188
|
+
};
|
|
195
189
|
|
|
196
190
|
module.exports = PostgresDriver;
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
module.exports = {
|
|
6
|
-
...spellbook,
|
|
3
|
+
const Spellbook = require('../abstract/spellbook');
|
|
7
4
|
|
|
5
|
+
class PostgresSpellBook extends Spellbook {
|
|
8
6
|
formatInsert(spell) {
|
|
9
7
|
if (!spell.returning) spell.returning = true;
|
|
10
|
-
const { sql, values } =
|
|
8
|
+
const { sql, values } = super.formatInsert(spell);
|
|
11
9
|
return {
|
|
12
10
|
sql,
|
|
13
11
|
values,
|
|
14
12
|
};
|
|
15
|
-
}
|
|
13
|
+
};
|
|
16
14
|
|
|
17
15
|
formatUpsert(spell) {
|
|
18
16
|
if (!spell.returning) spell.returning = true;
|
|
19
|
-
const { sql, values } =
|
|
17
|
+
const { sql, values } = super.formatUpsert(spell);
|
|
20
18
|
return {
|
|
21
19
|
sql,
|
|
22
20
|
values,
|
|
23
21
|
};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = PostgresSpellBook;
|
|
@@ -10,3 +10,127 @@ exports.escapeId = function escapeId(identifier) {
|
|
|
10
10
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* - https://www.postgresql.org/docs/9.1/sql-altertable.html
|
|
15
|
+
* @param {string} columnName
|
|
16
|
+
* @param {Object} attribute
|
|
17
|
+
*/
|
|
18
|
+
exports.formatAlterColumns = function formatAlterColumns(driver, columnName, attribute) {
|
|
19
|
+
const { allowNull, type, defaultValue } = attribute;
|
|
20
|
+
const sets = [
|
|
21
|
+
`TYPE ${type.toSqlString()}`,
|
|
22
|
+
allowNull ? 'DROP NOT NULL' : 'SET NOT NULL',
|
|
23
|
+
defaultValue == null
|
|
24
|
+
? 'DROP DEFAULT'
|
|
25
|
+
: `SET DEFAULT ${SqlString.escape(defaultValue)}`,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
return sets.map(entry => `ALTER COLUMN ${driver.escapeId(columnName)} ${entry}`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.formatAddColumn = function formatAddColumn(driver, columnName, attribute) {
|
|
32
|
+
return `ADD COLUMN ${attribute.toSqlString()}`;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
exports.formatDropColumn = function formatDropColumn(driver, columnName) {
|
|
36
|
+
return `DROP COLUMN ${driver.escapeId(columnName)}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The actual column type can be found by mapping the `oid` (which is called `dataTypeID`) in the `RowDescription`.
|
|
41
|
+
* - https://stackoverflow.com/questions/11829368/how-can-i-see-the-postgresql-column-type-from-a-rowdescription-message
|
|
42
|
+
* - https://www.postgresql.org/docs/8.4/static/catalog-pg-type.html
|
|
43
|
+
* - https://www.postgresql.org/docs/9.1/static/protocol-message-formats.html
|
|
44
|
+
* - https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
|
|
45
|
+
*/
|
|
46
|
+
const pgType = {
|
|
47
|
+
20: { oid: 20, typname: 'int8', type: Number }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Postgres tend to keep the returned value as string, even if the column type specified is something like 8 byte int. Hence this function is necessary to find the actual column type, and cast the string to the correct type if possible.
|
|
52
|
+
* @param {*} value
|
|
53
|
+
* @param {Object} field
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
function cast(value, field) {
|
|
57
|
+
const opts = pgType[field.dataTypeID];
|
|
58
|
+
|
|
59
|
+
if (opts) {
|
|
60
|
+
try {
|
|
61
|
+
return value == null ? null : opts.type(value);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new Error('unable to cast %s to type %s', value, opts);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
exports.cast = cast;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Postgres returns a nested array when `rowMode` is `array`:
|
|
73
|
+
*
|
|
74
|
+
* [ [ 1, 'New Post' ] ]
|
|
75
|
+
*
|
|
76
|
+
* the corresponding fields would be:
|
|
77
|
+
*
|
|
78
|
+
* [ { tableID: 15629, dataTypeID: 20, name: 'id' }
|
|
79
|
+
* { tableID: 15629, dataTypeID: 20, name: 'title' } ]
|
|
80
|
+
*
|
|
81
|
+
* This function is to turn above rows into nested objects with qualifers as keys:
|
|
82
|
+
*
|
|
83
|
+
* [ { articles: { id: 1, title: 'New Post' } } ]
|
|
84
|
+
*
|
|
85
|
+
* @param {Array} rows
|
|
86
|
+
* @param {Array} fields
|
|
87
|
+
* @param {Spell} spell
|
|
88
|
+
*/
|
|
89
|
+
exports.nest = function nest(rows, fields, spell) {
|
|
90
|
+
const results = [];
|
|
91
|
+
const qualifiers = [ spell.Model.tableAlias, ...Object.keys(spell.joins) ];
|
|
92
|
+
let defaultTableIndex = 0;
|
|
93
|
+
|
|
94
|
+
if (spell.groups.length > 0 && Object.keys(spell.joins).length > 0) {
|
|
95
|
+
defaultTableIndex = Infinity;
|
|
96
|
+
for (const token of spell.columns) {
|
|
97
|
+
if (token.type !== 'id') continue;
|
|
98
|
+
const index = qualifiers.indexOf(token.qualifiers[0]);
|
|
99
|
+
if (index >= 0 && index < defaultTableIndex) defaultTableIndex = index;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
const result = {};
|
|
105
|
+
let tableIndex = defaultTableIndex;
|
|
106
|
+
let tableIDWas;
|
|
107
|
+
let qualifier;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < fields.length; i++) {
|
|
110
|
+
const { name, tableID } = fields[i];
|
|
111
|
+
if (tableID !== tableIDWas) {
|
|
112
|
+
qualifier = tableID === 0 ? '' : qualifiers[tableIndex++];
|
|
113
|
+
tableIDWas = tableID;
|
|
114
|
+
}
|
|
115
|
+
const obj = result[qualifier] || (result[qualifier] = {});
|
|
116
|
+
obj[name] = cast(row[i], fields[i]);
|
|
117
|
+
}
|
|
118
|
+
results.push(result);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { rows: results, fields };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Postgres supports parameterized queries with `$i` slots.
|
|
126
|
+
* - https://node-postgres.com/features/queries#Parameterized%20query
|
|
127
|
+
* @param {string} sql
|
|
128
|
+
* @param {Array} values
|
|
129
|
+
*/
|
|
130
|
+
exports.parameterize = function parameterize(sql, values) {
|
|
131
|
+
let i = 0;
|
|
132
|
+
// starts with 1
|
|
133
|
+
const text = sql.replace(/\?/g, () => `$${++i}`);
|
|
134
|
+
return { text, values };
|
|
135
|
+
};
|
|
136
|
+
|