mariasql
Version:
A node.js binding to MariaDB's non-blocking (MySQL-compatible) client library
824 lines (743 loc) • 21 kB
JavaScript
var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;
var lookup = require('dns').lookup;
var isIP = require('net').isIP;
var ReadableStream = require('stream').Readable;
var LRU = require('lru-cache');
var addon;
try {
addon = require('../build/Release/sqlclient');
} catch (ex) {
addon = require('../build/Debug/sqlclient');
}
var binding = addon.ClientBinding;
var RE_PARAM = /(?:\?)|(?::(\d+|(?:[a-zA-Z][a-zA-Z0-9_]*)))/g;
var DQUOTE = 34;
var SQUOTE = 39;
var BSLASH = 92;
var EMPTY_LRU_FN = function(key, value) {};
Client.escape = addon.escape;
Client.version = addon.version;
function Client(config) {
if (!(this instanceof Client))
return new Client(config);
EventEmitter.call(this);
this._handle = null;
if (typeof config === 'object' && config !== null)
this._config = config;
else
this._config = {};
var queryCache;
var ncache = 30;
if (typeof this._config.queryCache === 'number')
ncache = this._config.queryCache;
else if (typeof this._config.queryCache === 'object')
queryCache = this._config.queryCache; // Assume lru-cache instance
if (this._config.queryCache !== false && !queryCache)
queryCache = new LRU({ max: ncache, dispose: EMPTY_LRU_FN });
this._req = undefined;
this._queue = [];
this._queryCache = queryCache;
this._handleClosing = false;
this._tmrInactive = undefined;
this._tmrPingWaitRes = undefined;
this.connecting = false;
this.connected = false;
this.closing = false;
this.threadId = undefined;
if (this._config.threadId === false)
return;
// XXX: hack to get thread ID first before any other queries
var self = this;
this._firstQuery = {
str: 'SELECT CONNECTION_ID()',
cb: function(err, rows) {
if (err) {
self.emit('error', err);
self.close(true);
return;
}
self.threadId = rows[0][0];
self.connecting = false;
self.connected = true;
self.emit('ready');
},
result: undefined,
results: undefined,
needMetadata: false,
needColumns: false
};
}
inherits(Client, EventEmitter);
Client.prototype._initHandle = function() {
if (this._handle !== null) {
return;
}
this._handle = new binding({
context: this,
config: this._config,
onconnect: this._onconnect,
onerror: this._onerror,
onidle: this._onidle,
onresultinfo: this._onresultinfo,
onrow: this._onrow,
onresultend: this._onresultend,
onping: this._onping,
onclose: this._onclose,
});
}
Client.prototype.connect = function(config, cb) {
if (typeof config === 'function') {
cb = config;
config = undefined;
}
this._initHandle();
if (this.connecting) {
if (typeof cb === 'function')
this.once('ready', cb);
return;
} else if (this.connected) {
if (typeof cb === 'function')
process.nextTick(cb);
return;
}
if (typeof cb === 'function')
this.once('ready', cb);
if (typeof config === 'object' && config !== null)
this._config = config;
var cfg = this._config;
var self = this;
if (typeof cfg !== 'object')
throw new Error('Missing config');
this.connecting = true;
setImmediate(function() {
// Doing a manual resolve prevents libmariadbclient from doing a blocking
// DNS resolve
self._initHandle();
if (!isIP(cfg.host)) {
lookup(cfg.host, function(err, address, family) {
if (err) {
self.connecting = false;
self.emit('error', err);
return self.emit('close');
}
cfg = clone(cfg);
cfg.host = address;
self._handle.connect(cfg);
});
} else
self._handle.connect(cfg);
});
};
Client.prototype.query = function(str, values, config, cb) {
var req;
var ret;
if (typeof str !== 'string')
throw new Error('Missing query string');
if (typeof values === 'function') {
// query(str, cb)
cb = values;
values = config = undefined;
} else if (typeof config === 'function') {
// query(str, ___, cb)
cb = config;
if (typeof values === 'boolean') {
config = values;
values = undefined;
} else
config = undefined;
} else if (typeof values === 'boolean') {
// query(str, config)
config = values;
values = undefined;
}
if (Array.isArray(values) || (typeof values === 'object' && values !== null))
str = this.prepare(str)(values);
var needColumns = (!config ||
(typeof config === 'object'
&& config !== null
&& !config.useArray));
var needMetadata = ((config && config.metadata === true)
|| this._config.metadata === true);
if (typeof cb === 'function') {
// We are buffering all rows
req = {
cb: cb,
result: undefined,
results: undefined,
metadata: undefined,
str: str,
needColumns: needColumns,
needMetadata: needMetadata,
rowBuilder: undefined
};
} else {
// We are streaming all rows
var hwm = (config && config.hwm) || this._config.streamHWM;
req = {
emitter: new ResultEmitter(this._handle, hwm),
stream: undefined,
str: str,
needColumns: needColumns,
needMetadata: needMetadata,
rowBuilder: undefined
};
ret = req.emitter;
}
this._queue.push(req);
if (!this.connected)
this.connect();
else if (this._req === undefined) {
var self = this;
process.nextTick(function() {
self._processQueue(false);
});
}
return ret;
};
Client.prototype.close = function(force) {
if (!this.closing && (this.connected || this.connecting)) {
this.closing = true;
if (force || (this._req === undefined && this._queue.length === 0)) {
this._handleClosing = true;
this._handle.close();
}
}
};
Client.prototype.destroy = function() {
this.close(true);
};
Client.prototype.end = Client.prototype.close;
Client.prototype.abort = function(killConn, cb) {
// TODO: support MariaDB-specific kill options
if (!this._req)
return;
if (typeof this.threadId !== 'string' || !this.threadId.length)
throw new Error('Cannot abort: no thread id');
if (typeof killConn === 'function') {
cb = killConn;
killConn = false;
}
var kind = (killConn ? 'CONNECTION' : 'QUERY');
var querystr = 'KILL ' + kind + ' ' + this.threadId;
var calledBack = false;
var abortClient = new Client(this._config);
abortClient._firstQuery = null;
abortClient.on('error', handler);
abortClient.connect(function() {
abortClient.query(querystr, handler);
abortClient.end();
});
function handler(err) {
if (calledBack)
return;
calledBack = true;
cb && cb(err);
}
};
Client.prototype.isMariaDB = function() {
this._initHandle();
return this._handle.isMariaDB();
};
Client.prototype.lastInsertId = function() {
this._initHandle();
return this._handle.lastInsertId();
};
Client.prototype.escape = function(str) {
this._initHandle();
return this._handle.escape(str);
};
Client.prototype.serverVersion = function() {
this._initHandle();
return this._handle.serverVersion();
};
Client.prototype.prepare = function(query) {
var cache = this._queryCache;
var cqfn;
if (cache && (cqfn = cache.get(query)))
return cqfn;
var ppos = RE_PARAM.exec(query);
var curpos = 0;
var start = 0;
var parts = [];
var wasInQuote = false;
var inQuote = false;
var escape = false;
var tokens = [];
var qcnt = 0;
var qchr;
var chr;
var end;
var fn;
var i;
if (ppos) {
do {
wasInQuote = inQuote;
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.slice(start, end));
if (wasInQuote)
tokens.push(null);
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 (end < query.length)
parts.push(query.slice(start));
var self = this;
fn = function(values) {
var ret = '';
for (var j = 0, t = 0; j < tokens.length; ++j) {
ret += parts[j];
if (tokens[j] === null)
continue;
ret += self._format_value(values[tokens[j]]);
}
if (j < parts.length)
ret += parts[j];
return ret;
};
var cache = this._queryCache;
cache && cache.set(query, fn);
return fn;
}
}
return function() { return query; };
};
Client.prototype._onconnect = function() {
var queue = this._queue;
if (queue.length === 0 || queue[0] !== this._firstQuery) {
if (this._firstQuery)
queue.unshift(this._firstQuery);
else {
this.connecting = false;
this.connected = true;
this.emit('ready');
}
}
this._processQueue(true);
};
Client.prototype._onerror = function(err) {
if (isDeadConn(err.code)) {
this.connecting = this.connected = false;
this.emit('error', err);
this._onclose(err);
} else {
var req = this._req;
if (req) {
if (req.cb !== undefined) {
var results = req.results;
if (results === undefined)
req.results = [err];
else
results.push(err);
} else {
var stream = req.stream;
if (stream === undefined) {
var emitter = req.emitter;
stream = emitter._createStream();
emitter.emit('result', stream);
stream.emit('error', err);
stream.push(null);
stream.read(0);
} else {
stream.emit('error', err);
stream.push(null);
req.stream = undefined;
}
}
} else
this.emit('error', err);
}
};
Client.prototype._onidle = function() {
var req = this._req;
if (req) {
// A query finished -- no more result sets
this._queue.shift();
this._req = undefined;
var cb = req.cb;
if (cb !== undefined) {
var results = req.results;
if (results.length === 1) {
// Single result set response
var r = results[0];
if (r instanceof Error)
cb(r);
else
cb(null, r);
} else {
// Multi-result set response
// TODO: "null" here can be a bit misleading if 1 of several
// results ended in error (can this even happen in reality?)
cb(null, results);
}
} else {
// Signal to the emitter that when the last QueryStream ends, that it's ok
// to emit 'end' as well ...
req.emitter._done = true;
if (!req.emitter._waitingForEnd)
req.emitter._complete(req.stream);
}
}
this._processQueue(false);
};
Client.prototype._onresultinfo = function(cols, metadata) {
var req = this._req;
if (req.cb !== undefined) {
if (req.needMetadata)
req.metadata = createMetadata(metadata);
} else {
var emitter = req.emitter;
var stream = req.stream = emitter._createStream();
if (req.needMetadata)
stream.info.metadata = createMetadata(metadata);
emitter.emit('result', stream);
}
if (req.needColumns)
req.rowBuilder = createRowBuilder(cols);
};
Client.prototype._onrow = function(row) {
var req = this._req;
var builder = req.rowBuilder;
if (req.cb !== undefined) {
if (builder) {
for (var i = 0; i < row.length; ++i)
row[i] = builder(row[i]);
}
req.result = row;
} else {
if (builder)
row = builder(row);
var stream = req.stream;
if (stream === undefined) {
var emitter = req.emitter;
stream = req.stream = emitter._createStream();
emitter.emit('result', stream);
}
if (stream.push(row) === false) {
this._handle.pause();
stream._needResume = true;
}
}
};
Client.prototype._onresultend = function(numRows, affectedRows, insertId) {
var req = this._req;
if (req.cb !== undefined) {
var results = req.results;
var result = req.result;
if (result === undefined) {
result = {
info: {
numRows: numRows,
affectedRows: affectedRows,
insertId: insertId,
metadata: req.metadata
}
};
} else {
req.result = undefined;
result.info = {
numRows: numRows,
affectedRows: affectedRows,
insertId: insertId,
metadata: req.metadata
};
}
req.metadata = undefined;
if (results !== undefined)
results.push(result);
else
req.results = [result];
} else {
var stream = req.stream;
if (stream === undefined) {
var emitter = req.emitter;
stream = emitter._createStream();
stream.info.numRows = numRows;
stream.info.affectedRows = affectedRows;
stream.info.insertId = insertId;
emitter.emit('result', stream);
stream.push(null);
stream.read(0);
} else {
stream.info.numRows = numRows;
stream.info.affectedRows = affectedRows;
stream.info.insertId = insertId;
stream.push(null);
req.stream = undefined;
}
}
};
Client.prototype._onping = function() {
clearTimeout(this._tmrPingWaitRes);
this._tmrPingWaitRes = undefined;
this._tmrInactive = undefined;
if (this._queue.length === 0)
this._ping();
};
Client.prototype._onclose = function(err) {
var self = this;
clearTimeout(this._tmrInactive);
clearTimeout(this._tmrPingWaitRes);
this._tmrInactive = undefined;
this._tmrPingWaitRes = undefined;
this.connecting = false;
this.connected = false;
this.closing = false;
this._handleClosing = false;
var keepQueries = this._config.keepQueries;
if (keepQueries === false || keepQueries === undefined) {
if (this._req !== undefined)
this._queue.unshift(this._req);
cleanupReqs(this._queue, err);
this._queue = [];
} else if (this._req !== undefined) {
// No easy way to "recover" the current request, so just remove it and clean
// it up
cleanupReqs([this._queue.shift()], err);
}
this._req = undefined;
if (!err)
this.emit('end');
this.emit('close');
// Allow addon handle to be garbage collected since we are no longer connected
// See: https://github.com/mscdex/node-mariasql/pull/130
// https://github.com/mscdex/node-mariasql/pull/133
process.nextTick(function() {
if (!self.connecting
&& !self.connected
&& !self.closing
&& !self._handleClosing) {
self._handle = null;
}
});
};
Client.prototype._processQueue = function(ignoreConnected) {
var connected = this.connected;
if (!ignoreConnected && !connected)
return;
var req = this._req;
if (req)
return;
var queue = this._queue;
if (queue.length > 0) {
// Allow an outstanding ping request to finish first
if (this._tmrPingWaitRes !== undefined)
return;
clearTimeout(this._tmrInactive);
this._tmrInactive = undefined;
req = this._req = queue[0];
this._handle.query(req.str,
req.needColumns,
req.needMetadata,
req.cb !== undefined);
} else if (connected) {
if (this.closing && !this._handleClosing) {
this._handleClosing = true;
this._handle.close();
} else if (!this.closing)
this._ping();
}
};
function pingNoAnswer(self) {
self.emit('error', new Error('Ping response lost'));
self.close(true);
}
function pingCb(self) {
self._tmrPingWaitRes = setTimeout(pingNoAnswer,
self._config.pingWaitRes,
self);
self._handle.ping();
}
Client.prototype._ping = function() {
if (this._tmrInactive === undefined
&& typeof this._config.pingInactive === 'number'
&& typeof this._config.pingWaitRes === 'number'
&& this._config.pingInactive > 0
&& this._config.pingWaitRes > 0) {
this._tmrInactive = setTimeout(pingCb, this._config.pingInactive, this);
}
};
Client.prototype._format_value = function(v) {
if (Buffer.isBuffer(v))
return "'" + Client.escape(v.toString('utf8')) + "'";
else if (Array.isArray(v)) {
var r = [];
for (var i = 0, len = v.length; i < len; ++i)
r.push(this._format_value(v[i]));
return r.join(',');
} else if (v !== null && v !== undefined)
return "'" + Client.escape(v + '') + "'";
return 'NULL';
};
var QueryStreamDefaultOpts = { objectMode: true };
function ResultEmitter(handle, hwm) {
EventEmitter.call(this);
if (typeof hwm === 'number')
this._streamOpts = { objectMode: true, highWaterMark: hwm };
else
this._streamOpts = QueryStreamDefaultOpts;
this._handle = handle;
this._done = false;
this._waitingForEnd = false;
}
inherits(ResultEmitter, EventEmitter);
ResultEmitter.prototype._createStream = function() {
var qs = new QueryStream(this._handle, this._streamOpts);
var self = this;
this._waitingForEnd = true;
qs.on('end', function() {
self._waitingForEnd = false;
self._complete(qs);
});
return qs;
};
ResultEmitter.prototype._complete = function(qs) {
if (this._done) {
if (qs && EventEmitter.listenerCount(qs, 'end') > 1) {
// We emit 'end' on the next tick to ensure proper event ordering
// (otherwise a user's QueryStream 'end' listener will get called
// *after* the ResultEmitter's 'end' because we add our 'end' listener
// first)
var self = this;
process.nextTick(function() {
self.emit('end');
});
} else
this.emit('end');
}
};
function QueryStream(handle, opts) {
ReadableStream.call(this, opts);
this._handle = handle;
this._needResume = false;
this.info = {
numRows: undefined,
affectedRows: undefined,
insertId: undefined,
metadata: undefined
};
}
inherits(QueryStream, ReadableStream);
QueryStream.prototype._read = function(n) {
if (this._needResume) {
this._needResume = false;
this._handle.resume();
}
};
function clone(obj) {
var ret = {};
var keys = Object.keys(obj);
var key;
for (var i = 0; i < keys.length; ++i) {
key = keys[i];
ret[key] = obj[key];
}
return ret;
}
function cleanupReqs(queue, err) {
var len = queue.length;
if (!err) {
err = new Error('Connection closed early');
err.code = -1;
}
for (var i = 0, req; i < len; ++i) {
req = queue[i];
if (req.cb !== undefined)
req.cb(err);
else {
req.emitter._done = true;
var stream = req.stream;
if (stream && stream.readable) {
stream.emit('error', err);
stream.push(null);
}
}
}
}
function isDeadConn(code) {
return (code === 2006 || code === 2013 || code === 2055);
}
function createRowBuilder(cols) {
var fn = 'return {';
for (var i = 0; i < cols.length; ++i)
fn += JSON.stringify(cols[i]) + ': v[' + i + '],';
return new Function('v', fn + '}');
}
function createMetadata(data) {
var result = {};
var len = data.length;
if (len > 0) {
for (var i = 0, name; i < len;) {
name = data[i++];
result[name] = {
org_name: data[i++],
type: data[i++],
flags: data[i++],
charsetnr: data[i++],
db: data[i++],
table: data[i++],
org_table: data[i++]
};
}
// TODO: need (fastest) way to make fast properties for metadata object
}
return result;
}
// Field cannot be NULL
Client.NOT_NULL_FLAG = 1;
// Field is part of a primary key
Client.PRI_KEY_FLAG = 2;
// Field is part of a unique key
Client.UNIQUE_KEY_FLAG = 4;
// Field is part of a nonunique key
Client.MULTIPLE_KEY_FLAG = 8;
// Field is a BLOB or TEXT (deprecated)
Client.BLOB_FLAG = 16;
// Field has the UNSIGNED attribute
Client.UNSIGNED_FLAG = 32;
// Field has the ZEROFILL attribute
Client.ZEROFILL_FLAG = 64;
// Field has the BINARY attribute
Client.BINARY_FLAG = 128;
// Field is an ENUM
Client.ENUM_FLAG = 256;
// Field has the AUTO_INCREMENT attribute
Client.AUTO_INCREMENT_FLAG = 512;
// Field is a TIMESTAMP (deprecated)
Client.TIMESTAMP_FLAG = 1024;
// Field is a SET
Client.SET_FLAG = 2048;
// Field has no default value
Client.NO_DEFAULT_VALUE_FLAG = 4096;
// Field is set to NOW on UPDATE
Client.ON_UPDATE_NOW_FLAG = 8192;
// Field is part of some key
Client.PART_KEY_FLAG = 16384;
// Field is numeric
Client.NUM_FLAG = 32768;
module.exports = Client;