Skip to content

Commit

Permalink
Merge pull request 1602#84 from JosephJNK/master
Browse files Browse the repository at this point in the history
Added support for migrations to postgres adapter
  • Loading branch information
1602 committed May 17, 2012
2 parents 8e312bf + 3dcdb1e commit e1bd896
Show file tree
Hide file tree
Showing 4 changed files with 447 additions and 47 deletions.
57 changes: 57 additions & 0 deletions coverage.html

Large diffs are not rendered by default.

238 changes: 191 additions & 47 deletions lib/adapters/postgres.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
});
Expand All @@ -286,75 +296,191 @@ 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);
return sql;

function datatypeChanged(propName, oldSettings){
var newSettings = m.properties[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 {
done();
sqlCommand = sqlCommand + "SET ";
}
sqlCommand = sqlCommand + "NOT NULL";
return sqlCommand;
};

function actualize(propName, oldSettings) {
var newSettings = m.properties[propName];
if (newSettings && changed(newSettings, oldSettings)) {
sql.push('CHANGE COLUMN "' + propName + '" "' + propName + '" ' + self.propertySettingsSQL(model, propName));
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));
});
return sql.join(',\n ');

};

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) {
Expand Down Expand Up @@ -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];
};
67 changes: 67 additions & 0 deletions test/postgres_default_values_test.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
juggling = require('../index')
Schema = juggling.Schema
Text = Schema.Text

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'

require('./spec_helper').init module.exports

schema = new Schema DBENGINE, database: '', username: DBUSER, password: DBPASS
schema.log = (q) -> console.log q

query = (sql, cb) ->
schema.adapter.query sql, cb

User = schema.define 'User',
name: {type: String, default: "guest"}
credits: {type: Number, default: 0}

withBlankDatabase = (cb) ->
db = schema.settings.database = DBNAME
query 'DROP DATABASE IF EXISTS ' + db, (err) ->
query 'CREATE DATABASE ' + db, ->
schema.automigrate(cb)

it 'default values should not interfere with fully specified objects', (test)->
withBlankDatabase (err)->
test.ok !err, "error while setting up blank database"
new User()
User.create {name: "Steve", credits: 47}, (err, obj)->
console.log "error creating user: #{err}"
test.ok !err, "error occurred when saving user with all values specified"
test.ok obj.id?, 'saved object has no id'
console.log "id: #{obj.id}"
test.equals obj.name, "Steve", "User's name didn't save correctly"
test.equals obj.credits, 47, "User's credits didn't save correctly"
test.done()

it 'objects should have default values when some fields are unspecified', (test)->
User.create {credits: 2}, (err, obj)->
console.log "error creating user: #{err}"
test.ok !err, "error occurred when saving user with some values unspecified"
test.ok obj.id?, 'saved object has no id'
test.equals obj.name, "guest", "User's name didn't save correctly"
test.equals obj.credits, 2, "User's credits didn't save correctly"
User.create {name: "Jeanette Adele McKenzie"}, (err, obj)->
console.log "error creating user: #{err}"
test.ok !err, "error occurred when saving user with some values unspecified"
test.ok obj.id?, 'saved object has no id'
test.equals obj.name, "Jeanette Adele McKenzie", "User's name didn't save correctly"
test.equals obj.credits, 0, "User's credits didn't save correctly"
test.done()

it 'objects should have default values when all fields are left unspecified', (test)->
User.create {}, (err, obj)->
console.log "error creating user: #{err}"
test.ok !err, "error occurred when saving user with all values specified"
test.ok obj.id?, 'saved object has no id'
test.equals obj.name, "guest", "User's name didn't save correctly"
test.equals obj.credits, 0, "User's credits didn't save correctly"
test.done()

it 'should disconnect when done', (test)->
schema.disconnect()
test.done()
Loading

0 comments on commit e1bd896

Please sign in to comment.