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 pg = safeRequire('pg');
var BaseSQL = require('../sql'); var BaseSQL = require('../sql');
var util = require('util');
exports.initialize = function initializeSchema(schema, callback) { exports.initialize = function initializeSchema(schema, callback) {
if (!pg) return; if (!pg) return;
@ -187,9 +188,6 @@ PG.prototype.fromDatabase = function (model, data) {
var props = this._models[model].properties; var props = this._models[model].properties;
Object.keys(data).forEach(function (key) { Object.keys(data).forEach(function (key) {
var val = data[key]; var val = data[key];
if (props[key]) {
// if (props[key])
}
data[key] = val; data[key] = val;
}); });
return data; return data;
@ -269,12 +267,24 @@ PG.prototype.toFilter = function (model, filter) {
return out; 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) { PG.prototype.autoupdate = function (cb) {
var self = this; var self = this;
var wait = 0; var wait = 0;
Object.keys(this._models).forEach(function (model) { Object.keys(this._models).forEach(function (model) {
wait += 1; 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); self.alterTable(model, fields, done);
}); });
}); });
@ -286,64 +296,173 @@ PG.prototype.autoupdate = function (cb) {
if (--wait === 0 && cb) { if (--wait === 0 && cb) {
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) { 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 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 propNames = Object.keys(m.properties);
var sql = []; var sql = [];
// change/add new fields
propNames.forEach(function (propName) { propNames.forEach(function (propName) {
var found; var found = searchForPropertyInActual.call(self, propName, actualFields);
actualFields.forEach(function (f) { if(!found && propertyHasNotBeenDeleted.call(self, model, propName)){
if (f.Field === propName) { sql.push(addPropertyToActual.call(self, model, propName));
found = f;
}
});
if (found) {
actualize(propName, found);
} else {
sql.push('ADD COLUMN "' + propName + '" ' + self.propertySettingsSQL(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) { actualFields.forEach(function (f) {
var notFound = !~propNames.indexOf(f.Field); if (f.Field === propName) {
if (f.Field === 'id') return; found = f;
if (notFound || !m.properties[f.Field]) { return;
sql.push('DROP COLUMN "' + f.Field + '"'); }
});
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) { return sql;
this.query('ALTER TABLE ' + this.tableEscaped(model) + ' ' + sql.join(',\n'), done);
} else {
done();
}
function actualize(propName, oldSettings) { function datatypeChanged(propName, oldSettings){
var newSettings = m.properties[propName]; var newSettings = m.properties[propName];
if (newSettings && changed(newSettings, oldSettings)) { if(!newSettings) return false;
sql.push('CHANGE COLUMN "' + propName + '" "' + propName + '" ' + self.propertySettingsSQL(model, propName)); 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) { function callback(err, data){
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true; if(err) console.log(err);
if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true;
return false;
} }
done();
}; };
PG.prototype.propertiesSQL = function (model) { PG.prototype.propertiesSQL = function (model) {
var self = this; 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) { Object.keys(this._models[model].properties).forEach(function (prop) {
sql.push('"' + prop + '" ' + self.propertySettingsSQL(model, prop)); sql.push('"' + prop + '" ' + self.propertySettingsSQL(model, prop));
}); });
@ -351,10 +470,17 @@ PG.prototype.propertiesSQL = function (model) {
}; };
PG.prototype.propertySettingsSQL = function (model, prop) { PG.prototype.propertySettingsSQL = function (model, propName) {
var p = this._models[model].properties[prop]; var self = this;
return datatype(p) + ' ' + var p = self._models[model].properties[propName];
(p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL'); 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) { function escape(val) {
@ -390,14 +516,32 @@ function escape(val) {
function datatype(p) { function datatype(p) {
switch (p.type.name) { switch (p.type.name) {
case 'String': case 'String':
return 'VARCHAR(' + (p.limit || 255) + ')'; return 'varchar';
case 'Text': case 'Text':
return 'TEXT'; return 'text';
case 'Number': case 'Number':
return 'INTEGER'; return 'integer';
case 'Date': case 'Date':
return 'TIMESTAMP'; return 'timestamp';
case 'Boolean': 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 Schema = juggling.Schema
Text = Schema.Text 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' DBUSER = process.env.DBUSER || 'root'
DBPASS = '' DBPASS = ''
DBENGINE = process.env.DBENGINE || 'postgres' DBENGINE = process.env.DBENGINE || 'postgres'

View File

@ -2,7 +2,7 @@ juggling = require('../index')
Schema = juggling.Schema Schema = juggling.Schema
Text = Schema.Text 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' DBUSER = process.env.DBUSER || 'root'
DBPASS = '' DBPASS = ''
DBENGINE = process.env.DBENGINE || 'postgres' DBENGINE = process.env.DBENGINE || 'postgres'