diff --git a/lib/cartodb/models/dataview/base.js b/lib/cartodb/models/dataview/base.js index 29069d37..8c1ada5d 100644 --- a/lib/cartodb/models/dataview/base.js +++ b/lib/cartodb/models/dataview/base.js @@ -1,67 +1,16 @@ -var dot = require('dot'); -dot.templateSettings.strip = false; - -function BaseDataview() {} - -module.exports = BaseDataview; - -BaseDataview.prototype.getResult = function(psql, override, callback) { - var self = this; - this.sql(psql, override, function(err, query) { - if (err) { - return callback(err); - } - - psql.query(query, function(err, result) { - if (err) { - return callback(err, result); - } - - result = self.format(result, override); - result.type = self.getType(); - - return callback(null, result); - - }, true); // use read-only transaction - }); - -}; - -BaseDataview.prototype.search = function(psql, userQuery, callback) { - return callback(null, this.format({ rows: [] })); -}; - -var FLOAT_OIDS = { +const FLOAT_OIDS = { 700: true, 701: true, 1700: true }; -var DATE_OIDS = { +const DATE_OIDS = { 1082: true, 1114: true, 1184: true }; -var columnTypeQueryTpl = dot.template( - 'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_column_type limit 1' -); - -BaseDataview.prototype.getColumnType = function (psql, column, query, callback) { - var readOnlyTransaction = true; - - var columnTypeQuery = columnTypeQueryTpl({ - column: column, query: query - }); - - psql.query(columnTypeQuery, function(err, result) { - if (err) { - return callback(err); - } - var pgType = result.rows[0].pg_typeof; - callback(null, getPGTypeName(pgType)); - }, readOnlyTransaction); -}; +const columnTypeQueryTpl = ctx => `SELECT pg_typeof(${ctx.column})::oid FROM (${ctx.query}) _cdb_column_type limit 1`; function getPGTypeName (pgType) { return { @@ -69,3 +18,42 @@ function getPGTypeName (pgType) { date: DATE_OIDS.hasOwnProperty(pgType) }; } + +module.exports = class BaseDataview { + getResult (psql, override, callback) { + this.sql(psql, override, (err, query) => { + if (err) { + return callback(err); + } + + psql.query(query, (err, result) => { + if (err) { + return callback(err, result); + } + + result = this.format(result, override); + result.type = this.getType(); + + return callback(null, result); + + }, true); // use read-only transaction + }); + } + + search (psql, userQuery, callback) { + return callback(null, this.format({ rows: [] })); + } + + getColumnType (psql, column, query, callback) { + const readOnlyTransaction = true; + const columnTypeQuery = columnTypeQueryTpl({ column, query }); + + psql.query(columnTypeQuery, (err, result) => { + if (err) { + return callback(err); + } + const pgType = result.rows[0].pg_typeof; + callback(null, getPGTypeName(pgType)); + }, readOnlyTransaction); + } +}; diff --git a/lib/cartodb/models/dataview/formula.js b/lib/cartodb/models/dataview/formula.js index 7ec356b7..daaeaf0c 100644 --- a/lib/cartodb/models/dataview/formula.js +++ b/lib/cartodb/models/dataview/formula.js @@ -1,28 +1,36 @@ -var _ = require('underscore'); -var BaseWidget = require('./base'); -var debug = require('debug')('windshaft:widget:formula'); +const BaseDataview = require('./base'); +const debug = require('debug')('windshaft:dataview:formula'); -var dot = require('dot'); -dot.templateSettings.strip = false; +const countInfinitiesQueryTpl = ctx => ` + SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities + WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float +`; -var formulaQueryTpl = dot.template([ - 'SELECT', - ' {{=it._operation}}({{=it._column}}) AS result,', - ' (SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count', - ' {{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls', - ' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count', - ' ,(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls', - ' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}', - 'FROM ({{=it._query}}) _cdb_formula', - '{{?it._isFloatColumn && it._operation !== \'count\'}}WHERE', - ' {{=it._column}} != \'infinity\'::float', - 'AND', - ' {{=it._column}} != \'-infinity\'::float', - 'AND', - ' {{=it._column}} != \'NaN\'::float{{?}}' -].join('\n')); +const countNansQueryTpl = ctx => ` + SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans + WHERE ${ctx.column} = 'NaN'::float +`; -var VALID_OPERATIONS = { +const filterOutSpecialNumericValuesTpl = ctx => ` + WHERE + ${ctx.column} != 'infinity'::float + AND + ${ctx.column} != '-infinity'::float + AND + ${ctx.column} != 'NaN'::float +`; + +const formulaQueryTpl = ctx => ` + SELECT + ${ctx.operation}(${ctx.column}) AS result, + (SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count + ${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''} + ${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''} + FROM (${ctx.query}) __cdb_formula + ${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''} +`; + +const VALID_OPERATIONS = { count: true, avg: true, sum: true, @@ -30,7 +38,7 @@ var VALID_OPERATIONS = { max: true }; -var TYPE = 'formula'; +const TYPE = 'formula'; /** { @@ -41,93 +49,90 @@ var TYPE = 'formula'; } } */ -function Formula(query, options, queries) { - if (!_.isString(options.operation)) { - throw new Error('Formula expects `operation` in widget options'); +module.exports = class Formula extends BaseDataview { + constructor (query, options = {}, queries = {}) { + super(); + + this._checkOptions(options); + + this.query = query; + this.queries = queries; + this.column = options.column || '1'; + this.operation = options.operation; + this._isFloatColumn = null; } - if (!VALID_OPERATIONS[options.operation]) { - throw new Error("Formula does not support '" + options.operation + "' operation"); + _checkOptions (options) { + if (typeof options.operation !== 'string') { + throw new Error(`Formula expects 'operation' in dataview options`); + } + + if (!VALID_OPERATIONS[options.operation]) { + throw new Error(`Formula does not support '${options.operation}' operation`); + } + + if (options.operation !== 'count' && typeof options.column !== 'string') { + throw new Error(`Formula expects 'column' in dataview options`); + } } - if (options.operation !== 'count' && !_.isString(options.column)) { - throw new Error('Formula expects `column` in widget options'); - } - BaseWidget.apply(this); + sql (psql, override, callback) { + if (!callback) { + callback = override; + override = {}; + } - this.query = query; - this.queries = queries; - this.column = options.column || '1'; - this.operation = options.operation; - this._isFloatColumn = null; -} + if (this._isFloatColumn === null) { + this._isFloatColumn = false; + this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => { + if (!err && !!type) { + this._isFloatColumn = type.float; + } + this.sql(psql, override, callback); + }); + return null; + } -Formula.prototype = new BaseWidget(); -Formula.prototype.constructor = Formula; - -module.exports = Formula; - -Formula.prototype.sql = function(psql, override, callback) { - var self = this; - - if (!callback) { - callback = override; - override = {}; - } - - if (this._isFloatColumn === null) { - this._isFloatColumn = false; - this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) { - if (!err && !!type) { - self._isFloatColumn = type.float; - } - self.sql(psql, override, callback); + const formulaSql = formulaQueryTpl({ + isFloatColumn: this._isFloatColumn, + query: this.query, + operation: this.operation, + column: this.column }); - return null; + + debug(formulaSql); + + return callback(null, formulaSql); } - var formulaSql = formulaQueryTpl({ - _isFloatColumn: this._isFloatColumn, - _query: this.query, - _operation: this.operation, - _column: this.column - }); + format (res) { + const { + result = 0, + nulls_count = 0, + nans_count, + infinities_count + } = res.rows[0] || {}; - debug(formulaSql); - - return callback(null, formulaSql); -}; - -Formula.prototype.format = function(result) { - var formattedResult = { - operation: this.operation, - result: 0, - nulls: 0, - nans: 0, - infinities: 0 - }; - - if (result.rows.length) { - formattedResult.operation = this.operation; - formattedResult.result = result.rows[0].result; - formattedResult.nulls = result.rows[0].nulls_count; - formattedResult.nans = result.rows[0].nans_count; - formattedResult.infinities = result.rows[0].infinities_count; + return { + operation: this.operation, + result, + nulls: nulls_count, + nans: nans_count, + infinities: infinities_count + }; } - return formattedResult; -}; + getType () { + return TYPE; + } -Formula.prototype.getType = function() { - return TYPE; -}; - -Formula.prototype.toString = function() { - return JSON.stringify({ - _type: TYPE, - _query: this.query, - _column: this.column, - _operation: this.operation - }); + toString () { + return JSON.stringify({ + _type: TYPE, + _query: this.query, + _column: this.column, + _operation: this.operation + }); + } };