heavily modified sections of postgres adapter, postgres is now passing all tests

This commit is contained in:
Joseph Junker 2012-05-16 14:39:43 -07:00
parent 61a0795447
commit 3dcdb1e911
4 changed files with 251 additions and 50 deletions

57
coverage.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@ var safeRequire = require('../utils').safeRequire;
*/
var pg = safeRequire('pg');
var BaseSQL = require('../sql');
var util = require('util');
exports.initialize = function initializeSchema(schema, callback) {
if (!pg) return;
@ -187,9 +188,6 @@ PG.prototype.fromDatabase = function (model, data) {
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
var val = data[key];
if (props[key]) {
// if (props[key])
}
data[key] = val;
});
return data;
@ -269,12 +267,24 @@ PG.prototype.toFilter = function (model, filter) {
return out;
};
function getTableStatus(model, cb){
function decoratedCallback(err, data){
data.forEach(function(field){
field.Type = mapPostgresDatatypes(field.Type);
});
cb(err, data);
};
this.query('SELECT column_name as "Field", udt_name as "Type", is_nullable as "Null", column_default as "Default" FROM information_schema.COLUMNS WHERE table_name = \'' + this.table(model) + '\'', decoratedCallback);
};
PG.prototype.autoupdate = function (cb) {
var self = this;
var wait = 0;
Object.keys(this._models).forEach(function (model) {
wait += 1;
self.query('SELECT column_name as "Field", udt_name as "Type", is_nullable as "Null", column_default as "Default" FROM information_schema.COLUMNS WHERE table_name = \''+ self.table(model) + '\'', function (err, fields) {
var fields;
getTableStatus.call(self, model, function(err, fields){
if(err) console.log(err);
self.alterTable(model, fields, done);
});
});
@ -286,64 +296,173 @@ PG.prototype.autoupdate = function (cb) {
if (--wait === 0 && cb) {
cb();
}
}
};
};
PG.prototype.isActual = function(cb) {
var self = this;
var wait = 0;
changes = [];
Object.keys(this._models).forEach(function (model) {
wait += 1;
getTableStatus.call(self, model, function(err, fields){
changes = changes.concat(getPendingChanges.call(self, model, fields));
done(err, changes);
});
});
function done(err, fields) {
if (err) {
console.log(err);
}
if (--wait === 0 && cb) {
var actual = (changes.length === 0);
cb(null, actual);
}
};
};
PG.prototype.alterTable = function (model, actualFields, done) {
var self = this;
var pendingChanges = getPendingChanges.call(self, model, actualFields);
applySqlChanges.call(self, model, pendingChanges, done);
};
function getPendingChanges(model, actualFields){
var sql = [];
var self = this;
var m = this._models[model];
sql = sql.concat(getColumnsToAdd.call(self, model, actualFields));
sql = sql.concat(getPropertiesToModify.call(self, model, actualFields));
sql = sql.concat(getColumnsToDrop.call(self, model, actualFields));
return sql;
};
function getColumnsToAdd(model, actualFields){
var self = this;
var m = self._models[model];
var propNames = Object.keys(m.properties);
var sql = [];
// change/add new fields
propNames.forEach(function (propName) {
var found;
actualFields.forEach(function (f) {
if (f.Field === propName) {
found = f;
}
});
if (found) {
actualize(propName, found);
} else {
sql.push('ADD COLUMN "' + propName + '" ' + self.propertySettingsSQL(model, propName));
var found = searchForPropertyInActual.call(self, propName, actualFields);
if(!found && propertyHasNotBeenDeleted.call(self, model, propName)){
sql.push(addPropertyToActual.call(self, model, propName));
}
});
return sql;
};
// drop columns
function addPropertyToActual(model, propName){
var self = this;
var p = self._models[model].properties[propName];
sqlCommand = 'ADD COLUMN "' + propName + '" ' + datatype(p) + " " + (propertyCanBeNull.call(self, model, propName) ? "" : " NOT NULL");
return sqlCommand;
};
function searchForPropertyInActual(propName, actualFields){
var found = false;
actualFields.forEach(function (f) {
var notFound = !~propNames.indexOf(f.Field);
if (f.Field === 'id') return;
if (notFound || !m.properties[f.Field]) {
sql.push('DROP COLUMN "' + f.Field + '"');
if (f.Field === propName) {
found = f;
return;
}
});
return found;
};
function getPropertiesToModify(model, actualFields){
var self = this;
var sql = [];
var m = self._models[model];
var propNames = Object.keys(m.properties);
var found;
propNames.forEach(function (propName) {
found = searchForPropertyInActual.call(self, propName, actualFields);
if(found && propertyHasNotBeenDeleted.call(self, model, propName)){
if (datatypeChanged(propName, found)) {
sql.push(modifyDatatypeInActual.call(self, model, propName));
}
if (nullabilityChanged(propName, found)){
sql.push(modifyNullabilityInActual.call(self, model, propName));
}
}
});
if (sql.length) {
this.query('ALTER TABLE ' + this.tableEscaped(model) + ' ' + sql.join(',\n'), done);
} else {
done();
}
return sql;
function actualize(propName, oldSettings) {
function datatypeChanged(propName, oldSettings){
var newSettings = m.properties[propName];
if (newSettings && changed(newSettings, oldSettings)) {
sql.push('CHANGE COLUMN "' + propName + '" "' + propName + '" ' + self.propertySettingsSQL(model, propName));
if(!newSettings) return false;
return oldSettings.Type.toLowerCase() !== datatype(newSettings);
};
function nullabilityChanged(propName, oldSettings){
var newSettings = m.properties[propName];
if(!newSettings) return false;
var changed = false;
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) changed = true;
if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) changed = true;
return changed;
};
};
function modifyDatatypeInActual(model, propName) {
var self = this;
var sqlCommand = 'ALTER COLUMN "' + propName + '" TYPE ' + datatype(self._models[model].properties[propName]);
return sqlCommand;
};
function modifyNullabilityInActual(model, propName) {
var self = this;
var sqlCommand = 'ALTER COLUMN "' + propName + '" ';
if(propertyCanBeNull.call(self, model, propName)){
sqlCommand = sqlCommand + "DROP ";
} else {
sqlCommand = sqlCommand + "SET ";
}
sqlCommand = sqlCommand + "NOT NULL";
return sqlCommand;
};
function getColumnsToDrop(model, actualFields){
var self = this;
var sql = [];
actualFields.forEach(function (actualField) {
if (actualField.Field === 'id') return;
if (actualFieldNotPresentInModel(actualField, model)) {
sql.push('DROP COLUMN "' + actualField.Field + '"');
}
});
return sql;
function actualFieldNotPresentInModel(actualField, model){
return !(self._models[model].properties[actualField.Field]);
};
};
function applySqlChanges(model, pendingChanges, done){
var self = this;
if (pendingChanges.length) {
var thisQuery = 'ALTER TABLE ' + self.tableEscaped(model);
var ranOnce = false;
pendingChanges.forEach(function(change){
if(ranOnce) thisQuery = thisQuery + ',';
thisQuery = thisQuery + ' ' + change;
ranOnce = true;
});
thisQuery = thisQuery + ';';
self.query(thisQuery, callback);
}
function changed(newSettings, oldSettings) {
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true;
return false;
function callback(err, data){
if(err) console.log(err);
}
done();
};
PG.prototype.propertiesSQL = function (model) {
var self = this;
var sql = ['"id" SERIAL NOT NULL UNIQUE PRIMARY KEY'];
var sql = ['"id" SERIAL PRIMARY KEY'];
Object.keys(this._models[model].properties).forEach(function (prop) {
sql.push('"' + prop + '" ' + self.propertySettingsSQL(model, prop));
});
@ -351,10 +470,17 @@ PG.prototype.propertiesSQL = function (model) {
};
PG.prototype.propertySettingsSQL = function (model, prop) {
var p = this._models[model].properties[prop];
return datatype(p) + ' ' +
(p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
PG.prototype.propertySettingsSQL = function (model, propName) {
var self = this;
var p = self._models[model].properties[propName];
var result = datatype(p) + ' ';
if(!propertyCanBeNull.call(self, model, propName)) result = result + 'NOT NULL ';
return result;
};
function propertyCanBeNull(model, propName){
var p = this._models[model].properties[propName];
return !(p.allowNull === false || p['null'] === false);
};
function escape(val) {
@ -390,14 +516,32 @@ function escape(val) {
function datatype(p) {
switch (p.type.name) {
case 'String':
return 'VARCHAR(' + (p.limit || 255) + ')';
return 'varchar';
case 'Text':
return 'TEXT';
return 'text';
case 'Number':
return 'INTEGER';
return 'integer';
case 'Date':
return 'TIMESTAMP';
return 'timestamp';
case 'Boolean':
return 'BOOLEAN';
return 'boolean';
default:
console.log("Warning: postgres adapter does not explicitly handle type '" + p.type.name +"'");
return p.type.toLowerCase();
//TODO a default case might not be the safest thing here... postgres has a fair number of extra types though
}
}
};
function mapPostgresDatatypes(typeName) {
//TODO there are a lot of synonymous type names that should go here-- this is just what i've run into so far
switch (typeName){
case 'int4':
return 'integer';
default:
return typeName;
}
};
function propertyHasNotBeenDeleted(model, propName){
return !!this._models[model].properties[propName];
};

View File

@ -2,7 +2,7 @@ juggling = require('../index')
Schema = juggling.Schema
Text = Schema.Text
DBNAME = process.env.DBNAME || 'myapp_test'
DBNAME = process.env.DBNAME || 'myapp_test' #this db must already exist and will be destroyed
DBUSER = process.env.DBUSER || 'root'
DBPASS = ''
DBENGINE = process.env.DBENGINE || 'postgres'

View File

@ -2,7 +2,7 @@ juggling = require('../index')
Schema = juggling.Schema
Text = Schema.Text
DBNAME = process.env.DBNAME || 'myapp_test'
DBNAME = process.env.DBNAME || 'myapp_test' #this db must already exist and will be destroyed
DBUSER = process.env.DBUSER || 'root'
DBPASS = ''
DBENGINE = process.env.DBENGINE || 'postgres'