loopback-connector-redshift
Version:
LoopBack Redshift Connector
505 lines (459 loc) • 14.1 kB
JavaScript
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-connector-redshift
// This file is licensed under the Artistic License 2.0.
// License text available at https://opensource.org/licenses/Artistic-2.0
/*!
* Redshift connector for LoopBack
*/
'use strict';
var SG = require('strong-globalize');
var g = SG();
var postgresql = require('pg');
var SqlConnector = require('loopback-connector').SqlConnector;
var ParameterizedSQL = SqlConnector.ParameterizedSQL;
var util = require('util');
var debug = require('debug')('loopback:connector:redshift');
/**
*
* Initialize the Redshift connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @header Redshift.initialize(dataSource, [callback])
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
if (!postgresql) {
return;
}
var dbSettings = dataSource.settings || {};
dbSettings.host = dbSettings.host || dbSettings.hostname;
dbSettings.user = dbSettings.user || dbSettings.username;
dbSettings.debug = dbSettings.debug || debug.enabled;
dataSource.connector = new Redshift(postgresql, dbSettings);
dataSource.connector.dataSource = dataSource;
if (callback) {
if (dbSettings.lazyConnect) {
process.nextTick(function() {
callback();
});
} else {
dataSource.connecting = true;
dataSource.connector.connect(callback);
}
}
};
/**
* Redshift connector constructor
*
* @param {Redshift} postgresql Redshift node.js binding
* @options {Object} settings An object for the data source settings.
* See [node-postgres documentation](https://github.com/brianc/node-postgres/wiki/Client#parameters).
* @property {String} url URL to the database, such as 'postgres://test:mypassword@localhost:5432/devdb'.
* Other parameters can be defined as query string of the url
* @property {String} hostname The host name or ip address of the Redshift DB server
* @property {Number} port The port number of the Redshift DB Server
* @property {String} user The user name
* @property {String} password The password
* @property {String} database The database name
* @property {Boolean} ssl Whether to try SSL/TLS to connect to server
*
* @constructor
*/
function Redshift(postgresql, settings) {
this.constructor.super_.call(this, 'redshift', settings);
if (settings.url) {
// pg-pool doesn't handle string config correctly
this.clientConfig = {
connectionString: settings.url,
};
} else {
this.clientConfig = settings;
}
this.pg = new postgresql.Pool(this.clientConfig);
this.settings = settings;
if (settings.debug) {
debug('Settings %j', settings);
}
}
// Inherit from loopback-datasource-juggler BaseSQL
util.inherits(Redshift, SqlConnector);
Redshift.prototype.debug = function() {
if (this.settings.debug) {
debug.apply(debug, arguments);
}
};
Redshift.prototype.getDefaultSchemaName = function() {
return 'public';
};
/**
* Connect to Redshift
* @callback {Function} [callback] The callback after the connection is established
*/
Redshift.prototype.connect = function(callback) {
var self = this;
self.pg.connect(function(err, client, done) {
self.client = client;
process.nextTick(done);
callback && callback(err, client);
});
};
/**
* Execute the sql statement
*
* @param {String} sql The SQL statement
* @param {String[]} params The parameter values for the SQL statement
* @param {Object} [options] Options object
* @callback {Function} [callback] The callback after the SQL statement is executed
* @param {String|Error} err The error string or object
* @param {Object[]) data The result from the SQL
*/
Redshift.prototype.executeSQL = function(sql, params, options, callback) {
var self = this;
if (self.settings.debug) {
if (params && params.length > 0) {
self.debug('SQL: %s\nParameters: %j', sql, params);
} else {
self.debug('SQL: %s', sql);
}
}
function executeWithConnection(connection, done) {
connection.query(sql, params, function(err, data) {
// if(err) console.error(err);
if (err && self.settings.debug) {
self.debug(err);
}
if (self.settings.debug && data) self.debug('%j', data);
if (done) {
process.nextTick(function() {
// Release the connection in next tick
done(err);
});
}
var result = null;
if (data) {
switch (data.command) {
case 'DELETE':
case 'UPDATE':
result = {count: data.rowCount};
break;
default:
result = data.rows;
}
}
callback(err ? err : null, result);
});
}
var transaction = options.transaction;
if (transaction && transaction.connection &&
transaction.connector === this) {
debug('Execute SQL within a transaction');
// Do not release the connection
executeWithConnection(transaction.connection, null);
} else {
self.pg.connect(function(err, connection, done) {
if (err) return callback(err);
executeWithConnection(connection, done);
});
}
};
Redshift.prototype.buildInsertReturning = function(model, data, options) {
// Redshift does not support the RETURNING keyword.
return;
};
Redshift.prototype.buildInsertDefaultValues = function(model, data, options) {
return 'DEFAULT VALUES';
};
// FIXME: [rfeng] The native implementation of upsert only works with
// postgresql 9.1 or later as it requres writable CTE
// See https://github.com/strongloop/loopback-connector-postgresql/issues/27
/**
* Update if the model instance exists with the same id or create a new instance
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Object} The updated model instance
*/
/*
Redshift.prototype.updateOrCreate = function (model, data, callback) {
var self = this;
data = self.mapToDB(model, data);
var props = self._categorizeProperties(model, data);
var idColumns = props.ids.map(function(key) {
return self.columnEscaped(model, key); }
);
var nonIdsInData = props.nonIdsInData;
var query = [];
query.push('WITH update_outcome AS (UPDATE ', self.tableEscaped(model), ' SET ');
query.push(self.toFields(model, data, false));
query.push(' WHERE ');
query.push(idColumns.map(function (key, i) {
return ((i > 0) ? ' AND ' : ' ') + key + '=$' + (nonIdsInData.length + i + 1);
}).join(','));
query.push(' RETURNING ', idColumns.join(','), ')');
query.push(', insert_outcome AS (INSERT INTO ', self.tableEscaped(model), ' ');
query.push(self.toFields(model, data, true));
query.push(' WHERE NOT EXISTS (SELECT * FROM update_outcome) RETURNING ', idColumns.join(','), ')');
query.push(' SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome');
var queryParams = [];
nonIdsInData.forEach(function(key) {
queryParams.push(data[key]);
});
props.ids.forEach(function(key) {
queryParams.push(data[key] || null);
});
var idColName = self.idColumn(model);
self.query(query.join(''), queryParams, function(err, info) {
if (err) {
return callback(err);
}
var idValue = null;
if (info && info[0]) {
idValue = info[0][idColName];
}
callback(err, idValue);
});
};
*/
Redshift.prototype.fromColumnValue = function(prop, val) {
if (val == null) {
return val;
}
var type = prop.type && prop.type.name;
if (prop && type === 'Boolean') {
if (typeof val === 'boolean') {
return val;
} else {
return (val === 'Y' || val === 'y' || val === 'T' ||
val === 't' || val === '1');
}
} else {
return val;
}
};
/*!
* Convert to the Database name
* @param {String} name The name
* @returns {String} The converted name
*/
Redshift.prototype.dbName = function(name) {
if (!name) {
return name;
}
// Redshift default to lowercase names
return name.toLowerCase();
};
function escapeIdentifier(str) {
var escaped = '"';
for (var i = 0; i < str.length; i++) {
var c = str[i];
if (c === '"') {
escaped += c + c;
} else {
escaped += c;
}
}
escaped += '"';
return escaped;
}
function escapeLiteral(str) {
var hasBackslash = false;
var escaped = '\'';
for (var i = 0; i < str.length; i++) {
var c = str[i];
if (c === '\'') {
escaped += c + c;
} else if (c === '\\\\') {
escaped += c + c;
hasBackslash = true;
} else {
escaped += c;
}
}
escaped += '\'';
if (hasBackslash === true) {
escaped = ' E' + escaped;
}
return escaped;
}
/*!
* Escape the name for Redshift DB
* @param {String} name The name
* @returns {String} The escaped name
*/
Redshift.prototype.escapeName = function(name) {
if (!name) {
return name;
}
return escapeIdentifier(name);
};
Redshift.prototype.escapeValue = function(value) {
if (typeof value === 'string') {
return escapeLiteral(value);
}
if (typeof value === 'number' || typeof value === 'boolean') {
return value;
}
return value;
};
Redshift.prototype.tableEscaped = function(model) {
var schema = this.schema(model) || 'public';
return this.escapeName(schema) + '.' +
this.escapeName(this.table(model));
};
function buildLimit(limit, offset) {
var clause = [];
if (isNaN(limit)) {
limit = 0;
}
if (isNaN(offset)) {
offset = 0;
}
if (!limit && !offset) {
return '';
}
if (limit) {
clause.push('LIMIT ' + limit);
}
if (offset) {
clause.push('OFFSET ' + offset);
}
return clause.join(' ');
}
Redshift.prototype.applyPagination = function(model, stmt, filter) {
var limitClause = buildLimit(filter.limit, filter.offset || filter.skip);
return stmt.merge(limitClause);
};
Redshift.prototype.buildExpression = function(columnName, operator,
operatorValue, propertyDefinition) {
switch (operator) {
case 'like':
return new ParameterizedSQL(columnName + ' LIKE ?',
[operatorValue]);
case 'nlike':
return new ParameterizedSQL(columnName + ' NOT LIKE ?',
[operatorValue]);
case 'ilike':
return new ParameterizedSQL(columnName + " ILIKE ?",
[operatorValue]);
case 'nilike':
return new ParameterizedSQL(columnName + " NOT ILIKE ?",
[operatorValue]);
case 'regexp':
if (operatorValue.global)
g.warn('{{Redshift}} regex syntax does not respect the {{`g`}} flag');
if (operatorValue.multiline)
g.warn('{{Redshift}} regex syntax does not respect the {{`m`}} flag');
if (operatorValue.ignoreCase)
g.warn('{{Redshift}} regex syntax does not respect the {{`i`}} flag');
var regexOperator = ' ~ ?';
return new ParameterizedSQL(columnName + regexOperator,
[operatorValue.source]);
default:
// invoke the base implementation of `buildExpression`
return this.invokeSuper('buildExpression', columnName, operator,
operatorValue, propertyDefinition);
}
};
/**
* Disconnect from Redshift
* @param {Function} [cb] The callback function
*/
Redshift.prototype.disconnect = function disconnect(cb) {
if (this.pg) {
if (this.settings.debug) {
this.debug('Disconnecting from ' + this.settings.hostname);
}
var pg = this.pg;
this.pg = null;
pg.end(); // This is sync
}
if (cb) {
process.nextTick(cb);
}
};
Redshift.prototype.ping = function(cb) {
this.execute('SELECT 1 AS result', [], cb);
};
Redshift.prototype.getInsertedId = function(model, info) {
var idColName = this.idColumn(model);
var idValue;
if (info && info[0]) {
idValue = info[0][idColName];
}
return idValue;
};
/*!
* Convert property name/value to an escaped DB column value
* @param {Object} prop Property descriptor
* @param {*} val Property value
* @returns {*} The escaped value of DB column
*/
Redshift.prototype.toColumnValue = function(prop, val) {
if (val == null) {
// Redshift complains with NULLs in not null columns
// If we have an autoincrement value, return DEFAULT instead
if (prop.autoIncrement || prop.id) {
return new ParameterizedSQL('DEFAULT');
} else {
return null;
}
}
if (prop.type === String) {
return String(val);
}
if (prop.type === Number) {
if (isNaN(val)) {
// Map NaN to NULL
return val;
}
return val;
}
if (prop.type === Date || prop.type.name === 'Timestamp') {
if (!val.toISOString) {
val = new Date(val);
}
var iso = val.toISOString();
// Pass in date as UTC and make sure Redshift stores using UTC timezone.
// Redshift does not support the TIMESTAMP WITH TIME ZONE data type.
return new ParameterizedSQL({
sql: '?::TIMESTAMP WITHOUT TIME ZONE',
params: [iso],
});
}
if (prop.type === Boolean) {
if (val) {
return true;
} else {
return false;
}
}
if (prop.type.name === 'GeoPoint' || prop.type.name === 'Point') {
throw new Error(g.f('The Point datatype is not supported in Redshift.'));
}
return val;
};
/**
* Get the place holder in SQL for identifiers, such as ??
* @param {String} key Optional key, such as 1 or id
* @returns {String} The place holder
*/
Redshift.prototype.getPlaceholderForIdentifier = function(key) {
throw new Error(g.f('{{Placeholder}} for identifiers is not supported'));
};
/**
* Get the place holder in SQL for values, such as :1 or ?
* @param {String} key Optional key, such as 1 or id
* @returns {String} The place holder
*/
Redshift.prototype.getPlaceholderForValue = function(key) {
return '$' + key;
};
Redshift.prototype.getCountForAffectedRows = function(model, info) {
return info && info.count;
};
require('./discovery')(Redshift);
require('./migration')(Redshift);
require('./transaction')(Redshift);