knex 3.2.9 → 3.3.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.
@@ -17,16 +17,10 @@ const isPlainObjectOrArray = (value) =>
17
17
  class QueryCompiler_MySQL extends QueryCompiler {
18
18
  constructor(client, builder, formatter) {
19
19
  super(client, builder, formatter);
20
-
21
- const { returning } = this.single;
22
- if (returning) {
23
- this.client.logger.warn(
24
- '.returning() is not supported by mysql and will not have any effect.'
25
- );
26
- }
27
-
20
+ this._returningCheck();
28
21
  this._emptyInsertValue = '() values ()';
29
22
  }
23
+
30
24
  // Compiles a `delete` query, allowing comments and LIMIT.
31
25
  del() {
32
26
  const sql = super.del();
@@ -288,6 +282,15 @@ class QueryCompiler_MySQL extends QueryCompiler {
288
282
  onJsonPathEquals(clause) {
289
283
  return this._onJsonPathEquals('json_extract', clause);
290
284
  }
285
+
286
+ _returningCheck() {
287
+ const { returning } = this.single;
288
+ if (returning) {
289
+ this.client.logger.warn(
290
+ '.returning() is not supported by mysql and will not have any effect.'
291
+ );
292
+ }
293
+ }
291
294
  }
292
295
 
293
296
  // MySQL supports LIMIT on single-table DELETE statements.
@@ -34,6 +34,13 @@ class Client_MySQL2 extends Client_MySQL {
34
34
  }
35
35
  }
36
36
 
37
+ // Support both mysql2.createPool() and mysql2/promise.createPool().
38
+ // The promise variant wraps a callback pool and exposes it as .pool.
39
+ wrapNativePool(nativePool) {
40
+ const callbackPool = nativePool.pool || nativePool;
41
+ return super.wrapNativePool(callbackPool);
42
+ }
43
+
37
44
  validateConnection(connection) {
38
45
  return (
39
46
  connection &&
@@ -4,6 +4,7 @@ const each = require('lodash/each');
4
4
  const flatten = require('lodash/flatten');
5
5
  const isEmpty = require('lodash/isEmpty');
6
6
  const map = require('lodash/map');
7
+ const uniqueId = require('lodash/uniqueId');
7
8
 
8
9
  const Formatter = require('../../formatter');
9
10
  const QueryCompiler = require('./query/oracledb-querycompiler');
@@ -165,8 +166,51 @@ class Client_Oracledb extends Client_Oracle {
165
166
 
166
167
  // Used to explicitly close a connection, called internally by the pool
167
168
  // when a connection times out or the pool is shutdown.
169
+ /**
170
+ * @param {import('oracledb').Connection} connection
171
+ */
168
172
  destroyRawConnection(connection) {
169
- return connection.release();
173
+ return connection.release({ drop: true });
174
+ }
175
+
176
+ // Wraps a native oracledb pool with the interface knex expects from this.pool.
177
+ /**
178
+ * @param {import('oracledb').Pool} nativePool
179
+ */
180
+ wrapNativePool(nativePool) {
181
+ const client = this;
182
+ return {
183
+ acquire() {
184
+ return {
185
+ promise: nativePool.getConnection().then((connection) => {
186
+ connection.__knexUid = uniqueId('__knexUid');
187
+ monkeyPatchConnection(connection, client);
188
+ return connection;
189
+ }),
190
+ };
191
+ },
192
+ /**
193
+ * @param {import('oracledb').Connection} connection
194
+ */
195
+ release(connection) {
196
+ // connection.close() returns the connection to the native pool
197
+ // (or closes it if standalone). Works for both healthy and disposed connections.
198
+ if (typeof connection.close === 'function') {
199
+ const opts = connection.__knex__disposed ? { drop: true } : {};
200
+ connection.close(opts, (err) => {
201
+ if (err) {
202
+ console.error(`Error closing oracle connection`, err);
203
+ }
204
+ });
205
+ return true;
206
+ }
207
+ return false;
208
+ },
209
+ destroy() {
210
+ // No-op: knex does not own the native pool
211
+ return Promise.resolve();
212
+ },
213
+ };
170
214
  }
171
215
 
172
216
  // Handle oracle version resolution on acquiring connection from pool instead of connection creation.
@@ -2,6 +2,7 @@
2
2
  // -------
3
3
  const extend = require('lodash/extend');
4
4
  const map = require('lodash/map');
5
+ const uniqueId = require('lodash/uniqueId');
5
6
  const { promisify } = require('util');
6
7
  const Client = require('../../client');
7
8
 
@@ -120,6 +121,41 @@ class Client_PG extends Client {
120
121
  return end();
121
122
  }
122
123
 
124
+ // Wraps a native pg.Pool with the interface knex expects from this.pool.
125
+ wrapNativePool(nativePool) {
126
+ const client = this;
127
+ return {
128
+ acquire() {
129
+ return {
130
+ promise: nativePool.connect().then(async (connection) => {
131
+ connection.__knexUid = uniqueId('__knexUid');
132
+ if (!client.version) {
133
+ try {
134
+ client.version = await client.checkVersion(connection);
135
+ } catch (_e) {
136
+ // version check is best-effort
137
+ }
138
+ }
139
+ return connection;
140
+ }),
141
+ };
142
+ },
143
+ release(connection) {
144
+ if (typeof connection.release === 'function') {
145
+ // pg: release(true) destroys the connection instead of returning to pool
146
+ const shouldDestroy = !!connection.__knex__disposed;
147
+ connection.release(shouldDestroy);
148
+ return true;
149
+ }
150
+ return false;
151
+ },
152
+ destroy() {
153
+ // No-op: knex does not own the native pool
154
+ return Promise.resolve();
155
+ },
156
+ };
157
+ }
158
+
123
159
  // In PostgreSQL, we need to do a version check to do some feature
124
160
  // checking on the database.
125
161
  checkVersion(connection) {
@@ -49,14 +49,15 @@ class QueryCompiler_PG extends QueryCompiler {
49
49
  update() {
50
50
  const withSQL = this.with();
51
51
  const updateData = this._prepUpdate(this.single.update);
52
- const wheres = this.where();
53
52
  const { returning, updateFrom } = this.single;
53
+ const updateFromSQL = this._updateFrom(updateFrom);
54
+ const wheres = this.where();
54
55
  return {
55
56
  sql:
56
57
  withSQL +
57
58
  `update ${this.single.only ? 'only ' : ''}${this.tableName} ` +
58
59
  `set ${updateData.join(', ')}` +
59
- this._updateFrom(updateFrom) +
60
+ updateFromSQL +
60
61
  (wheres ? ` ${wheres}` : '') +
61
62
  this._returning(returning),
62
63
  returning,
@@ -84,11 +85,11 @@ class QueryCompiler_PG extends QueryCompiler {
84
85
  // Make sure tableName is processed by the formatter first.
85
86
  const { tableName } = this;
86
87
  const withSQL = this.with();
87
- let wheres = this.where() || '';
88
88
  let using = this.using() || '';
89
89
  const joins = this.grouped.join;
90
90
 
91
91
  const tableJoins = [];
92
+ const joinWhereStatements = [];
92
93
  if (Array.isArray(joins)) {
93
94
  for (const join of joins) {
94
95
  tableJoins.push(
@@ -101,19 +102,13 @@ class QueryCompiler_PG extends QueryCompiler {
101
102
  )
102
103
  );
103
104
 
104
- const joinWheres = [];
105
105
  for (const clause of join.clauses) {
106
- joinWheres.push(
107
- this.whereBasic({
108
- column: clause.column,
109
- operator: '=',
110
- value: clause.value,
111
- asColumn: true,
112
- })
113
- );
114
- }
115
- if (joinWheres.length > 0) {
116
- wheres += (wheres ? ' and ' : 'where ') + joinWheres.join(' and ');
106
+ joinWhereStatements.push({
107
+ column: clause.column,
108
+ operator: '=',
109
+ value: clause.value,
110
+ asColumn: true,
111
+ });
117
112
  }
118
113
  }
119
114
  if (tableJoins.length > 0) {
@@ -121,6 +116,16 @@ class QueryCompiler_PG extends QueryCompiler {
121
116
  }
122
117
  }
123
118
 
119
+ // Delete joins are rewritten into USING plus WHERE, so defer join predicates
120
+ // until after this.where() to keep bindings aligned with SQL placeholder order.
121
+ let wheres = this.where() || '';
122
+ const joinWhereClauses = joinWhereStatements.map((joinWhere) =>
123
+ this.whereBasic(joinWhere)
124
+ );
125
+ if (joinWhereClauses.length > 0) {
126
+ wheres += (wheres ? ' and ' : 'where ') + joinWhereClauses.join(' and ');
127
+ }
128
+
124
129
  // With 'using' syntax, no tablename between DELETE and FROM.
125
130
  const sql =
126
131
  withSQL +
@@ -197,29 +202,19 @@ class QueryCompiler_PG extends QueryCompiler {
197
202
  }
198
203
  }
199
204
 
200
- // Join array of table names and apply default schema.
201
- _tableNames(tables) {
202
- const schemaName = this.single.schema;
203
- const sql = [];
204
-
205
- for (let i = 0; i < tables.length; i++) {
206
- let tableName = tables[i];
207
-
208
- if (tableName) {
209
- if (schemaName) {
210
- tableName = `${schemaName}.${tableName}`;
211
- }
212
- sql.push(this.formatter.wrap(tableName));
213
- }
214
- }
215
-
216
- return sql.join(', ');
217
- }
218
-
219
205
  _lockingClause(lockMode) {
220
206
  const tables = this.single.lockTables || [];
221
207
 
222
- return lockMode + (tables.length ? ' of ' + this._tableNames(tables) : '');
208
+ return (
209
+ lockMode +
210
+ (tables.length
211
+ ? ' of ' +
212
+ tables
213
+ .filter(Boolean)
214
+ .map((table) => this.formatter.wrap(table))
215
+ .join(', ')
216
+ : '')
217
+ );
223
218
  }
224
219
 
225
220
  _groupOrder(item, type) {
@@ -140,6 +140,8 @@ class QueryCompiler_SQLite3 extends QueryCompiler {
140
140
  else if (onConflict && merge) {
141
141
  sql +=
142
142
  ' where true' + this._merge(merge.updates, onConflict, insertValues);
143
+ const wheres = this.where();
144
+ if (wheres) sql += ` ${wheres}`;
143
145
  }
144
146
 
145
147
  const { returning } = this.single;
@@ -65,11 +65,17 @@ class Runner {
65
65
  const stream = new Transform({
66
66
  objectMode: true,
67
67
  transform: (chunk, _, callback) => {
68
- callback(null, this.client.postProcessResponse(chunk, queryContext));
68
+ try {
69
+ callback(null, this.client.postProcessResponse(chunk, queryContext));
70
+ } catch (err) {
71
+ callback(err);
72
+ }
69
73
  },
70
74
  });
71
75
  stream.on('close', () => {
72
- this.client.releaseConnection(this.connection);
76
+ if (this.connection) {
77
+ this.client.releaseConnection(this.connection);
78
+ }
73
79
  });
74
80
 
75
81
  // If the stream is manually destroyed, the close event is not
@@ -67,7 +67,11 @@ const operators = transform(
67
67
 
68
68
  // Accepts a string or array of columns to wrap as appropriate. Column can be raw
69
69
  function columnize(target, builder, client, bindingHolder) {
70
- const columns = Array.isArray(target) ? target : [target];
70
+ if (!Array.isArray(target)) {
71
+ return wrap(target, undefined, builder, client, bindingHolder);
72
+ }
73
+
74
+ const columns = target;
71
75
  let str = '',
72
76
  i = -1;
73
77
  while (++i < columns.length) {
@@ -137,9 +141,27 @@ function operator(value, builder, client, bindingsHolder) {
137
141
  return operator;
138
142
  }
139
143
 
144
+ // Find a case-insensitive " as " without allocating a lowercased string.
145
+ // ASCII uppercase letters differ from lowercase by bit 0x20, so `| 32`
146
+ // folds A/S to a/s before comparing to 97 ("a") and 115 ("s").
147
+ function getAliasSeparatorIndex(value) {
148
+ let i = -1;
149
+ while ((i = value.indexOf(' ', i + 1)) !== -1) {
150
+ if (
151
+ i + 3 < value.length &&
152
+ (value.charCodeAt(i + 1) | 32) === 97 &&
153
+ (value.charCodeAt(i + 2) | 32) === 115 &&
154
+ value.charCodeAt(i + 3) === 32
155
+ ) {
156
+ return i;
157
+ }
158
+ }
159
+ return -1;
160
+ }
161
+
140
162
  // Coerce to string to prevent strange errors when it's not a string.
141
163
  function wrapString(value, builder, client) {
142
- const asIndex = value.toLowerCase().indexOf(' as ');
164
+ const asIndex = getAliasSeparatorIndex(value);
143
165
  if (asIndex !== -1) {
144
166
  const first = value.slice(0, asIndex);
145
167
  const second = value.slice(asIndex + 4);
@@ -148,18 +170,34 @@ function wrapString(value, builder, client) {
148
170
  wrapAsIdentifier(second, builder, client)
149
171
  );
150
172
  }
151
- const wrapped = [];
152
- let i = -1;
153
- const segments = value.split('.');
154
- while (++i < segments.length) {
155
- value = segments[i];
156
- if (i === 0 && segments.length > 1) {
157
- wrapped.push(wrapString((value || '').trim(), builder, client));
158
- } else {
159
- wrapped.push(wrapAsIdentifier(value, builder, client));
160
- }
173
+
174
+ let segmentStart = 0;
175
+ let dotIndex = value.indexOf('.');
176
+
177
+ if (dotIndex === -1) {
178
+ return wrapAsIdentifier(value, builder, client);
161
179
  }
162
- return wrapped.join('.');
180
+
181
+ let wrapped = '';
182
+ let i = 0;
183
+
184
+ while (dotIndex !== -1) {
185
+ const segment = value.slice(segmentStart, dotIndex);
186
+ if (i > 0) wrapped += '.';
187
+ wrapped +=
188
+ i === 0
189
+ ? wrapString((segment || '').trim(), builder, client)
190
+ : wrapAsIdentifier(segment, builder, client);
191
+ segmentStart = dotIndex + 1;
192
+ dotIndex = value.indexOf('.', segmentStart);
193
+ i++;
194
+ }
195
+
196
+ return `${wrapped}.${wrapAsIdentifier(
197
+ value.slice(segmentStart),
198
+ builder,
199
+ client
200
+ )}`;
163
201
  }
164
202
 
165
203
  // Key-value notation for alias
@@ -246,6 +284,7 @@ module.exports = {
246
284
  direction,
247
285
  operator,
248
286
  outputQuery,
287
+ getAliasSeparatorIndex,
249
288
  rawOrFn,
250
289
  unwrapRaw,
251
290
  wrap,
@@ -32,6 +32,7 @@ class FunctionHelper {
32
32
  return this.client.raw('(random_uuid())');
33
33
  case 'mysql':
34
34
  case 'mysql2':
35
+ case 'mariadb':
35
36
  return this.client.raw('(UUID())');
36
37
  default:
37
38
  throw new Error(
@@ -4,6 +4,7 @@ const QueryInterface = require('../query/method-constants');
4
4
 
5
5
  const makeKnex = require('./make-knex');
6
6
  const { KnexTimeoutError } = require('../util/timeout');
7
+ const { KnexPool } = require('../pool');
7
8
  const { resolveConfig } = require('./internal/config-resolver');
8
9
  const SchemaBuilder = require('../schema/builder');
9
10
  const ViewBuilder = require('../schema/viewbuilder');
@@ -24,6 +25,7 @@ function knex(config) {
24
25
  knex.Client = Client;
25
26
 
26
27
  knex.KnexTimeoutError = KnexTimeoutError;
28
+ knex.KnexPool = KnexPool;
27
29
 
28
30
  knex.QueryBuilder = {
29
31
  extend: function (methodName, fn) {
@@ -37,6 +37,13 @@ function resolveConfig(config) {
37
37
  Dialect = getDialectByNameOrAlias(clientName);
38
38
  }
39
39
 
40
+ // connectionPool and connection are mutually exclusive
41
+ if (parsedConfig.connectionPool && parsedConfig.connection) {
42
+ throw new Error(
43
+ `knex: 'connectionPool' and 'connection' are mutually exclusive. Use one or the other.`
44
+ );
45
+ }
46
+
40
47
  // If config connection parameter is passed as string, try to parse it
41
48
  if (typeof parsedConfig.connection === 'string') {
42
49
  resolvedConfig = Object.assign({}, parsedConfig, {
@@ -245,6 +245,136 @@ class Migrator {
245
245
  });
246
246
  }
247
247
 
248
+ // Migrate up to a specific migration (inclusive).
249
+ // Runs all pending migrations up to and including the specified one.
250
+ async to(config) {
251
+ this._disableProcessing();
252
+ this.config = getMergedConfig(config, this.config, this.knex.client.logger);
253
+
254
+ const name = this.config.name;
255
+ if (!name) {
256
+ throw new Error(
257
+ 'A migration name must be specified when using migrate.to()'
258
+ );
259
+ }
260
+
261
+ const allAndCompleted = await migrationListResolver.listAllAndCompleted(
262
+ this.config,
263
+ this.knex
264
+ );
265
+
266
+ if (!this.config.disableMigrationsListValidation) {
267
+ validateMigrationList(this.config.migrationSource, allAndCompleted);
268
+ }
269
+
270
+ const [all, completed] = allAndCompleted;
271
+
272
+ const targetIndex = all.findIndex(
273
+ (migration) =>
274
+ this.config.migrationSource.getMigrationName(migration) === name
275
+ );
276
+
277
+ if (targetIndex === -1) {
278
+ throw new Error(`Migration "${name}" not found.`);
279
+ }
280
+
281
+ const completedNames = completed.map((m) => m.name);
282
+
283
+ const migrationsToRun = all
284
+ .slice(0, targetIndex + 1)
285
+ .filter((migration) => {
286
+ return !completedNames.includes(
287
+ this.config.migrationSource.getMigrationName(migration)
288
+ );
289
+ });
290
+
291
+ const transactionForAll =
292
+ !this.config.disableTransactions &&
293
+ !(
294
+ await Promise.all(
295
+ migrationsToRun.map(async (migration) => {
296
+ const migrationContents =
297
+ await this.config.migrationSource.getMigration(migration);
298
+ return !this._useTransaction(migrationContents);
299
+ })
300
+ )
301
+ ).some((isTransactionUsed) => isTransactionUsed);
302
+
303
+ if (transactionForAll) {
304
+ return this.knex.transaction((trx) => {
305
+ return this._runBatch(migrationsToRun, 'up', trx);
306
+ });
307
+ } else {
308
+ return this._runBatch(migrationsToRun, 'up');
309
+ }
310
+ }
311
+
312
+ // Migrate up to just before a specific migration (exclusive).
313
+ // Runs all pending migrations before the specified one.
314
+ async before(config) {
315
+ this._disableProcessing();
316
+ this.config = getMergedConfig(config, this.config, this.knex.client.logger);
317
+
318
+ const name = this.config.name;
319
+ if (!name) {
320
+ throw new Error(
321
+ 'A migration name must be specified when using migrate.before()'
322
+ );
323
+ }
324
+
325
+ const allAndCompleted = await migrationListResolver.listAllAndCompleted(
326
+ this.config,
327
+ this.knex
328
+ );
329
+
330
+ if (!this.config.disableMigrationsListValidation) {
331
+ validateMigrationList(this.config.migrationSource, allAndCompleted);
332
+ }
333
+
334
+ const [all, completed] = allAndCompleted;
335
+
336
+ const targetIndex = all.findIndex(
337
+ (migration) =>
338
+ this.config.migrationSource.getMigrationName(migration) === name
339
+ );
340
+
341
+ if (targetIndex === -1) {
342
+ throw new Error(`Migration "${name}" not found.`);
343
+ }
344
+
345
+ if (targetIndex === 0) {
346
+ return [0, []];
347
+ }
348
+
349
+ const completedNames = completed.map((m) => m.name);
350
+
351
+ const migrationsToRun = all.slice(0, targetIndex).filter((migration) => {
352
+ return !completedNames.includes(
353
+ this.config.migrationSource.getMigrationName(migration)
354
+ );
355
+ });
356
+
357
+ const transactionForAll =
358
+ !this.config.disableTransactions &&
359
+ !(
360
+ await Promise.all(
361
+ migrationsToRun.map(async (migration) => {
362
+ const migrationContents =
363
+ await this.config.migrationSource.getMigration(migration);
364
+ return !this._useTransaction(migrationContents);
365
+ })
366
+ )
367
+ ).some((isTransactionUsed) => isTransactionUsed);
368
+
369
+ if (transactionForAll) {
370
+ return this.knex.transaction((trx) => {
371
+ return this._runBatch(migrationsToRun, 'up', trx);
372
+ });
373
+ } else {
374
+ return this._runBatch(migrationsToRun, 'up');
375
+ }
376
+ }
377
+
248
378
  status(config) {
249
379
  this._disableProcessing();
250
380
  this.config = getMergedConfig(config, this.config, this.knex.client.logger);
package/lib/pool.js ADDED
@@ -0,0 +1,24 @@
1
+ const { Pool } = require('tarn');
2
+
3
+ class KnexPool extends Pool {
4
+ constructor(config) {
5
+ super(config);
6
+ }
7
+ }
8
+
9
+ // Check if an object is a tarn Pool (or KnexPool) vs a native driver pool.
10
+ // Uses instanceof plus a duck-type fallback checking tarn-specific methods.
11
+ function isTarnPool(obj) {
12
+ if (obj instanceof Pool) return true;
13
+ // Duck-type: tarn pools expose these numeric methods that native pools don't
14
+ return (
15
+ typeof obj.acquire === 'function' &&
16
+ typeof obj.release === 'function' &&
17
+ typeof obj.destroy === 'function' &&
18
+ typeof obj.numFree === 'function' &&
19
+ typeof obj.numUsed === 'function' &&
20
+ typeof obj.numPendingAcquires === 'function'
21
+ );
22
+ }
23
+
24
+ module.exports = { KnexPool, isTarnPool };
@@ -1243,7 +1243,7 @@ class QueryCompiler {
1243
1243
  sql += ')';
1244
1244
 
1245
1245
  if (stmt.alias) {
1246
- sql += ' as ' + stmt.alias;
1246
+ sql += ' as ' + self.formatter.wrap(stmt.alias);
1247
1247
  }
1248
1248
 
1249
1249
  return sql;
@@ -328,6 +328,7 @@ class TableCompiler {
328
328
  output: () => {
329
329
  return this.client
330
330
  .queryBuilder()
331
+ .withSchema(this.schemaNameRaw)
331
332
  .from(this.tableNameRaw)
332
333
  .columnInfo(column)
333
334
  .then((columnInfo) => {