From 3c8987ff3e3b13034b7b1fa7310b6ae1aed67d3e Mon Sep 17 00:00:00 2001
From: Tom Robinson <tlrobinson@gmail.com>
Date: Thu, 9 Jul 2015 23:05:49 -0700
Subject: [PATCH] Refactory Query functions from GuiQueryEditor into own file
 in preparation for Data Reference using them

---
 .../query_builder/gui_query_editor.react.js   | 337 +++---------------
 .../app/query_builder/query.js                | 280 +++++++++++++++
 2 files changed, 330 insertions(+), 287 deletions(-)
 create mode 100644 resources/frontend_client/app/query_builder/query.js

diff --git a/resources/frontend_client/app/query_builder/gui_query_editor.react.js b/resources/frontend_client/app/query_builder/gui_query_editor.react.js
index 1e773b0e8d3..4c7fe857ed1 100644
--- a/resources/frontend_client/app/query_builder/gui_query_editor.react.js
+++ b/resources/frontend_client/app/query_builder/gui_query_editor.react.js
@@ -10,6 +10,8 @@ import RunButton from './run_button.react';
 import SelectionModule from './selection_module.react';
 import SortWidget from './sort_widget.react';
 
+import Query from './query';
+
 var cx = React.addons.classSet;
 var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
 
@@ -32,7 +34,7 @@ export default React.createClass({
         };
     },
 
