2017-09-14 01:17:16 +08:00
|
|
|
const BaseWidget = require('./base');
|
|
|
|
const debug = require('debug')('windshaft:widget:aggregation');
|
2016-03-22 20:10:42 +08:00
|
|
|
|
2017-09-14 00:38:54 +08:00
|
|
|
const filteredQueryTpl = ctx => `
|
|
|
|
filtered_source AS (
|
|
|
|
SELECT *
|
2017-09-14 19:14:12 +08:00
|
|
|
FROM (${ctx.query}) _cdb_filtered_source
|
|
|
|
${ctx.aggregationColumn && ctx.isFloatColumn ? `
|
2017-09-14 00:38:54 +08:00
|
|
|
WHERE
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.aggregationColumn} != 'infinity'::float
|
2017-09-14 00:38:54 +08:00
|
|
|
AND
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.aggregationColumn} != '-infinity'::float
|
2017-09-14 00:38:54 +08:00
|
|
|
AND
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.aggregationColumn} != 'NaN'::float` :
|
2017-09-14 00:38:54 +08:00
|
|
|
''
|
|
|
|
}
|
|
|
|
)
|
|
|
|
`;
|
|
|
|
|
|
|
|
const summaryQueryTpl = ctx => `
|
|
|
|
summary AS (
|
|
|
|
SELECT
|
|
|
|
count(1) AS count,
|
2017-09-14 19:14:12 +08:00
|
|
|
sum(CASE WHEN ${ctx.column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
|
|
|
|
${ctx.isFloatColumn ? `,
|
2017-09-14 00:38:54 +08:00
|
|
|
sum(
|
|
|
|
CASE
|
2017-09-14 19:14:12 +08:00
|
|
|
WHEN ${ctx.aggregationColumn} = 'infinity'::float OR ${ctx.aggregationColumn} = '-infinity'::float
|
2017-09-14 00:38:54 +08:00
|
|
|
THEN 1
|
|
|
|
ELSE 0
|
|
|
|
END
|
|
|
|
) AS infinities_count,
|
2017-09-14 19:14:12 +08:00
|
|
|
sum(CASE WHEN ${ctx.aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
|
2017-09-14 00:38:54 +08:00
|
|
|
''
|
|
|
|
}
|
2017-09-14 19:14:12 +08:00
|
|
|
FROM (${ctx.query}) _cdb_aggregation_nulls
|
2017-09-14 00:38:54 +08:00
|
|
|
)
|
|
|
|
`;
|
|
|
|
|
|
|
|
const rankedCategoriesQueryTpl = ctx => `
|
|
|
|
categories AS(
|
|
|
|
SELECT
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.column} AS category,
|
|
|
|
${ctx.aggregationFn} AS value,
|
|
|
|
row_number() OVER (ORDER BY ${ctx.aggregationFn} desc) as rank
|
2017-09-14 00:38:54 +08:00
|
|
|
FROM filtered_source
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.aggregationColumn !== null ? `WHERE ${ctx.aggregationColumn} IS NOT NULL` : ''}
|
|
|
|
GROUP BY ${ctx.column}
|
2017-09-14 00:38:54 +08:00
|
|
|
ORDER BY 2 DESC
|
|
|
|
)
|
|
|
|
`;
|
|
|
|
|
|
|
|
const categoriesSummaryMinMaxQueryTpl = () => `
|
|
|
|
categories_summary_min_max AS(
|
|
|
|
SELECT
|
|
|
|
max(value) max_val,
|
|
|
|
min(value) min_val
|
|
|
|
FROM categories
|
|
|
|
)
|
|
|
|
`;
|
|
|
|
|
|
|
|
const categoriesSummaryCountQueryTpl = ctx => `
|
|
|
|
categories_summary_count AS(
|
|
|
|
SELECT count(1) AS categories_count
|
|
|
|
FROM (
|
2017-09-14 19:14:12 +08:00
|
|
|
SELECT ${ctx.column} AS category
|
|
|
|
FROM (${ctx.query}) _cdb_categories
|
|
|
|
GROUP BY ${ctx.column}
|
2017-09-14 00:38:54 +08:00
|
|
|
) _cdb_categories_count
|
|
|
|
)
|
|
|
|
`;
|
|
|
|
|
|
|
|
const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
|
|
|
|
|
|
|
|
const rankedAggregationQueryTpl = ctx => `
|
|
|
|
SELECT
|
|
|
|
CAST(category AS text),
|
|
|
|
value,
|
|
|
|
false as agg,
|
|
|
|
nulls_count,
|
|
|
|
min_val,
|
|
|
|
max_val,
|
|
|
|
count,
|
|
|
|
categories_count
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
2017-09-14 00:38:54 +08:00
|
|
|
FROM categories, summary, categories_summary_min_max, categories_summary_count
|
2017-09-14 19:14:12 +08:00
|
|
|
WHERE rank < ${ctx.limit}
|
2017-09-14 00:38:54 +08:00
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
|
|
'Other' category,
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
|
2017-09-14 00:38:54 +08:00
|
|
|
true as agg,
|
|
|
|
nulls_count,
|
|
|
|
min_val,
|
|
|
|
max_val,
|
|
|
|
count,
|
|
|
|
categories_count
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
2017-09-14 00:38:54 +08:00
|
|
|
FROM categories, summary, categories_summary_min_max, categories_summary_count
|
2017-09-14 19:14:12 +08:00
|
|
|
WHERE rank >= ${ctx.limit}
|
2017-09-14 00:38:54 +08:00
|
|
|
GROUP BY
|
|
|
|
nulls_count,
|
|
|
|
min_val,
|
|
|
|
max_val,
|
|
|
|
count,
|
|
|
|
categories_count
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
2017-09-14 00:38:54 +08:00
|
|
|
`;
|
|
|
|
|
|
|
|
const aggregationQueryTpl = ctx => `
|
|
|
|
SELECT
|
2017-09-14 19:14:12 +08:00
|
|
|
CAST(${ctx.column} AS text) AS category,
|
|
|
|
${ctx.aggregationFn} AS value,
|
2017-09-14 00:38:54 +08:00
|
|
|
false as agg,
|
|
|
|
nulls_count,
|
|
|
|
min_val,
|
|
|
|
max_val,
|
|
|
|
count,
|
|
|
|
categories_count
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
|
|
|
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
|
2017-09-14 00:38:54 +08:00
|
|
|
GROUP BY
|
|
|
|
category,
|
|
|
|
nulls_count,
|
|
|
|
min_val,
|
|
|
|
max_val,
|
|
|
|
count,
|
|
|
|
categories_count
|
2017-09-14 19:14:12 +08:00
|
|
|
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
2017-09-14 00:38:54 +08:00
|
|
|
ORDER BY value DESC
|
|
|
|
`;
|
2016-03-22 20:10:42 +08:00
|
|
|
|
2017-09-14 17:27:03 +08:00
|
|
|
const categoriesCTESqlTpl = ctx => `
|
|
|
|
WITH
|
|
|
|
${filteredQueryTpl({
|
2017-09-14 19:14:12 +08:00
|
|
|
isFloatColumn: ctx.isFloatColumn,
|
|
|
|
query: ctx.query,
|
|
|
|
column: ctx.column,
|
|
|
|
aggregationColumn: ctx.aggregationColumn
|
2017-09-14 17:27:03 +08:00
|
|
|
})},
|
|
|
|
${summaryQueryTpl({
|
2017-09-14 19:14:12 +08:00
|
|
|
isFloatColumn: ctx.isFloatColumn,
|
|
|
|
query: ctx.query,
|
|
|
|
column: ctx.column,
|
|
|
|
aggregationColumn: ctx.aggregationColumn
|
2017-09-14 17:27:03 +08:00
|
|
|
})},
|
|
|
|
${rankedCategoriesQueryTpl({
|
2017-09-14 19:14:12 +08:00
|
|
|
query: ctx.query,
|
|
|
|
column: ctx.column,
|
|
|
|
aggregationFn: ctx.aggregationFn,
|
|
|
|
aggregationColumn: ctx.aggregationColumn
|
2017-09-14 17:27:03 +08:00
|
|
|
})},
|
|
|
|
${categoriesSummaryMinMaxQueryTpl({
|
2017-09-14 19:14:12 +08:00
|
|
|
query: ctx.query,
|
|
|
|
column: ctx.column
|
2017-09-14 17:27:03 +08:00
|
|
|
})},
|
|
|
|
${categoriesSummaryCountQueryTpl({
|
2017-09-14 19:14:12 +08:00
|
|
|
query: ctx.query,
|
|
|
|
column: ctx.column
|
2017-09-14 17:27:03 +08:00
|
|
|
})}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn})`;
|
|
|
|
|
2017-09-14 18:18:03 +08:00
|
|
|
const aggregationSqlTpl = ctx => `
|
2017-09-14 19:14:12 +08:00
|
|
|
${categoriesCTESqlTpl(ctx)}
|
|
|
|
${!!ctx.override.ownFilter ? `${aggregationQueryTpl(ctx)}` : `${rankedAggregationQueryTpl(ctx)}`}
|
2017-09-14 18:18:03 +08:00
|
|
|
`;
|
|
|
|
|
2017-09-14 00:40:09 +08:00
|
|
|
const CATEGORIES_LIMIT = 6;
|
2016-03-22 20:10:42 +08:00
|
|
|
|
2017-09-14 00:40:09 +08:00
|
|
|
const VALID_OPERATIONS = {
|
2016-03-22 20:10:42 +08:00
|
|
|
count: [],
|
2016-06-20 22:20:48 +08:00
|
|
|
sum: ['aggregationColumn'],
|
|
|
|
avg: ['aggregationColumn'],
|
|
|
|
min: ['aggregationColumn'],
|
|
|
|
max: ['aggregationColumn']
|
2016-03-22 20:10:42 +08:00
|
|
|
};
|
|
|
|
|
2017-09-14 00:40:09 +08:00
|
|
|
const TYPE = 'aggregation';
|
2016-03-22 20:10:42 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
{
|
|
|
|
type: 'aggregation',
|
|
|
|
options: {
|
|
|
|
column: 'name',
|
|
|
|
aggregation: 'count' // it could be, e.g., sum if column is numeric
|
|
|
|
}
|
|
|
|
}
|
|
|
|
*/
|
2017-09-14 01:19:25 +08:00
|
|
|
function Aggregation(query, options = {}, queries = {}) {
|
2017-09-14 01:16:08 +08:00
|
|
|
if (typeof options.column !== 'string') {
|
2017-09-14 01:27:25 +08:00
|
|
|
throw new Error(`Aggregation expects 'column' in widget options`);
|
2016-03-22 20:10:42 +08:00
|
|
|
}
|
|
|
|
|
2017-09-14 01:16:08 +08:00
|
|
|
if (typeof options.aggregation !== 'string') {
|
2017-09-14 01:27:25 +08:00
|
|
|
throw new Error(`Aggregation expects 'aggregation' operation in widget options`);
|
2016-03-22 20:10:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!VALID_OPERATIONS[options.aggregation]) {
|
2017-09-14 01:27:25 +08:00
|
|
|
throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
|
2016-03-22 20:10:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
var requiredOptions = VALID_OPERATIONS[options.aggregation];
|
2017-09-14 01:16:08 +08:00
|
|
|
var missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));
|
|
|
|
|
2016-03-22 20:10:42 +08:00
|
|
|
if (missingOptions.length > 0) {
|
2017-09-14 01:27:25 +08:00
|
|
|
throw new Error(`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`);
|
2016-03-22 20:10:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
BaseWidget.apply(this);
|
|
|
|
|
|
|
|
this.query = query;
|
2017-06-16 01:07:31 +08:00
|
|
|
this.queries = queries;
|
2016-03-22 20:10:42 +08:00
|
|
|
this.column = options.column;
|
|
|
|
this.aggregation = options.aggregation;
|
|
|
|
this.aggregationColumn = options.aggregationColumn;
|
2017-06-16 01:07:31 +08:00
|
|
|
this._isFloatColumn = null;
|
2016-03-22 20:10:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Aggregation.prototype = new BaseWidget();
|
|
|
|
Aggregation.prototype.constructor = Aggregation;
|
|
|
|
|
|
|
|
module.exports = Aggregation;
|
|
|
|
|
2016-06-01 17:42:24 +08:00
|
|
|
Aggregation.prototype.sql = function(psql, override, callback) {
|
2017-06-16 01:07:31 +08:00
|
|
|
var self = this;
|
|
|
|
|
2016-03-22 20:10:42 +08:00
|
|
|
if (!callback) {
|
|
|
|
callback = override;
|
|
|
|
override = {};
|
|
|
|
}
|
|
|
|
|
2017-06-16 01:07:31 +08:00
|
|
|
if (this.aggregationColumn && this._isFloatColumn === null) {
|
|
|
|
this._isFloatColumn = false;
|
|
|
|
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
|
|
|
|
if (!err && !!type) {
|
|
|
|
self._isFloatColumn = type.float;
|
|
|
|
}
|
|
|
|
self.sql(psql, override, callback);
|
|
|
|
});
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-09-14 18:18:03 +08:00
|
|
|
var aggregationSql = aggregationSqlTpl({
|
|
|
|
override: override,
|
|
|
|
query: this.query,
|
|
|
|
column: this.column,
|
|
|
|
aggregation: this.aggregation,
|
2017-09-14 19:14:12 +08:00
|
|
|
aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null,
|
|
|
|
aggregationFn: aggregationFnQueryTpl({
|
|
|
|
aggregation: this.aggregation,
|
|
|
|
aggregationColumn: this.aggregationColumn || 1
|
|
|
|
}),
|
|
|
|
isFloatColumn: this._isFloatColumn,
|
|
|
|
limit: CATEGORIES_LIMIT
|
2017-09-14 18:18:03 +08:00
|
|
|
});
|
2016-03-22 20:10:42 +08:00
|
|
|
|
|
|
|
debug(aggregationSql);
|
|
|
|
|
|
|
|
return callback(null, aggregationSql);
|
|
|
|
};
|
|
|
|
|
|
|
|
Aggregation.prototype.format = function(result) {
|
|
|
|
var categories = [];
|
|
|
|
var count = 0;
|
|
|
|
var nulls = 0;
|
2017-06-13 00:55:33 +08:00
|
|
|
var nans = 0;
|
|
|
|
var infinities = 0;
|
2016-03-22 20:10:42 +08:00
|
|
|
var minValue = 0;
|
|
|
|
var maxValue = 0;
|
|
|
|
var categoriesCount = 0;
|
|
|
|
|
|
|
|
if (result.rows.length) {
|
|
|
|
var firstRow = result.rows[0];
|
|
|
|
count = firstRow.count;
|
|
|
|
nulls = firstRow.nulls_count;
|
2017-06-13 00:55:33 +08:00
|
|
|
nans = firstRow.nans_count;
|
|
|
|
infinities = firstRow.infinities_count;
|
2016-03-22 20:10:42 +08:00
|
|
|
minValue = firstRow.min_val;
|
|
|
|
maxValue = firstRow.max_val;
|
|
|
|
categoriesCount = firstRow.categories_count;
|
2017-09-14 01:16:08 +08:00
|
|
|
categories = result.rows.map(({ category, value, agg }) => ({ category, value, agg }));
|
2016-03-22 20:10:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2016-06-20 22:20:48 +08:00
|
|
|
aggregation: this.aggregation,
|
2016-03-22 20:10:42 +08:00
|
|
|
count: count,
|
|
|
|
nulls: nulls,
|
2017-06-13 00:55:33 +08:00
|
|
|
nans: nans,
|
|
|
|
infinities: infinities,
|
2016-03-22 20:10:42 +08:00
|
|
|
min: minValue,
|
|
|
|
max: maxValue,
|
|
|
|
categoriesCount: categoriesCount,
|
|
|
|
categories: categories
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2017-09-14 00:38:54 +08:00
|
|
|
const filterCategoriesQueryTpl = ctx => `
|
|
|
|
SELECT
|
|
|
|
${ctx._column} AS category,
|
|
|
|
${ctx._value} AS value
|
|
|
|
FROM (${ctx._query}) _cdb_aggregation_search
|
|
|
|
WHERE CAST(${ctx._column} as text) ILIKE ${ctx._userQuery}
|
|
|
|
GROUP BY ${ctx._column}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const searchQueryTpl = ctx => `
|
|
|
|
WITH
|
|
|
|
search_unfiltered AS (
|
|
|
|
${ctx._searchUnfiltered}
|
|
|
|
),
|
|
|
|
search_filtered AS (
|
|
|
|
${ctx._searchFiltered}
|
|
|
|
),
|
|
|
|
search_union AS (
|
|
|
|
SELECT * FROM search_unfiltered
|
|
|
|
UNION ALL
|
|
|
|
SELECT * FROM search_filtered
|
|
|
|
)
|
|
|
|
SELECT category, sum(value) AS value
|
|
|
|
FROM search_union
|
|
|
|
GROUP BY category
|
|
|
|
ORDER BY value desc
|
|
|
|
`;
|
2016-03-22 20:10:42 +08:00
|
|
|
|
|
|
|
Aggregation.prototype.search = function(psql, userQuery, callback) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
|
2017-07-07 23:44:32 +08:00
|
|
|
var _value = this.aggregation !== 'count' && this.aggregationColumn ?
|
|
|
|
this.aggregation + '(' + this.aggregationColumn + ')' : 'count(1)';
|
2016-03-22 20:10:42 +08:00
|
|
|
|
|
|
|
// TODO unfiltered will be wrong as filters are already applied at this point
|
|
|
|
var query = searchQueryTpl({
|
|
|
|
_searchUnfiltered: filterCategoriesQueryTpl({
|
|
|
|
_query: this.query,
|
|
|
|
_column: this.column,
|
|
|
|
_value: '0',
|
|
|
|
_userQuery: _userQuery
|
|
|
|
}),
|
|
|
|
_searchFiltered: filterCategoriesQueryTpl({
|
|
|
|
_query: this.query,
|
|
|
|
_column: this.column,
|
2017-07-07 23:09:17 +08:00
|
|
|
_value: _value,
|
2016-03-22 20:10:42 +08:00
|
|
|
_userQuery: _userQuery
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
psql.query(query, function(err, result) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err, result);
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback(null, {type: self.getType(), categories: result.rows });
|
|
|
|
}, true); // use read-only transaction
|
|
|
|
};
|
|
|
|
|
|
|
|
Aggregation.prototype.getType = function() {
|
|
|
|
return TYPE;
|
|
|
|
};
|
|
|
|
|
|
|
|
Aggregation.prototype.toString = function() {
|
|
|
|
return JSON.stringify({
|
|
|
|
_type: TYPE,
|
|
|
|
_query: this.query,
|
|
|
|
_column: this.column,
|
|
|
|
_aggregation: this.aggregation
|
|
|
|
});
|
|
|
|
};
|