944 lines
31 KiB
JavaScript
944 lines
31 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
/* jshint -W110 */
|
||
|
|
var Utils = require('../../utils')
|
||
|
|
, hstore = require('./hstore')
|
||
|
|
, range = require('./range')
|
||
|
|
, util = require('util')
|
||
|
|
, DataTypes = require('../../data-types')
|
||
|
|
, SqlString = require('../../sql-string')
|
||
|
|
, AbstractQueryGenerator = require('../abstract/query-generator')
|
||
|
|
, primaryKeys = {};
|
||
|
|
|
||
|
|
var QueryGenerator = {
|
||
|
|
options: {},
|
||
|
|
dialect: 'postgres',
|
||
|
|
|
||
|
|
createSchema: function(schema) {
|
||
|
|
var query = 'CREATE SCHEMA <%= schema%>;';
|
||
|
|
return Utils._.template(query)({schema: schema});
|
||
|
|
},
|
||
|
|
|
||
|
|
dropSchema: function(schema) {
|
||
|
|
var query = 'DROP SCHEMA <%= schema%> CASCADE;';
|
||
|
|
return Utils._.template(query)({schema: schema});
|
||
|
|
},
|
||
|
|
|
||
|
|
showSchemasQuery: function() {
|
||
|
|
return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';";
|
||
|
|
},
|
||
|
|
|
||
|
|
versionQuery: function() {
|
||
|
|
return 'SELECT VERSION() as "version"';
|
||
|
|
},
|
||
|
|
|
||
|
|
createTableQuery: function(tableName, attributes, options) {
|
||
|
|
var self = this;
|
||
|
|
|
||
|
|
options = Utils._.extend({
|
||
|
|
}, options || {});
|
||
|
|
|
||
|
|
primaryKeys[tableName] = [];
|
||
|
|
|
||
|
|
var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)<%= comments %>'
|
||
|
|
, comments = ''
|
||
|
|
, attrStr = []
|
||
|
|
, i;
|
||
|
|
|
||
|
|
if (options.comment && Utils._.isString(options.comment)) {
|
||
|
|
comments += '; COMMENT ON TABLE <%= table %> IS ' + this.escape(options.comment);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (var attr in attributes) {
|
||
|
|
if ((i = attributes[attr].indexOf('COMMENT')) !== -1) {
|
||
|
|
// Move comment to a seperate query
|
||
|
|
comments += '; ' + attributes[attr].substring(i);
|
||
|
|
attributes[attr] = attributes[attr].substring(0, i);
|
||
|
|
}
|
||
|
|
|
||
|
|
var dataType = this.pgDataTypeMapping(tableName, attr, attributes[attr]);
|
||
|
|
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType);
|
||
|
|
}
|
||
|
|
|
||
|
|
var values = {
|
||
|
|
table: this.quoteTable(tableName),
|
||
|
|
attributes: attrStr.join(', '),
|
||
|
|
comments: Utils._.template(comments)({ table: this.quoteTable(tableName)})
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!!options.uniqueKeys) {
|
||
|
|
Utils._.each(options.uniqueKeys, function(columns) {
|
||
|
|
if (!columns.singleField) { // If it's a single field its handled in column def, not as an index
|
||
|
|
values.attributes += ', UNIQUE (' + columns.fields.map(function(f) { return self.quoteIdentifiers(f); }).join(', ') + ')';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
var pks = primaryKeys[tableName].map(function(pk) {
|
||
|
|
return this.quoteIdentifier(pk);
|
||
|
|
}.bind(this)).join(',');
|
||
|
|
|
||
|
|
if (pks.length > 0) {
|
||
|
|
values.attributes += ', PRIMARY KEY (' + pks + ')';
|
||
|
|
}
|
||
|
|
|
||
|
|
return Utils._.template(query)(values).trim() + ';';
|
||
|
|
},
|
||
|
|
|
||
|
|
dropTableQuery: function(tableName, options) {
|
||
|
|
options = options || {};
|
||
|
|
var query = 'DROP TABLE IF EXISTS <%= table %><%= cascade %>;';
|
||
|
|
return Utils._.template(query)({
|
||
|
|
table: this.quoteTable(tableName),
|
||
|
|
cascade: options.cascade ? ' CASCADE' : ''
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
showTablesQuery: function() {
|
||
|
|
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';";
|
||
|
|
},
|
||
|
|
|
||
|
|
describeTableQuery: function(tableName, schema) {
|
||
|
|
if (!schema) {
|
||
|
|
schema = 'public';
|
||
|
|
}
|
||
|
|
|
||
|
|
var query = 'SELECT c.column_name as "Field", c.column_default as "Default", c.is_nullable as "Null", ' +
|
||
|
|
"CASE WHEN c.udt_name = 'hstore' " +
|
||
|
|
'THEN c.udt_name ELSE c.data_type END as "Type", (SELECT array_agg(e.enumlabel) ' +
|
||
|
|
'FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special" ' +
|
||
|
|
'FROM information_schema.columns c WHERE table_name = <%= table %> AND table_schema = <%= schema %>';
|
||
|
|
|
||
|
|
return Utils._.template(query)({
|
||
|
|
table: this.escape(tableName),
|
||
|
|
schema: this.escape(schema)
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
// A recursive parser for nested where conditions
|
||
|
|
parseConditionObject: function(_conditions, path) {
|
||
|
|
var self = this;
|
||
|
|
|
||
|
|
path = path || [];
|
||
|
|
return Utils._.reduce(_conditions, function (r, v, k) { // result, key, value
|
||
|
|
if (Utils._.isObject(v)) {
|
||
|
|
r = r.concat(self.parseConditionObject(v, path.concat(k))); // Recursively parse objects
|
||
|
|
} else {
|
||
|
|
r.push({ path: path.concat(k), value: v });
|
||
|
|
}
|
||
|
|
return r;
|
||
|
|
}, []);
|
||
|
|
},
|
||
|
|
|
||
|
|
handleSequelizeMethod: function (smth, tableName, factory, options, prepend) {
|
||
|
|
var _ = Utils._;
|
||
|
|
if (smth instanceof Utils.json) {
|
||
|
|
// Parse nested object
|
||
|
|
if (smth.conditions) {
|
||
|
|
var conditions = _.map(this.parseConditionObject(smth.conditions), function generateSql(condition) {
|
||
|
|
return util.format("%s#>>'{%s}' = '%s'",
|
||
|
|
_.first(condition.path),
|
||
|
|
_.rest(condition.path).join(','),
|
||
|
|
condition.value);
|
||
|
|
});
|
||
|
|
|
||
|
|
return conditions.join(' and ');
|
||
|
|
} else if (smth.path) {
|
||
|
|
var str;
|
||
|
|
|
||
|
|
// Allow specifying conditions using the postgres json syntax
|
||
|
|
if (_.any(['->', '->>', '#>'], _.partial(_.contains, smth.path))) {
|
||
|
|
str = smth.path;
|
||
|
|
} else {
|
||
|
|
// Also support json dot notation
|
||
|
|
var path = smth.path.split('.');
|
||
|
|
str = util.format("%s#>>'{%s}'",
|
||
|
|
_.first(path),
|
||
|
|
_.rest(path).join(','));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (smth.value) {
|
||
|
|
str += util.format(' = %s', this.escape(smth.value));
|
||
|
|
}
|
||
|
|
|
||
|
|
return str;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return AbstractQueryGenerator.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
addColumnQuery: function(table, key, dataType) {
|
||
|
|
var query = 'ALTER TABLE <%= table %> ADD COLUMN <%= attribute %>;'
|
||
|
|
, dbDataType = this.attributeToSQL(dataType, {context: 'addColumn'})
|
||
|
|
, attribute;
|
||
|
|
|
||
|
|
if (dataType.type && dataType.type instanceof DataTypes.ENUM || dataType instanceof DataTypes.ENUM) {
|
||
|
|
query = this.pgEnum(table, key, dataType) + query;
|
||
|
|
}
|
||
|
|
|
||
|
|
attribute = Utils._.template('<%= key %> <%= definition %>')({
|
||
|
|
key: this.quoteIdentifier(key),
|
||
|
|
definition: this.pgDataTypeMapping(table, key, dbDataType)
|
||
|
|
});
|
||
|
|
|
||
|
|
return Utils._.template(query)({
|
||
|
|
table: this.quoteTable(table),
|
||
|
|
attribute: attribute
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
removeColumnQuery: function(tableName, attributeName) {
|
||
|
|
var query = 'ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;';
|
||
|
|
return Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
attributeName: this.quoteIdentifier(attributeName)
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
changeColumnQuery: function(tableName, attributes) {
|
||
|
|
var query = 'ALTER TABLE <%= tableName %> ALTER COLUMN <%= query %>;'
|
||
|
|
, sql = [];
|
||
|
|
|
||
|
|
for (var attributeName in attributes) {
|
||
|
|
var definition = this.pgDataTypeMapping(tableName, attributeName, attributes[attributeName]);
|
||
|
|
var attrSql = '';
|
||
|
|
|
||
|
|
if (definition.indexOf('NOT NULL') > 0) {
|
||
|
|
attrSql += Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
query: this.quoteIdentifier(attributeName) + ' SET NOT NULL'
|
||
|
|
});
|
||
|
|
|
||
|
|
definition = definition.replace('NOT NULL', '').trim();
|
||
|
|
} else {
|
||
|
|
attrSql += Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
query: this.quoteIdentifier(attributeName) + ' DROP NOT NULL'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (definition.indexOf('DEFAULT') > 0) {
|
||
|
|
attrSql += Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
query: this.quoteIdentifier(attributeName) + ' SET DEFAULT ' + definition.match(/DEFAULT ([^;]+)/)[1]
|
||
|
|
});
|
||
|
|
|
||
|
|
definition = definition.replace(/(DEFAULT[^;]+)/, '').trim();
|
||
|
|
} else {
|
||
|
|
attrSql += Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
query: this.quoteIdentifier(attributeName) + ' DROP DEFAULT'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attributes[attributeName].match(/^ENUM\(/)) {
|
||
|
|
query = this.pgEnum(tableName, attributeName, attributes[attributeName]) + query;
|
||
|
|
definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier('enum_' + tableName + '_' + attributeName));
|
||
|
|
definition += ' USING (' + this.quoteIdentifier(attributeName) + '::' + this.quoteIdentifier(definition) + ')';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (definition.match(/UNIQUE;*$/)) {
|
||
|
|
definition = definition.replace(/UNIQUE;*$/, '');
|
||
|
|
|
||
|
|
attrSql += Utils._.template(query.replace('ALTER COLUMN', ''))({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
query: 'ADD CONSTRAINT ' + this.quoteIdentifier(attributeName + '_unique_idx') + ' UNIQUE (' + this.quoteIdentifier(attributeName) + ')'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
attrSql += Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition
|
||
|
|
});
|
||
|
|
|
||
|
|
sql.push(attrSql);
|
||
|
|
}
|
||
|
|
|
||
|
|
return sql.join('');
|
||
|
|
},
|
||
|
|
|
||
|
|
renameColumnQuery: function(tableName, attrBefore, attributes) {
|
||
|
|
var query = 'ALTER TABLE <%= tableName %> RENAME COLUMN <%= attributes %>;';
|
||
|
|
var attrString = [];
|
||
|
|
|
||
|
|
for (var attributeName in attributes) {
|
||
|
|
attrString.push(Utils._.template('<%= before %> TO <%= after %>')({
|
||
|
|
before: this.quoteIdentifier(attrBefore),
|
||
|
|
after: this.quoteIdentifier(attributeName)
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
return Utils._.template(query)({
|
||
|
|
tableName: this.quoteTable(tableName),
|
||
|
|
attributes: attrString.join(', ')
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
fn: function(fnName, tableName, body, returns, language) {
|
||
|
|
fnName = fnName || 'testfunc';
|
||
|
|
language = language || 'plpgsql';
|
||
|
|
returns = returns || 'SETOF ' + this.quoteTable(tableName);
|
||
|
|
|
||
|
|
var query = 'CREATE OR REPLACE FUNCTION pg_temp.<%= fnName %>() RETURNS <%= returns %> AS $func$ BEGIN <%= body %> END; $func$ LANGUAGE <%= language %>; SELECT * FROM pg_temp.<%= fnName %>();';
|
||
|
|
|
||
|
|
return Utils._.template(query)({
|
||
|
|
fnName: fnName,
|
||
|
|
returns: returns,
|
||
|
|
language: language,
|
||
|
|
body: body
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
exceptionFn: function(fnName, tableName, main, then, when, returns, language) {
|
||
|
|
when = when || 'unique_violation';
|
||
|
|
|
||
|
|
var body = '<%= main %> EXCEPTION WHEN <%= when %> THEN <%= then %>;';
|
||
|
|
body = Utils._.template(body)({
|
||
|
|
main: main,
|
||
|
|
when: when,
|
||
|
|
then: then
|
||
|
|
});
|
||
|
|
|
||
|
|
return this.fn(fnName, tableName, body, returns, language);
|
||
|
|
},
|
||
|
|
|
||
|
|
// http://www.maori.geek.nz/post/postgres_upsert_update_or_insert_in_ger_using_knex_js
|
||
|
|
upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) {
|
||
|
|
var insert = this.insertQuery(tableName, insertValues, rawAttributes, options);
|
||
|
|
var update = this.updateQuery(tableName, updateValues, where, options, rawAttributes);
|
||
|
|
|
||
|
|
// The numbers here are selected to match the number of affected rows returned by MySQL
|
||
|
|
return this.exceptionFn(
|
||
|
|
'sequelize_upsert',
|
||
|
|
tableName,
|
||
|
|
insert + ' RETURN 1;',
|
||
|
|
update + '; RETURN 2',
|
||
|
|
'unique_violation',
|
||
|
|
'integer'
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
bulkInsertQuery: function(tableName, attrValueHashes, options, modelAttributes) {
|
||
|
|
options = options || {};
|
||
|
|
|
||
|
|
var query = 'INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>'
|
||
|
|
, tuples = []
|
||
|
|
, serials = []
|
||
|
|
, allAttributes = [];
|
||
|
|
|
||
|
|
if (options.returning) {
|
||
|
|
query += ' RETURNING *';
|
||
|
|
}
|
||
|
|
|
||
|
|
Utils._.forEach(attrValueHashes, function(attrValueHash) {
|
||
|
|
Utils._.forOwn(attrValueHash, function(value, key) {
|
||
|
|
if (allAttributes.indexOf(key) === -1) {
|
||
|
|
allAttributes.push(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (modelAttributes && modelAttributes[key] && modelAttributes[key].autoIncrement === true) {
|
||
|
|
serials.push(key);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
Utils._.forEach(attrValueHashes, function(attrValueHash) {
|
||
|
|
tuples.push('(' +
|
||
|
|
allAttributes.map(function(key) {
|
||
|
|
if (serials.indexOf(key) !== -1) {
|
||
|
|
return attrValueHash[key] || 'DEFAULT';
|
||
|
|
}
|
||
|
|
return this.escape(attrValueHash[key], modelAttributes && modelAttributes[key]);
|
||
|
|
}.bind(this)).join(',') +
|
||
|
|
')');
|
||
|
|
}.bind(this));
|
||
|
|
|
||
|
|
var replacements = {
|
||
|
|
table: this.quoteTable(tableName)
|
||
|
|
, attributes: allAttributes.map(function(attr) {
|
||
|
|
return this.quoteIdentifier(attr);
|
||
|
|
}.bind(this)).join(',')
|
||
|
|
, tuples: tuples.join(',')
|
||
|
|
};
|
||
|
|
|
||
|
|
query = query + ';';
|
||
|
|
|
||
|
|
return Utils._.template(query)(replacements);
|
||
|
|
},
|
||
|
|
|
||
|
|
deleteQuery: function(tableName, where, options, model) {
|
||
|
|
var query;
|
||
|
|
|
||
|
|
options = options || {};
|
||
|
|
|
||
|
|
tableName = Utils.removeTicks(this.quoteTable(tableName), '"');
|
||
|
|
|
||
|
|
if (options.truncate === true) {
|
||
|
|
query = 'TRUNCATE ' + QueryGenerator.quoteIdentifier(tableName);
|
||
|
|
|
||
|
|
if (options.cascade) {
|
||
|
|
query += ' CASCADE';
|
||
|
|
}
|
||
|
|
|
||
|
|
return query;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Utils._.isUndefined(options.limit)) {
|
||
|
|
options.limit = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
primaryKeys[tableName] = primaryKeys[tableName] || [];
|
||
|
|
|
||
|
|
if (!!model && primaryKeys[tableName].length < 1) {
|
||
|
|
primaryKeys[tableName] = Object.keys(model.primaryKeys);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.limit) {
|
||
|
|
query = 'DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %><%= where %><%= limit %>)';
|
||
|
|
} else {
|
||
|
|
query = 'DELETE FROM <%= table %><%= where %>';
|
||
|
|
}
|
||
|
|
|
||
|
|
var pks;
|
||
|
|
if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) {
|
||
|
|
pks = primaryKeys[tableName].map(function(pk) {
|
||
|
|
return this.quoteIdentifier(pk);
|
||
|
|
}.bind(this)).join(',');
|
||
|
|
} else {
|
||
|
|
pks = this.quoteIdentifier('id');
|
||
|
|
}
|
||
|
|
|
||
|
|
var replacements = {
|
||
|
|
table: this.quoteIdentifiers(tableName),
|
||
|
|
where: this.getWhereConditions(where),
|
||
|
|
limit: !!options.limit ? ' LIMIT ' + this.escape(options.limit) : '',
|
||
|
|
primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks,
|
||
|
|
primaryKeysSelection: pks
|
||
|
|
};
|
||
|
|
|
||
|
|
if (replacements.where) {
|
||
|
|
replacements.where = ' WHERE ' + replacements.where;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Utils._.template(query)(replacements);
|
||
|
|
},
|
||
|
|
|
||
|
|
showIndexesQuery: function(tableName) {
|
||
|
|
if (!Utils._.isString(tableName)) {
|
||
|
|
tableName = tableName.tableName;
|
||
|
|
}
|
||
|
|
|
||
|
|
// This is ARCANE!
|
||
|
|
var query = 'SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, ' +
|
||
|
|
'array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) ' +
|
||
|
|
'AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a ' +
|
||
|
|
'WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND '+
|
||
|
|
"t.relkind = 'r' and t.relname = '<%= tableName %>' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;";
|
||
|
|
|
||
|
|
return Utils._.template(query)({ tableName: tableName });
|
||
|
|
},
|
||
|
|
|
||
|
|
removeIndexQuery: function(tableName, indexNameOrAttributes) {
|
||
|
|
var sql = 'DROP INDEX IF EXISTS <%= indexName %>'
|
||
|
|
, indexName = indexNameOrAttributes;
|
||
|
|
|
||
|
|
if (typeof indexName !== 'string') {
|
||
|
|
indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
|
||
|
|
}
|
||
|
|
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
tableName: this.quoteIdentifiers(tableName),
|
||
|
|
indexName: this.quoteIdentifiers(indexName)
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
addLimitAndOffset: function(options) {
|
||
|
|
var fragment = '';
|
||
|
|
if (options.limit) fragment += ' LIMIT ' + options.limit;
|
||
|
|
if (options.offset) fragment += ' OFFSET ' + options.offset;
|
||
|
|
|
||
|
|
return fragment;
|
||
|
|
},
|
||
|
|
|
||
|
|
attributeToSQL: function(attribute) {
|
||
|
|
if (!Utils._.isPlainObject(attribute)) {
|
||
|
|
attribute = {
|
||
|
|
type: attribute
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
var template = '<%= type %>'
|
||
|
|
, replacements = {};
|
||
|
|
|
||
|
|
if (attribute.type instanceof DataTypes.ENUM) {
|
||
|
|
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
|
||
|
|
|
||
|
|
if (Array.isArray(attribute.values) && (attribute.values.length > 0)) {
|
||
|
|
replacements.type = 'ENUM(' + Utils._.map(attribute.values, function(value) {
|
||
|
|
return this.escape(value);
|
||
|
|
}.bind(this)).join(', ') + ')';
|
||
|
|
} else {
|
||
|
|
throw new Error("Values for ENUM haven't been defined.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!replacements.type) {
|
||
|
|
replacements.type = attribute.type;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.hasOwnProperty('allowNull') && (!attribute.allowNull)) {
|
||
|
|
template += ' NOT NULL';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.autoIncrement) {
|
||
|
|
template += ' SERIAL';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||
|
|
template += ' DEFAULT <%= defaultValue %>';
|
||
|
|
replacements.defaultValue = this.escape(attribute.defaultValue, attribute);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.unique === true) {
|
||
|
|
template += ' UNIQUE';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.primaryKey) {
|
||
|
|
template += ' PRIMARY KEY';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.references) {
|
||
|
|
attribute = Utils.formatReferences(attribute);
|
||
|
|
template += ' REFERENCES <%= referencesTable %> (<%= referencesKey %>)';
|
||
|
|
replacements.referencesTable = this.quoteTable(attribute.references.model);
|
||
|
|
|
||
|
|
if (attribute.references.key) {
|
||
|
|
replacements.referencesKey = this.quoteIdentifiers(attribute.references.key);
|
||
|
|
} else {
|
||
|
|
replacements.referencesKey = this.quoteIdentifier('id');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.onDelete) {
|
||
|
|
template += ' ON DELETE <%= onDeleteAction %>';
|
||
|
|
replacements.onDeleteAction = attribute.onDelete.toUpperCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.onUpdate) {
|
||
|
|
template += ' ON UPDATE <%= onUpdateAction %>';
|
||
|
|
replacements.onUpdateAction = attribute.onUpdate.toUpperCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attribute.references.deferrable) {
|
||
|
|
template += ' <%= deferrable %>';
|
||
|
|
replacements.deferrable = attribute.references.deferrable.toString(this);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return Utils._.template(template)(replacements);
|
||
|
|
},
|
||
|
|
|
||
|
|
deferConstraintsQuery: function (options) {
|
||
|
|
return options.deferrable.toString(this);
|
||
|
|
},
|
||
|
|
|
||
|
|
setConstraintQuery: function (columns, type) {
|
||
|
|
var columnFragment = 'ALL';
|
||
|
|
|
||
|
|
if (columns) {
|
||
|
|
columnFragment = columns.map(function (column) {
|
||
|
|
return this.quoteIdentifier(column);
|
||
|
|
}.bind(this)).join(', ');
|
||
|
|
}
|
||
|
|
|
||
|
|
return 'SET CONSTRAINTS ' + columnFragment + ' ' + type;
|
||
|
|
},
|
||
|
|
|
||
|
|
setDeferredQuery: function (columns) {
|
||
|
|
return this.setConstraintQuery(columns, 'DEFERRED');
|
||
|
|
},
|
||
|
|
|
||
|
|
setImmediateQuery: function (columns) {
|
||
|
|
return this.setConstraintQuery(columns, 'IMMEDIATE');
|
||
|
|
},
|
||
|
|
|
||
|
|
attributesToSQL: function(attributes, options) {
|
||
|
|
var result = {}
|
||
|
|
, key
|
||
|
|
, attribute;
|
||
|
|
|
||
|
|
for (key in attributes) {
|
||
|
|
attribute = attributes[key];
|
||
|
|
result[attribute.field || key] = this.attributeToSQL(attribute, options);
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
},
|
||
|
|
|
||
|
|
findAutoIncrementField: function(factory) {
|
||
|
|
var fields = [];
|
||
|
|
|
||
|
|
for (var name in factory.attributes) {
|
||
|
|
var definition = factory.attributes[name];
|
||
|
|
|
||
|
|
if (definition && definition.autoIncrement) {
|
||
|
|
fields.push(name);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return fields;
|
||
|
|
},
|
||
|
|
|
||
|
|
createTrigger: function(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
|
||
|
|
var sql = [
|
||
|
|
'CREATE <%= constraintVal %>TRIGGER <%= triggerName %>'
|
||
|
|
, '<%= eventType %> <%= eventSpec %>'
|
||
|
|
, 'ON <%= tableName %>'
|
||
|
|
, '<%= optionsSpec %>'
|
||
|
|
, 'EXECUTE PROCEDURE <%= functionName %>(<%= paramList %>);'
|
||
|
|
].join('\n\t');
|
||
|
|
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
constraintVal: this.triggerEventTypeIsConstraint(eventType),
|
||
|
|
triggerName: triggerName,
|
||
|
|
eventType: this.decodeTriggerEventType(eventType),
|
||
|
|
eventSpec: this.expandTriggerEventSpec(fireOnSpec),
|
||
|
|
tableName: tableName,
|
||
|
|
optionsSpec: this.expandOptions(optionsArray),
|
||
|
|
functionName: functionName,
|
||
|
|
paramList: this.expandFunctionParamList(functionParams)
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
dropTrigger: function(tableName, triggerName) {
|
||
|
|
var sql = 'DROP TRIGGER <%= triggerName %> ON <%= tableName %> RESTRICT;';
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
triggerName: triggerName,
|
||
|
|
tableName: tableName
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
renameTrigger: function(tableName, oldTriggerName, newTriggerName) {
|
||
|
|
var sql = 'ALTER TRIGGER <%= oldTriggerName %> ON <%= tableName %> RENAME TO <%= newTriggerName%>;';
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
tableName: tableName,
|
||
|
|
oldTriggerName: oldTriggerName,
|
||
|
|
newTriggerName: newTriggerName
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
createFunction: function(functionName, params, returnType, language, body, options) {
|
||
|
|
var sql = ['CREATE FUNCTION <%= functionName %>(<%= paramList %>)'
|
||
|
|
, 'RETURNS <%= returnType %> AS $func$'
|
||
|
|
, 'BEGIN'
|
||
|
|
, '\t<%= body %>'
|
||
|
|
, 'END;'
|
||
|
|
, "$func$ language '<%= language %>'<%= options %>;"
|
||
|
|
].join('\n');
|
||
|
|
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
functionName: functionName,
|
||
|
|
paramList: this.expandFunctionParamList(params),
|
||
|
|
returnType: returnType,
|
||
|
|
body: body.replace('\n', '\n\t'),
|
||
|
|
language: language,
|
||
|
|
options: this.expandOptions(options)
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
dropFunction: function(functionName, params) {
|
||
|
|
// RESTRICT is (currently, as of 9.2) default but we'll be explicit
|
||
|
|
var sql = 'DROP FUNCTION <%= functionName %>(<%= paramList %>) RESTRICT;';
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
functionName: functionName,
|
||
|
|
paramList: this.expandFunctionParamList(params)
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
renameFunction: function(oldFunctionName, params, newFunctionName) {
|
||
|
|
var sql = 'ALTER FUNCTION <%= oldFunctionName %>(<%= paramList %>) RENAME TO <%= newFunctionName %>;';
|
||
|
|
return Utils._.template(sql)({
|
||
|
|
oldFunctionName: oldFunctionName,
|
||
|
|
paramList: this.expandFunctionParamList(params),
|
||
|
|
newFunctionName: newFunctionName
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
databaseConnectionUri: function(config) {
|
||
|
|
var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %><% if(ssl) { %>?ssl=<%= ssl %><% } %>';
|
||
|
|
|
||
|
|
return Utils._.template(template)({
|
||
|
|
user: config.username,
|
||
|
|
password: config.password,
|
||
|
|
database: config.database,
|
||
|
|
host: config.host,
|
||
|
|
port: config.port,
|
||
|
|
protocol: config.protocol,
|
||
|
|
ssl: config.ssl
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
pgEscapeAndQuote: function(val) {
|
||
|
|
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"));
|
||
|
|
},
|
||
|
|
|
||
|
|
expandFunctionParamList: function expandFunctionParamList(params) {
|
||
|
|
if (Utils._.isUndefined(params) || !Utils._.isArray(params)) {
|
||
|
|
throw new Error('expandFunctionParamList: function parameters array required, including an empty one for no arguments');
|
||
|
|
}
|
||
|
|
|
||
|
|
var paramList = Utils._.each(params, function expandParam(curParam) {
|
||
|
|
var paramDef = [];
|
||
|
|
if (Utils._.has(curParam, 'type')) {
|
||
|
|
if (Utils._.has(curParam, 'direction')) { paramDef.push(curParam.direction); }
|
||
|
|
if (Utils._.has(curParam, 'name')) { paramDef.push(curParam.name); }
|
||
|
|
paramDef.push(curParam.type);
|
||
|
|
} else {
|
||
|
|
throw new Error('createFunction called with a parameter with no type');
|
||
|
|
}
|
||
|
|
return paramDef.join(' ');
|
||
|
|
});
|
||
|
|
return paramList.join(', ');
|
||
|
|
},
|
||
|
|
|
||
|
|
expandOptions: function expandOptions(options) {
|
||
|
|
return Utils._.isUndefined(options) || Utils._.isEmpty(options) ?
|
||
|
|
'' : '\n\t' + options.join('\n\t');
|
||
|
|
},
|
||
|
|
|
||
|
|
decodeTriggerEventType: function decodeTriggerEventType(eventSpecifier) {
|
||
|
|
var EVENT_DECODER = {
|
||
|
|
'after': 'AFTER',
|
||
|
|
'before': 'BEFORE',
|
||
|
|
'instead_of': 'INSTEAD OF',
|
||
|
|
'after_constraint': 'AFTER'
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!Utils._.has(EVENT_DECODER, eventSpecifier)) {
|
||
|
|
throw new Error('Invalid trigger event specified: ' + eventSpecifier);
|
||
|
|
}
|
||
|
|
|
||
|
|
return EVENT_DECODER[eventSpecifier];
|
||
|
|
},
|
||
|
|
|
||
|
|
triggerEventTypeIsConstraint: function triggerEventTypeIsConstraint(eventSpecifier) {
|
||
|
|
return eventSpecifier === 'after_constrain' ? 'CONSTRAINT ' : '';
|
||
|
|
},
|
||
|
|
|
||
|
|
expandTriggerEventSpec: function expandTriggerEventSpec(fireOnSpec) {
|
||
|
|
if (Utils._.isEmpty(fireOnSpec)) {
|
||
|
|
throw new Error('no table change events specified to trigger on');
|
||
|
|
}
|
||
|
|
|
||
|
|
return Utils._.map(fireOnSpec, function parseTriggerEventSpec(fireValue, fireKey) {
|
||
|
|
var EVENT_MAP = {
|
||
|
|
'insert': 'INSERT',
|
||
|
|
'update': 'UPDATE',
|
||
|
|
'delete': 'DELETE',
|
||
|
|
'truncate': 'TRUNCATE'
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!Utils._.has(EVENT_MAP, fireKey)) {
|
||
|
|
throw new Error('parseTriggerEventSpec: undefined trigger event ' + fireKey);
|
||
|
|
}
|
||
|
|
|
||
|
|
var eventSpec = EVENT_MAP[fireKey];
|
||
|
|
if (eventSpec === 'UPDATE') {
|
||
|
|
if (Utils._.isArray(fireValue) && fireValue.length > 0) {
|
||
|
|
eventSpec += ' OF ' + fireValue.join(', ');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return eventSpec;
|
||
|
|
}).join(' OR ');
|
||
|
|
},
|
||
|
|
|
||
|
|
pgListEnums: function(tableName, attrName, options) {
|
||
|
|
if (arguments.length === 1) {
|
||
|
|
options = tableName;
|
||
|
|
tableName = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
var enumName = '';
|
||
|
|
|
||
|
|
if (!!tableName && !!attrName) {
|
||
|
|
enumName = ' AND t.typname=' + this.escape('enum_' + tableName + '_' + attrName) + ' ';
|
||
|
|
}
|
||
|
|
|
||
|
|
var query = 'SELECT t.typname enum_name, array_agg(e.enumlabel) enum_value FROM pg_type t ' +
|
||
|
|
'JOIN pg_enum e ON t.oid = e.enumtypid ' +
|
||
|
|
'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
|
||
|
|
"WHERE n.nspname = 'public' " + enumName + ' GROUP BY 1';
|
||
|
|
|
||
|
|
return query;
|
||
|
|
},
|
||
|
|
|
||
|
|
pgEnum: function(tableName, attr, dataType, options) {
|
||
|
|
var enumName = this.pgEscapeAndQuote('enum_' + tableName + '_' + attr)
|
||
|
|
, values;
|
||
|
|
|
||
|
|
if (dataType.values) {
|
||
|
|
values = "ENUM('" + dataType.values.join("', '") + "')";
|
||
|
|
} else {
|
||
|
|
values = dataType.toString().match(/^ENUM\(.+\)/)[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
var sql = 'CREATE TYPE ' + enumName + ' AS ' + values + '; ';
|
||
|
|
if (!!options && options.force === true) {
|
||
|
|
sql = this.pgEnumDrop(tableName, attr) + sql;
|
||
|
|
}
|
||
|
|
return sql;
|
||
|
|
},
|
||
|
|
|
||
|
|
pgEnumAdd: function(tableName, attr, value, options) {
|
||
|
|
var enumName = this.pgEscapeAndQuote('enum_' + tableName + '_' + attr);
|
||
|
|
var sql = 'ALTER TYPE ' + enumName + ' ADD VALUE ' + this.escape(value);
|
||
|
|
|
||
|
|
if (!!options.before) {
|
||
|
|
sql += ' BEFORE ' + this.escape(options.before);
|
||
|
|
}
|
||
|
|
else if (!!options.after) {
|
||
|
|
sql += ' AFTER ' + this.escape(options.after);
|
||
|
|
}
|
||
|
|
|
||
|
|
return sql;
|
||
|
|
},
|
||
|
|
|
||
|
|
pgEnumDrop: function(tableName, attr, enumName) {
|
||
|
|
enumName = enumName || this.pgEscapeAndQuote('enum_' + tableName + '_' + attr);
|
||
|
|
return 'DROP TYPE IF EXISTS ' + enumName + '; ';
|
||
|
|
},
|
||
|
|
|
||
|
|
fromArray: function(text) {
|
||
|
|
text = text.replace(/^{/, '').replace(/}$/, '');
|
||
|
|
var matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig);
|
||
|
|
|
||
|
|
if (matches.length < 1) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
matches = matches.map(function(m) {
|
||
|
|
return m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/, '');
|
||
|
|
});
|
||
|
|
|
||
|
|
return matches.slice(0, -1);
|
||
|
|
},
|
||
|
|
|
||
|
|
padInt: function(i) {
|
||
|
|
return (i < 10) ? '0' + i.toString() : i.toString();
|
||
|
|
},
|
||
|
|
|
||
|
|
pgDataTypeMapping: function(tableName, attr, dataType) {
|
||
|
|
return this.dataTypeMapping(tableName, attr, dataType);
|
||
|
|
},
|
||
|
|
|
||
|
|
dataTypeMapping: function(tableName, attr, dataType) {
|
||
|
|
if (Utils._.includes(dataType, 'PRIMARY KEY')) {
|
||
|
|
primaryKeys[tableName].push(attr);
|
||
|
|
dataType = dataType.replace(/PRIMARY KEY/, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Utils._.includes(dataType, 'SERIAL')) {
|
||
|
|
if (Utils._.includes(dataType, 'BIGINT')) {
|
||
|
|
dataType = dataType.replace(/SERIAL/, 'BIGSERIAL');
|
||
|
|
dataType = dataType.replace(/BIGINT/, '');
|
||
|
|
} else {
|
||
|
|
dataType = dataType.replace(/INTEGER/, '');
|
||
|
|
}
|
||
|
|
dataType = dataType.replace(/NOT NULL/, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (dataType.match(/^ENUM\(/)) {
|
||
|
|
dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEscapeAndQuote('enum_' + tableName + '_' + attr));
|
||
|
|
}
|
||
|
|
|
||
|
|
return dataType;
|
||
|
|
},
|
||
|
|
|
||
|
|
quoteIdentifier: function(identifier, force) {
|
||
|
|
if (identifier === '*') return identifier;
|
||
|
|
if (!force && this.options && this.options.quoteIdentifiers === false) { // default is `true`
|
||
|
|
// In Postgres, if tables or attributes are created double-quoted,
|
||
|
|
// they are also case sensitive. If they contain any uppercase
|
||
|
|
// characters, they must always be double-quoted. This makes it
|
||
|
|
// impossible to write queries in portable SQL if tables are created in
|
||
|
|
// this way. Hence, we strip quotes if we don't want case sensitivity.
|
||
|
|
return Utils.removeTicks(identifier, '"');
|
||
|
|
} else {
|
||
|
|
return Utils.addTicks(identifier, '"');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/*
|
||
|
|
Escape a value (e.g. a string, number or date)
|
||
|
|
*/
|
||
|
|
escape: function(value, field) {
|
||
|
|
if (value && value._isSequelizeMethod) {
|
||
|
|
return this.handleSequelizeMethod(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Utils._.isObject(value) && field && (field.type instanceof DataTypes.HSTORE || DataTypes.ARRAY.is(field.type, DataTypes.HSTORE))) {
|
||
|
|
if (field.type instanceof DataTypes.HSTORE){
|
||
|
|
return "'" + hstore.stringify(value) + "'";
|
||
|
|
} else if (DataTypes.ARRAY.is(field.type, DataTypes.HSTORE)) {
|
||
|
|
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + hstore.stringify(v) + "'::hstore";}).join(',') + ']::HSTORE[]';
|
||
|
|
}
|
||
|
|
} else if(Utils._.isArray(value) && field && (field.type instanceof DataTypes.RANGE || DataTypes.ARRAY.is(field.type, DataTypes.RANGE))) {
|
||
|
|
if(field.type instanceof DataTypes.RANGE) { // escape single value
|
||
|
|
return "'" + range.stringify(value) + "'";
|
||
|
|
}
|
||
|
|
else if (DataTypes.ARRAY.is(field.type, DataTypes.RANGE)) { // escape array of ranges
|
||
|
|
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + range.stringify(v) + "'";}).join(',') + ']::' + field.type.toString();
|
||
|
|
}
|
||
|
|
} else if (value!==null && field && field.type instanceof DataTypes.JSON) {
|
||
|
|
value = JSON.stringify(value);
|
||
|
|
} else if (Array.isArray(value) && field && DataTypes.ARRAY.is(field.type, DataTypes.JSON)) {
|
||
|
|
var jsonType = field.type.type; // type may be JSON or JSONB
|
||
|
|
return 'ARRAY[' + value.map(function (v) {
|
||
|
|
return SqlString.escape(JSON.stringify(v), false, this.options.timezone, this.dialect, field);
|
||
|
|
}, this).join(',') + ']::' + jsonType.key + '[]';
|
||
|
|
}
|
||
|
|
|
||
|
|
return SqlString.escape(value, false, this.options.timezone, this.dialect, field);
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generates an SQL query that returns all foreign keys of a table.
|
||
|
|
*
|
||
|
|
* @param {String} tableName The name of the table.
|
||
|
|
* @param {String} schemaName The name of the schema.
|
||
|
|
* @return {String} The generated sql query.
|
||
|
|
*/
|
||
|
|
getForeignKeysQuery: function(tableName, schemaName) {
|
||
|
|
return 'SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r ' +
|
||
|
|
"WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '" + tableName + "' LIMIT 1) AND r.contype = 'f' ORDER BY 1;";
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generates an SQL query that removes a foreign key from a table.
|
||
|
|
*
|
||
|
|
* @param {String} tableName The name of the table.
|
||
|
|
* @param {String} foreignKey The name of the foreign key constraint.
|
||
|
|
* @return {String} The generated sql query.
|
||
|
|
*/
|
||
|
|
dropForeignKeyQuery: function(tableName, foreignKey) {
|
||
|
|
return 'ALTER TABLE ' + this.quoteTable(tableName) + ' DROP CONSTRAINT ' + this.quoteIdentifier(foreignKey) + ';';
|
||
|
|
},
|
||
|
|
|
||
|
|
|
||
|
|
setAutocommitQuery: function(value, options) {
|
||
|
|
if (options.parent) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// POSTGRES does not support setting AUTOCOMMIT = OFF
|
||
|
|
if (!value) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
return AbstractQueryGenerator.setAutocommitQuery.call(this, value, options);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
module.exports = Utils._.extend(Utils._.clone(AbstractQueryGenerator), QueryGenerator);
|