2012-01-30 19:34:30 +00:00
var safeRequire = require ( '../utils' ) . safeRequire ;
2012-01-10 13:26:24 +00:00
/ * *
* Module dependencies
* /
2012-01-30 19:34:30 +00:00
var pg = safeRequire ( 'pg' ) ;
2012-03-10 07:55:25 +00:00
var BaseSQL = require ( '../sql' ) ;
2012-05-16 21:39:43 +00:00
var util = require ( 'util' ) ;
2012-01-10 15:43:32 +00:00
2012-01-10 13:26:24 +00:00
exports . initialize = function initializeSchema ( schema , callback ) {
2012-01-30 19:34:30 +00:00
if ( ! pg ) return ;
var Client = pg . Client ;
2012-01-10 13:26:24 +00:00
var s = schema . settings ;
schema . client = new Client ( s . url ? s . url : {
host : s . host || 'localhost' ,
port : s . port || 5432 ,
user : s . username ,
password : s . password ,
database : s . database ,
debug : s . debug
} ) ;
schema . adapter = new PG ( schema . client ) ;
2012-03-11 04:48:38 +00:00
schema . adapter . connect ( callback ) ;
2012-01-10 13:26:24 +00:00
} ;
function PG ( client ) {
this . _models = { } ;
this . client = client ;
}
2012-03-10 07:55:25 +00:00
require ( 'util' ) . inherits ( PG , BaseSQL ) ;
2012-01-10 13:26:24 +00:00
2012-03-11 04:48:38 +00:00
PG . prototype . connect = function ( callback ) {
this . client . connect ( function ( err ) {
if ( ! err ) {
callback ( ) ;
} else {
console . error ( err ) ;
throw err ;
}
} ) ;
} ;
2012-01-10 13:26:24 +00:00
PG . prototype . query = function ( sql , callback ) {
var time = Date . now ( ) ;
var log = this . log ;
this . client . query ( sql , function ( err , data ) {
2012-04-11 17:51:55 +00:00
if ( log ) log ( sql , time ) ;
2012-03-10 07:55:25 +00:00
callback ( err , data ? data . rows : null ) ;
2012-01-10 13:26:24 +00:00
} ) ;
} ;
/ * *
* Must invoke callback ( err , id )
* /
PG . prototype . create = function ( model , data , callback ) {
2012-03-22 19:46:16 +00:00
var fields = this . toFields ( model , data , true ) ;
2012-03-10 07:55:25 +00:00
var sql = 'INSERT INTO ' + this . tableEscaped ( model ) + '' ;
2012-01-10 13:26:24 +00:00
if ( fields ) {
sql += ' ' + fields ;
} else {
sql += ' VALUES ()' ;
}
2012-01-10 15:43:32 +00:00
sql += ' RETURNING id' ;
2012-01-10 13:26:24 +00:00
this . query ( sql , function ( err , info ) {
2012-01-10 15:43:32 +00:00
if ( err ) return callback ( err ) ;
2012-03-10 07:55:25 +00:00
callback ( err , info && info [ 0 ] && info [ 0 ] . id ) ;
2012-01-10 13:26:24 +00:00
} ) ;
} ;
2012-03-22 19:46:16 +00:00
PG . prototype . updateOrCreate = function ( model , data , callback ) {
var pg = this ;
var fieldsNames = [ ] ;
var fieldValues = [ ] ;
var combined = [ ] ;
var props = this . _models [ model ] . properties ;
Object . keys ( data ) . forEach ( function ( key ) {
if ( props [ key ] || key === 'id' ) {
var k = '"' + key + '"' ;
var v ;
if ( key !== 'id' ) {
v = pg . toDatabase ( props [ key ] , data [ key ] ) ;
} else {
v = data [ key ] ;
}
fieldsNames . push ( k ) ;
fieldValues . push ( v ) ;
if ( key !== 'id' ) combined . push ( k + ' = ' + v ) ;
}
} ) ;
var sql = 'UPDATE ' + this . tableEscaped ( model ) ;
sql += ' SET ' + combined + ' WHERE id = ' + data . id + ';' ;
sql += ' INSERT INTO ' + this . tableEscaped ( model ) ;
sql += ' (' + fieldsNames . join ( ', ' ) + ')' ;
sql += ' SELECT ' + fieldValues . join ( ', ' )
sql += ' WHERE NOT EXISTS (SELECT 1 FROM ' + this . tableEscaped ( model ) ;
sql += ' WHERE id = ' + data . id + ') RETURNING id' ;
this . query ( sql , function ( err , info ) {
if ( ! err && info && info [ 0 ] && info [ 0 ] . id ) {
data . id = info [ 0 ] . id ;
}
callback ( err , data ) ;
} ) ;
} ;
2012-01-10 13:26:24 +00:00
PG . prototype . toFields = function ( model , data , forCreate ) {
var fields = [ ] ;
var props = this . _models [ model ] . properties ;
if ( forCreate ) {
var columns = [ ] ;
Object . keys ( data ) . forEach ( function ( key ) {
if ( props [ key ] ) {
columns . push ( '"' + key + '"' ) ;
fields . push ( this . toDatabase ( props [ key ] , data [ key ] ) ) ;
}
} . bind ( this ) ) ;
return '(' + columns . join ( ',' ) + ') VALUES (' + fields . join ( ',' ) + ')' ;
} else {
Object . keys ( data ) . forEach ( function ( key ) {
if ( props [ key ] ) {
fields . push ( '"' + key + '" = ' + this . toDatabase ( props [ key ] , data [ key ] ) ) ;
}
} . bind ( this ) ) ;
return fields . join ( ',' ) ;
}
} ;
2012-01-30 13:27:26 +00:00
function dateToPostgres ( val ) {
return [
val . getUTCFullYear ( ) ,
fz ( val . getUTCMonth ( ) + 1 ) ,
fz ( val . getUTCDate ( ) )
] . join ( '-' ) + ' ' + [
fz ( val . getUTCHours ( ) ) ,
fz ( val . getUTCMinutes ( ) ) ,
fz ( val . getUTCSeconds ( ) )
] . join ( ':' ) ;
function fz ( v ) {
return v < 10 ? '0' + v : v ;
}
}
2012-01-10 13:26:24 +00:00
PG . prototype . toDatabase = function ( prop , val ) {
2012-03-13 20:43:27 +00:00
if ( val === null ) {
// Postgres complains with NULLs in not null columns
// If we have an autoincrement value, return DEFAULT instead
if ( prop . autoIncrement ) {
return 'DEFAULT' ;
}
else {
return 'NULL' ;
}
}
2012-02-01 17:33:08 +00:00
if ( val . constructor . name === 'Object' ) {
var operator = Object . keys ( val ) [ 0 ]
val = val [ operator ] ;
if ( operator === 'between' ) {
return this . toDatabase ( prop , val [ 0 ] ) + ' AND ' + this . toDatabase ( prop , val [ 1 ] ) ;
}
}
2012-01-10 15:43:32 +00:00
if ( prop . type . name === 'Number' ) return val ;
2012-01-10 13:26:24 +00:00
if ( prop . type . name === 'Date' ) {
2012-03-13 20:43:27 +00:00
if ( ! val ) {
if ( prop . autoIncrement ) {
return 'DEFAULT' ;
}
else {
return 'NULL' ;
}
}
2012-01-10 13:26:24 +00:00
if ( ! val . toUTCString ) {
val = new Date ( val ) ;
}
2012-01-30 13:27:26 +00:00
return escape ( dateToPostgres ( val ) ) ;
2012-01-10 13:26:24 +00:00
}
return escape ( val . toString ( ) ) ;
2012-01-30 13:27:26 +00:00
2012-01-10 13:26:24 +00:00
} ;
PG . prototype . fromDatabase = function ( model , data ) {
if ( ! data ) return null ;
var props = this . _models [ model ] . properties ;
Object . keys ( data ) . forEach ( function ( key ) {
var val = data [ key ] ;
data [ key ] = val ;
} ) ;
return data ;
} ;
2012-03-10 07:55:25 +00:00
PG . prototype . escapeName = function ( name ) {
2012-04-18 23:20:44 +00:00
return '"' + name . replace ( /\./g , '"."' ) + '"' ;
2012-03-10 07:55:25 +00:00
} ;
2012-01-10 13:26:24 +00:00
PG . prototype . all = function all ( model , filter , callback ) {
2012-03-10 08:39:39 +00:00
this . query ( 'SELECT * FROM ' + this . tableEscaped ( model ) + ' ' + this . toFilter ( model , filter ) , function ( err , data ) {
2012-01-10 13:26:24 +00:00
if ( err ) {
return callback ( err , [ ] ) ;
}
2012-03-10 07:55:25 +00:00
callback ( err , data ) ;
2012-01-10 13:26:24 +00:00
} . bind ( this ) ) ;
} ;
PG . prototype . toFilter = function ( model , filter ) {
2012-01-10 15:43:32 +00:00
if ( filter && typeof filter . where === 'function' ) {
2012-01-10 13:26:24 +00:00
return filter ( ) ;
}
2012-01-10 15:43:32 +00:00
if ( ! filter ) return '' ;
2012-01-10 13:26:24 +00:00
var props = this . _models [ model ] . properties ;
2012-01-30 13:27:26 +00:00
var out = '' ;
2012-01-10 15:43:32 +00:00
if ( filter . where ) {
2012-01-10 13:26:24 +00:00
var fields = [ ] ;
2012-02-01 17:33:08 +00:00
var conds = filter . where ;
Object . keys ( conds ) . forEach ( function ( key ) {
2012-01-10 15:43:32 +00:00
if ( filter . where [ key ] && filter . where [ key ] . constructor . name === 'RegExp' ) {
return ;
}
2012-01-10 13:26:24 +00:00
if ( props [ key ] ) {
2012-01-10 15:43:32 +00:00
var filterValue = this . toDatabase ( props [ key ] , filter . where [ key ] ) ;
if ( filterValue === 'NULL' ) {
fields . push ( '"' + key + '" IS ' + filterValue ) ;
2012-02-01 17:33:08 +00:00
} else if ( conds [ key ] . constructor . name === 'Object' ) {
var condType = Object . keys ( conds [ key ] ) [ 0 ] ;
var sqlCond = key ;
switch ( condType ) {
case 'gt' :
sqlCond += ' > ' ;
break ;
case 'gte' :
sqlCond += ' >= ' ;
break ;
case 'lt' :
sqlCond += ' < ' ;
break ;
case 'lte' :
sqlCond += ' <= ' ;
break ;
case 'between' :
sqlCond += ' BETWEEN ' ;
break ;
}
sqlCond += filterValue ;
fields . push ( sqlCond ) ;
2012-01-10 15:43:32 +00:00
} else {
fields . push ( '"' + key + '" = ' + filterValue ) ;
}
2012-01-10 13:26:24 +00:00
}
} . bind ( this ) ) ;
2012-01-10 15:43:32 +00:00
if ( fields . length ) {
2012-01-30 13:27:26 +00:00
out += ' WHERE ' + fields . join ( ' AND ' ) ;
2012-01-10 15:43:32 +00:00
}
2012-01-10 13:26:24 +00:00
}
2012-01-30 13:27:26 +00:00
if ( filter . order ) {
out += ' ORDER BY ' + filter . order ;
}
if ( filter . limit ) {
out += ' LIMIT ' + filter . limit + ' ' + ( filter . offset || '' ) ;
}
2012-01-10 13:26:24 +00:00
return out ;
} ;
2012-05-16 21:39:43 +00:00
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 ) ;
} ;
2012-01-10 13:26:24 +00:00
PG . prototype . autoupdate = function ( cb ) {
var self = this ;
var wait = 0 ;
Object . keys ( this . _models ) . forEach ( function ( model ) {
wait += 1 ;
2012-05-16 21:39:43 +00:00
var fields ;
getTableStatus . call ( self , model , function ( err , fields ) {
if ( err ) console . log ( err ) ;
2012-01-10 13:26:24 +00:00
self . alterTable ( model , fields , done ) ;
} ) ;
} ) ;
function done ( err ) {
if ( err ) {
console . log ( err ) ;
}
if ( -- wait === 0 && cb ) {
cb ( ) ;
}
2012-05-16 21:39:43 +00:00
} ;
} ;
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 ) ;
}
} ;
2012-01-10 13:26:24 +00:00
} ;
PG . prototype . alterTable = function ( model , actualFields , done ) {
2012-05-16 21:39:43 +00:00
var self = this ;
var pendingChanges = getPendingChanges . call ( self , model , actualFields ) ;
applySqlChanges . call ( self , model , pendingChanges , done ) ;
} ;
function getPendingChanges ( model , actualFields ) {
var sql = [ ] ;
2012-01-10 13:26:24 +00:00
var self = this ;
2012-05-16 21:39:43 +00:00
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 ] ;
2012-01-10 13:26:24 +00:00
var propNames = Object . keys ( m . properties ) ;
var sql = [ ] ;
propNames . forEach ( function ( propName ) {
2012-05-16 21:39:43 +00:00
var found = searchForPropertyInActual . call ( self , propName , actualFields ) ;
if ( ! found && propertyHasNotBeenDeleted . call ( self , model , propName ) ) {
sql . push ( addPropertyToActual . call ( self , model , propName ) ) ;
2012-01-10 13:26:24 +00:00
}
} ) ;
2012-05-16 21:39:43 +00:00
return sql ;
} ;
2012-01-10 13:26:24 +00:00
2012-05-16 21:39:43 +00:00
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 ;
2012-01-10 13:26:24 +00:00
actualFields . forEach ( function ( f ) {
2012-05-16 21:39:43 +00:00
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 ) ) ;
}
2012-01-10 13:26:24 +00:00
}
} ) ;
2012-05-16 21:39:43 +00:00
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 " ;
2012-01-10 13:26:24 +00:00
} else {
2012-05-16 21:39:43 +00:00
sqlCommand = sqlCommand + "SET " ;
2012-01-10 13:26:24 +00:00
}
2012-05-16 21:39:43 +00:00
sqlCommand = sqlCommand + "NOT NULL" ;
return sqlCommand ;
} ;
2012-01-10 13:26:24 +00:00
2012-05-16 21:39:43 +00:00
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 + '"' ) ;
2012-01-10 13:26:24 +00:00
}
2012-05-16 21:39:43 +00:00
} ) ;
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 ) ;
2012-01-10 13:26:24 +00:00
}
2012-05-16 21:39:43 +00:00
function callback ( err , data ) {
if ( err ) console . log ( err ) ;
2012-01-10 13:26:24 +00:00
}
2012-05-16 21:39:43 +00:00
done ( ) ;
2012-01-10 13:26:24 +00:00
} ;
PG . prototype . propertiesSQL = function ( model ) {
var self = this ;
2012-05-16 21:39:43 +00:00
var sql = [ '"id" SERIAL PRIMARY KEY' ] ;
2012-01-10 13:26:24 +00:00
Object . keys ( this . _models [ model ] . properties ) . forEach ( function ( prop ) {
sql . push ( '"' + prop + '" ' + self . propertySettingsSQL ( model , prop ) ) ;
} ) ;
return sql . join ( ',\n ' ) ;
} ;
2012-05-16 21:39:43 +00:00
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 ) ;
2012-01-10 13:26:24 +00:00
} ;
function escape ( val ) {
if ( val === undefined || val === null ) {
return 'NULL' ;
}
switch ( typeof val ) {
case 'boolean' : return ( val ) ? 'true' : 'false' ;
case 'number' : return val + '' ;
}
if ( typeof val === 'object' ) {
val = ( typeof val . toISOString === 'function' )
? val . toISOString ( )
: val . toString ( ) ;
}
val = val . replace ( /[\0\n\r\b\t\\\'\"\x1a]/g , function ( s ) {
switch ( s ) {
case "\0" : return "\\0" ;
case "\n" : return "\\n" ;
case "\r" : return "\\r" ;
case "\b" : return "\\b" ;
case "\t" : return "\\t" ;
case "\x1a" : return "\\Z" ;
default : return "\\" + s ;
}
} ) ;
return "'" + val + "'" ;
} ;
function datatype ( p ) {
switch ( p . type . name ) {
case 'String' :
2012-05-16 21:39:43 +00:00
return 'varchar' ;
2012-01-10 13:26:24 +00:00
case 'Text' :
2012-05-16 21:39:43 +00:00
return 'text' ;
2012-01-10 13:26:24 +00:00
case 'Number' :
2012-05-16 21:39:43 +00:00
return 'integer' ;
2012-01-10 13:26:24 +00:00
case 'Date' :
2012-05-16 21:39:43 +00:00
return 'timestamp' ;
2012-01-10 13:26:24 +00:00
case 'Boolean' :
2012-05-16 21:39:43 +00:00
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
2012-01-10 13:26:24 +00:00
}
2012-05-16 21:39:43 +00:00
} ;
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 ] ;
} ;