From 75088c89d3c822b9b25fc5c2757e39d8c33e659e Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 30 Jul 2014 13:45:53 +0200 Subject: [PATCH 1/9] Style fixes --- lib/cartodb/cartodb_windshaft.js | 2 +- lib/cartodb/server_options.js | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index 2ee45be3..f71fc891 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -2,7 +2,7 @@ var _ = require('underscore') , Step = require('step') , Windshaft = require('windshaft') - , redisPool = new require('redis-mpool')(global.environment.redis) + , redisPool = require('redis-mpool')(global.environment.redis) // TODO: instanciate cartoData with redisPool , cartoData = require('cartodb-redis')(global.environment.redis) , SignedMaps = require('./signed_maps.js') diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 4b12f0d8..cb97e38e 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -211,7 +211,7 @@ module.exports = function(){ var hash = crypto.createHash('md5'); hash.update(data); return hash.digest('hex'); - } + }; me.generateCacheChannel = function(app, req, callback){ @@ -241,7 +241,6 @@ module.exports = function(){ // See http://github.com/CartoDB/Windshaft-cartodb/issues/152 if ( ! app.mapStore ) { throw new Error('missing channel cache for token ' + req.params.token); - return; } var next = this; var mapStore = app.mapStore; @@ -397,7 +396,7 @@ module.exports = function(){ err = errors.length ? new Error(errors.join('\n')) : null; callback(err); } - } + }; // include in layergroup response the variables in serverMedata // those variables are useful to send to the client information @@ -490,7 +489,7 @@ module.exports = function(){ return; } return mat[1]; - } + }; // Set db authentication parameters to those of the given username // @@ -742,7 +741,7 @@ module.exports = function(){ //console.log("type of req.query.lzma is " + typeof(req.query.lzma)); // Decode (from base64) - var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 }) + var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 }); // Decompress LZMA.decompress( @@ -750,8 +749,8 @@ module.exports = function(){ function(result) { if (req.profiler) req.profiler.done('LZMA decompress'); try { - delete req.query.lzma - _.extend(req.query, JSON.parse(result)) + delete req.query.lzma; + _.extend(req.query, JSON.parse(result)); me.req2params(req, callback); } catch (err) { callback(new Error('Error parsing lzma as JSON: ' + err)); @@ -783,7 +782,7 @@ module.exports = function(){ req.params.signer = tksplit.shift(); if ( ! req.params.signer ) req.params.signer = user; else if ( req.params.signer != user ) { - var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' + user + '"') + var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' + user + '"'); err.http_status = 403; callback(err); return; From 3af45e1a32e7fee1d6b21152eff6a4a9a61f8c8e Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 30 Jul 2014 13:46:46 +0200 Subject: [PATCH 2/9] Moves calls to SQL API to its own entity. Groups affected tables and last updated time for affected tables into one request. --- lib/cartodb/api/query_tables_api.js | 94 ++++++++++++++ lib/cartodb/cache/cache_api.js | 159 ++++++++++++++++++++++++ lib/cartodb/server_options.js | 184 +++++----------------------- lib/cartodb/sql/sql_api.js | 66 ++++++++++ 4 files changed, 351 insertions(+), 152 deletions(-) create mode 100644 lib/cartodb/api/query_tables_api.js create mode 100644 lib/cartodb/cache/cache_api.js create mode 100644 lib/cartodb/sql/sql_api.js diff --git a/lib/cartodb/api/query_tables_api.js b/lib/cartodb/api/query_tables_api.js new file mode 100644 index 00000000..9782d91a --- /dev/null +++ b/lib/cartodb/api/query_tables_api.js @@ -0,0 +1,94 @@ +var sqlApi = require('../sql/sql_api'); + +function QueryTablesApi() { +} + +var affectedTableRegexCache = { + bbox: /!bbox!/g, + pixel_width: /!pixel_width!/g, + pixel_height: /!pixel_height!/g +}; + +module.exports = QueryTablesApi; + +QueryTablesApi.prototype.getLastUpdatedTime = function (username, api_key, tableNames, callback) { + var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY['+ + tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(',') + + '])'; + + // call sql api + sqlApi.query(username, api_key, sql, function(err, rows){ + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not find last updated timestamp: ' + msg)); + return; + } + // when the table has not updated_at means it hasn't been changed so a default last_updated is set + var last_updated = 0; + if(rows.length !== 0) { + last_updated = rows[0].max || 0; + } + + callback(null, last_updated*1000); + }); +}; + +QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, api_key, sql, callback) { + // Replace mapnik tokens + sql = sql + .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') + .replace(affectedTableRegexCache.pixel_width, '1') + .replace(affectedTableRegexCache.pixel_height, '1') + ; + + // Pass to CDB_QueryTables + sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; + + // call sql api + sqlApi.query(username, api_key, sql, function(err, rows){ + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not fetch source tables: ' + msg)); + return; + } + var qtables = rows[0].cdb_querytables; + var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + tableNames = tableNames ? tableNames.split(',') : []; + callback(null, tableNames); + }); +}; + +QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, api_key, sql, callback) { + sql = sql + .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') + .replace(affectedTableRegexCache.pixel_width, '1') + .replace(affectedTableRegexCache.pixel_height, '1') + ; + + var query = [ + 'SELECT', + 'CDB_QueryTables($windshaft$' + sql + '$windshaft$) as tablenames,', + 'EXTRACT(EPOCH FROM max(updated_at)) as max', + 'FROM CDB_TableMetadata m', + 'WHERE m.tabname = any (CDB_QueryTables($windshaft$' + sql + '$windshaft$)::regclass[])' + ].join(' '); + + sqlApi.query(username, api_key, query, function(err, rows){ + if (err || rows.length === 0) { + var msg = err.message ? err.message : err; + callback(new Error('could not fetch affected tables and last updated time: ' + msg)); + return; + } + + var qtables = rows[0].tablenames; + var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + tableNames = tableNames ? tableNames.split(',') : []; + + var lastUpdatedTime = rows[0].max || 0; + + callback(null, { + affectedTables: tableNames, + lastUpdatedTime: lastUpdatedTime * 1000 + }); + }); +}; diff --git a/lib/cartodb/cache/cache_api.js b/lib/cartodb/cache/cache_api.js new file mode 100644 index 00000000..915cc929 --- /dev/null +++ b/lib/cartodb/cache/cache_api.js @@ -0,0 +1,159 @@ +var _ = require('underscore'), + request = require('request'); + +function QueryTablesApi() { +} + +var affectedTableRegexCache = { + bbox: /!bbox!/g, + pixel_width: /!pixel_width!/g, + pixel_height: /!pixel_height!/g +}; + +module.exports = QueryTablesApi; + +QueryTablesApi.prototype.getLastUpdatedTime = function (username, api_key, tableNames, callback) { + var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY['+ + tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(',') + + '])'; + + // call sql api + sqlQuery(username, api_key, sql, function(err, rows){ + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not find last updated timestamp: ' + msg)); + return; + } + // when the table has not updated_at means it hasn't been changed so a default last_updated is set + var last_updated = 0; + if(rows.length !== 0) { + last_updated = rows[0].max || 0; + } + + callback(null, last_updated*1000); + }); +}; + +QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, api_key, sql, callback) { + // Replace mapnik tokens + sql = sql + .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') + .replace(affectedTableRegexCache.pixel_width, '1') + .replace(affectedTableRegexCache.pixel_height, '1') + ; + + // Pass to CDB_QueryTables + sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; + + // call sql api + sqlQuery(username, api_key, sql, function(err, rows){ + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not fetch source tables: ' + msg)); + return; + } + var qtables = rows[0].cdb_querytables; + var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + tableNames = tableNames ? tableNames.split(',') : []; + callback(null, tableNames); + }); +}; + +QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, api_key, sql, callback) { + sql = sql + .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') + .replace(affectedTableRegexCache.pixel_width, '1') + .replace(affectedTableRegexCache.pixel_height, '1') + ; + + var query = [ + 'SELECT', + 'CDB_QueryTables($windshaft$' + sql + '$windshaft$) as tablenames,', + 'EXTRACT(EPOCH FROM max(updated_at)) as max', + 'FROM CDB_TableMetadata m', + 'WHERE m.tabname = any (CDB_QueryTables($windshaft$' + sql + '$windshaft$)::regclass[])' + ].join(' '); + + sqlQuery(username, api_key, query, function(err, rows){ + if (err || rows.length === 0) { + var msg = err.message ? err.message : err; + callback(new Error('could not fetch affected tables and last updated time: ' + msg)); + return; + } + + var qtables = rows[0].tablenames; + var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + tableNames = tableNames ? tableNames.split(',') : []; + + var lastUpdatedTime = rows[0].max || 0; + + callback(null, { + affectedTables: tableNames, + lastUpdatedTime: lastUpdatedTime * 1000 + }); + }); +}; + +function sqlQuery(username, api_key, sql, callback) { + var api = global.environment.sqlapi; + + // build up api string + var sqlapihostname = username; + if ( api.domain ) sqlapihostname += '.' + api.domain; + + var sqlapi = api.protocol + '://'; + if ( api.host && api.host != api.domain ) sqlapi += api.host; + else sqlapi += sqlapihostname; + sqlapi += ':' + api.port + '/api/' + api.version + '/sql'; + + var qs = { q: sql }; + + // add api_key if given + if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; } + + // call sql api + // + // NOTE: using POST to avoid size limits: + // See http://github.com/CartoDB/Windshaft-cartodb/issues/111 + // + // NOTE: uses "host" header to allow IP based specification + // of sqlapi address (and avoid a DNS lookup) + // + // NOTE: allows for keeping up to "maxConnections" concurrent + // sockets opened per SQL-API host. + // See http://nodejs.org/api/http.html#http_agent_maxsockets + // + var maxSockets = global.environment.maxConnections || 128; + var maxGetLen = api.max_get_sql_length || 2048; + var maxSQLTime = api.timeout || 100; // 1/10 of a second by default + var reqSpec = { + url:sqlapi, + json:true, + headers:{host: sqlapihostname} + // http://nodejs.org/api/http.html#http_agent_maxsockets + ,pool:{maxSockets:maxSockets} + // timeout in milliseconds + ,timeout:maxSQLTime + }; + if ( sql.length > maxGetLen ) { + reqSpec.method = 'POST'; + reqSpec.body = qs; + } else { + reqSpec.method = 'GET'; + reqSpec.qs = qs; + } + request(reqSpec, function(err, res, body) { + if (err){ + console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err); + callback(err); + return; + } + if (res.statusCode != 200) { + var msg = res.body.error ? res.body.error : res.body; + callback(new Error(msg)); + console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg); + return; + } + callback(null, body.rows); + }); +} \ No newline at end of file diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index cb97e38e..cd8098ad 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -1,10 +1,10 @@ var _ = require('underscore') , Step = require('step') , cartoData = require('cartodb-redis')(global.environment.redis) - , Cache = require('./cache_validator') + , Cache = require('./cache_validator') + , QueryTablesApi = require('./api/query_tables_api') , mapnik = require('mapnik') , crypto = require('crypto') - , request = require('request') , LZMA = require('lzma/lzma_worker.js').LZMA ; @@ -19,6 +19,8 @@ if ( _.isUndefined(global.environment.sqlapi.domain) ) { module.exports = function(){ + var queryTablesApi = new QueryTablesApi(); + var rendererConfig = _.defaults(global.environment.renderer || {}, { cache_ttl: 60000, // milliseconds metatile: 4, @@ -88,121 +90,6 @@ module.exports = function(){ // we have no SQL after layer creation. me.channelCache = {}; - // Run a query through the SQL api - me.sqlQuery = function (username, api_key, sql, callback) { - var api = global.environment.sqlapi; - - // build up api string - var sqlapihostname = username; - if ( api.domain ) sqlapihostname += '.' + api.domain; - - var sqlapi = api.protocol + '://'; - if ( api.host && api.host != api.domain ) sqlapi += api.host; - else sqlapi += sqlapihostname; - sqlapi += ':' + api.port + '/api/' + api.version + '/sql'; - - var qs = { q: sql } - - // add api_key if given - if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; } - - // call sql api - // - // NOTE: using POST to avoid size limits: - // See http://github.com/CartoDB/Windshaft-cartodb/issues/111 - // - // NOTE: uses "host" header to allow IP based specification - // of sqlapi address (and avoid a DNS lookup) - // - // NOTE: allows for keeping up to "maxConnections" concurrent - // sockets opened per SQL-API host. - // See http://nodejs.org/api/http.html#http_agent_maxsockets - // - var maxSockets = global.environment.maxConnections || 128; - var maxGetLen = api.max_get_sql_length || 2048; - var maxSQLTime = api.timeout || 100; // 1/10 of a second by default - var reqSpec = { - url:sqlapi, - json:true, - headers:{host: sqlapihostname} - // http://nodejs.org/api/http.html#http_agent_maxsockets - ,pool:{maxSockets:maxSockets} - // timeout in milliseconds - ,timeout:maxSQLTime - } - if ( sql.length > maxGetLen ) { - reqSpec.method = 'POST'; - reqSpec.body = qs; - } else { - reqSpec.method = 'GET'; - reqSpec.qs = qs; - } - request(reqSpec, function(err, res, body) { - if (err){ - console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err); - callback(err); - return; - } - if (res.statusCode != 200) { - var msg = res.body.error ? res.body.error : res.body; - callback(new Error(msg)); - console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg); - return; - } - callback(null, body.rows); - }); - }; - - // - // Invoke callback with number of milliseconds since - // last update in any of the given tables - // - me.findLastUpdated = function (username, api_key, tableNames, callback) { - var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY['+ - tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(',') + - '])'; - - // call sql api - me.sqlQuery(username, api_key, sql, function(err, rows){ - if (err){ - var msg = err.message ? err.message : err; - callback(new Error('could not find last updated timestamp: ' + msg)); - return; - } - // when the table has not updated_at means it hasn't been changed so a default last_updated is set - var last_updated = 0; - if(rows.length !== 0) { - last_updated = rows[0].max || 0; - } - callback(null, last_updated*1000); - }); - }; - - me.affectedTables = function (username, api_key, sql, callback) { - - // Replace mapnik tokens - sql = sql.replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)') - .replace(RegExp('!pixel_width!', 'g'), '1') - .replace(RegExp('!pixel_height!', 'g'), '1') - ; - - // Pass to CDB_QueryTables - sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; - - // call sql api - me.sqlQuery(username, api_key, sql, function(err, rows){ - if (err){ - var msg = err.message ? err.message : err; - callback(new Error('could not fetch source tables: ' + msg)); - return; - } - var qtables = rows[0].cdb_querytables; - var tableNames = qtables.split(/^\{(.*)\}$/)[1]; - tableNames = tableNames ? tableNames.split(',') : []; - callback(null, tableNames); - }); - }; - me.buildCacheChannel = function (dbName, tableNames){ return dbName + ':' + tableNames.join(','); }; @@ -304,7 +191,7 @@ module.exports = function(){ if ( req.profiler ) req.profiler.done('getSignerMapKey'); key = data; } - me.affectedTables(user, key, sql, this); // in addCacheChannel + queryTablesApi.getAffectedTablesInQuery(user, key, sql, this); // in addCacheChannel }, function finish(err, data) { next(err,data); @@ -426,44 +313,37 @@ module.exports = function(){ var key = req.params.map_key || req.params.api_key; var cacheKey = dbName + ':' + token; - var tabNames; Step( - function getTables() { - me.affectedTables(usr, key, sql, this); // in afterLayergroupCreate - }, - function getLastupdated(err, tableNames) { - if (req.profiler) req.profiler.done('affectedTables'); - if ( err ) throw err; - var cacheChannel = me.buildCacheChannel(dbName,tableNames); - // store for caching from me.afterLayergroupCreate - me.channelCache[cacheKey] = cacheChannel; - if (req.res && req.method == 'GET') { - var res = req.res; - if ( req.query && req.query.cache_policy == 'persist' ) { - res.header('Cache-Control', 'public,max-age=31536000'); // 1 year - } else { - var ttl = global.environment.varnish.ttl || 86400; - res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); + function getAffectedTablesAndLastUpdatedTime() { + queryTablesApi.getAffectedTablesAndLastUpdatedTime(usr, key, sql, this); + }, + function handleAffectedTablesAndLastUpdatedTime(err, result) { + if (req.profiler) req.profiler.done('queryTablesAndLastUpdated'); + if ( err ) throw err; + var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables); + me.channelCache[cacheKey] = cacheChannel; + + if (req.res && req.method == 'GET') { + var res = req.res; + if ( req.query && req.query.cache_policy == 'persist' ) { + res.header('Cache-Control', 'public,max-age=31536000'); // 1 year + } else { + var ttl = global.environment.varnish.ttl || 86400; + res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); + } + res.header('Last-Modified', (new Date()).toUTCString()); + res.header('X-Cache-Channel', cacheChannel); } - res.header('Last-Modified', (new Date()).toUTCString()); - res.header('X-Cache-Channel', cacheChannel); + + // last update for layergroup cache buster + response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime; + response.last_updated = new Date(result.lastUpdatedTime).toISOString(); + return null; + }, + function finish(err) { + done(err); } - // find last updated - if ( ! tableNames.length ) return 0; // skip for no affected tables - tabNames = tableNames; - me.findLastUpdated(usr, key, tableNames, this); - }, - function(err, lastUpdated) { - if ( err ) throw err; - if (req.profiler && tabNames) req.profiler.done('findLastUpdated'); - response.layergroupid = response.layergroupid + ':' + lastUpdated; // use epoch - response.last_updated = new Date(lastUpdated).toISOString(); - return null; - }, - function finish(err) { - done(err); - } ); }; diff --git a/lib/cartodb/sql/sql_api.js b/lib/cartodb/sql/sql_api.js new file mode 100644 index 00000000..44c831e2 --- /dev/null +++ b/lib/cartodb/sql/sql_api.js @@ -0,0 +1,66 @@ +var _ = require('underscore'), + request = require('request'); + +module.exports.query = function (username, api_key, sql, callback) { + var api = global.environment.sqlapi; + + // build up api string + var sqlapihostname = username; + if ( api.domain ) sqlapihostname += '.' + api.domain; + + var sqlapi = api.protocol + '://'; + if ( api.host && api.host != api.domain ) sqlapi += api.host; + else sqlapi += sqlapihostname; + sqlapi += ':' + api.port + '/api/' + api.version + '/sql'; + + var qs = { q: sql }; + + // add api_key if given + if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; } + + // call sql api + // + // NOTE: using POST to avoid size limits: + // See http://github.com/CartoDB/Windshaft-cartodb/issues/111 + // + // NOTE: uses "host" header to allow IP based specification + // of sqlapi address (and avoid a DNS lookup) + // + // NOTE: allows for keeping up to "maxConnections" concurrent + // sockets opened per SQL-API host. + // See http://nodejs.org/api/http.html#http_agent_maxsockets + // + var maxSockets = global.environment.maxConnections || 128; + var maxGetLen = api.max_get_sql_length || 2048; + var maxSQLTime = api.timeout || 100; // 1/10 of a second by default + var reqSpec = { + url:sqlapi, + json:true, + headers:{host: sqlapihostname} + // http://nodejs.org/api/http.html#http_agent_maxsockets + ,pool:{maxSockets:maxSockets} + // timeout in milliseconds + ,timeout:maxSQLTime + }; + if ( sql.length > maxGetLen ) { + reqSpec.method = 'POST'; + reqSpec.body = qs; + } else { + reqSpec.method = 'GET'; + reqSpec.qs = qs; + } + request(reqSpec, function(err, res, body) { + if (err){ + console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err); + callback(err); + return; + } + if (res.statusCode != 200) { + var msg = res.body.error ? res.body.error : res.body; + callback(new Error(msg)); + console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg); + return; + } + callback(null, body.rows); + }); +}; From 9f8d73a1dfa04c885347d65eb02016558f825033 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 30 Jul 2014 18:17:14 +0200 Subject: [PATCH 3/9] Removes duplicated file --- lib/cartodb/cache/cache_api.js | 159 --------------------------------- 1 file changed, 159 deletions(-) delete mode 100644 lib/cartodb/cache/cache_api.js diff --git a/lib/cartodb/cache/cache_api.js b/lib/cartodb/cache/cache_api.js deleted file mode 100644 index 915cc929..00000000 --- a/lib/cartodb/cache/cache_api.js +++ /dev/null @@ -1,159 +0,0 @@ -var _ = require('underscore'), - request = require('request'); - -function QueryTablesApi() { -} - -var affectedTableRegexCache = { - bbox: /!bbox!/g, - pixel_width: /!pixel_width!/g, - pixel_height: /!pixel_height!/g -}; - -module.exports = QueryTablesApi; - -QueryTablesApi.prototype.getLastUpdatedTime = function (username, api_key, tableNames, callback) { - var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY['+ - tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(',') + - '])'; - - // call sql api - sqlQuery(username, api_key, sql, function(err, rows){ - if (err){ - var msg = err.message ? err.message : err; - callback(new Error('could not find last updated timestamp: ' + msg)); - return; - } - // when the table has not updated_at means it hasn't been changed so a default last_updated is set - var last_updated = 0; - if(rows.length !== 0) { - last_updated = rows[0].max || 0; - } - - callback(null, last_updated*1000); - }); -}; - -QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, api_key, sql, callback) { - // Replace mapnik tokens - sql = sql - .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') - .replace(affectedTableRegexCache.pixel_width, '1') - .replace(affectedTableRegexCache.pixel_height, '1') - ; - - // Pass to CDB_QueryTables - sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; - - // call sql api - sqlQuery(username, api_key, sql, function(err, rows){ - if (err){ - var msg = err.message ? err.message : err; - callback(new Error('could not fetch source tables: ' + msg)); - return; - } - var qtables = rows[0].cdb_querytables; - var tableNames = qtables.split(/^\{(.*)\}$/)[1]; - tableNames = tableNames ? tableNames.split(',') : []; - callback(null, tableNames); - }); -}; - -QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, api_key, sql, callback) { - sql = sql - .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') - .replace(affectedTableRegexCache.pixel_width, '1') - .replace(affectedTableRegexCache.pixel_height, '1') - ; - - var query = [ - 'SELECT', - 'CDB_QueryTables($windshaft$' + sql + '$windshaft$) as tablenames,', - 'EXTRACT(EPOCH FROM max(updated_at)) as max', - 'FROM CDB_TableMetadata m', - 'WHERE m.tabname = any (CDB_QueryTables($windshaft$' + sql + '$windshaft$)::regclass[])' - ].join(' '); - - sqlQuery(username, api_key, query, function(err, rows){ - if (err || rows.length === 0) { - var msg = err.message ? err.message : err; - callback(new Error('could not fetch affected tables and last updated time: ' + msg)); - return; - } - - var qtables = rows[0].tablenames; - var tableNames = qtables.split(/^\{(.*)\}$/)[1]; - tableNames = tableNames ? tableNames.split(',') : []; - - var lastUpdatedTime = rows[0].max || 0; - - callback(null, { - affectedTables: tableNames, - lastUpdatedTime: lastUpdatedTime * 1000 - }); - }); -}; - -function sqlQuery(username, api_key, sql, callback) { - var api = global.environment.sqlapi; - - // build up api string - var sqlapihostname = username; - if ( api.domain ) sqlapihostname += '.' + api.domain; - - var sqlapi = api.protocol + '://'; - if ( api.host && api.host != api.domain ) sqlapi += api.host; - else sqlapi += sqlapihostname; - sqlapi += ':' + api.port + '/api/' + api.version + '/sql'; - - var qs = { q: sql }; - - // add api_key if given - if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; } - - // call sql api - // - // NOTE: using POST to avoid size limits: - // See http://github.com/CartoDB/Windshaft-cartodb/issues/111 - // - // NOTE: uses "host" header to allow IP based specification - // of sqlapi address (and avoid a DNS lookup) - // - // NOTE: allows for keeping up to "maxConnections" concurrent - // sockets opened per SQL-API host. - // See http://nodejs.org/api/http.html#http_agent_maxsockets - // - var maxSockets = global.environment.maxConnections || 128; - var maxGetLen = api.max_get_sql_length || 2048; - var maxSQLTime = api.timeout || 100; // 1/10 of a second by default - var reqSpec = { - url:sqlapi, - json:true, - headers:{host: sqlapihostname} - // http://nodejs.org/api/http.html#http_agent_maxsockets - ,pool:{maxSockets:maxSockets} - // timeout in milliseconds - ,timeout:maxSQLTime - }; - if ( sql.length > maxGetLen ) { - reqSpec.method = 'POST'; - reqSpec.body = qs; - } else { - reqSpec.method = 'GET'; - reqSpec.qs = qs; - } - request(reqSpec, function(err, res, body) { - if (err){ - console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err); - callback(err); - return; - } - if (res.statusCode != 200) { - var msg = res.body.error ? res.body.error : res.body; - callback(new Error(msg)); - console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg); - return; - } - callback(null, body.rows); - }); -} \ No newline at end of file From 654b3ad6d331968b2d640bfeebbdf2a319f8dee8 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 30 Jul 2014 18:23:45 +0200 Subject: [PATCH 4/9] Fixes reference to redis-mpool --- npm-shrinkwrap.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b7d64bc3..c9f02463 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -938,8 +938,8 @@ "from": "git://github.com/CartoDB/node-cartodb-redis.git#0.5.0" }, "redis-mpool": { - "version": "0.0.5", - "from": "http://github.com/CartoDB/node-redis-mpool/tarball/0.0.5", + "version": "0.0.4", + "from": "http://github.com/CartoDB/node-redis-mpool/tarball/0.0.4", "dependencies": { "generic-pool": { "version": "2.0.4" From 799a999148803d005157ba55c5baa5070907b58d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 4 Aug 2014 01:28:30 +0200 Subject: [PATCH 5/9] CDB-3686 Makes SQL API emulator to handle new query with both names and updated time for affected tables. --- test/support/SQLAPIEmu.js | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/test/support/SQLAPIEmu.js b/test/support/SQLAPIEmu.js index 9bb52a9c..074e7ada 100644 --- a/test/support/SQLAPIEmu.js +++ b/test/support/SQLAPIEmu.js @@ -2,7 +2,7 @@ var http = require('http'); var url = require('url'); var _ = require('underscore'); -var o = function(port, cb) { +var SQLAPIEmulator = function(port, cb) { this.queries = []; var that = this; @@ -37,47 +37,45 @@ var o = function(port, cb) { }).listen(port, cb); }; -o.prototype.handleQuery = function(query, res) { +SQLAPIEmulator.prototype.handleQuery = function(query, res) { this.queries.push(query); if ( query.q.match('SQLAPIERROR') ) { res.statusCode = 400; res.write(JSON.stringify({'error':'Some error occurred'})); } else if ( query.q.match('SQLAPINOANSWER') ) { - console.log("SQLAPIEmulator will never respond, on request"); - return; + console.log("SQLAPIEmulator will never respond, on request"); + return; + } else if (query.q.match('tablenames')) { + var tableNames = JSON.stringify(query); + res.write(queryResult({tablenames: '{' + tableNames + '}', max: 1234567890.123})); } else if ( query.q.match('EPOCH.* as max') ) { // This is the structure of the known query sent by tiler - var row = { - 'max': 1234567890.123 - }; - res.write(JSON.stringify({rows: [ row ]})); + res.write(queryResult({max: 1234567890.123})); } else { if ( query.q.match('_private_') && query.api_key === undefined) { res.statusCode = 403; res.write(JSON.stringify({'error':'forbidden: ' + JSON.stringify(query)})); } else { var qs = JSON.stringify(query); - var row = { - // This is the structure of the known query sent by tiler - 'cdb_querytables': '{' + qs + '}', - 'max': qs - }; - var out_obj = {rows: [ row ]}; - var out = JSON.stringify(out_obj); - res.write(out); + res.write(queryResult({cdb_querytables: '{' + qs + '}', max: 1234567890.123})); } } res.end(); }; - -o.prototype.close = function(cb) { +SQLAPIEmulator.prototype.close = function(cb) { this.sqlapi_server.close(cb); }; -o.prototype.getLastRequest = function() { +SQLAPIEmulator.prototype.getLastRequest = function() { return this.requests.pop(); }; -module.exports = o; +function queryResult(row) { + return JSON.stringify({ + rows: [row] + }); +} + +module.exports = SQLAPIEmulator; From 9b5921e8e19baadf3a4be4641a17a32c701bdfda Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 4 Aug 2014 01:29:23 +0200 Subject: [PATCH 6/9] CDB-3686 Fixes expected queries based on changes to request table names and last updated time in one request --- test/acceptance/multilayer.js | 40 +++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 9fadb619..31f91d7b 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -108,10 +108,14 @@ suite('multilayer', function() { assert.equal(cc.substring(0, dbname.length), dbname); var jsonquery = cc.substring(dbname.length+1); var sentquery = JSON.parse(jsonquery); + var expectedQuery = [layergroup.layers[0].options.sql, ';', layergroup.layers[1].options.sql].join(''); assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' - + layergroup.layers[0].options.sql + ';' - + layergroup.layers[1].options.sql - + '$windshaft$)'); + + expectedQuery + + '$windshaft$) as tablenames, EXTRACT(EPOCH FROM max(updated_at)) as max' + + ' FROM CDB_TableMetadata m' + + ' WHERE m.tabname = any (CDB_QueryTables($windshaft$' + + expectedQuery + + '$windshaft$)::regclass[])'); assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2, function(err, similarity) { @@ -384,12 +388,17 @@ suite('multilayer', function() { assert.equal(cc.substring(0, dbname.length), dbname); var jsonquery = cc.substring(dbname.length+1); var sentquery = JSON.parse(jsonquery); + var expectedQuery = layergroup.layers[0].options.sql + .replace(/!bbox!/g, 'ST_MakeEnvelope(0,0,0,0)') + .replace(/!pixel_width!/g, '1') + .replace(/!pixel_height!/g, '1'); assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' - + layergroup.layers[0].options.sql - .replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)') - .replace(RegExp('!pixel_width!', 'g'), '1') - .replace(RegExp('!pixel_height!', 'g'), '1') - + '$windshaft$)'); + + expectedQuery + + '$windshaft$) as tablenames, EXTRACT(EPOCH FROM max(updated_at)) as max' + + ' FROM CDB_TableMetadata m' + + ' WHERE m.tabname = any (CDB_QueryTables($windshaft$' + + expectedQuery + + '$windshaft$)::regclass[])'); assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2, function(err, similarity) { @@ -417,12 +426,17 @@ suite('multilayer', function() { assert.equal(cc.substring(0, dbname.length), dbname); var jsonquery = cc.substring(dbname.length+1); var sentquery = JSON.parse(jsonquery); + var expectedQuery = layergroup.layers[0].options.sql + .replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)') + .replace('!pixel_width!', '1') + .replace('!pixel_height!', '1'); assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' - + layergroup.layers[0].options.sql - .replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)') - .replace('!pixel_width!', '1') - .replace('!pixel_height!', '1') - + '$windshaft$)'); + + expectedQuery + + '$windshaft$) as tablenames, EXTRACT(EPOCH FROM max(updated_at)) as max' + + ' FROM CDB_TableMetadata m' + + ' WHERE m.tabname = any (CDB_QueryTables($windshaft$' + + expectedQuery + + '$windshaft$)::regclass[])'); assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2, function(err, similarity) { From 73d1db3bd2b89da858b1a08e57e4caabf310e5fb Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 4 Aug 2014 01:30:24 +0200 Subject: [PATCH 7/9] CDB-3686 Adds support for per mil tolerance when comparing images as in Mac OS X some results from ImageMagick are a bit odd --- test/acceptance/multilayer.js | 11 +++--- test/acceptance/server.js | 21 ++++++----- test/support/assert.js | 67 ++++++++++++++++++++++------------- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 31f91d7b..89e4c25d 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -14,6 +14,9 @@ var helper = require(__dirname + '/../support/test_helper'); var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures'; +var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20; +var IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL = 25; + var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); var ServerOptions = require(__dirname + '/../../lib/cartodb/server_options'); serverOptions = ServerOptions(); @@ -117,7 +120,7 @@ suite('multilayer', function() { + expectedQuery + '$windshaft$)::regclass[])'); - assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2, + assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL, function(err, similarity) { next(err); }); @@ -400,7 +403,7 @@ suite('multilayer', function() { + expectedQuery + '$windshaft$)::regclass[])'); - assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2, + assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { next(err); }); @@ -438,7 +441,7 @@ suite('multilayer', function() { + expectedQuery + '$windshaft$)::regclass[])'); - assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2, + assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { next(err); }); @@ -1067,7 +1070,7 @@ suite('multilayer', function() { }, {}, function(res) { assert.equal(res.statusCode, 200, res.body); assert.equal(res.headers['content-type'], "image/png"); - assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png', 2, + assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { next(err); }); diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 5c688fcc..0a430dd1 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -11,6 +11,9 @@ var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js'); var helper = require(__dirname + '/../support/test_helper'); +var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20, + IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL = 0; + var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); var server = new CartodbWindshaft(serverOptions); @@ -842,7 +845,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2, + assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { if (err) throw err; done(); @@ -873,7 +876,7 @@ suite('server', function() { assert.equal(ct, 'image/png'); assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', - 2, this); + IMAGE_EQUALS_TOLERANCE_PER_MIL, this); }, function checkImage(err, similarity) { if (err) throw err; @@ -910,7 +913,7 @@ suite('server', function() { assert.equal(ct, 'image/png'); assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', - 2, this); + IMAGE_EQUALS_TOLERANCE_PER_MIL, this); }, function checkImage(err, similarity) { if (err) throw err; @@ -934,7 +937,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2, + assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { if (err) throw err; done(); @@ -971,7 +974,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2, + assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { next(err); }); @@ -1011,7 +1014,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0, + assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL, function(err, similarity) { if (err) next(err); else next(); @@ -1031,7 +1034,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0, + assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL, function(err, similarity) { if (err) next(err); else next(); @@ -1068,7 +1071,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2, + assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { // NOTE: we expect them to be EQUAL here if (err) { next(err); return; } @@ -1105,7 +1108,7 @@ suite('server', function() { assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); var ct = res.headers['content-type']; assert.equal(ct, 'image/png'); - assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2, + assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { // NOTE: we expect them to be different here if (err) next(); diff --git a/test/support/assert.js b/test/support/assert.js index 26d3221f..2c7e174f 100644 --- a/test/support/assert.js +++ b/test/support/assert.js @@ -1,10 +1,11 @@ // Cribbed from the ever prolific Konstantin Kaefer // https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js -var fs = require('fs'); -var http = require('http'); -var path = require('path'); -var exec = require('child_process').exec; +var exec = require('child_process').exec, + fs = require('fs'), + http = require('http'), + path = require('path'), + util = require('util'); var assert = module.exports = exports = require('assert'); @@ -66,35 +67,51 @@ assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) { callback(err); }; -// -// @param tol tolerated color distance as a percent over max channel value -// by default this is zero. For meaningful values, see -// http://www.imagemagick.org/script/command-line-options.php#metric -// -assert.imageEqualsFile = function(buffer, file_b, tol, callback) { +/** + * Takes an image data as an input and an image path and compare them using ImageMagick fuzz algorithm, if case the + * similarity is not within the tolerance limit it will callback with an error. + * + * @param buffer The image data to compare from + * @param {string} referenceImageRelativeFilePath The relative file to compare against + * @param {number} tolerance tolerated mean color distance, as a per mil (‰) + * @param {function} callback Will call to home with null in case there is no error, otherwise with the error itself + * @see FUZZY in http://www.imagemagick.org/script/command-line-options.php#metric + */ +assert.imageEqualsFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) { if (!callback) callback = function(err) { if (err) throw err; }; - file_b = path.resolve(file_b); - var file_a = '/tmp/windshaft-test-image-test.png'; // + (Math.random() * 1e16); // TODO: make predictable - var err = fs.writeFileSync(file_a, buffer, 'binary'); + var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath), + testImageFilePath = '/tmp/windshaft-test-image-' + (Math.random() * 1e16); // TODO: make predictable + var err = fs.writeFileSync(testImageFilePath, buffer, 'binary'); if (err) throw err; - var fuzz = tol + '%'; - exec('compare -fuzz ' + fuzz + ' -metric AE "' + file_a + '" "' + - file_b + '" /dev/null', function(err, stdout, stderr) { + var imageMagickCmd = util.format( + 'compare -metric fuzz "%s" "%s" /dev/null', + testImageFilePath, referenceImageFilePath + ); + + exec(imageMagickCmd, function(err, stdout, stderr) { if (err) { - fs.unlinkSync(file_a); + fs.unlinkSync(testImageFilePath); callback(err); } else { stderr = stderr.trim(); - var similarity = parseFloat(stderr); - if ( similarity > 0 ) { - var err = new Error('Images not equal(' + similarity + '): ' + - file_a + ' ' + file_b); - err.similarity = similarity; - callback(err); + var metrics = stderr.match(/([0-9]*) \((.*)\)/); + if ( ! metrics ) { + callback(new Error("No match for " + stderr)); + return; + } + var similarity = parseFloat(metrics[2]), + tolerancePerMil = (tolerance / 1000); + if (similarity > tolerancePerMil) { + err = new Error(util.format( + 'Images %s and %s are not equal (got %d similarity, expected %d)', + testImageFilePath, referenceImageFilePath, similarity, tolerancePerMil) + ); + err.similarity = similarity; + callback(err); } else { - fs.unlinkSync(file_a); - callback(null); + fs.unlinkSync(testImageFilePath); + callback(null); } } }); From 507a6a89795bf0cd4d741514afd444c3cf401453 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 4 Aug 2014 01:32:49 +0200 Subject: [PATCH 8/9] CDB-3686 Style changes --- lib/cartodb/api/query_tables_api.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/cartodb/api/query_tables_api.js b/lib/cartodb/api/query_tables_api.js index 9782d91a..18920b59 100644 --- a/lib/cartodb/api/query_tables_api.js +++ b/lib/cartodb/api/query_tables_api.js @@ -80,11 +80,12 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam return; } - var qtables = rows[0].tablenames; - var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + var result = rows[0]; + + var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1]; tableNames = tableNames ? tableNames.split(',') : []; - var lastUpdatedTime = rows[0].max || 0; + var lastUpdatedTime = result.max || 0; callback(null, { affectedTables: tableNames, From 528815a564b710a0a055d971b812b1080efd5bf1 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 4 Aug 2014 13:24:44 +0200 Subject: [PATCH 9/9] Updates news --- NEWS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS.md b/NEWS.md index 85cbd03b..565efd99 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,13 @@ 1.13.2 -- 2014-mm-dd -------------------- +Enhancements: + - SQL API requests moved to its own entity + +New features: + - Affected tables and last updated time for a query are performed in a single request to the SQL API + + 1.13.1 -- 2014-08-04 --------------------