ci_mariasql
Version:
A node.js binding to MariaDB's non-blocking (MySQL-compatible) client library
394 lines (359 loc) • 11.7 kB
JavaScript
var lookup = require('dns').lookup,
isIP = require('net').isIP,
inherits = require('util').inherits,
EventEmitter = require('events').EventEmitter;
var LRU = require('lru-cache');
var addon = require('../build/Release/sqlclient');
addon.Client.prototype.__proto__ = EventEmitter.prototype;
function clone(o) {
var n = Object.create(Object.getPrototypeOf(o)),
props = Object.getOwnPropertyNames(o),
pName, p;
for (p in props) {
pName = props[p];
Object.defineProperty(n, pName, Object.getOwnPropertyDescriptor(o, pName));
}
return n;
}
var ABORT_QUERY = 1,
ABORT_RESULTS = 2;
var EMPTY_LRU_FN = function(key, value) {};
// for prepare()
var RE_PARAM = /(?:\?)|(?::(\d+|(?:[a-zA-Z][a-zA-Z0-9_]*)))/g,
DQUOTE = 34,
SQUOTE = 39,
BSLASH = 92;
function Client() {
var self = this;
this.threadId = undefined;
this.connected = false;
this.connecting = false;
this.pingInterval = 60000;
this._keepQueries = undefined;
this._pinger = undefined;
this._queryCache = undefined;
this._closeOnEmpty = false;
this._reusableAfterClose = false;
this._queries = [];
this._curResults = undefined;
this._client = new addon.Client();
// Client-level events
this._client.on('connect', function() {
self.connected = true;
self.connecting = false;
// use CONNECTION_ID() query instead of C library's mysql_thread_id()
// because according to the MySQL docs, mysql_thread_id() does not work
// correctly when the thread ID exceeds 32 bits
self.query('SELECT CONNECTION_ID()', true, true)
.on('result', function(r) {
r.on('row', function(row) {
self.threadId = row[0];
});
})
// caller has no way to attach an error listener to avoid crash
.on('error', function() {})
.on('end', function() {
self.emit('connect');
});
// doing everything in utf8 greatly simplifies things --
// end users can use iconv/iconv-lite if they need to convert to something
// else
self.query("SET NAMES 'utf8'", true, true).on('error', function() {});
});
this._client.on('conn.error', function(err) {
// 'close' event will be fired immediately
// so don't set connected = connecting = false here
// another reason not to set false here is to make sure when connected == connecting == false
// either connect() never called or 'close' event already fired
// and _client.connect() already scheduled by process.nextTick will see destroyed == true
self.emit('error', err);
});
this._client.on('close', function(had_err) {
self.connected = false;
self.connecting = false;
self.threadId = undefined;
self._reset();
if (!self._reusableAfterClose) {
self.destroyed = true;
self._client.removeAllListeners();
}
self.emit('close', had_err);
});
// Results-level events
this._client.on('results.abort', function() {
var r = self._curResults;
self._curResults = undefined;
r && r.emit('abort');
self._processQueries();
});
this._client.on('results.error', function(err) {
if (err.code === 2013 || err.code === 10001/*ERROR_HANGUP*/) {
// connection closed by server
self.emit('error', err);
} else {
var r = self._curResults;
self._curResults = undefined;
r && r.emit('error', err);
self._processQueries();
}
});
this._client.on('results.query', function() {
if (self._curResults) {
self._curResults._curQuery = new Query(self._curResults);
self._curResults.emit('result', self._curResults._curQuery);
}
});
this._client.on('results.done', function() {
var r = self._curResults;
self._curResults = undefined;
r && r.emit('end');
self._processQueries();
});
// Query-level events
var fnQueryErr = function(err) {
if (err.code === 2013 || err.code === 10001/*ERROR_HANGUP*/) {
// connection closed by server
self.emit('error', err);
} else {
if (!self._curResults)
return;
if (self._curResults._curQuery.listeners('error').length > 0)
self._curResults._curQuery.emit('error', err);
else
self._curResults.emit('error', err, self._curResults._curQuery);
}
};
this._client.on('query.error', fnQueryErr);
this._client.on('query.row.error', fnQueryErr);
this._client.on('query.abort', function() {
if (self._curResults)
self._curResults._curQuery.emit('abort');
});
this._client.on('query.row', function(res, metadata) {
if (self._curResults)
self._curResults._curQuery.emit('row', res, metadata);
});
this._client.on('query.done', function(info) {
if (self._curResults) {
var q = self._curResults._curQuery;
self._curResults._curQuery = undefined;
q.emit('end', info);
}
});
}
inherits(Client, EventEmitter);
Client.prototype.isMariaDB = function() {
return this._client.isMariaDB();
};
Client.prototype.connect = function(cfg) {
if (this.connected || this.connecting || this.destroyed)
return;
var self = this;
var ncache = 30, queryCache;
if (typeof cfg.queryCache === 'number')
ncache = cfg.queryCache;
else if (typeof cfg.queryCache === 'object')
queryCache = cfg.queryCache; // assume lru-cache instance
if (cfg.queryCache !== false && !queryCache)
queryCache = LRU({ max: ncache, dispose: EMPTY_LRU_FN });
this._queryCache = queryCache;
this._keepQueries = cfg.keepQueries;
this._reusableAfterClose = cfg.reusableAfterClose
process.nextTick(function() {
if (!isIP(cfg.host)) {
lookup(cfg.host, function(err, address, family) {
if (err)
return self.emit('error', err);
cfg = clone(cfg);
cfg.host = address;
if (cfg.pingInterval)
self.pingInterval = cfg.pingInterval * 1000;
if (!self.destroyed) {
self.connecting = true;
self._client.connect(cfg);
}
});
} else {
if (cfg.pingInterval)
self.pingInterval = cfg.pingInterval * 1000;
if (!self.destroyed) {
self.connecting = true;
self._client.connect(cfg);
}
}
});
};
Client.prototype.end = function() {
if (this._curQuery === undefined && !this._queries.length)
this.destroy();
else {
this._reusableAfterClose = false;
this._closeOnEmpty = true;
}
};
Client.prototype.destroy = function() {
if (this.connected || this.connecting) {
this._reusableAfterClose = false;
this._client.end();
} else {
this.destroyed = true;
this._client.removeAllListeners();
}
};
Client.prototype.query = function(query, values, useArray, promote) {
var results, self = this;
if (Array.isArray(values) || typeof values === 'object')
query = this.prepare(query)(values);
else {
promote = useArray;
useArray = values;
}
results = new Results(this, query, useArray);
if (promote)
this._queries.unshift(results);
else
this._queries.push(results);
// queries can actually start emitting results before the results object is
// returned here, so we defer processing until the next tick
process.nextTick(function() { self._processQueries(); });
return results;
};
Client.prototype.escape = function(str) {
return this._client.escape(str);
};
Client.prototype.prepare = function(query) {
var cqfn;
if (this._queryCache && (cqfn = this._queryCache.get(query)))
return cqfn;
var ppos = RE_PARAM.exec(query), curpos = 0, start = 0, end, parts = [],
i, chr, inQuote = false, escape = false, qchr, tokens = [], qcnt = 0, fn;
if (ppos) {
do {
for (i=curpos,end=ppos.index; i<end; ++i) {
chr = query.charCodeAt(i);
if (chr === BSLASH)
escape = !escape;
else {
if (escape) {
escape = false;
continue;
}
if (inQuote && chr === qchr) {
if (query.charCodeAt(i + 1) === qchr) {
// quote escaped via "" or ''
++i;
continue;
}
inQuote = false;
} else if (chr === DQUOTE || chr === SQUOTE) {
inQuote = true;
qchr = chr;
}
}
}
if (!inQuote) {
parts.push(query.substring(start, end));
tokens.push(ppos[0].length === 1 ? qcnt++ : ppos[1]);
start = end + ppos[0].length;
}
curpos = end + ppos[0].length;
} while (ppos = RE_PARAM.exec(query));
if (tokens.length) {
if (curpos < query.length)
parts.push(query.substring(curpos));
var self = this;
fn = function(values) {
var ret = '', j, len, val;
for (j=0,len=tokens.length; j<len; ++j) {
ret += parts[j];
ret += self._format_value(values[tokens[j]]);
}
if (j < parts.length)
ret += parts[j];
return ret;
};
this._queryCache && this._queryCache.set(query, fn);
return fn;
}
}
return function() { return query; };
};
Client.prototype._format_value = function (v) {
if (Buffer.isBuffer(v)) return "'" + addon.escape(v.toString('utf8')) + "'";
else if (Array.isArray(v)) {
var r = [];
for (var i = 0; i < v.length; i++) r.push(this._format_value(v[i]));
return r.join(',');
}
else if (v !== null) return "'" + addon.escape(v + '') + "'";
else return 'NULL';
};
Client.prototype._reset = function() {
clearTimeout(this._pinger);
this._closeOnEmpty = false;
if (!this._keepQueries)
this._queries = [];
else
// only conn.error and close event cause _reset(),
// neither abort nor end event has been fired for _curResults
// so keep _curResults for future error handling or replay after auto-reconnect
if (this._curResults)
this._queries.unshift(this._curResults);
this._curResults = undefined;
};
Client.prototype._processQueries = function() {
if (this._curResults === undefined && this.connected) {
clearTimeout(this._pinger);
if (this._queries.length) {
this._curResults = this._queries.shift();
this._client.query(this._curResults._query, this._curResults._useArray);
} else if (this._closeOnEmpty)
this.destroy();
else {
var self = this;
this._pinger = setTimeout(function ping() {
self.query('DO 0', true, true).on('error', function() {});
}, this.pingInterval);
}
}
};
function Query(parent) {
this._removed = false;
this._parent = parent;
}
inherits(Query, EventEmitter);
Query.prototype.abort = function(active) {
if (!this._removed) {
// if active perform "KILL QUERY n" on separate connection
this._removed = true;
this._parent.client._client.abortQuery(ABORT_QUERY);
}
return this._removed;
};
function Results(client, query, useArray) {
this.client = client;
this._curQuery = undefined;
this._query = query;
this._useArray = useArray;
this._aborted = false;
}
inherits(Results, EventEmitter);
Results.prototype.abort = function(active) {
if (!this._aborted) {
// if active perform "KILL QUERY n" on separate connection
this._aborted = true;
var i;
if (this === this.client._curResults) {
// abort immediately if we're the currently running query
this.client._client.abortQuery(ABORT_RESULTS);
} else if ((i = this.client._queries.indexOf(this)) > -1) {
// remove from the queue if the query hasn't started executing yet
this.client._queries.splice(i, 1);
this.emit('abort');
}
}
return this._aborted;
};
Client.LIB_VERSION = addon.version();
Client.escape = addon.escape;
module.exports = Client;