knex 0.14.3 → 0.14.4

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/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
 
2
2
  # Master (Unreleased)
3
3
 
4
+ # 0.14.4 - 19 Feb, 2018
5
+
6
+ ### Bug fixes:
7
+
8
+ - containsUndefined only validate plain objects. Fixes #1898 (#2468)
9
+ - Add warning when using .returning() in sqlite3. Fixes #1660 (#2471)
10
+ - Throw an error if .update() results in an empty sql (#2472)
11
+ - Removed unnecessary createTableIfNotExist and replaced with createTable (#2473)
12
+
13
+ ### New Features:
14
+
15
+ - Allow calling lock procedures (such as forUpdate) outside of transaction. Fixes #2403. (#2475)
16
+ - Added test and documentation for Event 'start' (#2488)
17
+
18
+ ### Test / internal changes
19
+
20
+ - Added stress test, which uses TCP proxy to simulate flaky connection #2460
21
+ - Removed old docker tests, new stress test setup (#2474)
22
+ - Removed unused property __cid on the base client (#2481)
23
+ - Changed rm to rimraf in 'npm run dev' (#2483)
24
+ - Changed babel preset and use latest node as target when running dev (#2484)
25
+
4
26
  # 0.14.3 - 8 Feb, 2018
5
27
 
6
28
  ### Bug fixes:
package/lib/client.js CHANGED
@@ -96,11 +96,6 @@ var debug = require('debug')('knex:client');
96
96
  var debugQuery = require('debug')('knex:query');
97
97
  var debugBindings = require('debug')('knex:bindings');
98
98
 
99
- var id = 0;
100
- function clientId() {
101
- return 'client' + id++;
102
- }
103
-
104
99
  // The base client provides the general structure
105
100
  // for a dialect specific client object.
106
101
  function Client() {
@@ -119,7 +114,6 @@ function Client() {
119
114
  if (this.driverName && config.connection) {
120
115
  this.initializeDriver();
121
116
  if (!config.pool || config.pool && config.pool.max !== 0) {
122
- this.__cid = clientId();
123
117
  this.initializePool(config);
124
118
  }
125
119
  }
@@ -103,11 +103,15 @@ function Client_MySQL(config) {
103
103
 
104
104
  return new _bluebird2.default(function (resolver, rejecter) {
105
105
  var connection = _this.driver.createConnection(_this.connectionSettings);
106
+ connection.on('error', function (err) {
107
+ connection.__knex__disposed = err;
108
+ });
106
109
  connection.connect(function (err) {
107
- if (err) return rejecter(err);
108
- connection.on('error', function (err) {
109
- connection.__knex__disposed = err;
110
- });
110
+ if (err) {
111
+ // if connection is rejected, remove listener that was registered above...
112
+ connection.removeAllListeners();
113
+ return rejecter(err);
114
+ }
111
115
  resolver(connection);
112
116
  });
113
117
  });
@@ -117,16 +121,16 @@ function Client_MySQL(config) {
117
121
  // Used to explicitly close a connection, called internally by the pool
118
122
  // when a connection times out or the pool is shutdown.
119
123
  destroyRawConnection: function destroyRawConnection(connection) {
120
- connection.removeAllListeners();
121
124
  return _bluebird2.default.fromCallback(connection.end.bind(connection)).catch(function (err) {
122
125
  connection.__knex__disposed = err;
126
+ }).finally(function () {
127
+ return connection.removeAllListeners();
123
128
  });
124
129
  },
125
130
  validateConnection: function validateConnection(connection) {
126
131
  if (connection.state === 'connected' || connection.state === 'authenticated') {
127
132
  return true;
128
133
  }
129
-
130
134
  return false;
131
135
  },
132
136
 
@@ -6,10 +6,6 @@ var _assign2 = require('lodash/assign');
6
6
 
7
7
  var _assign3 = _interopRequireDefault(_assign2);
8
8
 
9
- var _map2 = require('lodash/map');
10
-
11
- var _map3 = _interopRequireDefault(_map2);
12
-
13
9
  var _inherits = require('inherits');
14
10
 
15
11
  var _inherits2 = _interopRequireDefault(_inherits);
@@ -18,20 +14,10 @@ var _mysql = require('../mysql');
18
14
 
19
15
  var _mysql2 = _interopRequireDefault(_mysql);
20
16
 
21
- var _bluebird = require('bluebird');
22
-
23
- var _bluebird2 = _interopRequireDefault(_bluebird);
24
-
25
- var _helpers = require('../../helpers');
26
-
27
- var helpers = _interopRequireWildcard(_helpers);
28
-
29
17
  var _transaction = require('./transaction');
30
18
 
31
19
  var _transaction2 = _interopRequireDefault(_transaction);
32
20
 
33
- function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
34
-
35
21
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
36
22
 
37
23
  // Always initialize with the "QueryBuilder" and "QueryCompiler"
@@ -60,52 +46,7 @@ function Client_MySQL2(config) {
60
46
  if (connection._fatalError) {
61
47
  return false;
62
48
  }
63
-
64
49
  return true;
65
- },
66
-
67
-
68
- // Get a raw connection, called by the `pool` whenever a new
69
- // connection needs to be added to the pool.
70
- acquireRawConnection: function acquireRawConnection() {
71
- var connection = this.driver.createConnection(this.connectionSettings);
72
- connection.on('error', function (err) {
73
- connection.__knex__disposed = err;
74
- });
75
- return new _bluebird2.default(function (resolver, rejecter) {
76
- connection.connect(function (err) {
77
- if (err) {
78
- return rejecter(err);
79
- }
80
- resolver(connection);
81
- });
82
- });
83
- },
84
- processResponse: function processResponse(obj, runner) {
85
- var response = obj.response;
86
- var method = obj.method;
87
-
88
- var rows = response[0];
89
- var fields = response[1];
90
- if (obj.output) return obj.output.call(runner, rows, fields);
91
- switch (method) {
92
- case 'select':
93
- case 'pluck':
94
- case 'first':
95
- {
96
- var resp = helpers.skim(rows);
97
- if (method === 'pluck') return (0, _map3.default)(resp, obj.pluck);
98
- return method === 'first' ? resp[0] : resp;
99
- }
100
- case 'insert':
101
- return [rows.insertId];
102
- case 'del':
103
- case 'update':
104
- case 'counter':
105
- return rows.affectedRows;
106
- default:
107
- return response;
108
- }
109
50
  }
110
51
  });
111
52
 
@@ -42,13 +42,22 @@ var _compiler = require('../../../query/compiler');
42
42
 
43
43
  var _compiler2 = _interopRequireDefault(_compiler);
44
44
 
45
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
45
+ var _helpers = require('../../../helpers');
46
46
 
47
- // SQLite3 Query Builder & Compiler
47
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
48
48
 
49
49
  function QueryCompiler_SQLite3(client, builder) {
50
50
  _compiler2.default.call(this, client, builder);
51
+
52
+ var returning = this.single.returning;
53
+
54
+
55
+ if (returning) {
56
+ (0, _helpers.warn)('.returning() is not supported by sqlite3 and will not have any effect.');
57
+ }
51
58
  }
59
+ // SQLite3 Query Builder & Compiler
60
+
52
61
  (0, _inherits2.default)(QueryCompiler_SQLite3, _compiler2.default);
53
62
 
54
63
  (0, _assign3.default)(QueryCompiler_SQLite3.prototype, {
package/lib/helpers.js CHANGED
@@ -10,9 +10,9 @@ var _isArray2 = require('lodash/isArray');
10
10
 
11
11
  var _isArray3 = _interopRequireDefault(_isArray2);
12
12
 
13
- var _isObject2 = require('lodash/isObject');
13
+ var _isPlainObject2 = require('lodash/isPlainObject');
14
14
 
15
- var _isObject3 = _interopRequireDefault(_isObject2);
15
+ var _isPlainObject3 = _interopRequireDefault(_isPlainObject2);
16
16
 
17
17
  var _isUndefined2 = require('lodash/isUndefined');
18
18
 
@@ -110,7 +110,7 @@ function containsUndefined(mixed) {
110
110
  if (argContainsUndefined) break;
111
111
  argContainsUndefined = this.containsUndefined(mixed[i]);
112
112
  }
113
- } else if ((0, _isObject3.default)(mixed)) {
113
+ } else if ((0, _isPlainObject3.default)(mixed)) {
114
114
  for (var key in mixed) {
115
115
  if (mixed.hasOwnProperty(key)) {
116
116
  if (argContainsUndefined) break;
@@ -262,13 +262,10 @@ var Migrator = function () {
262
262
  });
263
263
  };
264
264
 
265
- // Create the migration table, if it doesn't already exist.
266
-
267
-
268
265
  Migrator.prototype._createMigrationTable = function _createMigrationTable(tableName) {
269
266
  var trx = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.knex;
270
267
 
271
- return trx.schema.createTableIfNotExists(tableName, function (t) {
268
+ return trx.schema.createTable(tableName, function (t) {
272
269
  t.increments();
273
270
  t.string('name');
274
271
  t.integer('batch');
@@ -279,7 +276,7 @@ var Migrator = function () {
279
276
  Migrator.prototype._createMigrationLockTable = function _createMigrationLockTable(tableName) {
280
277
  var trx = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.knex;
281
278
 
282
- return trx.schema.createTableIfNotExists(tableName, function (t) {
279
+ return trx.schema.createTable(tableName, function (t) {
283
280
  t.integer('is_locked');
284
281
  });
285
282
  };
@@ -468,11 +468,7 @@ var components = ['columns', 'join', 'where', 'union', 'group', 'having', 'order
468
468
  // Compiles the "locks".
469
469
  lock: function lock() {
470
470
  if (this.single.lock) {
471
- if (!this.client.transacting) {
472
- helpers.warn('You are attempting to perform a "lock" command outside of a transaction.');
473
- } else {
474
- return this[this.single.lock]();
475
- }
471
+ return this[this.single.lock]();
476
472
  }
477
473
  },
478
474
 
@@ -644,6 +640,11 @@ var components = ['columns', 'join', 'where', 'union', 'group', 'having', 'order
644
640
  while (++i < columns.length) {
645
641
  vals.push(this.formatter.wrap(columns[i]) + ' = ' + this.formatter.parameter(data[columns[i]]));
646
642
  }
643
+
644
+ if ((0, _isEmpty3.default)(vals)) {
645
+ throw new Error(['Empty .update() call detected!', 'Update data does not contain any values to update.', 'This will result in a faulty query.'].join(' '));
646
+ }
647
+
647
648
  return vals;
648
649
  },
649
650
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knex",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "A batteries-included SQL query & schema builder for Postgres, MySQL and SQLite3 and the Browser",
5
5
  "main": "knex.js",
6
6
  "dependencies": {
@@ -31,10 +31,9 @@
31
31
  "babel-plugin-add-module-exports": "^0.2.1",
32
32
  "babel-plugin-lodash": "3.3.2",
33
33
  "babel-plugin-transform-runtime": "^6.23.0",
34
- "babel-preset-es2015": "^6.24.1",
34
+ "babel-preset-env": "^1.6.1",
35
35
  "chai": "^4.1.2",
36
36
  "coveralls": "~3.0.0",
37
- "dockerode": "^2.5.3",
38
37
  "eslint": "4.16.0",
39
38
  "eslint-plugin-import": "^2.8.0",
40
39
  "estraverse": "^4.2.0",
@@ -45,7 +44,7 @@
45
44
  "mock-fs": "^4.4.2",
46
45
  "mssql": "^4.1.0",
47
46
  "mysql": "^2.15.0",
48
- "mysql2": "^1.5.1",
47
+ "mysql2": "^1.5.2",
49
48
  "pg": "^7.4.1",
50
49
  "pg-query-stream": "^1.1.1",
51
50
  "rimraf": "^2.6.2",
@@ -55,7 +54,8 @@
55
54
  "sqlite3": "^3.1.13",
56
55
  "tap-spec": "^4.1.1",
57
56
  "tape": "^4.8.0",
58
- "through": "^2.3.8"
57
+ "through": "^2.3.8",
58
+ "toxiproxy-node-client": "^2.0.6"
59
59
  },
60
60
  "buildDependencies": [
61
61
  "babel-cli",
@@ -71,15 +71,17 @@
71
71
  "debug_test": "node-debug _mocha -t 0 test/index.js",
72
72
  "babel": "rimraf ./lib && babel src --out-dir lib --copy-files",
73
73
  "coveralls": "cat ./test/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
74
- "dev": "rm -rf ./lib && babel -w src --out-dir lib --copy-files",
74
+ "dev": "rimraf ./lib && babel -w src --out-dir lib --copy-files",
75
75
  "lint": "eslint src/**",
76
76
  "plaintest": "mocha --check-leaks -b -R spec test/index.js && npm run tape",
77
77
  "prepublish": "npm run babel",
78
- "docker_test": "bash ./scripts/docker-for-test.sh",
79
- "pre_test": "npm run lint && npm run docker_test",
78
+ "pre_test": "npm run lint",
80
79
  "tape": "node test/tape/index.js | tap-spec",
81
80
  "debug_tape": "node-debug test/tape/index.js",
82
- "test": "npm run pre_test && istanbul --config=test/.istanbul.yml cover node_modules/mocha/bin/_mocha -- --check-leaks -t 10000 -b -R spec test/index.js && npm run tape"
81
+ "test": "npm run pre_test && istanbul --config=test/.istanbul.yml cover node_modules/mocha/bin/_mocha -- --check-leaks -t 10000 -b -R spec test/index.js && npm run tape",
82
+ "stress:init": "docker-compose -f scripts/stress-test/docker-compose.yml up --no-start && docker-compose -f scripts/stress-test/docker-compose.yml start",
83
+ "stress:test": "node scripts/stress-test/knex-stress-test.js | grep -A 3 -- '- STATS '",
84
+ "stress:destroy": "docker-compose -f scripts/stress-test/docker-compose.yml stop"
83
85
  },
84
86
  "bin": {
85
87
  "knex": "./bin/cli.js"
@@ -0,0 +1,18 @@
1
+ # Test scripts to evaluate stability of drivers / pool etc.
2
+
3
+ # To run this test you need to be in this directory + have node >= 8
4
+ # and startup docker containers with proxy and sql servers
5
+
6
+ docker-compose up --no-start
7
+ docker-compose start
8
+
9
+ # Select different test script to run:
10
+
11
+ node mysql2-random-hanging-every-now-and-then.js 2> /dev/null | grep -B500 -A2 -- "- STATS"
12
+ node mysql2-sudden-exit-without-error
13
+ node knex-stress-test.js | grep -A 3 -- "- STATS "
14
+ node reconnect-test-mysql-based-drivers.js 2> /dev/null | grep -A 3 -- "- STATS "
15
+
16
+ # Shut down docker instances when done:
17
+
18
+ docker-compose down
File without changes
@@ -0,0 +1,44 @@
1
+ version: '3'
2
+
3
+ services:
4
+ toxiproxy:
5
+ image: shopify/toxiproxy
6
+ ports:
7
+ - "8474:8474"
8
+ - "23306:23306"
9
+ - "25432:25432"
10
+ - "21521:21521"
11
+ links:
12
+ - "mysql"
13
+ - "postgresql"
14
+
15
+ mysql:
16
+ image: mysql:5.7
17
+ ports:
18
+ - "33306:3306"
19
+ environment:
20
+ - TZ=UTC
21
+ - MYSQL_ROOT_PASSWORD=mysqlrootpassword
22
+
23
+ postgresql:
24
+ image: mdillon/postgis
25
+ ports:
26
+ - "35432:5432"
27
+ environment:
28
+ - POSTGRES_PASSWORD=postgresrootpassword
29
+ - POSTGRES_USER=postgres
30
+
31
+ oracledbxe:
32
+ image: wnameless/oracle-xe-11g
33
+ ports:
34
+ - "31521:1521"
35
+ environment:
36
+ - ORACLE_ALLOW_REMOTE=true
37
+
38
+ # mssql:
39
+ # image: microsoft/mssql-server-linux
40
+ # ports:
41
+ # - "31433:1433"
42
+ # environment:
43
+ # - ACCEPT_EULA=Y
44
+ # - SA_PASSWORD=mssqlpassword
@@ -0,0 +1,162 @@
1
+ const Knex = require('../../lib');
2
+
3
+ const Bluebird = require('bluebird');
4
+ const toxiproxy = require('toxiproxy-node-client');
5
+ const toxicli = new toxiproxy.Toxiproxy('http://localhost:8474');
6
+ const rp = require('request-promise-native');
7
+
8
+ // init instances
9
+ const pg = Knex({
10
+ client: 'pg',
11
+ connection: 'postgres://postgres:postgresrootpassword@localhost:25432/postgres',
12
+ pool: { max: 50 }
13
+ });
14
+
15
+ const mysql2 = Knex({
16
+ client: 'mysql2',
17
+ connection: 'mysql://root:mysqlrootpassword@localhost:23306/?charset=utf8&connectTimeout=500',
18
+ pool: { max: 50 }
19
+ });
20
+
21
+ const mysql = Knex({
22
+ client: 'mysql',
23
+ connection: 'mysql://root:mysqlrootpassword@localhost:23306/?charset=utf8&connectTimeout=500',
24
+ pool: { max: 50 }
25
+ });
26
+
27
+ /* TODO: figure out how to nicely install oracledb node driver on osx
28
+ const mysql = Knex({
29
+ client: 'oracledb',
30
+ connection: {
31
+ user : "travis",
32
+ password : "travis",
33
+ connectString : "localhost/XE",
34
+ // https://github.com/oracle/node-oracledb/issues/525
35
+ stmtCacheSize : 0
36
+ },
37
+ pool: { max: 50 }
38
+ });
39
+ */
40
+
41
+ const counters = {};
42
+
43
+ function setQueryCounters(instance, name) {
44
+ const counts = counters[name] = {queries: 0, results: 0, errors: 0};
45
+ instance.on('query', () => counts.queries += 1);
46
+ instance.on('query-response', () => counts.results += 1);
47
+ instance.on('query-error', () => counts.errors += 1);
48
+ }
49
+
50
+ setQueryCounters(pg, 'pg');
51
+ setQueryCounters(mysql, 'mysql');
52
+ setQueryCounters(mysql2, 'mysql2');
53
+
54
+ const _ = require('lodash');
55
+
56
+ // start printing out counters
57
+ let lastCounters = _.cloneDeep(counters);
58
+
59
+ setInterval(() => {
60
+ const reqsPerSec = {};
61
+ for (let key of Object.keys(counters)) {
62
+ reqsPerSec[key] = {
63
+ queries: (counters[key].queries - lastCounters[key].queries) / 2,
64
+ results: (counters[key].results - lastCounters[key].results) / 2,
65
+ errors: (counters[key].errors - lastCounters[key].errors) / 2,
66
+ }
67
+ }
68
+ console.log('------------------------ STATS PER SECOND ------------------------');
69
+ console.dir(reqsPerSec, {colors: true});
70
+ console.log('------------------------------- EOS ------------------------------');
71
+ lastCounters = _.cloneDeep(counters);
72
+ }, 2000);
73
+
74
+
75
+ async function killConnectionsPg() {
76
+ return pg.raw(`SELECT pg_terminate_backend(pg_stat_activity.pid)
77
+ FROM pg_stat_activity
78
+ WHERE pg_stat_activity.datname = 'postgres'
79
+ AND pid <> pg_backend_pid()`);
80
+ }
81
+
82
+ async function killConnectionsMyslq(client) {
83
+ const [rows, colDefs] = await client.raw(`SHOW FULL PROCESSLIST`);
84
+ await Promise.all(rows.map(row => client.raw(`KILL ${row.Id}`)));
85
+ }
86
+
87
+ async function main() {
88
+ async function loopQueries(prefix, query) {
89
+ const queries = () => [
90
+ ...Array(50).fill(query),
91
+ ];
92
+
93
+ while(true) {
94
+ try {
95
+ await Promise.all(queries());
96
+ } catch (err) {
97
+ console.log(prefix, err);
98
+ }
99
+ }
100
+ }
101
+
102
+ async function recreateProxy(serviceName, listenPort, proxyToPort) {
103
+ try {
104
+ await rp.delete({
105
+ url: `${toxicli.host}/proxies/${serviceName}`
106
+ });
107
+ } catch(err) {}
108
+
109
+ const proxy = await toxicli.createProxy({
110
+ name: serviceName,
111
+ listen: `0.0.0.0:${listenPort}`,
112
+ upstream: `${serviceName}:${proxyToPort}`
113
+ });
114
+
115
+ // add some latency
116
+ await proxy.addToxic(new toxiproxy.Toxic(proxy, {
117
+ type: 'latency',
118
+ attributes: {latency: 1, jitter: 1}
119
+ }));
120
+
121
+ // cause connections to be closed every 500 bytes
122
+ await proxy.addToxic(new toxiproxy.Toxic(proxy, {
123
+ type: 'limit_data',
124
+ attributes: {bytes: 5000}
125
+ }));
126
+ }
127
+
128
+ // create TCP proxies for simulating bad connections etc.
129
+ async function recreateProxies() {
130
+ await recreateProxy('postgresql', 25432, 5432);
131
+ await recreateProxy('mysql', 23306, 3306);
132
+ await recreateProxy('oracledbxe', 21521, 1521);
133
+ }
134
+
135
+ await recreateProxies();
136
+
137
+ loopQueries('PSQL:', pg.raw('select 1'));
138
+ loopQueries('PSQL TO:', pg.raw('select 1').timeout(20));
139
+
140
+ loopQueries('MYSQL:', mysql.raw('select 1'));
141
+ loopQueries('MYSQL TO:', mysql.raw('select 1').timeout(20));
142
+
143
+ loopQueries('MYSQL2:', mysql2.raw('select 1'));
144
+ loopQueries('MYSQL2 TO:', mysql2.raw('select 1').timeout(20));
145
+
146
+
147
+ while(true) {
148
+ await Bluebird.delay(20); // kill everything every quite often from server side
149
+ try {
150
+ await Promise.all([
151
+ killConnectionsPg(),
152
+ killConnectionsMyslq(mysql),
153
+ killConnectionsMyslq(mysql2),
154
+ ]);
155
+ } catch (err) {
156
+ console.log('KILLER ERROR:', err);
157
+ }
158
+ }
159
+ }
160
+
161
+ process.on('exit', () => console.log('- STATS PRINT NEAR END LOGS ')); // marker for grep...
162
+ main();
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Test case for figuring out robust way to recognize if connection is dead
3
+ * for mysql based drivers.
4
+ */
5
+ const Bluebird = require('bluebird');
6
+ const toxiproxy = require('toxiproxy-node-client');
7
+ const toxicli = new toxiproxy.Toxiproxy('http://localhost:8474');
8
+ const rp = require('request-promise-native');
9
+ const _ = require('lodash');
10
+
11
+ async function stdMysqlQuery(con, sql) {
12
+ return new Promise((resolve, reject) => {
13
+ try {
14
+ con.query({
15
+ sql,
16
+ timeout: 500
17
+ }, function (error, results, fields) {
18
+ if (error) {
19
+ reject(error);
20
+ } else {
21
+ resolve(results);
22
+ }
23
+ });
24
+ } catch (err) {
25
+ reject(err); // double sure...
26
+ }
27
+ });
28
+ }
29
+
30
+ const mysql2 = require('mysql2');
31
+ let mysql2Con = {_fatalError: 'initmefirst'};
32
+ async function mysql2Query(sql) {
33
+ // recreate connection on fatal error
34
+ if (mysql2Con._fatalError) {
35
+ console.log('========== Reconnecting mysql2');
36
+ mysql2Con = mysql2.createConnection({
37
+ host: 'localhost',
38
+ user: 'root',
39
+ password: 'mysqlrootpassword',
40
+ port: 23306,
41
+ connectTimeout: 500,
42
+ debug: true
43
+ });
44
+ mysql2Con.on('error', err => {
45
+ console.log('- STATS Mysql2 connection died:', err);
46
+ });
47
+ }
48
+ console.log('================ MYSQL2 Running query ======');
49
+ const res = await stdMysqlQuery(mysql2Con, sql);
50
+ console.log('====================== done ================');
51
+ return res;
52
+ }
53
+
54
+ const counters = {};
55
+ function setMysqlQueryCounters(name) {
56
+ const counts = counters[name] = {queries: 0, results: 0, errors: 0};
57
+ }
58
+ setMysqlQueryCounters('mysql2');
59
+
60
+ // start printing out counters
61
+ let lastCounters = _.cloneDeep(counters);
62
+ setInterval(() => {
63
+ const reqsPerSec = {};
64
+ for (let key of Object.keys(counters)) {
65
+ reqsPerSec[key] = {
66
+ queries: (counters[key].queries - lastCounters[key].queries),
67
+ results: (counters[key].results - lastCounters[key].results),
68
+ errors: (counters[key].errors - lastCounters[key].errors),
69
+ }
70
+ }
71
+ console.log('------------------------ STATS PER SECOND ------------------------');
72
+ console.dir(reqsPerSec, {colors: true});
73
+ console.log('------------------------------- EOS ------------------------------');
74
+ lastCounters = _.cloneDeep(counters);
75
+ }, 1000);
76
+
77
+ async function recreateProxy(serviceName, listenPort, proxyToPort) {
78
+ try {
79
+ await rp.delete({
80
+ url: `${toxicli.host}/proxies/${serviceName}`
81
+ });
82
+ } catch(err) {}
83
+
84
+ const proxy = await toxicli.createProxy({
85
+ name: serviceName,
86
+ listen: `0.0.0.0:${listenPort}`,
87
+ upstream: `${serviceName}:${proxyToPort}`
88
+ });
89
+
90
+ // add some latency
91
+ await proxy.addToxic(new toxiproxy.Toxic(proxy, {
92
+ type: 'latency',
93
+ attributes: {latency: 1, jitter: 1}
94
+ }));
95
+
96
+ // cause connections to be closed every some transferred bytes
97
+ await proxy.addToxic(new toxiproxy.Toxic(proxy, {
98
+ type: 'limit_data',
99
+ attributes: {bytes: 1000}
100
+ }));
101
+ }
102
+
103
+ async function main() {
104
+ await recreateProxy('mysql', 23306, 3306);
105
+ setInterval(() => recreateProxy('mysql', 23306, 3306), 2000);
106
+
107
+ async function loopQueries(prefix, query) {
108
+ const counts = counters[prefix];
109
+
110
+ while(true) {
111
+ try {
112
+ counts.queries += 1;
113
+ // without this delay we might endup to busy failure loop
114
+ await Bluebird.delay(0);
115
+ await query();
116
+ counts.results += 1;
117
+ } catch (err) {
118
+ counts.errors += 1;
119
+ console.log(prefix, err);
120
+ }
121
+ }
122
+ }
123
+
124
+ loopQueries('mysql2', () => mysql2Query('select 1'));
125
+
126
+ // wait forever
127
+ while(true) {
128
+ await Bluebird.delay(1000);
129
+ }
130
+ }
131
+
132
+ main().then(() => console.log('DONE')).catch(err => console.log(err));
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Test case when mysql2 driver strangely exits when one tries to send query
3
+ * to dead connection.
4
+ */
5
+
6
+ const Bluebird = require('bluebird');
7
+ const toxiproxy = require('toxiproxy-node-client');
8
+ const toxicli = new toxiproxy.Toxiproxy('http://localhost:8474');
9
+ const rp = require('request-promise-native');
10
+
11
+ // drops old toxicproxy and creates new
12
+ async function recreateProxy(serviceName, listenPort, proxyToPort) {
13
+ try {
14
+ await rp.delete({
15
+ url: `${toxicli.host}/proxies/${serviceName}`
16
+ });
17
+ } catch(err) {
18
+ // there was no proxy by that name... its ok
19
+ }
20
+
21
+ const proxy = await toxicli.createProxy({
22
+ name: serviceName,
23
+ listen: `0.0.0.0:${listenPort}`,
24
+ upstream: `${serviceName}:${proxyToPort}`
25
+ });
26
+ }
27
+
28
+ async function insanelyParanoidQuery(con) {
29
+ console.log('sending query');
30
+ const res = await new Promise((resolve,reject) => {
31
+ try {
32
+ con.query('select 1', [], function(err, rows, fields) {
33
+ if (err) {
34
+ reject(err);
35
+ } else {
36
+ resolve(rows);
37
+ }
38
+ });
39
+ } catch(err) {
40
+ console.log('Huh synchronous exception?! (shouldnt be possible)');
41
+ reject(err);
42
+ }
43
+ });
44
+ console.log(res);
45
+ }
46
+
47
+
48
+ async function main() {
49
+ // create proxy from localhost:23306 -> mysqldocker:3306
50
+ await recreateProxy('mysql', 23306, 3306);
51
+
52
+ // ------------- setup mysql2 db driver connection
53
+ const mysql2 = require('mysql2'); // with mysql this works...
54
+ const mysql2Con = await mysql2.createConnection({
55
+ host: 'localhost',
56
+ user: 'root',
57
+ password: 'mysqlrootpassword',
58
+ port: 23306
59
+ });
60
+
61
+ mysql2Con.on('error', function(err) {
62
+ console.log("I'm dead", err);
63
+ })
64
+
65
+ console.log('Going to cut connections');
66
+
67
+ // cut connection during recreate
68
+ await recreateProxy('mysql', 23306, 3306);
69
+
70
+ console.log('Proxy recreated... start waiting');
71
+
72
+ // wait forever
73
+ while(true) {
74
+ await Bluebird.delay(1000);
75
+ try {
76
+ await insanelyParanoidQuery(mysql2Con);
77
+ } catch (err) {
78
+ console.log('query failed:', err);
79
+ }
80
+ await recreateProxy('mysql', 23306, 3306);
81
+ }
82
+ }
83
+
84
+ main()
85
+ .then(() => console.log('Process stopped normally'))
86
+ .catch(err => {
87
+ console.log('Process stopped to failure', err);
88
+ })
89
+
90
+ console.log('Waiting for eventloop to stop...');
91
+
92
+ process.on('uncaughtException', function (err) {
93
+ console.log('uncaughtException', err);
94
+ });
95
+
96
+ process.on('exit', function () {
97
+ console.log('Did someone call process.exit() or wat? exitCode:', process.exitCode);
98
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Test case for figuring out robust way to recognize if connection is dead
3
+ * for mysql based drivers.
4
+ */
5
+ const Bluebird = require('bluebird');
6
+ const toxiproxy = require('toxiproxy-node-client');
7
+ const toxicli = new toxiproxy.Toxiproxy('http://localhost:8474');
8
+ const rp = require('request-promise-native');
9
+
10
+ async function stdMysqlQuery(con, sql) {
11
+ return new Promise((resolve, reject) => {
12
+ try {
13
+ con.query({
14
+ sql,
15
+ timeout: 4000
16
+ }, function (error, results, fields) {
17
+ if (error) {
18
+ reject(error);
19
+ } else {
20
+ resolve(results);
21
+ }
22
+ });
23
+ } catch (err) {
24
+ reject(err); // double sure...
25
+ }
26
+ });
27
+ }
28
+
29
+ // ALL THE DRIVERS HAS DIFFERENT BAG OF TRICKS TO RECOVER AND
30
+ // RECOGNIZE WHEN CONNECTION HAS BEEN CLOSED
31
+
32
+ // ------------- setup mysql db driver connection
33
+ const mysql = require('mysql');
34
+ let mysqlCon = {state: 'disconnected'};
35
+ async function mysqlQuery(sql) {
36
+ // best way to check if connection is still alive
37
+ if (mysqlCon.state === 'disconnected') {
38
+ console.log('reconnecting mysql');
39
+ mysqlCon = mysql.createConnection({
40
+ host: 'localhost',
41
+ user: 'root',
42
+ password: 'mysqlrootpassword',
43
+ port: 23306,
44
+ connectTimeout: 500
45
+ });
46
+ // not always triggered, if this happens during query
47
+ mysqlCon.on('error', err => {
48
+ console.log('- STATS Mysql connection died:', err);
49
+ });
50
+ }
51
+ return stdMysqlQuery(mysqlCon, sql);
52
+ }
53
+
54
+ // ------------- setup mysql2 db driver connection
55
+ const mysql2 = require('mysql2');
56
+ let mysql2Con = {_fatalError: 'initmefirst'};
57
+ async function mysql2Query(sql) {
58
+ if (mysql2Con._fatalError) {
59
+ console.log('reconnecting mysql2');
60
+ mysql2Con = mysql2.createConnection({
61
+ host: 'localhost',
62
+ user: 'root',
63
+ password: 'mysqlrootpassword',
64
+ port: 23306,
65
+ connectTimeout: 500
66
+ });
67
+ mysql2Con.on('error', err => {
68
+ console.log('- STATS Mysql2 connection died:', err);
69
+ });
70
+ }
71
+ console.log('================ MYSQL2 Running query....');
72
+ const res = await stdMysqlQuery(mysql2Con, sql);
73
+ console.log('=========== done');
74
+ return res;
75
+ }
76
+
77
+ const counters = {};
78
+ function setMysqlQueryCounters(name) {
79
+ const counts = counters[name] = {queries: 0, results: 0, errors: 0};
80
+ }
81
+ setMysqlQueryCounters('mysql');
82
+ setMysqlQueryCounters('mysql2');
83
+
84
+ const _ = require('lodash');
85
+
86
+ // start printing out counters
87
+ let lastCounters = _.cloneDeep(counters);
88
+
89
+ setInterval(() => {
90
+ const reqsPerSec = {};
91
+ for (let key of Object.keys(counters)) {
92
+ reqsPerSec[key] = {
93
+ queries: (counters[key].queries - lastCounters[key].queries),
94
+ results: (counters[key].results - lastCounters[key].results),
95
+ errors: (counters[key].errors - lastCounters[key].errors),
96
+ }
97
+ }
98
+ console.log('------------------------ STATS PER SECOND ------------------------');
99
+ console.dir(reqsPerSec, {colors: true});
100
+ console.log('------------------------------- EOS ------------------------------');
101
+ lastCounters = _.cloneDeep(counters);
102
+
103
+ // if hang
104
+ ///if (reqsPerSec.mysql2.queries === 0) process.exit(0);
105
+ }, 1000);
106
+
107
+
108
+ async function main() {
109
+
110
+ async function recreateProxy(serviceName, listenPort, proxyToPort) {
111
+ try {
112
+ await rp.delete({
113
+ url: `${toxicli.host}/proxies/${serviceName}`
114
+ });
115
+ } catch(err) {}
116
+
117
+ const proxy = await toxicli.createProxy({
118
+ name: serviceName,
119
+ listen: `0.0.0.0:${listenPort}`,
120
+ upstream: `${serviceName}:${proxyToPort}`
121
+ });
122
+
123
+ // add some latency
124
+ await proxy.addToxic(new toxiproxy.Toxic(proxy, {
125
+ type: 'latency',
126
+ attributes: {latency: 1, jitter: 1}
127
+ }));
128
+
129
+ // cause connections to be closed every some transferred bytes
130
+ await proxy.addToxic(new toxiproxy.Toxic(proxy, {
131
+ type: 'limit_data',
132
+ attributes: {bytes: 1000}
133
+ }));
134
+ }
135
+
136
+
137
+ // create TCP proxies for simulating bad connections etc.
138
+ async function recreateProxies() {
139
+ console.log('----- Recreating proxies -> cutting connections completely');
140
+ await recreateProxy('postgresql', 25432, 5432);
141
+ await recreateProxy('mysql', 23306, 3306);
142
+ await recreateProxy('oracledbxe', 21521, 1521);
143
+ }
144
+ setInterval(() => recreateProxies(), 2000);
145
+
146
+ async function loopQueries(prefix, query) {
147
+ const counts = counters[prefix];
148
+
149
+ while(true) {
150
+ try {
151
+ counts.queries += 1;
152
+ // without this delay we endup to busy failure loop
153
+ await Bluebird.delay(0);
154
+ await query();
155
+ counts.results += 1;
156
+ } catch (err) {
157
+ counts.errors += 1;
158
+ console.log(prefix, err);
159
+ }
160
+ }
161
+ }
162
+
163
+ await recreateProxies();
164
+
165
+ loopQueries('mysql', () => mysqlQuery('select 1'));
166
+ loopQueries('mysql2', () => mysql2Query('select 1'));
167
+
168
+ // wait forever
169
+ while(true) {
170
+ await Bluebird.delay(1000);
171
+ }
172
+ }
173
+
174
+ main().then(() => console.log('DONE')).catch(err => console.log(err));
package/src/client.js CHANGED
@@ -27,11 +27,6 @@ const debug = require('debug')('knex:client')
27
27
  const debugQuery = require('debug')('knex:query')
28
28
  const debugBindings = require('debug')('knex:bindings')
29
29
 
30
- let id = 0
31
- function clientId() {
32
- return `client${id++}`
33
- }
34
-
35
30
  // The base client provides the general structure
36
31
  // for a dialect specific client object.
37
32
  function Client(config = {}) {
@@ -48,7 +43,6 @@ function Client(config = {}) {
48
43
  if (this.driverName && config.connection) {
49
44
  this.initializeDriver()
50
45
  if (!config.pool || (config.pool && config.pool.max !== 0)) {
51
- this.__cid = clientId()
52
46
  this.initializePool(config)
53
47
  }
54
48
  }
@@ -64,33 +64,36 @@ assign(Client_MySQL.prototype, {
64
64
  // connection needs to be added to the pool.
65
65
  acquireRawConnection() {
66
66
  return new Promise((resolver, rejecter) => {
67
- const connection = this.driver.createConnection(this.connectionSettings)
67
+ const connection = this.driver.createConnection(this.connectionSettings);
68
+ connection.on('error', err => {
69
+ connection.__knex__disposed = err;
70
+ });
68
71
  connection.connect((err) => {
69
- if (err) return rejecter(err)
70
- connection.on('error', err => {
71
- connection.__knex__disposed = err
72
- })
73
- resolver(connection)
74
- })
72
+ if (err) {
73
+ // if connection is rejected, remove listener that was registered above...
74
+ connection.removeAllListeners();
75
+ return rejecter(err);
76
+ }
77
+ resolver(connection);
78
+ });
75
79
  })
76
80
  },
77
81
 
78
82
  // Used to explicitly close a connection, called internally by the pool
79
83
  // when a connection times out or the pool is shutdown.
80
84
  destroyRawConnection(connection) {
81
- connection.removeAllListeners()
82
85
  return Promise
83
86
  .fromCallback(connection.end.bind(connection))
84
87
  .catch(err => {
85
88
  connection.__knex__disposed = err
86
89
  })
90
+ .finally(() => connection.removeAllListeners());
87
91
  },
88
92
 
89
93
  validateConnection(connection) {
90
94
  if (connection.state === 'connected' || connection.state === 'authenticated') {
91
95
  return true
92
96
  }
93
-
94
97
  return false
95
98
  },
96
99
 
@@ -3,9 +3,7 @@
3
3
  // -------
4
4
  import inherits from 'inherits';
5
5
  import Client_MySQL from '../mysql';
6
- import Promise from 'bluebird';
7
- import * as helpers from '../../helpers';
8
- import { map, assign } from 'lodash'
6
+ import { assign } from 'lodash'
9
7
  import Transaction from './transaction';
10
8
 
11
9
  // Always initialize with the "QueryBuilder" and "QueryCompiler"
@@ -33,52 +31,8 @@ assign(Client_MySQL2.prototype, {
33
31
  if (connection._fatalError) {
34
32
  return false
35
33
  }
36
-
37
34
  return true
38
- },
39
-
40
- // Get a raw connection, called by the `pool` whenever a new
41
- // connection needs to be added to the pool.
42
- acquireRawConnection() {
43
- const connection = this.driver.createConnection(this.connectionSettings)
44
- connection.on('error', err => {
45
- connection.__knex__disposed = err
46
- })
47
- return new Promise((resolver, rejecter) => {
48
- connection.connect((err) => {
49
- if (err) {
50
- return rejecter(err)
51
- }
52
- resolver(connection)
53
- })
54
- })
55
- },
56
-
57
- processResponse(obj, runner) {
58
- const { response } = obj
59
- const { method } = obj
60
- const rows = response[0]
61
- const fields = response[1]
62
- if (obj.output) return obj.output.call(runner, rows, fields)
63
- switch (method) {
64
- case 'select':
65
- case 'pluck':
66
- case 'first': {
67
- const resp = helpers.skim(rows)
68
- if (method === 'pluck') return map(resp, obj.pluck)
69
- return method === 'first' ? resp[0] : resp
70
- }
71
- case 'insert':
72
- return [rows.insertId]
73
- case 'del':
74
- case 'update':
75
- case 'counter':
76
- return rows.affectedRows
77
- default:
78
- return response
79
- }
80
35
  }
81
-
82
36
  })
83
37
 
84
38
  export default Client_MySQL2;
@@ -4,9 +4,16 @@
4
4
  import inherits from 'inherits';
5
5
  import QueryCompiler from '../../../query/compiler';
6
6
  import { assign, each, isEmpty, isString, noop, reduce, identity } from 'lodash'
7
+ import { warn } from '../../../helpers';
7
8
 
8
9
  function QueryCompiler_SQLite3(client, builder) {
9
10
  QueryCompiler.call(this, client, builder)
11
+
12
+ const {returning} = this.single;
13
+
14
+ if(returning) {
15
+ warn('.returning() is not supported by sqlite3 and will not have any effect.');
16
+ }
10
17
  }
11
18
  inherits(QueryCompiler_SQLite3, QueryCompiler)
12
19
 
package/src/helpers.js CHANGED
@@ -1,6 +1,15 @@
1
1
  /* eslint no-console:0 */
2
2
 
3
- import { map, pick, keys, isFunction, isUndefined, isObject, isArray, isTypedArray } from 'lodash'
3
+ import {
4
+ map,
5
+ pick,
6
+ keys,
7
+ isFunction,
8
+ isUndefined,
9
+ isPlainObject,
10
+ isArray,
11
+ isTypedArray
12
+ } from 'lodash'
4
13
  import chalk from 'chalk';
5
14
 
6
15
  // Pick off the attributes from only the current layer of the object.
@@ -60,7 +69,7 @@ export function containsUndefined(mixed) {
60
69
  if(argContainsUndefined) break;
61
70
  argContainsUndefined = this.containsUndefined(mixed[i]);
62
71
  }
63
- } else if(isObject(mixed)) {
72
+ } else if(isPlainObject(mixed)) {
64
73
  for(const key in mixed) {
65
74
  if (mixed.hasOwnProperty(key)) {
66
75
  if(argContainsUndefined) break;
@@ -152,9 +152,8 @@ export default class Migrator {
152
152
  .then(data => !data.length && trx.into(lockTable).insert({ is_locked: 0 }));
153
153
  }
154
154
 
155
- // Create the migration table, if it doesn't already exist.
156
155
  _createMigrationTable(tableName, trx = this.knex) {
157
- return trx.schema.createTableIfNotExists(tableName, function(t) {
156
+ return trx.schema.createTable(tableName, function(t) {
158
157
  t.increments();
159
158
  t.string('name');
160
159
  t.integer('batch');
@@ -163,7 +162,7 @@ export default class Migrator {
163
162
  }
164
163
 
165
164
  _createMigrationLockTable(tableName, trx = this.knex) {
166
- return trx.schema.createTableIfNotExists(tableName, function(t) {
165
+ return trx.schema.createTable(tableName, function(t) {
167
166
  t.integer('is_locked');
168
167
  });
169
168
  }
@@ -425,11 +425,7 @@ assign(QueryCompiler.prototype, {
425
425
  // Compiles the "locks".
426
426
  lock() {
427
427
  if (this.single.lock) {
428
- if (!this.client.transacting) {
429
- helpers.warn('You are attempting to perform a "lock" command outside of a transaction.')
430
- } else {
431
- return this[this.single.lock]()
432
- }
428
+ return this[this.single.lock]()
433
429
  }
434
430
  },
435
431
 
@@ -618,6 +614,15 @@ assign(QueryCompiler.prototype, {
618
614
  this.formatter.parameter(data[columns[i]])
619
615
  );
620
616
  }
617
+
618
+ if(isEmpty(vals)) {
619
+ throw new Error([
620
+ 'Empty .update() call detected!',
621
+ 'Update data does not contain any values to update.',
622
+ 'This will result in a faulty query.',
623
+ ].join(' '));
624
+ }
625
+
621
626
  return vals;
622
627
  },
623
628
 
@@ -1,93 +0,0 @@
1
- const Bluebird = require('bluebird');
2
-
3
- // TODO: setup all DBs with docker
4
- // TODO: setup proxy for closing connections
5
- // TODO: use all dialects
6
-
7
- const pg = require('../lib')({
8
- client: 'pg',
9
- connection: 'postgres:///knex_test',
10
- pool: { max: 90 }
11
- });
12
-
13
- const mysql = require('../lib')({
14
- dialect: 'mysql2',
15
- connection: {
16
- database: "knex_test",
17
- user: "root",
18
- charset: 'utf8'
19
- },
20
- pool: { max: 90 }
21
- });
22
-
23
- async function main() {
24
- const pgQueries = [
25
- ...Array(40).fill(pg('accounts').timeout(100)), // these might eat memory
26
- ...Array(40).fill(pg('accounts')) // these might hang indefinitely
27
- ];
28
-
29
- function killConnectionsPg() {
30
- return pg.raw(`SELECT pg_terminate_backend(pg_stat_activity.pid)
31
- FROM pg_stat_activity
32
- WHERE pg_stat_activity.datname = 'knex_test'
33
- AND pid <> pg_backend_pid()`);
34
- }
35
-
36
- async function killConnectionsMyslq() {
37
- const [rows, colDefs] = await mysql.raw(`SHOW FULL PROCESSLIST`);
38
- await Promise.all(rows.map(row => mysql.raw(`KILL ${row.Id}`)));
39
- }
40
-
41
- const mysqlQueries = [
42
- ...Array(40).fill(mysql('accounts').timeout(100)),
43
- ...Array(40).fill(mysql('accounts'))
44
- ];
45
-
46
- const counters = {
47
- pgQueries: 0,
48
- pgResults: 0,
49
- pgErrors: 0,
50
- mysqlQueries: 0,
51
- mysqlResults: 0,
52
- mysqlErrors: 0
53
- };
54
-
55
- let lastCounters = {...counters};
56
-
57
- pg.on('query', () => counters.pgQueries += 1);
58
- pg.on('query-response', () => counters.pgResults += 1);
59
- pg.on('query-error', () => counters.pgErrors += 1);
60
- mysql.on('query', () => counters.mysqlQueries += 1);
61
- mysql.on('query-response', () => counters.mysqlResults += 1);
62
- mysql.on('query-error', () => counters.mysqlErrors += 1);
63
-
64
- setInterval(() => {
65
- console.dir({
66
- markerPerSecond: '-------------------------------------------------------------------------------------------',
67
- pgQueriesPerSecond: (counters.pgQueries - lastCounters.pgQueries)/2,
68
- pgResultsPerSecond: (counters.pgResults - lastCounters.pgResults)/2,
69
- pgErrorsPerSecond: (counters.pgErrors - lastCounters.pgErrors)/2,
70
- mysqlQueriesPerSecond: (counters.mysqlQueries - lastCounters.mysqlQueries)/2,
71
- mysqlResultsPerSecond: (counters.mysqlResults - lastCounters.mysqlResults)/2,
72
- mysqlErrorsPerSecond: (counters.mysqlErrors - lastCounters.mysqlErrors)/2,
73
- }, {colors: true});
74
-
75
- lastCounters = {...counters};
76
- }, 2000);
77
-
78
- while(true) {
79
- try {
80
- await Promise.all([
81
- ...pgQueries,
82
- killConnectionsPg(),
83
- // ...mysqlQueries,
84
- // killConnectionsMyslq()
85
- ]);
86
- } catch (err) {
87
- console.log(err);
88
- // pass on error and try again...
89
- }
90
- }
91
- }
92
-
93
- main();