I put together the start of a a dynamic query set builder today. It is still in the beginning stages, but it already has some real power.
Actionscript:
-
var qs:QS = new QS().filter([{name__iexact: 'adsf', insider_id__notnull: '', age__gt: 18},{name: 'bob'}]).exclude([{age: 21}])
-
trace(qs.output());
Outputs:
SQL:
-
...((age> :age1 AND insider_id IS NOT NULL AND LOWER(name) = LOWER(:name1)) OR (name = :name2)) AND NOT ((age = :age2))
With the bind parameters all taken care of for you in a nice object:
Actionscript:
-
qs.parameters = {
-
':name2': 'bob',
-
':age1': 18,
-
':insider_id1': '',
-
':age2': 21,
-
':name1': 'adsf',
-
};
Nice, eh?
The QS class file follows. I will be working on wiring everything together over the weekend.
Have fun.
If anyone has already gone down this road, I would love to hear from them.
`
Actionscript:
-
package com.angrysprite.query {
-
-
import com.angrysprite.errors.QuerySetError;
-
-
public class QS {
-
-
protected var filterGroups:Object;
-
protected var filters:Array;
-
protected var excludes:Array;
-
protected var bindParameters:Object;
-
protected var parameterCount:Object;
-
protected var queryTerms:Array = [
-
'exact', // exact match "="
-
'iexact', // case insensitive match
-
'contains', // like match: col LIKE %parameter%
-
'icontains', // case insensitive like match: lower(col) LIKE %parameter% (SQLite LIKE is case insensitive)
-
'gt', // greater than match: col> parameter
-
'gte', // greater than or equal match: col>= parameter
-
'lt', // less than match: col <parameter
-
'lte', // less than or equal match: col <= parameter
-
'in', // in match: col IN (parameter1, parameter2, parameter3)
-
'startswith', // starts with match: col LIKE parameter%
-
'istartswith', // case insensitive starts with match: col LIKE parameter% (SQLite LIKE is case insensitive)
-
'endswith', // ends with match: col LIKE %parameter
-
'iendswith', // case insensitive ends with match: col LIKE %parameter (SQLite LIKE is case insensitive)
-
'isnull', // null match: col IS NULL
-
'notnull', // is not null match: col IS NOT NULL
-
//'range', //
-
//'year', //
-
//'month','day', //
-
//'search', //
-
//'regex', //
-
//'iregex', //
-
];
-
public function QS() {
-
filterGroups = {filters: [], excludes: []};
-
bindParameters = new Object();
-
parameterCount = new Object();
-
}
-
-
public function filter(filter:Array):QS {
-
for (var i:int = 0; i <filter.length; i++) {
-
filterGroups.filters.push(parseFilters(filter[i]));
-
}
-
return this;
-
}
-
-
public function exclude(filter:Array):QS {
-
for (var i:int = 0; i <filter.length; i++) {
-
filterGroups.excludes.push(parseFilters(filter[i]));
-
}
-
return this;
-
}
-
-
public function get parameters():Object {
-
return bindParameters;
-
}
-
-
public function output():String {
-
var sql:String = "";
-
var filters:Array = new Array();
-
var excludes:Array = new Array();
-
for (var filterIndex:int = 0; filterIndex <filterGroups.filters.length; filterIndex++) {
-
filters.push("(" + filterGroups.filters[filterIndex].join(" AND ") + ")");
-
}
-
for (var excludeIndex:int = 0; excludeIndex <filterGroups.excludes.length; excludeIndex++) {
-
excludes.push("(" + filterGroups.excludes[excludeIndex].join(" AND ") + ")");
-
}
-
if (filters.length> 0) {
-
sql += "(" + filters.join(" OR ") + ")";
-
if (excludes.length> 0) {
-
sql += " AND NOT (" + excludes.join(" OR ") + ")";
-
}
-
} else if (excludes.length> 0) {
-
sql += "NOT (" + excludes.join(" AND ") + ")";
-
}
-
return sql;
-
}
-
-
protected function parseFilters(filter:Object):Array {
-
var sqlArray:Array = new Array();
-
for (var term:String in filter) {
-
var sql:String = "";
-
if (term.indexOf("__") == -1) {
-
var bindParameter:String = getBindParameterKey(term);
-
setBindParameter(bindParameter, filter[term]);
-
sql += term + " = " + bindParameter;
-
} else {
-
sql += parseTerm(term, filter[term]);
-
}
-
sqlArray.push(sql);
-
}
-
return sqlArray;
-
}
-
-
protected function parseTerm(term:String, value:Object):String {
-
var tokens:Array = term.split("__");
-
if (tokens.length != 2) {
-
throw new QuerySetError("Invalid number of query terms");
-
}
-
var col:String = tokens[0];
-
var term:String = tokens[1];
-
var sql:String = '';
-
var bindParameter:String = getBindParameterKey(col);
-
setBindParameter(bindParameter, value);
-
switch (term) {
-
case 'exact':
-
sql = col + " = " + bindParameter;
-
break;
-
case 'iexact':
-
sql = "LOWER(" + col + ") = LOWER(" + bindParameter + ")";
-
break;
-
case 'contains':
-
sql = col + " LIKE " + bindParameter;
-
break;
-
case 'icontains':
-
sql = col + " LIKE " + bindParameter;
-
break;
-
case 'gt':
-
sql = col + "> " + bindParameter;
-
break;
-
case 'gte':
-
sql = col + ">= " + bindParameter;
-
break;
-
case 'lt':
-
sql = col + " <" + bindParameter;
-
break;
-
case 'lte':
-
sql = col + ">= " + bindParameter;
-
break;
-
case 'in':
-
sql = col + " IN (" + bindParameter + ")";
-
break;
-
case 'startswith':
-
sql = col + " LIKE " + bindParameter + "%";
-
break;
-
case 'istartswith':
-
sql = col + " LIKE " + bindParameter + "%";
-
break;
-
case 'endswith':
-
sql = col + " LIKE %" + bindParameter;
-
break;
-
case 'iendswith':
-
sql = col + " LIKE %" + bindParameter;
-
break;
-
case 'isnull':
-
sql = col + " IS NULL";
-
break;
-
case 'notnull':
-
sql = col + " IS NOT NULL";
-
break;
-
default:
-
throw new QuerySetError('Invalid query term: ' + term);
-
break;
-
}
-
return sql;
-
}
-
-
protected function getBindParameterKey(bindParameter:String):String {
-
if (parameterCount[bindParameter]) {
-
parameterCount[bindParameter]++;
-
} else {
-
parameterCount[bindParameter] = 1;
-
}
-
return ":" + bindParameter + parameterCount[bindParameter].toString();
-
}
-
-
protected function setBindParameter(key:String, value:Object):void {
-
if (typeof(value) == 'Array') {
-
var a:Array = value as Array;
-
bindParameters[key] = a.join(', ');
-
}
-
bindParameters[key] = value;
-
}
-
}
-
}
-
-
/**
-
*
-
* var qs = new QS().filter({name__iexact: 'adsf', notnull__insider_id: '', gt__age: 18}).exclude({age: 21});
-
*
-
* ...WHERE (LOWER(name) = LOWER(:name1) AND insider_id IS NOT NULL AND age> :age1) AND NOT (age = :age2);
-
* bindParameters = {
-
* ':name1': 'asdf',
-
* ':age1': 18,
-
* ':age2': 21
-
* }
-
*
-
**/
Gareth | 18-Apr-08 at 9:07 am | Permalink
I really like what you’ve done so far. It would certainly make the SQL creation much easier. The Adobe application, Employee Directory, (I think) uses the Cairngorm model and moves all of the SQL calls into separate AS classes. These are then passed to a SQLmanager which invokes the necessary SQL call, and passes the results back to the calling page. This somewhat separates the SQL code from the AS code, but requires one page for each SQL statement, which can get a little overwhelming if you have many calls to the SQLite dB.
The only thing that I did not like *too* much is passing in the objects into the filter array. It’s all a little mysterious as to what to pass in to the array. A new user would not know exactly what to pass in without either the code or an API.
Here’s something that I’m not sure would work completely, but might or might be a step in the right direction
How about creating a FilterableObject? This object would have public methods for each of the properties within your case statements in your parseTerms method. Each of the methods would return “this” (the FilterableObject), so you could do
new FilterableObject().isNotNull( ‘insider_id’ ).iExact( ‘name’, ‘adsf’ ) etc., and chain everything together. Eclipse would then be able to generate the “hints” as you’re typing and the array that is passed in wouldn’t be as mysterious to a new user.
Anyway, this will be a really useful piece of code once it’s finished, and I look forward to seeing it completed.
Matthew | 22-Apr-08 at 2:21 pm | Permalink
Thank you for the feedback.
I agree that passing the objects/arrays in is not very elegant. I went with that for the now because this allow for me to move forward with my testing and get the big picture working.
This is very much a work in progress.