Closes #7, Add advanced search functionality to data layer
Implemented advanced/saved search architecture -- to use, you create a new search with var search = new Scholar.Search(), add conditions to it with addCondition(condition, operator, value), and run it with search(). The standard conditions with their respective operators can be retrieved with Scholar.SearchConditions.getStandardConditions(). Others are for special search flags and can be specified as follows (condition, operator, value): 'context', null, collectionIDToSearchWithin 'recursive', 'true'|'false' (as strings!--defaults to false if not specified, though, so should probably just be removed if not wanted), null 'joinMode', 'any'|'all', null For standard conditions, currently only 'title' and the itemData fields are supported -- more coming soon. Localized strings created for the standard search operators API: search.setName(name) -- must be called before save() on new searches search.load(savedSearchID) search.save() -- saves search to DB and returns a savedSearchID search.addCondition(condition, operator, value) search.updateCondition(searchConditionID, condition, operator, value) search.removeCondition(searchConditionID) search.getSearchCondition(searchConditionID) -- returns a specific search condition used in the search search.getSearchConditions() -- returns search conditions used in the search search.search() -- runs search and returns an array of item ids for results search.getSQL() -- will be used by Dan for search-within-search Scholar.Searches.getAll() -- returns an array of saved searches with 'id' and 'name', in alphabetical order Scholar.Searches.erase(savedSearchID) -- deletes a given saved search from the DB Scholar.SearchConditions.get(condition) -- get condition data (operators, etc.) Scholar.SearchConditions.getStandardConditions() -- retrieve conditions for use in drop-down menu (as opposed to special search flags) Scholar.SearchConditions.hasOperator() -- used by Dan for error-checking
This commit is contained in:
parent
216f0c7581
commit
d67d96c321
5 changed files with 592 additions and 4 deletions
|
@ -391,7 +391,7 @@ Scholar.Schema = new function(){
|
|||
//
|
||||
// Change this value to match the schema version
|
||||
//
|
||||
var toVersion = 33;
|
||||
var toVersion = 34;
|
||||
|
||||
if (toVersion != _getSchemaSQLVersion()){
|
||||
throw('Schema version does not match version in _migrateSchema()');
|
||||
|
@ -415,7 +415,7 @@ Scholar.Schema = new function(){
|
|||
}
|
||||
}
|
||||
|
||||
if (i==33){
|
||||
if (i==34){
|
||||
_initializeSchema();
|
||||
}
|
||||
}
|
||||
|
|
557
chrome/chromeFiles/content/scholar/xpcom/search.js
Normal file
557
chrome/chromeFiles/content/scholar/xpcom/search.js
Normal file
|
@ -0,0 +1,557 @@
|
|||
Scholar.Search = function(){
|
||||
this._sql = null;
|
||||
this._sqlParams = null;
|
||||
this._maxSearchConditionID = 0;
|
||||
this._conditions = [];
|
||||
this._savedSearchID = null;
|
||||
this._savedSearchName = null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Set the name for the saved search
|
||||
*
|
||||
* Must be called before save() for new searches
|
||||
*/
|
||||
Scholar.Search.prototype.setName = function(name){
|
||||
if (!name){
|
||||
throw("Invalid saved search name '" + name + '"');
|
||||
}
|
||||
|
||||
this._savedSearchName = name;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load a saved search from the DB
|
||||
*/
|
||||
Scholar.Search.prototype.load = function(savedSearchID){
|
||||
var sql = "SELECT savedSearchName, MAX(searchConditionID) AS maxID "
|
||||
+ "FROM savedSearches NATURAL JOIN savedSearchConditions "
|
||||
+ "WHERE savedSearchID=" + savedSearchID + " GROUP BY savedSearchID";
|
||||
var row = Scholar.DB.rowQuery(sql);
|
||||
|
||||
if (!row){
|
||||
throw('Saved search ' + savedSearchID + ' does not exist');
|
||||
}
|
||||
|
||||
this._sql = null;
|
||||
this._sqlParams = null;
|
||||
this._maxSearchConditionID = row['maxID'];
|
||||
this._conditions = [];
|
||||
this._savedSearchID = savedSearchID;
|
||||
this._savedSearchName = row['savedSearchName'];
|
||||
|
||||
var conditions = Scholar.DB.query("SELECT * FROM savedSearchConditions "
|
||||
+ "WHERE savedSearchID=" + savedSearchID + " ORDER BY searchConditionID");
|
||||
|
||||
for (var i in conditions){
|
||||
this._conditions[conditions[i]['searchConditionID']] = {
|
||||
condition: conditions[i]['condition'],
|
||||
operator: conditions[i]['operator'],
|
||||
value: conditions[i]['value']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Save the search to the DB and return a savedSearchID
|
||||
*
|
||||
* For new searches, setName() must be called before saving
|
||||
*/
|
||||
Scholar.Search.prototype.save = function(){
|
||||
if (!this._savedSearchName){
|
||||
throw('Name not provided for saved search');
|
||||
}
|
||||
|
||||
Scholar.DB.beginTransaction();
|
||||
|
||||
if (this._savedSearchID){
|
||||
var sql = "UPDATE savedSearches SET savedSearchName=? WHERE savedSearchID=?";
|
||||
Scholar.DB.query(sql, [this._savedSearchName, this._savedSearchID]);
|
||||
|
||||
Scholar.DB.query("DELETE FROM savedSearchConditions "
|
||||
+ "WHERE savedSearchID=" + this._savedSearchID);
|
||||
}
|
||||
else {
|
||||
this._savedSearchID
|
||||
= Scholar.getRandomID('savedSearches', 'savedSearchID');
|
||||
|
||||
var sql = "INSERT INTO savedSearches (savedSearchID, savedSearchName) "
|
||||
+ "VALUES (?,?)";
|
||||
Scholar.DB.query(sql,
|
||||
[this._savedSearchID, {string: this._savedSearchName}]);
|
||||
}
|
||||
|
||||
// TODO: use proper bound parameters once DB class is updated
|
||||
for (var i in this._conditions){
|
||||
var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
|
||||
+ "searchConditionID, condition, operator, value) VALUES (?,?,?,?,?)";
|
||||
|
||||
var sqlParams = [this._savedSearchID, i, this._conditions[i]['condition'],
|
||||
this._conditions[i]['operator']
|
||||
? this._conditions[i]['operator'] : null,
|
||||
this._conditions[i]['value']
|
||||
? this._conditions[i]['value'] : null];
|
||||
Scholar.DB.query(sql, sqlParams);
|
||||
}
|
||||
|
||||
Scholar.DB.commitTransaction();
|
||||
|
||||
return this._savedSearchID;
|
||||
}
|
||||
|
||||
|
||||
Scholar.Search.prototype.addCondition = function(condition, operator, value){
|
||||
if (!Scholar.SearchConditions.hasOperator(condition, operator)){
|
||||
throw ("Invalid operator '" + operator + "' for condition " + condition);
|
||||
}
|
||||
|
||||
var searchConditionID = this._maxSearchConditionID++;
|
||||
|
||||
this._conditions[searchConditionID] = {
|
||||
condition: condition,
|
||||
operator: operator,
|
||||
value: value
|
||||
};
|
||||
|
||||
this._sql = null;
|
||||
this._sqlParams = null;
|
||||
return searchConditionID;
|
||||
}
|
||||
|
||||
|
||||
Scholar.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value){
|
||||
if (typeof this._conditions[searchConditionID] == 'undefined'){
|
||||
throw ('Invalid searchConditionID ' + searchConditionID + ' in updateCondition()');
|
||||
}
|
||||
|
||||
if (!Scholar.SearchConditions.hasOperator(condition, operator)){
|
||||
throw ("Invalid operator '" + operator + "' for condition " + condition);
|
||||
}
|
||||
|
||||
this._conditions[searchConditionID] = {
|
||||
condition: condition,
|
||||
operator: operator,
|
||||
value: value
|
||||
};
|
||||
|
||||
this._sql = null;
|
||||
this._sqlParams = null;
|
||||
}
|
||||
|
||||
|
||||
Scholar.Search.prototype.removeCondition = function(searchConditionID){
|
||||
if (typeof this._conditions[searchConditionID] == 'undefined'){
|
||||
throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()');
|
||||
}
|
||||
|
||||
delete this._conditions[searchConditionID];
|
||||
|
||||
var i = searchConditionID + 1;
|
||||
while (typeof this._conditions[i] != 'undefined'){
|
||||
this._conditions[i-1] = this._conditions[i];
|
||||
delete this._conditions[i];
|
||||
i++;
|
||||
}
|
||||
|
||||
this._maxSearchConditionID--;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Returns an array with 'condition', 'operator', and 'value'
|
||||
* for the given searchConditionID
|
||||
*/
|
||||
Scholar.Search.prototype.getSearchCondition = function(searchConditionID){
|
||||
return this._conditions[searchConditionID];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Returns a multidimensional array of conditions/operator/value sets
|
||||
* used in the search, indexed by searchConditionID
|
||||
*/
|
||||
Scholar.Search.prototype.getSearchConditions = function(){
|
||||
// TODO: make copy
|
||||
return this._conditions;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Run the search and return an array of item ids for results
|
||||
*/
|
||||
Scholar.Search.prototype.search = function(){
|
||||
if (!this._sql){
|
||||
this._buildQuery();
|
||||
}
|
||||
|
||||
return Scholar.DB.columnQuery(this._sql, this._sqlParams);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Get the SQL string for the search
|
||||
*/
|
||||
Scholar.Search.prototype.getSQL = function(){
|
||||
if (!this._sql){
|
||||
this._buildQuery();
|
||||
}
|
||||
|
||||
return this._sql;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Build the SQL query for the search
|
||||
*/
|
||||
Scholar.Search.prototype._buildQuery = function(){
|
||||
var sql = 'SELECT itemID FROM items';
|
||||
var sqlParams = [];
|
||||
|
||||
var tables = [];
|
||||
|
||||
for (var i in this._conditions){
|
||||
var data = Scholar.SearchConditions.get(this._conditions[i]['condition']);
|
||||
|
||||
// Group standard conditions by table
|
||||
if (data['table']){
|
||||
if (!tables[data['table']]){
|
||||
tables[data['table']] = [];
|
||||
}
|
||||
|
||||
tables[data['table']].push({
|
||||
name: data['name'],
|
||||
alias: data['name']!=this._conditions[i]['condition']
|
||||
? this._conditions[i]['condition'] : false,
|
||||
field: data['field'],
|
||||
operator: this._conditions[i]['operator'],
|
||||
value: this._conditions[i]['value']
|
||||
});
|
||||
|
||||
var hasConditions = true;
|
||||
}
|
||||
|
||||
// Handle special conditions
|
||||
else {
|
||||
switch (data['name']){
|
||||
// Collection to search in
|
||||
case 'context':
|
||||
var parentCollectionID = this._conditions[i]['value'];
|
||||
continue;
|
||||
|
||||
// Search subfolders
|
||||
case 'recursive':
|
||||
var recursive = this._conditions[i]['operator']=='true';
|
||||
continue;
|
||||
|
||||
// Join mode ('any' or 'all')
|
||||
case 'joinMode':
|
||||
var joinMode = this._conditions[i]['operator'].toUpperCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw ('Unhandled special condition ' + this._conditions[i]['condition']);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConditions){
|
||||
sql += " WHERE ";
|
||||
|
||||
// Join conditions using appropriate operator
|
||||
if (joinMode=='ALL'){
|
||||
var binOp = ' AND ';
|
||||
}
|
||||
else {
|
||||
var binOp = ' OR ';
|
||||
}
|
||||
|
||||
for (i in tables){
|
||||
for (var j in tables[i]){
|
||||
// Special table handling
|
||||
switch (i){
|
||||
case 'items':
|
||||
break;
|
||||
default:
|
||||
sql += 'itemID IN (SELECT itemID FROM ' + i + ' WHERE (';
|
||||
var openParens = 2;
|
||||
}
|
||||
|
||||
// For itemData fields, include fieldID
|
||||
if (tables[i][j]['name']=='field'){
|
||||
sql += 'fieldID=? AND ';
|
||||
sqlParams.push(Scholar.ItemFields.getID(tables[i][j]['alias']));
|
||||
}
|
||||
|
||||
sql += tables[i][j]['field'];
|
||||
switch (tables[i][j]['operator']){
|
||||
case 'contains':
|
||||
sql += ' LIKE ?';
|
||||
sqlParams.push('%' + tables[i][j]['value'] + '%');
|
||||
break;
|
||||
|
||||
case 'doesNotContain':
|
||||
sql += ' NOT LIKE ?';
|
||||
sqlParams.push('%' + tables[i][j]['value'] + '%');
|
||||
break;
|
||||
|
||||
case 'is':
|
||||
sql += '=?';
|
||||
sqlParams.push(tables[i][j]['value']);
|
||||
break;
|
||||
|
||||
case 'isNot':
|
||||
sql += '!=?';
|
||||
sqlParams.push(tables[i][j]['value']);
|
||||
break;
|
||||
|
||||
case 'greaterThan':
|
||||
sql += '>?';
|
||||
sqlParams.push({int:tables[i][j]['value']});
|
||||
break;
|
||||
|
||||
case 'lessThan':
|
||||
sql += '<?';
|
||||
sqlParams.push({int:tables[i][j]['value']});
|
||||
break;
|
||||
|
||||
case 'isBefore':
|
||||
// TODO
|
||||
break;
|
||||
|
||||
case 'isAfter':
|
||||
// TODO
|
||||
break;
|
||||
}
|
||||
|
||||
// Close open parentheses
|
||||
for (k=openParens; k>0; k--){
|
||||
sql += ')';
|
||||
}
|
||||
|
||||
sql += binOp;
|
||||
}
|
||||
}
|
||||
|
||||
sql = sql.substring(0, sql.length-binOp.length);
|
||||
}
|
||||
|
||||
if (parentCollectionID){
|
||||
sql = "SELECT itemID FROM (" + sql + ") WHERE itemID IN "
|
||||
+ "(SELECT itemID FROM collectionItems WHERE collectionID IN ("
|
||||
|
||||
sql += "?,";
|
||||
sqlParams.push({int:parentCollectionID});
|
||||
|
||||
if (recursive){
|
||||
var col = Scholar.Collections.get(parentCollectionID);
|
||||
var descendents = col.getDescendents(false, 'collection');
|
||||
if (descendents){
|
||||
for (var i in descendents){
|
||||
sql += '?,';
|
||||
sqlParams.push(descendents[i]['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip final comma
|
||||
sql = sql.substring(0, sql.length-1) + "))";
|
||||
}
|
||||
|
||||
this._sql = sql;
|
||||
this._sqlParams = sqlParams.length ? sqlParams : null;
|
||||
}
|
||||
|
||||
|
||||
Scholar.Searches = new function(){
|
||||
this.getAll = getAll;
|
||||
this.erase = erase;
|
||||
|
||||
/*
|
||||
* Returns an array of saved searches with 'id' and 'name', ordered by name
|
||||
*/
|
||||
function getAll(){
|
||||
var sql = "SELECT savedSearchID AS id, savedSearchName AS name "
|
||||
+ "FROM savedSearches ORDER BY name";
|
||||
return Scholar.DB.query(sql);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Delete a given saved search from the DB
|
||||
*/
|
||||
function erase(savedSearchID){
|
||||
Scholar.DB.beginTransaction();
|
||||
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID="
|
||||
+ savedSearchID;
|
||||
Scholar.DB.query(sql);
|
||||
|
||||
var sql = "DELETE FROM savedSearches WHERE savedSearchID="
|
||||
+ savedSearchID;
|
||||
Scholar.DB.query(sql);
|
||||
Scholar.DB.commitTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Scholar.SearchConditions = new function(){
|
||||
this.get = get;
|
||||
this.getStandardConditions = getStandardConditions;
|
||||
this.hasOperator = hasOperator;
|
||||
|
||||
|
||||
/*
|
||||
* Define the advanced search operators
|
||||
*/
|
||||
var _operators = {
|
||||
// Standard
|
||||
is: true,
|
||||
isNot: true,
|
||||
contains: true,
|
||||
doesNotContain: true,
|
||||
lessThan: true,
|
||||
greaterThan: true,
|
||||
isBefore: true,
|
||||
isAfter: true,
|
||||
|
||||
// Special
|
||||
any: true,
|
||||
all: true,
|
||||
true: true,
|
||||
false: true
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Define the advanced search conditions
|
||||
*/
|
||||
var _conditions = [
|
||||
//
|
||||
// Special conditions
|
||||
//
|
||||
|
||||
// Context (i.e. collection id to search within)
|
||||
{
|
||||
name: 'context'
|
||||
},
|
||||
|
||||
// Search recursively
|
||||
{
|
||||
name: 'recursive',
|
||||
operators: {
|
||||
true: true,
|
||||
false: true
|
||||
}
|
||||
},
|
||||
|
||||
// Join mode
|
||||
{
|
||||
name: 'joinMode',
|
||||
operators: {
|
||||
any: true,
|
||||
all: true
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Standard conditions
|
||||
//
|
||||
|
||||
{
|
||||
name: 'title',
|
||||
operators: {
|
||||
contains: true,
|
||||
doesNotContain: true
|
||||
},
|
||||
table: 'items',
|
||||
field: 'title'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'itemType',
|
||||
operators: {
|
||||
is: true,
|
||||
isNot: true
|
||||
},
|
||||
table: 'items',
|
||||
field: 'itemTypeID'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'field',
|
||||
operators: {
|
||||
is: true,
|
||||
isNot: true,
|
||||
contains: true,
|
||||
doesNotContain: true
|
||||
},
|
||||
table: 'itemData',
|
||||
field: 'value',
|
||||
aliases: Scholar.DB.columnQuery("SELECT fieldName FROM fields"),
|
||||
template: true // mark for special handling
|
||||
}
|
||||
];
|
||||
|
||||
// Index conditions by name and aliases
|
||||
for (var i in _conditions){
|
||||
_conditions[_conditions[i]['name']] = _conditions[i];
|
||||
if (_conditions[i]['aliases']){
|
||||
for (var j in _conditions[i]['aliases']){
|
||||
_conditions[_conditions[i]['aliases'][j]] = _conditions[i];
|
||||
}
|
||||
}
|
||||
_conditions[_conditions[i]['name']] = _conditions[i];
|
||||
delete _conditions[i];
|
||||
}
|
||||
|
||||
var _standardConditions = [];
|
||||
|
||||
// Separate standard conditions for menu display
|
||||
for (var i in _conditions){
|
||||
// Standard conditions a have associated tables
|
||||
if (_conditions[i]['table'] &&
|
||||
// If a template condition, not the original (e.g. 'field')
|
||||
(!_conditions[i]['template'] || i!=_conditions[i]['name'])){
|
||||
_standardConditions.push({
|
||||
name: i,
|
||||
operators: _conditions[i]['operators']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Get condition data
|
||||
*/
|
||||
function get(condition){
|
||||
return _conditions[condition];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Returns array of possible conditions
|
||||
*
|
||||
* Does not include special conditions, only ones that would show in a drop-down list
|
||||
*/
|
||||
function getStandardConditions(){
|
||||
// TODO: return copy instead
|
||||
return _standardConditions;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Check if an operator is valid for a given condition
|
||||
*/
|
||||
function hasOperator(condition, operator){
|
||||
if (!_conditions[condition]){
|
||||
throw ("Invalid condition '" + condition + "' in hasOperator()");
|
||||
}
|
||||
|
||||
if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!_conditions[condition]['operators'][operator];
|
||||
}
|
||||
}
|
|
@ -90,4 +90,13 @@ db.dbRestored = The Scholar database appears to have become corrupted.\n\nYo
|
|||
db.dbRestoreFailed = The Scholar database appears to have become corrupted, and an attempt to restore from the last automatic backup failed.\n\nA new database file has been created. The damaged file was saved in your Scholar directory.
|
||||
|
||||
fileInterface.itemsImported = Importing items...
|
||||
fileInterface.itemsExported = Exporting items...
|
||||
fileInterface.itemsExported = Exporting items...
|
||||
|
||||
searchOperator.is = is
|
||||
searchOperator.isNot = is not
|
||||
searchOperator.contains = contains
|
||||
searchOperator.doesNotContain = does not contain
|
||||
searchOperator.lessThan = is less than
|
||||
searchOperator.greaterThan = is greater than
|
||||
searchOperator.isBefore = is before
|
||||
searchOperator.isAfter = is after
|
|
@ -38,6 +38,10 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
|||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://scholar/content/xpcom/history.js");
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://scholar/content/xpcom/search.js");
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://scholar/content/xpcom/ingester.js");
|
||||
|
|
20
schema.sql
20
schema.sql
|
@ -1,4 +1,4 @@
|
|||
-- 33
|
||||
-- 34
|
||||
|
||||
DROP TABLE IF EXISTS version;
|
||||
CREATE TABLE version (
|
||||
|
@ -192,6 +192,24 @@
|
|||
DROP INDEX IF EXISTS itemID;
|
||||
CREATE INDEX itemID ON collectionItems(itemID);
|
||||
|
||||
DROP TABLE IF EXISTS savedSearches;
|
||||
CREATE TABLE savedSearches (
|
||||
savedSearchID INT,
|
||||
savedSearchName TEXT,
|
||||
PRIMARY KEY(savedSearchID)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS savedSearchConditions;
|
||||
CREATE TABLE savedSearchConditions (
|
||||
savedSearchID INT,
|
||||
searchConditionID INT,
|
||||
condition TEXT,
|
||||
operator TEXT,
|
||||
value TEXT,
|
||||
PRIMARY KEY(savedSearchID, searchConditionID),
|
||||
FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS translators;
|
||||
CREATE TABLE translators (
|
||||
translatorID TEXT PRIMARY KEY,
|
||||
|
|
Loading…
Add table
Reference in a new issue