-    setQuery: function(dataset_query, notify) {
+    setQuery: function(dataset_query) {
         this.props.notifyQueryModifiedFn(dataset_query);
     },
 
@@ -64,332 +66,93 @@ export default React.createClass({
         query.database = this.props.query.database;
         query.query.source_table = tableId;
 
-        this.setQuery(query, true);
+        this.setQuery(query);
     },
 
     canRun: function() {
-        if (this.hasValidAggregation()) {
-            return true;
-        }
-        return false;
+        return Query.canRun(this.props.query.query);
     },
 
     runQuery: function() {
-        var cleanQuery = this.cleanQuery(this.props.query);
-
-        this.props.runFn(cleanQuery);
-    },
-
-    cleanQuery: function(dataset_query) {
-        // it's possible the user left some half-done parts of the query on screen when they hit the run button, so find those
-        // things now and clear them out so that we have a nice clean set of valid clauses in our query
-
-        // TODO: breakouts
-
-        // filters
-        var queryFilters = this.getFilters();
-        if (queryFilters.length > 1) {
-            var hasNullValues = function(arr) {
-                for (var j=0; j < arr.length; j++) {
-                    if (arr[j] === null) {
-                        return true;
-                    }
-                }
-
-                return false;
-            };
-
-            var cleanFilters = [queryFilters[0]];
-            for (var i=1; i < queryFilters.length; i++) {
-                if (!hasNullValues(queryFilters[i])) {
-                    cleanFilters.push(queryFilters[i]);
-                }
-            }
-
-            if (cleanFilters.length > 1) {
-                dataset_query.query.filter = cleanFilters;
-            } else {
-                dataset_query.query.filter = [];
-            }
-        }
-
-        // TODO: limit
-
-        // TODO: sort
-
-        return dataset_query;
-    },
-
-    canAddDimensions: function() {
-        var MAX_DIMENSIONS = 2;
-        return (this.props.query.query.breakout.length < MAX_DIMENSIONS);
-    },
-
-    hasValidBreakout: function() {
-        return (this.props.query.query.breakout &&
-                    this.props.query.query.breakout.length > 0 &&
-                    this.props.query.query.breakout[0] !== null);
-    },
-
-    canSortByAggregateField: function() {
-        var SORTABLE_AGGREGATION_TYPES = new Set(["avg", "count", "distinct", "stddev", "sum"]);
-
-        return this.hasValidBreakout() && SORTABLE_AGGREGATION_TYPES.has(this.props.query.query.aggregation[0]);
+        Query.cleanQuery(this.props.query.query);
+        this.props.runFn(this.props.query);
     },
 
     addDimension: function() {
-        var query = this.props.query;
-        query.query.breakout.push(null);
-
-        this.setQuery(query, true);
+        Query.addDimension(this.props.query.query);
+        this.setQuery(this.props.query);
     },
 
     updateDimension: function(dimension, index) {
-        var query = this.props.query;
-        query.query.breakout[index] = dimension;
-
-        this.setQuery(query, true);
+        Query.updateDimension(this.props.query.query, dimension, index);
+        this.setQuery(this.props.query);
     },
 
     removeDimension: function(index) {
-        // TODO: when we remove breakouts we also need to remove any limits/sorts that don't make sense
-        var query = this.props.query;
-        query.query.breakout.splice(index, 1);
-
-        this.setQuery(query, true);
-    },
-
-    hasEmptyAggregation: function() {
-        var aggregation = this.props.query.query.aggregation;
-        if (aggregation !== undefined &&
-                aggregation.length > 0 &&
-                aggregation[0] !== null) {
-            return false;
-        }
-        return true;
-    },
-
-    hasValidAggregation: function() {
-        var aggregation = this.props.query.query.aggregation;
-        if (aggregation !== undefined &&
-                ((aggregation.length === 1 && aggregation[0] !== null) ||
-                 (aggregation.length === 2 && aggregation[0] !== null && aggregation[1] !== null))) {
-            return true;
-        }
-        return false;
-    },
-
-    isBareRowsAggregation: function() {
-        return (this.props.query.query.aggregation &&
-                    this.props.query.query.aggregation.length > 0 &&
-                    this.props.query.query.aggregation[0] === "rows");
+        Query.removeDimension(this.props.query.query, index);
+        this.setQuery(this.props.query);
     },
 
     updateAggregation: function(aggregationClause) {
-        var query = this.props.query;
-        query.query.aggregation = aggregationClause;
-
-        // for "rows" type aggregation we always clear out any dimensions because they don't make sense
-        if (aggregationClause.length > 0 && aggregationClause[0] === "rows") {
-            query.query.breakout = [];
-        }
-
-        this.setQuery(query, true);
-    },
-
-    renderAddIcon: function () {
-        return (
-            <span className="mr1">
-                <Icon name="add" width="12px" height="12px" />
-            </span>
-        )
-    },
-
-    getFilters: function() {
-        // Special handling for accessing query filters because it's been fairly complex to deal with their structure.
-        // This method provide a unified and consistent view of the filter definition for the rest of the tool to use.
-
-        var queryFilters = this.props.query.query.filter;
-
-        // quick check for older style filter definitions and tweak them to a format we want to work with
-        if (queryFilters && queryFilters.length > 0 && queryFilters[0] !== "AND") {
-            var reformattedFilters = [];
-
-            for (var i=0; i < queryFilters.length; i++) {
-                if (queryFilters[i] !== null) {
-                    reformattedFilters = ["AND", queryFilters];
-                    break;
-                }
-            }
-
-            queryFilters = reformattedFilters;
-        }
-
-        return queryFilters;
-    },
-
-    canAddFilter: function(queryFilters) {
-        var canAdd = true;
-
-        if (queryFilters && queryFilters.length > 0) {
-            var lastFilter = queryFilters[queryFilters.length - 1];
-
-            // simply make sure that there are no null values in the last filter
-            for (var i=0; i < lastFilter.length; i++) {
-                if (lastFilter[i] === null) {
-                    canAdd = false;
-                }
-            }
-        } else {
-            canAdd = false;
-        }
-
-        return canAdd;
+        Query.updateAggregation(this.props.query.query, aggregationClause);
+        this.setQuery(this.props.query);
     },
 
     addFilter: function() {
-        var query = this.props.query,
-            queryFilters = this.getFilters();
-
-        if (queryFilters.length === 0) {
-            queryFilters = ["AND", [null, null, null]];
-        } else {
-            queryFilters.push([null, null, null]);
-        }
-
-        query.query.filter = queryFilters;
-        this.setQuery(query, true);
+        Query.addFilter(this.props.query.query);
+        this.setQuery(this.props.query);
     },
 
     updateFilter: function(index, filter) {
-        var query = this.props.query,
-            queryFilters = this.getFilters();
-
-        queryFilters[index] = filter;
-
-        query.query.filter = queryFilters;
-        this.setQuery(query, true);
+        Query.updateFilter(this.props.query.query, index, filter);
+        this.setQuery(this.props.query);
     },
 
     removeFilter: function(index) {
-        var query = this.props.query,
-            queryFilters = this.getFilters();
-
-        if (queryFilters.length === 2) {
-            // this equates to having a single filter because the arry looks like ... ["AND" [a filter def array]]
-            queryFilters = [];
-        } else {
-            queryFilters.splice(index, 1);
-        }
-
-        query.query.filter = queryFilters;
-        this.setQuery(query, true);
-    },
-
-    canAddLimitAndSort: function() {
-        // limits and sorts only make sense if we know there will be multiple rows
-        var query = this.props.query;
-
-        if (this.isBareRowsAggregation()) {
-            return true;
-        } else if (this.hasValidBreakout()) {
-            return true;
-        } else {
-            return false;
-        }
-    },
-
-    getSortableFields: function() {
-        // in bare rows all fields are sortable, otherwise we only sort by our breakout columns
-        var query = this.props.query;
-
-        // start with all fields
-        var fieldList = [];
-        for(var key in this.props.options.fields_lookup) {
-            fieldList.push(this.props.options.fields_lookup[key]);
-        }
-
-        if (this.isBareRowsAggregation()) {
-            return fieldList;
-        } else if (this.hasValidBreakout()) {
-            // further filter field list down to only fields in our breakout clause
-            var breakoutFieldList = [];
-            this.props.query.query.breakout.map(function (breakoutFieldId) {
-                for (var idx in fieldList) {
-                    if (fieldList[idx].id === breakoutFieldId) {
-                        breakoutFieldList.push(fieldList[idx]);
-                    }
-                }
-            }.bind(this));
-
-            if (this.canSortByAggregateField()) {
-                breakoutFieldList.push({
-                    id: ["aggregation",  0],
-                    name: this.props.query.query.aggregation[0] // e.g. "sum"
-                });
-            }
-
-            return breakoutFieldList;
-        } else {
-            return [];
-        }
+        Query.removeFilter(this.props.query.query, index);
+        this.setQuery(this.props.query);
     },
 
     addLimit: function() {
-        var query = this.props.query;
-        query.query.limit = null;
-        this.setQuery(query, true);
+        Query.addLimit(this.props.query.query);
+        this.setQuery(this.props.query);
     },
 
     updateLimit: function(limit) {
-        var query = this.props.query;
-        query.query.limit = limit;
-        this.setQuery(query, true);
+        Query.updateLimit(this.props.query.query, limit);
+        this.setQuery(this.props.query);
     },
 
     removeLimit: function() {
-        var query = this.props.query;
-        delete query.query.limit;
-        this.setQuery(query, true);
-    },
-
-    canAddSort: function() {
-        // TODO: allow for multiple sorting choices
-        return false;
+        Query.removeLimit(this.props.query.query);
+        this.setQuery(this.props.query);
     },
 
     addSort: function() {
-        // TODO: make sure people don't try to sort by the same field multiple times
-        var query = this.props.query,
-            order_by = query.query.order_by;
-
-        if (!order_by) {
-            order_by = [];
-        }
-
-        order_by.push([null, "ascending"]);
-        query.query.order_by = order_by;
-
-        this.setQuery(query, true);
+        Query.addSort(this.props.query.query);
+        this.setQuery(this.props.query);
     },
 
     updateSort: function(index, sort) {
-        var query = this.props.query;
-        query.query.order_by[index] = sort;
-        this.setQuery(query, true);
+        Query.updateSort(this.props.query.query, index, sort);
+        this.setQuery(this.props.query);
     },
 
     removeSort: function(index) {
-        var query = this.props.query,
-            queryOrderBy = query.query.order_by;
+        Query.removeSort(this.props.query.query, index);
+        this.setQuery(this.props.query);
+    },
 
-        if (queryOrderBy.length === 1) {
-            delete query.query.order_by;
-        } else {
-            queryOrderBy.splice(index, 1);
-        }
+    getSortableFields: function() {
+        return Query.getSortableFields(this.props.query.query, this.props.options.fields_lookup);
+    },
 
-        this.setQuery(query, true);
+    renderAddIcon: function () {
+        return (
+            <span className="mr1">
+                <Icon name="add" width="12px" height="12px" />
+            </span>
+        )
     },
 
     renderDbSelector: function() {
@@ -439,7 +202,7 @@ export default React.createClass({
 
     renderFilterButton: function() {
         if (this.props.query.query.source_table &&
-                this.getFilters().length === 0 &&
+                Query.getFilters(this.props.query.query).length === 0 &&
                 this.props.options &&
                 this.props.options.fields.length > 0) {
             return (
@@ -455,7 +218,7 @@ export default React.createClass({
         // breakout clause.  must have table details available & a valid aggregation defined
         if (this.props.options &&
                 this.props.options.breakout_options.fields.length > 0 &&
-                !this.hasEmptyAggregation()) {
+                !Query.hasEmptyAggregation(this.props.query.query)) {
 
             // only render a label for our breakout if we have a valid breakout clause already
             var breakoutLabel;
@@ -536,7 +299,7 @@ export default React.createClass({
     },
 
     renderFilterSelector: function() {
-        var queryFilters = this.getFilters();
+        var queryFilters = Query.getFilters(this.props.query.query);
 
         if (this.props.options && queryFilters && queryFilters.length > 0) {
             var component = this;
@@ -563,7 +326,7 @@ export default React.createClass({
 
             // TODO: proper check for isFilterComplete(filter)
             var addFilterButton;
-            if (this.canAddFilter(queryFilters)) {
+            if (Query.canAddFilter(this.props.query.query)) {
                 addFilterButton = (
                     <a className="QueryOption p1 lg-p2" onClick={this.addFilter}>
                         {this.renderAddIcon()}
@@ -586,7 +349,7 @@ export default React.createClass({
     },
 
     renderLimitAndSort: function() {
-        if (this.props.options && !this.hasEmptyAggregation() &&
+        if (this.props.options && !Query.hasEmptyAggregation(this.props.query.query) &&
                 (this.props.query.query.limit !== undefined || this.props.query.query.order_by !== undefined)) {
 
             var limitSection;
@@ -640,7 +403,7 @@ export default React.createClass({
                 );
             } else {
                 var addSortButton;
-                if (this.canAddSort()) {
+                if (Query.canAddSort(this.props.query.query)) {
                     addSortButton = (
                         <a onClick={this.addSort}>Add another sort</a>
                     );
@@ -665,7 +428,7 @@ export default React.createClass({
                 </div>
             );
 
-        } else if (this.canAddLimitAndSort()) {
+        } else if (Query.canAddLimitAndSort(this.props.query.query)) {
             return (
                 <div className={this.props.querySectionClasses}>
                     <a className="QueryOption QueryOption--offset p1 lg-p2" onClick={this.addLimit}>
diff --git a/resources/frontend_client/app/query_builder/query.js b/resources/frontend_client/app/query_builder/query.js
new file mode 100644
index 00000000000..8e836e6c82c
--- /dev/null
+++ b/resources/frontend_client/app/query_builder/query.js
@@ -0,0 +1,280 @@
+'use strict';
+
+var Query = {
+
+    canRun: function(query) {
+        if (Query.hasValidAggregation(query)) {
+            return true;
+        }
+        return false;
+    },
+
+    cleanQuery: function(query) {
+        // it's possible the user left some half-done parts of the query on screen when they hit the run button, so find those
+        // things now and clear them out so that we have a nice clean set of valid clauses in our query
+
+        // TODO: breakouts
+
+        // filters
+        var queryFilters = Query.getFilters(query);
+        if (queryFilters.length > 1) {
+            var hasNullValues = function(arr) {
+                for (var j=0; j < arr.length; j++) {
+                    if (arr[j] === null) {
+                        return true;
+                    }
+                }
+
+                return false;
+            };
+
+            var cleanFilters = [queryFilters[0]];
+            for (var i=1; i < queryFilters.length; i++) {
+                if (!hasNullValues(queryFilters[i])) {
+                    cleanFilters.push(queryFilters[i]);
+                }
+            }
+
+            if (cleanFilters.length > 1) {
+                query.filter = cleanFilters;
+            } else {
+                query.filter = [];
+            }
+        }
+
+        // TODO: limit
+
+        // TODO: sort
+
+        return query;
+    },
+
+    canAddDimensions: function(query) {
+        var MAX_DIMENSIONS = 2;
+        return (query.breakout.length < MAX_DIMENSIONS);
+    },
+
+    hasValidBreakout: function(query) {
+        return (query.breakout &&
+                    query.breakout.length > 0 &&
+                    query.breakout[0] !== null);
+    },
+
+    canSortByAggregateField: function(query) {
+        var SORTABLE_AGGREGATION_TYPES = new Set(["avg", "count", "distinct", "stddev", "sum"]);
+
+        return Query.hasValidBreakout(query) && SORTABLE_AGGREGATION_TYPES.has(query.aggregation[0]);
+    },
+
+    addDimension: function(query) {
+        query.breakout.push(null);
+    },
+
+    updateDimension: function(query, dimension, index) {
+        query.breakout[index] = dimension;
+    },
+
+    removeDimension: function(query, index) {
+        // TODO: when we remove breakouts we also need to remove any limits/sorts that don't make sense
+        query.breakout.splice(index, 1);
+    },
+
+    hasEmptyAggregation: function(query) {
+        var aggregation = query.aggregation;
+        if (aggregation !== undefined &&
+                aggregation.length > 0 &&
+                aggregation[0] !== null) {
+            return false;
+        }
+        return true;
+    },
+
+    hasValidAggregation: function(query) {
+        var aggregation = query.aggregation;
+        if (aggregation !== undefined &&
+                ((aggregation.length === 1 && aggregation[0] !== null) ||
+                 (aggregation.length === 2 && aggregation[0] !== null && aggregation[1] !== null))) {
+            return true;
+        }
+        return false;
+    },
+
+    isBareRowsAggregation: function(query) {
+        return (query.aggregation &&
+                    query.aggregation.length > 0 &&
+                    query.aggregation[0] === "rows");
+    },
+
+    updateAggregation: function(query, aggregationClause) {
+        query.aggregation = aggregationClause;
+
+        // for "rows" type aggregation we always clear out any dimensions because they don't make sense
+        if (aggregationClause.length > 0 && aggregationClause[0] === "rows") {
+            query.breakout = [];
+        }
+    },
+
+    getFilters: function(query) {
+        // Special handling for accessing query filters because it's been fairly complex to deal with their structure.
+        // This method provide a unified and consistent view of the filter definition for the rest of the tool to use.
+
+        var queryFilters = query.filter;
+
+        // quick check for older style filter definitions and tweak them to a format we want to work with
+        if (queryFilters && queryFilters.length > 0 && queryFilters[0] !== "AND") {
+            var reformattedFilters = [];
+
+            for (var i=0; i < queryFilters.length; i++) {
+                if (queryFilters[i] !== null) {
+                    reformattedFilters = ["AND", queryFilters];
+                    break;
+                }
+            }
+
+            queryFilters = reformattedFilters;
+        }
+
+        return queryFilters;
+    },
+
+    canAddFilter: function(query) {
+        var canAdd = true;
+        var queryFilters = Query.getFilters(query);
+        if (queryFilters && queryFilters.length > 0) {
+            var lastFilter = queryFilters[queryFilters.length - 1];
+
+            // simply make sure that there are no null values in the last filter
+            for (var i=0; i < lastFilter.length; i++) {
+                if (lastFilter[i] === null) {
+                    canAdd = false;
+                }
+            }
+        } else {
+            canAdd = false;
+        }
+
+        return canAdd;
+    },
+
+    addFilter: function(query) {
+        var queryFilters = Query.getFilters(query);
+
+        if (queryFilters.length === 0) {
+            queryFilters = ["AND", [null, null, null]];
+        } else {
+            queryFilters.push([null, null, null]);
+        }
+
+        query.filter = queryFilters;
+    },
+
+    updateFilter: function(query, index, filter) {
+        var queryFilters = Query.getFilters(query);
+
+        queryFilters[index] = filter;
+
+        query.filter = queryFilters;
+    },
+
+    removeFilter: function(query, index) {
+        var queryFilters = Query.getFilters(query);
+
+        if (queryFilters.length === 2) {
+            // this equates to having a single filter because the arry looks like ... ["AND" [a filter def array]]
+            queryFilters = [];
+        } else {
+            queryFilters.splice(index, 1);
+        }
+
+        query.filter = queryFilters;
+    },
+
+    canAddLimitAndSort: function(query) {
+        if (Query.isBareRowsAggregation(query)) {
+            return true;
+        } else if (Query.hasValidBreakout(query)) {
+            return true;
+        } else {
+            return false;
+        }
+    },
+
+    getSortableFields: function(query, fields) {
+        // in bare rows all fields are sortable, otherwise we only sort by our breakout columns
+
+        // start with all fields
+        var fieldList = [];
+        for(var key in fields) {
+            fieldList.push(fields[key]);
+        }
+
+        if (Query.isBareRowsAggregation(query)) {
+            return fieldList;
+        } else if (Query.hasValidBreakout(query)) {
+            // further filter field list down to only fields in our breakout clause
+            var breakoutFieldList = [];
+            query.breakout.map(function (breakoutFieldId) {
+                for (var idx in fieldList) {
+                    if (fieldList[idx].id === breakoutFieldId) {
+                        breakoutFieldList.push(fieldList[idx]);
+                    }
+                }
+            });
+
+            if (Query.canSortByAggregateField(query)) {
+                breakoutFieldList.push({
+                    id: ["aggregation",  0],
+                    name: query.aggregation[0] // e.g. "sum"
+                });
+            }
+
+            return breakoutFieldList;
+        } else {
+            return [];
+        }
+    },
+
+    addLimit: function(query) {
+        query.limit = null;
+    },
+
+    updateLimit: function(query, limit) {
+        query.limit = limit;
+    },
+
+    removeLimit: function(query) {
+        delete query.limit;
+    },
+
+    canAddSort: function(query) {
+        // TODO: allow for multiple sorting choices
+        return false;
+    },
+
+    addSort: function(query) {
+        // TODO: make sure people don't try to sort by the same field multiple times
+        var order_by = query.order_by;
+        if (!order_by) {
+            order_by = [];
+        }
+
+        order_by.push([null, "ascending"]);
+        query.order_by = order_by;
+    },
+
+    updateSort: function(query, index, sort) {
+        query.order_by[index] = sort;
+    },
+
+    removeSort: function(query, index) {
+        var queryOrderBy = query.order_by;
+
+        if (queryOrderBy.length === 1) {
+            delete query.order_by;
+        } else {
+            queryOrderBy.splice(index, 1);
+        }
+    }
+}
+
+export default Query;
-- 
GitLab