2011-06-13 11:23:02 +08:00
var _ = require ( 'underscore' )
2011-11-22 08:06:14 +08:00
, Step = require ( 'step' )
, pg = require ( 'pg' ) ; //.native; // disabled for now due to: https://github.com/brianc/node-postgres/issues/48
_ . mixin ( require ( 'underscore.string' ) ) ;
2011-06-13 11:23:02 +08:00
2013-05-25 00:16:12 +08:00
// Max database connections in the pool
// Subsequent connections will block waiting for a free slot
pg . defaults . poolSize = global . settings . db _pool _size || 16 ;
// Milliseconds of idle time before removing connection from pool
// TODO: make config setting ?
pg . defaults . poolIdleTimeout = global . settings . db _pool _idleTimeout || 30000 ;
// Frequency to check for idle clients within the pool, ms
// TODO: make config setting ?
pg . defaults . reapIntervalMillis = global . settings . db _pool _reapInterval || 1000 ;
pg . on ( 'error' , function ( err , client ) {
console . log ( "PostgreSQL connection error: " + err ) ;
} ) ;
2013-07-10 03:50:28 +08:00
// Workaround for https://github.com/Vizzuality/CartoDB-SQL-API/issues/100
var types = require ( _ _dirname + '/../../node_modules/pg/lib/types' ) ;
var arrayParser = require ( _ _dirname + '/../../node_modules/pg/lib/types/arrayParser' ) ;
var floatParser = function ( val ) {
return parseFloat ( val ) ;
} ;
var floatArrayParser = function ( val ) {
if ( ! val ) { return null ; }
var p = arrayParser . create ( val , function ( entry ) {
return floatParser ( entry ) ;
} ) ;
return p . parse ( ) ;
} ;
types . setTypeParser ( 1700 , floatParser ) ;
types . setTypeParser ( 700 , floatParser ) ;
types . setTypeParser ( 701 , floatParser ) ;
types . setTypeParser ( 1021 , floatArrayParser ) ;
types . setTypeParser ( 1022 , floatArrayParser ) ;
types . setTypeParser ( 1231 , floatArrayParser ) ;
2013-05-25 00:16:12 +08:00
2011-06-13 11:23:02 +08:00
// PSQL
//
// A simple postgres wrapper with logic about username and database to connect
//
// * intended for use with pg_bouncer
// * defaults to connecting with a "READ ONLY" user to given DB if not passed a specific user_id
2013-05-24 16:22:17 +08:00
var PSQL = function ( user _id , db ) {
2011-08-22 20:33:12 +08:00
2011-11-22 08:06:14 +08:00
var error _text = "Incorrect access parameters. If you are accessing via OAuth, please check your tokens are correct. For public users, please ensure your table is published."
if ( ! _ . isString ( user _id ) && ! _ . isString ( db ) ) throw new Error ( error _text ) ;
var me = {
public _user : "publicuser"
, user _id : user _id
, db : db
} ;
me . username = function ( ) {
var username = this . public _user ;
if ( _ . isString ( this . user _id ) )
username = _ . template ( global . settings . db _user , { user _id : this . user _id } ) ;
return username ;
} ;
me . database = function ( ) {
var database = db ;
if ( _ . isString ( this . user _id ) )
database = _ . template ( global . settings . db _base _name , { user _id : this . user _id } ) ;
return database ;
} ;
2013-05-24 18:06:14 +08:00
me . conString = "tcp://" + me . username ( ) + "@" +
global . settings . db _host + ":" +
global . settings . db _port + "/" +
me . database ( ) ;
2013-05-29 20:36:31 +08:00
me . eventedQuery = function ( sql , callback ) {
var that = this ;
Step (
function ( ) {
that . sanitize ( sql , this ) ;
} ,
function ( err , clean ) {
if ( err ) throw err ;
pg . connect ( that . conString , this ) ;
} ,
function ( err , client , done ) {
if ( err ) throw err ;
var query = client . query ( sql ) ;
// NOTE: for some obscure reason passing "done" directly
// as the listener works but can be slower
// (by x2 factor!)
query . on ( 'end' , function ( ) { done ( ) ; } ) ;
return query ;
} ,
function ( err , query ) {
callback ( err , query )
}
) ;
} ,
2013-05-24 16:22:17 +08:00
me . query = function ( sql , callback ) {
2011-11-22 08:06:14 +08:00
var that = this ;
2013-05-24 18:06:14 +08:00
var finish ;
2011-11-22 08:06:14 +08:00
Step (
function ( ) {
that . sanitize ( sql , this ) ;
} ,
function ( err , clean ) {
if ( err ) throw err ;
2013-05-24 18:06:14 +08:00
pg . connect ( that . conString , this ) ;
2011-11-22 08:06:14 +08:00
} ,
2013-05-24 18:06:14 +08:00
function ( err , client , done ) {
2013-05-24 16:38:27 +08:00
if ( err ) throw err ;
2013-05-24 18:06:14 +08:00
finish = done ;
2013-05-24 16:22:17 +08:00
client . query ( sql , this ) ;
2011-11-22 08:06:14 +08:00
} ,
function ( err , res ) {
2013-05-24 18:06:14 +08:00
// Release client to the pool
// should this be postponed to after the callback ?
// NOTE: if we pass a true value to finish() the client
// will be removed from the pool.
// We don't want this. Not now.
if ( finish ) finish ( ) ;
2011-11-22 08:06:14 +08:00
callback ( err , res )
}
) ;
} ;
2013-04-09 17:49:05 +08:00
// throw exception if illegal operations are detected
// NOTE: this check is weak hack, better database
// permissions should be used instead.
2011-11-22 08:06:14 +08:00
me . sanitize = function ( sql , callback ) {
2013-04-09 18:36:37 +08:00
// NOTE: illegal table access is checked in main app
2013-04-09 17:49:05 +08:00
if ( sql . match ( /^\s+set\s+/i ) ) {
var error = new SyntaxError ( "SET command is forbidden" ) ;
error . http _status = 403 ;
callback ( error ) ;
return ;
2011-11-22 08:06:14 +08:00
}
2013-04-09 17:49:05 +08:00
callback ( null , true ) ;
2011-11-22 08:06:14 +08:00
} ;
return me ;
2011-06-13 11:23:02 +08:00
} ;
2013-06-14 17:18:16 +08:00
// little hack for UI
//
// TODO:drop, fix in the UI (it's not documented in doc/API)
//
PSQL . window _sql = function ( sql , limit , offset ) {
// only window select functions (NOTE: "values" will be broken, "with" will be broken)
if ( ! _ . isNumber ( limit ) || ! _ . isNumber ( offset ) ) return sql ;
var cte = '' ;
if ( sql . match ( /^\s*WITH\s/i ) ) {
var rem = sql ; // analyzed portion of sql
var q ; // quote char
var n = 0 ; // nested parens level
var s = 0 ; // 0:outQuote, 1:inQuote
var l ;
while ( 1 ) {
l = rem . search ( /[("')]/ ) ;
//console.log("REM Is " + rem);
if ( l < 0 ) {
console . log ( "Malformed SQL" ) ;
return sql ;
}
var f = rem . charAt ( l ) ;
//console.log("n:" + n + " s:" + s + " l:" + l + " charAt(l):" + f + " charAt(l+1):" + rem.charAt(l+1));
if ( s == 0 ) {
if ( f == '(' ) ++ n ;
else if ( f == ')' ) {
if ( ! -- n ) { // end of CTE
cte += rem . substr ( 0 , l + 1 ) ;
rem = rem . substr ( l + 1 ) ;
//console.log("Out of cte, rem is " + rem);
if ( rem . search ( /^s*,/ ) < 0 ) break ;
else continue ; // cte and rem already updated
}
}
else { // enter quoting
s = 1 ; q = f ;
}
}
else if ( f == q ) {
if ( rem . charAt ( l + 1 ) == f ) ++ l ; // escaped
else s = 0 ; // exit quoting
}
cte += rem . substr ( 0 , l + 1 ) ;
rem = rem . substr ( l + 1 ) ;
}
/ *
console . log ( "cte: " + cte ) ;
console . log ( "rem: " + rem ) ;
* /
sql = rem ; //sql.substr(l+1);
}
if ( sql . match ( /^\s*SELECT\s/i ) ) {
return cte + "SELECT * FROM (" + sql + ") AS cdbq_1 LIMIT " + limit + " OFFSET " + offset ;
}
return cte + sql ;
}
2011-06-13 18:31:50 +08:00
module . exports = PSQL ;