x2node-dbos
Version:
SQL database operations.
565 lines (462 loc) • 12.7 kB
JavaScript
;
const BasicDBDriver = require('./basic-driver.js');
/**
* Symbol used to indicate that there is an error on the query object.
*
* @private
* @constant {Symbol}
*/
const HAS_ERROR = Symbol();
/**
* Symbol used to store list of temporary anchor tables created for the
* transaction on the connection object.
*
* @private
* @constant {Symbol}
*/
const ANCHOR_TABLES = Symbol();
/**
* Symbol used to mark a connection that must be destroyed upon release no matter
* what.
*
* @private
* @constant {Symbol}
*/
const DESTROY = Symbol();
/**
* MySQL database driver.
*
* @private
* @memberof module:x2node-dbos
* @inner
* @extends {module:x2node-dbos~BasicDBDriver}
* @implements {module:x2node-dbos.DBDriver}
*/
class MySQLDBDriver extends BasicDBDriver {
constructor(options) {
super(options);
this._charset = ((options && options.databaseCharacterSet) || 'utf8');
}
datetimeLiteral(val) {
return '\'' + (
this._options.useLocalTimezone ?
new Date(val.getTime() - val.getTimezoneOffset() * 60000) : val
).toISOString().substring(0, 23) + '\'';
}
supportsRowLocksWithAggregates() { return true; }
safeLikePatternFromExpr(expr) {
return `REPLACE(REPLACE(REPLACE(${expr}, '\\\\', '\\\\\\\\'),` +
` '%', '\\%'), '_', '\\_')`;
}
stringSubstring(expr, from, len) {
return 'SUBSTRING(' + expr +
', ' + (
(typeof from) === 'number' ?
String(from + 1) : '(' + String(from) + ') + 1'
) +
(len !== undefined ? ', ' + String(len) : '') + ')';
}
nullableConcat() {
return 'CONCAT(' + Array.from(arguments).join(', ') + ')';
}
castToString(expr) {
return `CAST(${expr} AS CHAR)`;
}
patternMatch(expr, pattern, invert, caseSensitive) {
return expr +
' COLLATE ' + this._charset +
(caseSensitive ? '_bin' : '_general_ci') +
(invert ? ' NOT' : '') + ' LIKE ' + pattern;
}
regexpMatch(expr, regexp, invert, caseSensitive) {
return expr +
' COLLATE ' + this._charset +
(caseSensitive ? '_bin' : '_general_ci') +
(invert ? ' NOT' : '') + ' REGEXP ' + regexp;
}
/*datetimeToString(expr) {
return 'DATE_FORMAT(' + expr + ', \'%Y-%m%dT%TZ\')';
}*/
makeRangedSelect(selectStmt, offset, limit) {
return selectStmt + ' LIMIT ' +
(offset > 0 ? String(offset) + ', ' : '') + limit;
}
makeSelectWithLocks(selectStmt, exclusiveLockTables, sharedLockTables) {
return selectStmt + (
exclusiveLockTables && (exclusiveLockTables.length > 0) ?
' FOR UPDATE' : (
sharedLockTables && (sharedLockTables.length > 0) ?
' LOCK IN SHARE MODE' : ''
)
);
}
buildLockTables() {
throw new Error(
'Internal X2 error: transaction scope table locks are' +
' not supported by MySQL.');
}
buildDeleteWithJoins(
fromTableName, fromTableAlias, refTables, filterExpr, filterExprParen) {
const hasRefTables = (refTables && (refTables.length > 0));
const hasFilter = filterExpr;
return 'DELETE ' + fromTableAlias +
' FROM ' + fromTableName + ' AS ' + fromTableAlias +
(
hasRefTables ?
', ' + refTables.map(
t => t.tableName + ' AS ' + t.tableAlias).join(', ') :
''
) +
(
hasRefTables || hasFilter ?
' WHERE ' + (
(
hasRefTables ?
refTables.map(
t => t.joinCondition).join(' AND ') :
''
) + (
hasFilter && hasRefTables ?
' AND ' + (
filterExprParen ?
'(' + filterExpr + ')' : filterExpr
) :
''
) + (
hasFilter && !hasRefTables ? filterExpr : ''
)
) :
''
);
}
buildUpdateWithJoins(
updateTableName, updateTableAlias, sets, refTables, filterExpr,
filterExprParen) {
const hasRefTables = (refTables && (refTables.length > 0));
const hasFilter = filterExpr;
return 'UPDATE ' + updateTableName + ' AS ' + updateTableAlias +
(
hasRefTables ?
', ' + refTables.map(
t => t.tableName + ' AS ' + t.tableAlias).join(', ') :
''
) +
' SET ' + sets.map(
s => updateTableAlias + '.' + s.columnName + ' = ' + s.value)
.join(', ') +
(
hasRefTables || hasFilter ?
' WHERE ' + (
(
hasRefTables ?
refTables.map(
t => t.joinCondition).join(' AND ') :
''
) + (
hasFilter && hasRefTables ?
' AND ' + (
filterExprParen ?
'(' + filterExpr + ')' : filterExpr
) :
''
) + (
hasFilter && !hasRefTables ? filterExpr : ''
)
) :
''
);
}
buildUpsert(tableName, insertColumns, insertValues, uniqueColumn, sets) {
return `INSERT INTO ${tableName} (${insertColumns})` +
` VALUES (${insertValues}) ON DUPLICATE KEY UPDATE ${sets}`;
}
connect(source, handler) {
if ((typeof source.getConnection) === 'function') {
source.getConnection((err, connection) => {
if (err)
handler.onError(err);
else
handler.onSuccess(connection);
});
} else {
source.connect(err => {
if (err)
handler.onError(err);
else
handler.onSuccess(source);
});
}
}
releaseConnection(source, connection, err) {
if ((typeof source.getConnection) === 'function') {
if (err || connection[DESTROY])
connection.destroy();
else
connection.release();
} else {
connection.end();
}
}
startTransaction(connection, handler) {
connection.query('START TRANSACTION', err => {
if (err)
handler.onError(err);
else
handler.onSuccess();
});
}
rollbackTransaction(connection, handler) {
this._finishTransaction(connection, 'ROLLBACK', handler);
}
commitTransaction(connection, handler) {
this._finishTransaction(connection, 'COMMIT', handler);
}
_finishTransaction(connection, command, handler) {
const trace = (handler.trace || function() {});
let sql;
trace(sql = command);
connection.query(sql, err => {
if (err) {
connection[DESTROY] = true;
handler.onError(err);
} else {
const anchorTables = connection[ANCHOR_TABLES];
if (!anchorTables || (anchorTables.length === 0))
return handler.onSuccess();
trace(
sql = 'DROP TEMPORARY TABLE IF EXISTS ' +
anchorTables.join(', ')
);
connection.query(sql, err => {
if (err)
connection[DESTROY] = true;
handler.onSuccess();
});
}
});
}
setSessionVariable(connection, varName, valueExpr, handler) {
connection.query(`SET @${varName} = ${valueExpr}`, err => {
if (err)
handler.onError(err);
else
handler.onSuccess();
});
}
getSessionVariable(connection, varName, type, handler) {
connection.query(`SELECT @${varName}`, (err, result) => {
if (err)
return handler.onError(err);
const valRaw = result[0]['@' + varName];
if (valRaw === null)
return handler.onSuccess();
switch (type) {
case 'number':
handler.onSuccess(Number(valRaw));
break;
case 'boolean':
handler.onSuccess(valRaw ? true : false);
break;
default:
handler.onSuccess(valRaw);
}
});
}
selectIntoAnchorTable(
connection, anchorTableName, topTableName, idColumnName, idExpr,
statementStump, handler) {
const trace = (handler.trace || function() {});
let sql;
trace(
sql = `CREATE TEMPORARY TABLE IF NOT EXISTS ${anchorTableName}` +
' (UNIQUE(id), ord INTEGER UNSIGNED NOT NULL UNIQUE)' +
` AS SELECT ${idColumnName} AS id, 0 AS ord` +
` FROM ${topTableName} WHERE ${idColumnName} IS NULL`
);
connection.query(sql, err => {
if (err)
return handler.onError(err);
let anchorTables = connection[ANCHOR_TABLES];
if (!anchorTables)
connection[ANCHOR_TABLES] = anchorTables = [];
anchorTables.push(anchorTableName);
if (
this._options.mariaDB ||
(this._options.orderMode === 'pre') ||
(
(this._options.orderMode === 'preIfMariaDB') &&
this._isMariaDB(connection)
)
)
trace(sql = (
`INSERT INTO ${anchorTableName} (id, ord) ` +
statementStump.replace(
/\bSELECT\s+\{\*\}\s+FROM\b/i,
`SELECT ${idExpr} AS id, ` +
'(@x2node.ord := @x2node.ord + 1) AS ord FROM ' +
'(SELECT @x2node.ord := 0) AS init,'
)
));
else
trace(sql = (
`INSERT INTO ${anchorTableName} (id, ord) ` +
'SELECT q.id AS id, ' +
'(@x2node.ord := @x2node.ord + 1) AS ord FROM ' +
'(SELECT @x2node.ord := 0) AS init, ' +
'(' + statementStump.replace(
/\bSELECT\s+\{\*\}\s+FROM\b/i,
`SELECT ${idExpr} AS id FROM`) + ') AS q'
));
connection.query(sql, (err, result) => {
if (err)
return handler.onError(err);
handler.onSuccess(result.affectedRows);
});
});
}
executeQuery(connection, statement, handler) {
const query = connection.query(statement);
query[HAS_ERROR] = false;
query.on('error', err => {
if (query[HAS_ERROR])
return;
query[HAS_ERROR] = true;
handler.onError(err);
});
query.on('end', () => {
if (!query[HAS_ERROR])
handler.onSuccess();
});
if (handler.onHeader)
query.on('fields', fields => {
if (query[HAS_ERROR])
return;
try {
handler.onHeader(fields.map(field => field.name));
} catch (err) {
query[HAS_ERROR] = true;
handler.onError(err);
}
});
if (handler.onRow)
query.on('result', row => {
if (query[HAS_ERROR])
return;
try {
handler.onRow(row);
} catch (err) {
query[HAS_ERROR] = true;
handler.onError(err);
}
});
}
executeUpdate(connection, statement, handler) {
connection.query(statement, (err, result) => {
if (err)
handler.onError(err);
else
handler.onSuccess(result.affectedRows);
});
}
executeInsert(connection, statement, handler) {
connection.query(statement, (err, result) => {
if (err)
handler.onError(err);
else
handler.onSuccess(result.insertId);
});
}
createVersionTableIfNotExists(connection, tableName, itemNames, handler) {
const trace = (handler.trace || function() {});
let sql;
trace(
sql = `CREATE TABLE IF NOT EXISTS ${tableName} (` +
'name VARCHAR(64) PRIMARY KEY, ' +
'modified_on TIMESTAMP(3) DEFAULT 0, ' +
'version INTEGER UNSIGNED NOT NULL)'
);
connection.query(sql, err => {
if (err)
return handler.onError(err);
trace(sql = `LOCK TABLES ${tableName} WRITE`);
connection.query(sql, err => {
if (err)
return handler.onError(err);
trace(sql = `SELECT name FROM ${tableName}`);
connection.query(sql, (err, result) => {
if (err) {
trace(sql = 'UNLOCK TABLES');
return connection.query(
sql, () => handler.onError(err));
}
const namesToInsert = new Set(itemNames);
for (let i = 0, len = result.length; i < len; i++)
namesToInsert.delete(result[i].name);
if (namesToInsert.size > 0) {
trace(
sql = `INSERT INTO ${tableName}` +
' (name, modified_on, version) VALUES ' +
Array.from(namesToInsert).map(name => (
'(' + this.stringLiteral(name) +
', CURRENT_TIMESTAMP, 0)'
)).join(', ')
);
connection.query(sql, err => {
trace(sql = 'UNLOCK TABLES');
connection.query(sql, err2 => {
if (err || err2)
return handler.onError(err || err2);
handler.onSuccess();
});
});
} else {
trace(sql = 'UNLOCK TABLES');
connection.query(sql, err => {
if (err)
return handler.onError(err);
handler.onSuccess();
});
}
});
});
});
}
updateVersionTable(
connection, tableName, itemNames, modificationTimestamp, handler) {
const filterExpr = 'name' + (
itemNames.length === 1 ?
' = ' + this.stringLiteral(itemNames[0]) :
' IN (' + itemNames.map(v => this.stringLiteral(v)).join(', ') +
')'
);
const trace = (handler.trace || function() {});
let sql;
trace(
sql = `UPDATE ${tableName} SET ` +
`modified_on = ${this.datetimeLiteral(modificationTimestamp)}, ` +
`version = version + 1 WHERE ${filterExpr}`
);
connection.query(sql, (err, result) => {
if (err)
return handler.onError(err);
if (result.affectedRows !== itemNames.length)
return handler.onError(new Error(
'Version rows are missing for some of the following' +
' record types: ' + itemNames.join(', ')));
return handler.onSuccess();
});
}
_isMariaDB(connection) {
const handshakePacket = (
(
connection._protocol &&
connection._protocol._handshakeInitializationPacket
) ||
connection._handshakePacket
);
return (
handshakePacket && /-MariaDB$/.test(handshakePacket.serverVersion)
);
}
}
module.exports = MySQLDBDriver;