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.
@@ -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 require('./mysql');
11
+ return MysqlDriver;
7
12
  case 'pg':
8
13
  case 'postgres':
9
- return require('./postgres');
14
+ return PostgresDriver;
10
15
  case 'sqlite':
11
16
  case 'sqlite3':
12
- return require('./sqlite');
17
+ return SqliteDriver;
13
18
  default:
14
19
  throw new Error(`Unsupported database ${dialect}`);
15
20
  }
16
21
  }
17
22
 
18
- module.exports = { findDriver };
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 spellbook = require('./spellbook');
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.escape = this.pool.escape.bind(this.pool);
33
- }
38
+ this.Attribute = this.constructor.Attribute;
39
+ this.DataTypes = this.constructor.DataTypes;
40
+ this.spellbook = new this.constructor.Spellbook();
34
41
 
35
- get escapeId() {
36
- return this.pool.escapeId;
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
- format(spell) {
109
- return spellbook.format(spell);
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 spellbook = require('../abstract/spellbook');
7
+ const Spellbook = require('../abstract/spellbook');
8
8
 
9
- module.exports = {
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 = spellbook.formatUpdate.call(this, spell);
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 = spellbook.formatDelete.call(this, spell);
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;
@@ -41,8 +41,8 @@ class Postgres_BINARY extends DataTypes {
41
41
  }
42
42
 
43
43
  class Postgres_INTEGER extends DataTypes.INTEGER {
44
- constructor(length) {
45
- super(length);
44
+ constructor(dataLength) {
45
+ super(dataLength);
46
46
  }
47
47
 
48
48
  uncast(value) {
@@ -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 { escape, escapeId } = require('./sqlstring');
10
- const spellbook = require('./spellbook');
11
- const schema = require('./schema');
12
- const { calculateDuration } = require('../../utils');
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
- * 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.
27
- * @param {*} value
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
- for (const row of rows) {
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
- return { rows: results, fields };
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
- format(spell) {
184
- return spellbook.format(spell);
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
- Object.assign(PostgresDriver.prototype, {
189
- ...schema,
190
- Attribute,
191
- DataTypes,
192
- escape,
193
- escapeId,
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 spellbook = require('../abstract/spellbook');
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 } = spellbook.formatInsert(spell);
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 } = spellbook.formatUpsert(spell);
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
+