diff --git a/lib/cartodb/utils/overviews_query_rewriter.js b/lib/cartodb/utils/overviews_query_rewriter.js index b5b35503..4a23975c 100644 --- a/lib/cartodb/utils/overviews_query_rewriter.js +++ b/lib/cartodb/utils/overviews_query_rewriter.js @@ -1,5 +1,10 @@ +var _ = require('underscore'); var TableNameParser = require('./table_name_parser'); +// this is meant as a hack for development (otherwise we'd be coupling +// this to Camshaft impolementation) +var queryBuilder = require('camshaft/lib/filter/query-builder'); + function OverviewsQueryRewriter(options) { this.options = options; @@ -119,26 +124,34 @@ function replace_table_in_query(sql, old_table_name, replacement) { return sql.replace(new RegExp(regexp, 'g'), replacement); } -function overviews_query(query, overviews, zoom_level_expression) { + +function replace_table_in_query_with_schema(query, table, schema, replacement) { + if ( replacement ) { + query = replace_table_in_query(query, table, replacement); + var parsed_table = TableNameParser.parse(table); + if (!parsed_table.schema && schema) { + // replace also the qualified table name, if the table wasn't qualified + parsed_table.schema = schema; + table = TableNameParser.table_identifier(parsed_table); + query = replace_table_in_query(query, table, replacement); + } + } + return query; +} + +// Build query to use overviews for a variant zoom level (given by a expression to +// be evaluated by the database server) +function overviews_query_with_zoom_expression(query, overviews, zoom_level_expression) { var replaced_query = query; var sql = "WITH\n _vovw_scale AS ( SELECT " + zoom_level_expression + " AS _vovw_z )"; var replacement; - for ( var table in overviews ) { - if (overviews.hasOwnProperty(table)) { - var table_overviews = overviews[table]; - var table_view = overviews_view_name(table); - var schema = table_overviews.schema; - replacement = "(\n" + overviews_view_for_table(table, table_overviews) + "\n ) AS " + table_view; - replaced_query = replace_table_in_query(replaced_query, table, replacement); - var parsed_table = TableNameParser.parse(table); - if (!parsed_table.schema && schema) { - // replace also the qualified table name, if the table wasn't qualified - parsed_table.schema = schema; - table = TableNameParser.table_identifier(parsed_table); - replaced_query = replace_table_in_query(replaced_query, table, replacement); - } - } - } + _.each(_.keys(overviews), function(table) { + var table_overviews = overviews[table]; + var table_view = overviews_view_name(table); + var schema = table_overviews.schema; + replacement = "(\n" + overviews_view_for_table(table, table_overviews) + "\n ) AS " + table_view; + replaced_query = replace_table_in_query_with_schema(replaced_query, table, schema, replacement); + }); if ( replaced_query !== query ) { sql += "\n"; sql += replaced_query; @@ -148,6 +161,54 @@ function overviews_query(query, overviews, zoom_level_expression) { return sql; } +// Build query to use overviews for a specific zoom level value +function overviews_query_with_definite_zoom(query, overviews, zoom_level) { + var replaced_query = query; + var replacement; + _.each(_.keys(overviews), function(table) { + var table_overviews = overviews[table]; + var schema = table_overviews.schema; + replacement = overview_table_for_zoom_level(table_overviews, zoom_level); + replaced_query = replace_table_in_query_with_schema(replaced_query, table, schema, replacement); + }); + return replaced_query; +} + +// Find a suitable overview table for a specific zoom_level +function overview_table_for_zoom_level(table_overviews, zoom_level) { + var overview_table; + if ( table_overviews ) { + overview_table = table_overviews[zoom_level]; + if ( !overview_table ) { + _.every(_.keys(table_overviews).sort(function(x,y){ return x-y; }), function(overview_zoom) { + if ( +overview_zoom > +zoom_level ) { + overview_table = table_overviews[overview_zoom]; + return false; + } else { + return true; + } + }); + } + } + if ( overview_table ) { + overview_table = overview_table.table; + } + return overview_table; +} + +function apply_filters_to_query(query, filters) { + // TODO: it'd be nice to have a public API in camshaft to apply filters! + if ( filters && !_.isEmpty(filters)) { + query = queryBuilder.getSql(query, filters); + } + return query; +} + +function zoom_level_for_bounding_box(query, bounding_box) { + // TODO: implement + return '100'; // force use of base table +} + // Transform an SQL query so that it uses overviews. // overviews contains metadata about the overviews to be used: // { 'table-name': {1: { table: 'overview-table-1' }, ... }, ... } @@ -166,13 +227,50 @@ function overviews_query(query, overviews, zoom_level_expression) { // doesnn't prevent substitution inside literals). // But the transformation will currently only be applied to simple queries // of the form detected by the overviews_supported_query function. -OverviewsQueryRewriter.prototype.query = function(query, data) { - var overviews = this.overviews_metadata(data); - if ( !overviews || !this.is_supported_query(query)) { +OverviewsQueryRewriter.prototype.query = function(query, data, options) { + options = options || {}; + + var overviews = data && data.overviews; + var unfiltered_query = data && data.unfiltered_query; + var filters = data && data.filters; + + if ( !unfiltered_query ) { + unfiltered_query = query; + } + + if ( !overviews || !this.is_supported_query(unfiltered_query)) { return query; } - var zoom_level_expression = this.options.zoom_level || '0'; - return overviews_query(query, overviews, zoom_level_expression); + + var rewritten_query; + + var zoom_level_expression = this.options.zoom_level; + var zoom_level; + if ( _.has(options, 'bounding_box') ) { + // The presence of this key, even if it has a null value indicates the + // query cannot handle dynamic zoom expressinos (is not for the tiler) + zoom_level = zoom_level_for_bounding_box(unfiltered_query, options.bounding_box); + } else if ( options.zoom_level ) { + zoom_level = options.zoom_level; + } + if ( !zoom_level && !zoom_level_expression ) { + zoom_level = '0'; + } + + if ( zoom_level || zoom_level === '0' || zoom_level === 0 ) { + rewritten_query = overviews_query_with_definite_zoom(unfiltered_query, overviews, zoom_level); + } else { + rewritten_query = overviews_query_with_zoom_expression(unfiltered_query, overviews, zoom_level_expression); + } + + if ( rewritten_query === unfiltered_query ) { + // could not or didn't need to alter the query + rewritten_query = query; + } else { + rewritten_query = apply_filters_to_query(rewritten_query, filters); + } + + return rewritten_query; }; OverviewsQueryRewriter.prototype.is_supported_query = function(sql) { @@ -188,7 +286,3 @@ OverviewsQueryRewriter.prototype.is_supported_query = function(sql) { ); return !!(sql.match(unwrapped_query) || sql.match(wrapped_query)); }; - -OverviewsQueryRewriter.prototype.overviews_metadata = function(data) { - return data && data.overviews; -};