package flare.query { import flare.util.Filter; import flare.util.Property; import flare.util.Sort; /** * Performs query processing over a collection of ActionScript objects. * Queries can perform filtering, sorting, grouping, and aggregation * operations over a data collection. Arbitrary data collections can * be queried by providing a visitor function similar to the * Array.forEach method to the query eval method. * *

The select and where methods in the * flare.query.methods package are useful shorthands * for helping to construct queries in code.

* *

Here is an example of a query. It uses helper methods defined in the * flare.query.methods package. For example, the * sum method creates a Sum query operator and * the _ method creates as a Literal expression * for its input value.

* *
	 * import flare.query.methods.*;
	 * 
	 * var data:Array = [
	 *  {cat:"a", val:1}, {cat:"a", val:2}, {cat:"b", val:3}, {cat:"b", val:4},
	 *  {cat:"c", val:5}, {cat:"c", val:6}, {cat:"d", val:7}, {cat:"d", val:8}
	 * ];
	 * 
	 * var r:Array = select("cat", {sum:sum("val")}) // sum of values
	 *               .where(neq("cat", _("d"))       // exclude category "d"
	 *               .groupby("cat")                 // group by category
	 *               .eval(data);                    // evaluate with data array
	 * 
	 * // r == [{cat:"a", sum:3}, {cat:"b", sum:7}, {cat:"c", sum:11}]
	 * 
*/ public class Query { private var _select:Array; private var _orderby:Array; private var _groupby:Array; private var _where:Function; private var _sort:Sort; private var _aggrs:Array; private var _map:Boolean = false; private var _update:Boolean = false; /** * Creates a new Query. * @param select an array of select clauses. A select clause consists * of either a string representing the name of a variable to query or * an object of the form {name:expr}, where * name is the name of the query variable to include in * query result objects and expr is an Expression for * the actual query value. Expressions can be any legal expression, * including aggregate operators. * @param where a where expression for filtering an object collection * @param orderby directives for sorting query results, using the * format of the flare.util.Sort class methods. * @param groupby directives for grouping query results, using the * format of the flare.util.Sort class methods. * @see flare.util.Sort */ public function Query(select:Array=null, where:*=null, orderby:Array=null, groupby:Array=null) { if (select != null) setSelect(select); this.where(where); _orderby = orderby; _groupby = groupby; } // -- public methods -------------------------------------------------- /** * Sets the select clauses used by this query. A select clause consists * of either a string representing the name of a variable to query or * an object of the form {name:expr}, where * name is the name of the query variable to include in * query result objects and expr is an * Expression for the actual query value. *

Calling the select method will overwrite the effect * of any previous calls to the select or * update methods.

* @param terms a list of query terms (select clauses). If the first * element is an array, it will be used as the term list. * @return this query object */ public function select(...terms):Query { if (terms.length > 0 && terms[0] is Array) { terms = terms[0]; } setSelect(terms); _update = false; return this; } /** * Sets the select clauses used by this query to update the values * of the input set. An update clause consists of an object of the * form {name:expr}, where name is the name * of the data variable to set and expr is an * Expression for computing the value. When * eval is invoked for an update query, the values of the * input objects are updated and the returned result set is an array * containing these input objects. *

Calling the update method will overwrite the effect * of any previous calls to the select or * update methods.

* @param terms a list of query terms (update clauses). If the first * element is an array, it will be used as the term list. * @return this query object */ public function update(...terms):Query { if (terms==null || terms.length==0) { throw new Error("Nothing to update!"); } else if (terms[0] is Array) { terms = terms[0]; } setSelect(terms); _update = true; return this; } /** * Sets the where clause (filter conditions) used by this query. * @param e the filter expression. This can be a string, a literal * value, or an Expression instance. This input value * will be run through the Expression.expr method. * @return this query object */ public function where(e:*):Query { _where = Filter.$(e); return this; } /** * Sets the sort order for query results. * @param terms the sort terms as a list of field names to sort on. * By default, fields are sorted in ascending order. Add the prefix * "-" (negative sign) to the field name to sort in descending order. * @return this query object */ public function orderby(...terms):Query { _orderby = (terms.length > 0 ? terms : null); return this; } /** * Sets the group by terms for aggregate queries. * @param terms an ordered list of terms to group by. * @return this query object */ public function groupby(...terms):Query { _groupby = (terms.length > 0 ? terms : null); return this; } /** * Sets whether or not aggregate functions will be mapped to all * tuples in the data set. This allows the results of aggregate * operators to be applied for all tuples. For example, the * map directive allows queries of this form: *
		 * var q:Query = select({a:div(a,sum(a))}).map().eval(...);
		 * 
*

The result include normalized a values for * all tuples in the input data. Without the map directive, a * "group-by" for all data would be assumed and only a single tuple * would be returned in the result set (matching the normal behavior * of a SQL database). Map can also be specified with a * group-by clause, in which case the aggregate operators will be * handled separately for each group, but the result set will still * contain a result for every tuple in the input set.

*

The map directive has no effect on "update" queries, which * already apply map semantics. It only effects "select" queries.

* @param value if true (the default), aggregate operators will be * applied (mapped) to all tuples; if false, normal group-by semantics * will be used. * @return this query object */ public function map(value:Boolean=true):Query { _map = value; return this; } // -- helper methods -------------------------------------------------- private function setSelect(a:Array):void { _select = []; for each (var o:Object in a) { if (o is String) { _select.push({ name: o as String, expression: new Variable(o as String) }); } else { for (var n:String in o) { _select.push({ name: n, expression: Expression.expr(o[n]) }); } } } } private function sorter():Sort { var s:Array = [], i:int; if (_groupby != null) { for (i=0; i<_groupby.length; ++i) s.push(_groupby[i]); } if (_orderby != null) { for (i=0; i<_orderby.length; ++i) s.push(_orderby[i]); } return s.length==0 ? null : new Sort(s); } private function aggregates():Array { var aggrs:Array = []; for each (var pair:Object in _select) { var expr:Expression = pair.expression; expr.visit(function(e:Expression):void { if (e is AggregateExpression) aggrs.push(e); }); } return aggrs.length==0 ? null : aggrs; } // -- query processing ------------------------------------------------ /** * Evaluates this query on an object collection. The input argument can * either be an array of objects or a visitor function that takes * another function as input and applies it to all items in a * collection. * @param input either an array of objects or a visitor function * @return an array of processed query results */ public function eval(input:*):Vector. { // check for initialization if (_sort == null) _sort = sorter(); if (_aggrs == null) _aggrs = aggregates(); // TODO -- evaluate any sub-queries in WHERE clause var results:Vector. = new Vector.(); var visitor:Function; if (input is Array) { visitor = (input as Array).forEach; } else if (input is Vector.) { visitor = (input as Vector.).forEach; } else if (input is Function) { visitor = input as Function; } else if (Object(input).hasOwnProperty("visit") && Object(input).visit is Function) { visitor = Object(input).visit as Function; } else { throw new ArgumentError("Illegal input argument: "+input); } // collect and filter if (_where != null) { visitor(function(item:Object, ...rest):void { if (_where(item)) results.push(item); }); } else { visitor(function(item:Object, ...rest):void { results.push(item); }); } // sort the result set if (_sort != null) { _sort.sort(results); } if (_select == null) { return results; } else if (_update && _aggrs==null && _groupby==null) { return applyAll(results); } else if (_aggrs==null && _groupby==null) { return projectAll(results); } else { return aggregate(results); } } private function projectAll(results:Vector.):Vector. { for (var i:int=0; i):Vector. { var o:Object = {}; for each (var item:Object in results) { for each (var pair:Object in _select) { var name:String = pair.name; var expr:Expression = pair.expression; o[name] = expr.eval(item); } for each (pair in _select) { name = pair.name; var p:Property = Property.$(name); p.setValue(item, o[name]); } } return results; } // -- group-by and aggregation ---------------------------------------- /** * Performs grouping and aggregation of query results. * @param items the filtered query results object vector * @return aggregated query results object vector */ private function aggregate(items:Vector.):Vector. { var h:int, i:int, j:int, item:Object; var results:Vector. = new Vector.() var props:Vector. = new Vector.(); // get group-by properties as key if (_groupby != null) { for (i=_groupby.length; --i>=0;) { if (_groupby[i] is String) { props.push(Property.$(_groupby[i])); } } } // process all groups reset(_aggrs); for (i=1, h=0; i<=items.length; ++i) { item = items[i-1]; // update the aggregate functions for each (var aggr:AggregateExpression in _aggrs) { aggr.aggregate(items[i-1]); } // handle change of group if (i==items.length || !(_groupby==null || sameGroup(props, item, items[i]))) { if (_update) { for (j=h; j, x:Object, y:Object):Boolean { var a:*, b:*; for each (var p:Property in props) { a = p.getValue(x); b = p.getValue(y); if (a is Date && b is Date) { if ((a as Date).time != (b as Date).time) return false; } else if (a != b) { return false; } } return true; } } // end of class Query }