diff --git a/.gitignore b/.gitignore index 7f7017231..fbceb944c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/test/config.js *.swo *.un~ diff --git a/Makefile b/Makefile index 14c43a5ad..a3cf21c21 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ -test: - @find test/{simple,system}/test-*.js | xargs -n 1 -t node -test-all: test +test-simple: + @find test/simple/test-*.js | xargs -n 1 -t node +test-system: + @find test/system/test-*.js | xargs -n 1 -t node +test-system-slow: @find test/system/slow/test-*.js | xargs -n 1 -t node +# Dangerous tests potentially effect the MySql server settings, don't run these in production! +test-system-dangerous: + @find test/system/dangerous/test-*.js | xargs -n 1 -t node +test: test-simple test-system +test-all: test test-system-slow test-system-dangerous benchmark-node-mysql: @find benchmark/node-mysql/*.js | xargs -n 1 -t node benchmark-php: diff --git a/Readme.md b/Readme.md index ee7ef8efe..6e2b4bd02 100644 --- a/Readme.md +++ b/Readme.md @@ -11,6 +11,7 @@ A node.js module implementing the * Alan Gutierrez ([bigeasy](http://github.com/felixge/node-mysql/commits/master?author=bigeasy)) * Brian ([mscdex](http://github.com/felixge/node-mysql/commits/master?author=mscdex)) * Cal Henderson ([iamcal](http://github.com/felixge/node-mysql/commits/master?author=iamcal)) +* Frank Grimm ([FrankGrimm](http://github.com/felixge/node-mysql/commits/master?author=FrankGrimm)) ## Sponsors @@ -37,6 +38,16 @@ testing. npm install mysql +Or if you don't want to use npm / run the latest source: + + cd ~/.node_libraries + git clone git://github.com/felixge/node-mysql.git mysql + +## Compatibility + +This module should run in any node version >= v0.1.102 (July 26, 2010). +However, using a current version of node is encouraged. + ## Design Goals * TDD: All code is written using test driven development, code coverage should approach 100% @@ -152,7 +163,7 @@ data. Sends a ping command to the server. -### client.useDatbase(database, [cb]) +### client.useDatabase(database, [cb]) Same as issuing a `'USE '` query. @@ -212,7 +223,6 @@ parameter is provided which contains the information from the mysql OK packet. At this point the module is ready to be tried out, but a lot of things are yet to be done: -* Handle timeouts / reconnect * Pause / resume * Remaining mysql commands * Prepared Statements @@ -220,7 +230,7 @@ At this point the module is ready to be tried out, but a lot of things are yet t * Compression * Performance profiling * Handle re-connect after bad credential error (should query queue be kept?) -* ? +* Deal with stale connections / other potential network issues ## License diff --git a/lib/mysql/client.js b/lib/mysql/client.js index ed01e2750..4d7580e48 100644 --- a/lib/mysql/client.js +++ b/lib/mysql/client.js @@ -48,14 +48,13 @@ Client.prototype.connect = function(cb) { var connection = self._connection = new Stream(), parser = self._parser = new Parser(); - connection.connect(self.port, self.host); connection .on('error', function(err) { - if (err.errno == netBinding.ECONNREFUSED) { + if (err.errno & (netBinding.ECONNREFUSED | netBinding.ENOTFOUND)) { if (cb) { cb(err); + return; } - return; } self.emit('error', err); @@ -77,6 +76,7 @@ Client.prototype.connect = function(cb) { self.connected = false; self._prequeue(connect); }); + connection.connect(self.port, self.host); parser .on('packet', function(packet) { diff --git a/lib/mysql/parser.js b/lib/mysql/parser.js index 87bddf428..b737209d9 100644 --- a/lib/mysql/parser.js +++ b/lib/mysql/parser.js @@ -121,6 +121,14 @@ Parser.prototype.write = function(buffer) { // GREETING_PACKET case Parser.GREETING_PROTOCOL_VERSION: + // Nice undocumented MySql gem, the initial greeting can be an error + // packet. Happens for too many connections errors. + if (c === 0xff) { + packet.type = Parser.ERROR_PACKET; + advance(Parser.ERROR_NUMBER); + break; + } + // 1 byte packet.type = Parser.GREETING_PACKET; packet.protocolVersion = c; @@ -245,6 +253,13 @@ Parser.prototype.write = function(buffer) { packet.errorNumber += POWS[packet.index] * c; if (packet.index == 1) { + if (!this.greeted) { + // Turns out error packets are confirming to the 4.0 protocol when + // not greeted yet. Oh MySql, you are such a thing of beauty ... + advance(Parser.ERROR_MESSAGE); + break; + } + advance(); } break; diff --git a/lib/mysql/query.js b/lib/mysql/query.js index 33fcabc66..983bc8c4f 100644 --- a/lib/mysql/query.js +++ b/lib/mysql/query.js @@ -67,7 +67,7 @@ Query.prototype._handlePacket = function(packet) { if (remaining == 0) { self._rowIndex++; - if (self.typeCast) { + if (self.typeCast && buffer !== null) { switch (field.fieldType) { case Query.FIELD_TYPE_TIMESTAMP: case Query.FIELD_TYPE_DATE: diff --git a/package.json b/package.json index 596970239..3150ad4d8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name" : "mysql" -, "version": "0.7.0" -, "dependencies": {"gently": ">=0.8.0"} +, "version": "0.8.0" +, "devDependencies": {"gently": ">=0.8.0"} , "main" : "./lib/mysql" +, "scripts" : { "test" : "make test" } } diff --git a/test/common.js b/test/common.js index d89624c52..47c8e88da 100644 --- a/test/common.js +++ b/test/common.js @@ -2,14 +2,16 @@ var path = require('path'); require.paths.unshift(path.dirname(__dirname)+'/lib'); var sys = require('mysql/sys'); -global.TEST_DB = 'node_mysql_test'; -global.TEST_CONFIG = { - host: 'localhost', - port: 3306, - user: 'root', - password: 'root' -}; +if (module.parent.filename.match(/test\/system/)) { + try { + global.TEST_CONFIG = require('./config'); + } catch (e) { + console.log('Skipping. See test/config.template.js for more information.'); + process.exit(0); + } +} +global.TEST_DB = 'node_mysql_test'; global.TEST_TABLE = 'posts'; global.TEST_FIXTURES = path.join(__dirname, 'fixture'); diff --git a/test/config.template.js b/test/config.template.js new file mode 100644 index 000000000..e927ab8d8 --- /dev/null +++ b/test/config.template.js @@ -0,0 +1,8 @@ +// Copy this file to test/config.js and fill in your own credentials in order +// to run the system test suite. +module.exports = { + host: 'localhost', + port: 3306, + user: 'root', + password: 'root' +}; diff --git a/test/simple/test-client.js b/test/simple/test-client.js index 102c12eea..ca5cbee45 100644 --- a/test/simple/test-client.js +++ b/test/simple/test-client.js @@ -74,17 +74,17 @@ test(function connect() { gently.expect(StreamStub, 'new', function() { CONNECTION = this; - gently.expect(CONNECTION, 'connect', function(port, host) { - assert.equal(port, client.port); - assert.equal(host, client.host); - }); - var events = ['error', 'data', 'end']; gently.expect(CONNECTION, 'on', events.length, function(event, fn) { assert.equal(event, events.shift()); onConnection[event] = fn; return this; }); + + gently.expect(CONNECTION, 'connect', function(port, host) { + assert.equal(port, client.port); + assert.equal(host, client.host); + }); }); gently.expect(ParserStub, 'new', function() { diff --git a/test/simple/test-parser.js b/test/simple/test-parser.js index 83564bd45..9e95eecb6 100644 --- a/test/simple/test-parser.js +++ b/test/simple/test-parser.js @@ -45,12 +45,29 @@ test(function write() { ); })(); - (function testPacketSize() { + (function testPacketNumber() { parser.write(new Buffer([42])); assert.strictEqual(packet.number, 42); assert.equal(parser.state, Parser.GREETING_PROTOCOL_VERSION); })(); + (function testGreetingErrorPacket() { + parser.write(new Buffer([0xff])); + assert.equal(packet.type, Parser.ERROR_PACKET); + assert.equal(parser.state, Parser.ERROR_NUMBER); + + parser.write(new Buffer([5, 2])); + assert.equal(packet.errorNumber, Math.pow(256, 0) * 5 + Math.pow(256, 1) * 2); + + parser.write(new Buffer('Hello World')); + assert.equal(packet.errorMessage, 'Hello World'); + + // Reset back to previous state + packet.type = Parser.GREETING_PACKET; + packet.received = 0; + parser.state = Parser.GREETING_PROTOCOL_VERSION; + })(); + (function testGreetingPacket() { parser.write(new Buffer([15])); assert.equal(packet.type, Parser.GREETING_PACKET); @@ -58,7 +75,7 @@ test(function write() { assert.equal(parser.state, Parser.GREETING_SERVER_VERSION); var VERSION = 'MySql 5.1'; - parser.write(new Buffer(VERSION+'\0')); + parser.write(new Buffer(VERSION+'\0', 'binary')); assert.equal(packet.serverVersion, VERSION); assert.equal(parser.state, Parser.GREETING_THREAD_ID); diff --git a/test/simple/test-query.js b/test/simple/test-query.js index a23acfb98..7a64cccd2 100644 --- a/test/simple/test-query.js +++ b/test/simple/test-query.js @@ -117,7 +117,11 @@ test(function _handlePacket() { r = row.my_field; }); - fn(new Buffer(strValue), 0); + var val = (strValue === null) + ? null + : new Buffer(strValue); + + fn(val, 0); }); query._handlePacket(PACKET); @@ -142,4 +146,6 @@ test(function _handlePacket() { assert.strictEqual(typeCast(Query.FIELD_TYPE_FLOAT, '2.8'), 2.8); assert.strictEqual(typeCast(Query.FIELD_TYPE_DOUBLE, '2.8'), 2.8); assert.strictEqual(typeCast(Query.FIELD_TYPE_NEWDECIMAL, '2.8'), 2.8); + + assert.strictEqual(typeCast(Query.FIELD_TYPE_DATE, null), null); }); diff --git a/test/system/dangerous/test-client-max-connections.js b/test/system/dangerous/test-client-max-connections.js new file mode 100644 index 000000000..45e537d5a --- /dev/null +++ b/test/system/dangerous/test-client-max-connections.js @@ -0,0 +1,58 @@ +require('../../common'); +var Client = require('mysql').Client, + mainClient = Client(TEST_CONFIG), + originalMaxConnections; + + +function setMaxConnectionsToOne() { + mainClient.connect(); + // First we figure out the current max_connections value, so we can restore that after the test + mainClient.query('SHOW VARIABLES WHERE Variable_name = ?', ['max_connections'], function(err, results) { + if (err) throw err; + + originalMaxConnections = parseInt(results[0].Value); + if (originalMaxConnections === 1) { + console.log( + 'MySql already had max_connections set to 1. '+ + 'This probably happened because of a mal-function in this test, so re-setting to the MySql default of 100. '+ + 'If you had a higher value configured, you need to manually fix this now.' + ); + originalMaxConnections = 100; + } + + // Now we set max connections to 1, then we continue with our test + mainClient.query('SET GLOBAL max_connections = ?', [1], function() { + connectTwoClients(); + }); + }); +}; + +function connectTwoClients() { + var client1 = Client(TEST_CONFIG); + client1.connect(function(err) { + if (err) { + // There should be no error for the first connection, but if there is one + // anyway, let's try to at least restore the server config before throwing + restoreMaxConnections(function() { + throw err; + }); + return; + } + + var client2 = Client(TEST_CONFIG); + client2.connect(function(err) { + assert.strictEqual(err.number, Client.ERROR_CON_COUNT_ERROR); + + client1.end(); + restoreMaxConnections(); + }); + }); +} + +function restoreMaxConnections(cb) { + mainClient.query('SET GLOBAL max_connections = ?', [originalMaxConnections], cb); + mainClient.end(); +} + +setMaxConnectionsToOne(); + diff --git a/test/system/test-client-connection-error.js b/test/system/test-client-connection-error.js index c26f0f064..b61f48fdc 100644 --- a/test/system/test-client-connection-error.js +++ b/test/system/test-client-connection-error.js @@ -3,9 +3,10 @@ var Client = require('mysql').Client, client = Client(TEST_CONFIG), gently = new Gently(), ECONNREFUSED = process.binding('net').ECONNREFUSED; + ENOTFOUND = process.binding('net').ENOTFOUND; client.host = 'BADHOST'; client.connect(gently.expect(function connectCb(err, result) { - assert.equal(err.errno, ECONNREFUSED); + assert.ok(err.errno & (ECONNREFUSED | ENOTFOUND)); })); diff --git a/test/system/test-client-query.js b/test/system/test-client-query.js index 6a055de00..93bea5748 100644 --- a/test/system/test-client-query.js +++ b/test/system/test-client-query.js @@ -47,7 +47,7 @@ client.query( var query = client.query( 'INSERT INTO '+TEST_TABLE+' '+ 'SET title = ?, text = ?, created = ?', - ['another entry', 'because 2 entries make a better test', '2010-08-16 12:42:15'] + ['another entry', 'because 2 entries make a better test', null] ); query.on('end', gently.expect(function insertOkCb(packet) { @@ -63,7 +63,8 @@ var query = client.query( assert.equal(results.length, 2); assert.equal(results[1].title, 'another entry'); assert.ok(typeof results[1].id == 'number'); - assert.ok(results[1].created instanceof Date); + assert.ok(results[0].created instanceof Date); + assert.strictEqual(results[1].created, null); client.end(); }) );