wvanbergen-scoped_search 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +12 -0
- data/TODO +1 -4
- data/lib/scoped_search/query_conditions_builder.rb +158 -0
- data/lib/scoped_search/query_language_parser.rb +50 -23
- data/lib/scoped_search/reg_tokens.rb +51 -0
- data/lib/scoped_search.rb +13 -41
- data/test/query_conditions_builder_test.rb +114 -0
- data/test/query_language_test.rb +23 -1
- data/test/search_for_test.rb +37 -13
- data/test/test_helper.rb +18 -15
- metadata +7 -2
data/CHANGELOG
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
scoped_search 0.3.0
|
2
|
+
===================
|
3
|
+
- Detection of column types so they can be handled properly
|
4
|
+
- Date based queries supported on date and time fields
|
5
|
+
|
6
|
+
scoped_search 0.2.0
|
7
|
+
===================
|
8
|
+
- OR keyword supported in query language
|
9
|
+
|
10
|
+
scoped_search 0.1.0
|
11
|
+
===================
|
12
|
+
- Initial version
|
data/TODO
CHANGED
@@ -5,15 +5,12 @@ Contact willem AT vanbergen DOT org if you want to help out
|
|
5
5
|
|
6
6
|
New features:
|
7
7
|
- Search fields of associations as well
|
8
|
-
- Allow
|
8
|
+
- Allow smart searches. Example: "90 days ago", "Yesterday", "10 days from now"
|
9
9
|
|
10
10
|
Refactoring:
|
11
|
-
- Create a separate class to build the actual SQL queries.
|
12
11
|
- For searchable_on(*fields) make it so that instead of fields it accepts options (a hash) where
|
13
12
|
:only and :except can be values. That way it is possible for all fields to be loaded except
|
14
13
|
the ones specified with :except.
|
15
|
-
- Add checks for field types because the latest version of PostgreSQL (version 8.3.3) is more
|
16
|
-
strict about searching for strings in columns that are not string types.
|
17
14
|
|
18
15
|
Documentation & testing:
|
19
16
|
- Put something useful in the wiki
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module ScopedSearch
|
2
|
+
|
3
|
+
class QueryConditionsBuilder
|
4
|
+
|
5
|
+
# Build the query
|
6
|
+
def self.build_query(search_conditions, query_fields)
|
7
|
+
self.new.build(search_conditions, query_fields)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@query_fields = nil
|
12
|
+
@query_params = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# Build the query
|
17
|
+
#
|
18
|
+
# Hash query_options : A hash of fields and field types.
|
19
|
+
#
|
20
|
+
# Exampe:
|
21
|
+
# search_conditions = [["Wes", :like], ["Hays", :not], ["Hello World", :like], ["Goodnight Moon", :not],
|
22
|
+
# ["Bob OR Wes", :or], ["Happy cow OR Sad Frog", :or], ["Man made OR Dogs", :or],
|
23
|
+
# ["Cows OR Frog Toys", :or], ['9/28/1980, :datetime]]
|
24
|
+
# query_fields = {:first_name => :string, :created_at => :datetime}
|
25
|
+
#
|
26
|
+
# Exceptons :
|
27
|
+
# 1) If urlParams does not contain a :controller key.
|
28
|
+
def build(search_conditions, query_fields)
|
29
|
+
raise 'search_conditions must be a hash' unless search_conditions.class.to_s == 'Array'
|
30
|
+
raise 'query_fields must be a hash' unless query_fields.class.to_s == 'Hash'
|
31
|
+
@query_fields = query_fields
|
32
|
+
|
33
|
+
conditions = []
|
34
|
+
|
35
|
+
search_conditions.each_with_index do |search_condition, index|
|
36
|
+
keyword_name = "keyword_#{index}".to_sym
|
37
|
+
conditions << case search_condition.last
|
38
|
+
#when :integer: integer_conditions(keyword_name, search_condition.first)
|
39
|
+
|
40
|
+
when :like: like_condition(keyword_name, search_condition.first)
|
41
|
+
when :not: not_like_condition(keyword_name, search_condition.first)
|
42
|
+
|
43
|
+
when :or: or_condition(keyword_name, search_condition.first)
|
44
|
+
|
45
|
+
when :less_than_date: less_than_date(keyword_name, search_condition.first)
|
46
|
+
when :less_than_or_equal_to_date: less_than_or_equal_to_date(keyword_name, search_condition.first)
|
47
|
+
when :as_of_date: as_of_date(keyword_name, search_condition.first)
|
48
|
+
when :greater_than_date: greater_than_date(keyword_name, search_condition.first)
|
49
|
+
when :greater_than_or_equal_to_date: greater_than_or_equal_to_date(keyword_name, search_condition.first)
|
50
|
+
|
51
|
+
when :between_dates: between_dates(keyword_name, search_condition.first)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
[conditions.compact.join(' AND '), @query_params]
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# def integer_condition(keyword_name, value)
|
62
|
+
# end
|
63
|
+
|
64
|
+
def like_condition(keyword_name, value)
|
65
|
+
@query_params[keyword_name] = "%#{value}%"
|
66
|
+
retVal = []
|
67
|
+
@query_fields.each do |field, field_type| #|key,value|
|
68
|
+
if field_type == :string or field_type == :text
|
69
|
+
retVal << "#{field} LIKE :#{keyword_name.to_s}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
"(#{retVal.join(' OR ')})"
|
73
|
+
end
|
74
|
+
|
75
|
+
def not_like_condition(keyword_name, value)
|
76
|
+
@query_params[keyword_name] = "%#{value}%"
|
77
|
+
retVal = []
|
78
|
+
@query_fields.each do |field, field_type| #|key,value|
|
79
|
+
if field_type == :string or field_type == :text
|
80
|
+
retVal << "(#{field} NOT LIKE :#{keyword_name.to_s} OR #{field} IS NULL)"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
"(#{retVal.join(' AND ')})"
|
84
|
+
end
|
85
|
+
|
86
|
+
def or_condition(keyword_name, value)
|
87
|
+
retVal = []
|
88
|
+
word1, word2 = value.split(' OR ')
|
89
|
+
keyword_name_a = "#{keyword_name.to_s}a".to_sym
|
90
|
+
keyword_name_b = "#{keyword_name.to_s}b".to_sym
|
91
|
+
@query_params[keyword_name_a] = "%#{word1}%"
|
92
|
+
@query_params[keyword_name_b] = "%#{word2}%"
|
93
|
+
@query_fields.each do |field, field_type| #|key,value|
|
94
|
+
if field_type == :string or field_type == :text
|
95
|
+
retVal << "(#{field} LIKE :#{keyword_name_a.to_s} OR #{field} LIKE :#{keyword_name_b.to_s})"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
"(#{retVal.join(' OR ')})"
|
99
|
+
end
|
100
|
+
|
101
|
+
def less_than_date(keyword_name, value)
|
102
|
+
helper_date_operation('<', keyword_name, value)
|
103
|
+
end
|
104
|
+
|
105
|
+
def less_than_or_equal_to_date(keyword_name, value)
|
106
|
+
helper_date_operation('<=', keyword_name, value)
|
107
|
+
end
|
108
|
+
|
109
|
+
def as_of_date(keyword_name, value)
|
110
|
+
helper_date_operation('=', keyword_name, value)
|
111
|
+
end
|
112
|
+
|
113
|
+
def greater_than_date(keyword_name, value)
|
114
|
+
helper_date_operation('>', keyword_name, value)
|
115
|
+
end
|
116
|
+
|
117
|
+
def greater_than_or_equal_to_date(keyword_name, value)
|
118
|
+
helper_date_operation('>=', keyword_name, value)
|
119
|
+
end
|
120
|
+
|
121
|
+
def between_dates(keyword_name, value)
|
122
|
+
date1, date2 = value.split(' TO ')
|
123
|
+
dt1 = Date.parse(date1) # This will throw an exception if it is not valid
|
124
|
+
dt2 = Date.parse(date2) # This will throw an exception if it is not valid
|
125
|
+
keyword_name_a = "#{keyword_name.to_s}a".to_sym
|
126
|
+
keyword_name_b = "#{keyword_name.to_s}b".to_sym
|
127
|
+
@query_params[keyword_name_a] = dt1.to_s
|
128
|
+
@query_params[keyword_name_b] = dt2.to_s
|
129
|
+
|
130
|
+
retVal = []
|
131
|
+
@query_fields.each do |field, field_type| #|key,value|
|
132
|
+
if field_type == :date or field_type == :datetime or field_type == :timestamp
|
133
|
+
retVal << "(#{field} BETWEEN :#{keyword_name_a.to_s} AND :#{keyword_name_b.to_s})"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
"(#{retVal.join(' OR ')})"
|
137
|
+
rescue
|
138
|
+
# The date is not valid so just ignore it
|
139
|
+
return nil
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
def helper_date_operation(operator, keyword_name, value)
|
144
|
+
dt = Date.parse(value) # This will throw an exception if it is not valid
|
145
|
+
@query_params[keyword_name] = dt.to_s
|
146
|
+
retVal = []
|
147
|
+
@query_fields.each do |field, field_type| #|key,value|
|
148
|
+
if field_type == :date or field_type == :datetime or field_type == :timestamp
|
149
|
+
retVal << "#{field} #{operator} :#{keyword_name.to_s}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
"(#{retVal.join(' OR ')})"
|
153
|
+
rescue
|
154
|
+
# The date is not valid so just ignore it
|
155
|
+
return nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -23,38 +23,65 @@ module ScopedSearch
|
|
23
23
|
else
|
24
24
|
if /^.+[ ]OR[ ].+$/ =~ item
|
25
25
|
conditions_tree << [item, :or]
|
26
|
+
elsif /^#{RegTokens::BetweenDateFormatMMDDYYYY}$/ =~ item or
|
27
|
+
/^#{RegTokens::BetweenDateFormatYYYYMMDD}$/ =~ item or
|
28
|
+
/^#{RegTokens::BetweenDatabaseFormat}$/ =~ item
|
29
|
+
conditions_tree << [item, :between_dates]
|
30
|
+
elsif /^#{RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
|
31
|
+
/^#{RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
|
32
|
+
/^#{RegTokens::GreaterThanOrEqualToDatabaseFormat}$/ =~ item
|
33
|
+
conditions_tree << [item, :greater_than_or_equal_to_date]
|
34
|
+
elsif /^#{RegTokens::LessThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
|
35
|
+
/^#{RegTokens::LessThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
|
36
|
+
/^#{RegTokens::LessThanOrEqualToDatabaseFormat}$/ =~ item
|
37
|
+
conditions_tree << [item, :less_than_or_equal_to_date]
|
38
|
+
elsif /^#{RegTokens::GreaterThanDateFormatMMDDYYYY}$/ =~ item or
|
39
|
+
/^#{RegTokens::GreaterThanDateFormatYYYYMMDD}$/ =~ item or
|
40
|
+
/^#{RegTokens::GreaterThanDatabaseFormat}$/ =~ item
|
41
|
+
conditions_tree << [item, :greater_than_date]
|
42
|
+
elsif /^#{RegTokens::LessThanDateFormatMMDDYYYY}$/ =~ item or
|
43
|
+
/^#{RegTokens::LessThanDateFormatYYYYMMDD}$/ =~ item or
|
44
|
+
/^#{RegTokens::LessThanDatabaseFormat}$/ =~ item
|
45
|
+
conditions_tree << [item, :less_than_date]
|
46
|
+
elsif /^#{RegTokens::DateFormatMMDDYYYY}$/ =~ item or
|
47
|
+
/^#{RegTokens::DateFormatYYYYMMDD}$/ =~ item or
|
48
|
+
/^#{RegTokens::DatabaseFormat}$/ =~ item
|
49
|
+
conditions_tree << [item, :as_of_date]
|
26
50
|
else
|
27
51
|
conditions_tree << (negate ? [item, :not] : [item, :like])
|
28
52
|
negate = false
|
29
53
|
end
|
30
54
|
end
|
31
55
|
end
|
56
|
+
|
32
57
|
return conditions_tree
|
33
58
|
end
|
34
|
-
|
35
|
-
# **Patterns**
|
36
|
-
# Each pattern is sperated by a "|". With regular expressions the order of the expression does matter.
|
37
|
-
#
|
38
|
-
# ([\w]+[ ]OR[ ][\w]+)
|
39
|
-
# ([\w]+[ ]OR[ ]["][\w ]+["])
|
40
|
-
# (["][\w ]+["][ ]OR[ ][\w]+)
|
41
|
-
# (["][\w ]+["][ ]OR[ ]["][\w ]+["])
|
42
|
-
# Any two combinations of letters, numbers and underscores that are seperated by " OR " (a single space must
|
43
|
-
# be on each side of the "OR").
|
44
|
-
# THESE COULD BE COMBINED BUT BECAUSE OF THE WAY PARSING WORKS THIS IS NOT DONE ON PURPOSE!!
|
45
|
-
#
|
46
|
-
# ([-]?[\w]+)
|
47
|
-
# Any combination of letters, numbers and underscores that may or may not have a dash in front.
|
48
|
-
#
|
49
|
-
# ([-]?["][\w ]+["])
|
50
|
-
# Any combination of letters, numbers, underscores and spaces within double quotes that may or may not have a dash in front.
|
59
|
+
|
51
60
|
def tokenize(query)
|
52
|
-
pattern = [
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
61
|
+
pattern = [RegTokens::BetweenDateFormatMMDDYYYY,
|
62
|
+
RegTokens::BetweenDateFormatYYYYMMDD,
|
63
|
+
RegTokens::BetweenDatabaseFormat,
|
64
|
+
RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY,
|
65
|
+
RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD,
|
66
|
+
RegTokens::GreaterThanOrEqualToDatabaseFormat,
|
67
|
+
RegTokens::LessThanOrEqualToDateFormatMMDDYYYY,
|
68
|
+
RegTokens::LessThanOrEqualToDateFormatYYYYMMDD,
|
69
|
+
RegTokens::LessThanOrEqualToDatabaseFormat,
|
70
|
+
RegTokens::GreaterThanDateFormatMMDDYYYY,
|
71
|
+
RegTokens::GreaterThanDateFormatYYYYMMDD,
|
72
|
+
RegTokens::GreaterThanDatabaseFormat,
|
73
|
+
RegTokens::LessThanDateFormatMMDDYYYY,
|
74
|
+
RegTokens::LessThanDateFormatYYYYMMDD,
|
75
|
+
RegTokens::LessThanDatabaseFormat,
|
76
|
+
RegTokens::DateFormatMMDDYYYY,
|
77
|
+
RegTokens::DateFormatYYYYMMDD,
|
78
|
+
RegTokens::DatabaseFormat,
|
79
|
+
RegTokens::WordOrWord,
|
80
|
+
RegTokens::WordOrString,
|
81
|
+
RegTokens::StringOrWord,
|
82
|
+
RegTokens::StringOrString,
|
83
|
+
RegTokens::PossiblyNegatedWord,
|
84
|
+
RegTokens::PossiblyNegatedString]
|
58
85
|
pattern = Regexp.new(pattern.join('|'))
|
59
86
|
|
60
87
|
tokens = []
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Regular expression tokens to be used for parsing.
|
2
|
+
module RegTokens
|
3
|
+
|
4
|
+
WORD = '[\w]+'
|
5
|
+
SPACE = '[ ]'
|
6
|
+
STRING = '["][\w ]+["]'
|
7
|
+
OR = 'OR'
|
8
|
+
POSSIBLY_NEGATED = '[-]?'
|
9
|
+
MONTH = '[\d]{1,2}'
|
10
|
+
DAY = '[\d]{1,2}'
|
11
|
+
FULL_YEAR = '[\d]{4}'
|
12
|
+
LESS_THAN = '[<][ ]'
|
13
|
+
GREATER_THAN = '[>][ ]'
|
14
|
+
LESS_THAN_OR_EQUAL_TO = '[<][=][ ]'
|
15
|
+
GREATER_THAN_OR_EQUAL_TO = '[>][=][ ]'
|
16
|
+
TO = 'TO'
|
17
|
+
|
18
|
+
WordOrWord = "(#{WORD}#{SPACE}#{OR}#{SPACE}#{WORD})"
|
19
|
+
WordOrString = "(#{WORD}#{SPACE}#{OR}#{SPACE}#{STRING})"
|
20
|
+
StringOrWord = "(#{STRING}#{SPACE}#{OR}#{SPACE}#{WORD})"
|
21
|
+
StringOrString = "(#{STRING}#{SPACE}#{OR}#{SPACE}#{STRING})"
|
22
|
+
PossiblyNegatedWord = "(#{POSSIBLY_NEGATED}#{WORD})"
|
23
|
+
PossiblyNegatedString = "(#{POSSIBLY_NEGATED}#{STRING})"
|
24
|
+
|
25
|
+
DateFormatMMDDYYYY = "(#{MONTH}/#{DAY}/#{FULL_YEAR})" # This would be the same for DD/MM/YYYY
|
26
|
+
DateFormatYYYYMMDD = "(#{FULL_YEAR}/#{MONTH}/#{DAY})"
|
27
|
+
DatabaseFormat = "(#{FULL_YEAR}-#{MONTH}-#{DAY})"
|
28
|
+
|
29
|
+
LessThanDateFormatMMDDYYYY = "(#{LESS_THAN}#{MONTH}/#{DAY}/#{FULL_YEAR})"
|
30
|
+
LessThanDateFormatYYYYMMDD = "(#{LESS_THAN}#{FULL_YEAR}/#{MONTH}/#{DAY})"
|
31
|
+
LessThanDatabaseFormat = "(#{LESS_THAN}#{FULL_YEAR}-#{MONTH}-#{DAY})"
|
32
|
+
|
33
|
+
GreaterThanDateFormatMMDDYYYY = "(#{GREATER_THAN}#{MONTH}/#{DAY}/#{FULL_YEAR})"
|
34
|
+
GreaterThanDateFormatYYYYMMDD = "(#{GREATER_THAN}#{FULL_YEAR}/#{MONTH}/#{DAY})"
|
35
|
+
GreaterThanDatabaseFormat = "(#{GREATER_THAN}#{FULL_YEAR}-#{MONTH}-#{DAY})"
|
36
|
+
|
37
|
+
LessThanOrEqualToDateFormatMMDDYYYY = "(#{LESS_THAN_OR_EQUAL_TO}#{MONTH}/#{DAY}/#{FULL_YEAR})"
|
38
|
+
LessThanOrEqualToDateFormatYYYYMMDD = "(#{LESS_THAN_OR_EQUAL_TO}#{FULL_YEAR}/#{MONTH}/#{DAY})"
|
39
|
+
LessThanOrEqualToDatabaseFormat = "(#{LESS_THAN_OR_EQUAL_TO}#{FULL_YEAR}-#{MONTH}-#{DAY})"
|
40
|
+
|
41
|
+
GreaterThanOrEqualToDateFormatMMDDYYYY = "(#{GREATER_THAN_OR_EQUAL_TO}#{MONTH}/#{DAY}/#{FULL_YEAR})"
|
42
|
+
GreaterThanOrEqualToDateFormatYYYYMMDD = "(#{GREATER_THAN_OR_EQUAL_TO}#{FULL_YEAR}/#{MONTH}/#{DAY})"
|
43
|
+
GreaterThanOrEqualToDatabaseFormat = "(#{GREATER_THAN_OR_EQUAL_TO}#{FULL_YEAR}-#{MONTH}-#{DAY})"
|
44
|
+
|
45
|
+
BetweenDateFormatMMDDYYYY = "(#{MONTH}/#{DAY}/#{FULL_YEAR}#{SPACE}#{TO}#{SPACE}#{MONTH}/#{DAY}/#{FULL_YEAR})"
|
46
|
+
BetweenDateFormatYYYYMMDD = "(#{FULL_YEAR}/#{MONTH}/#{DAY}#{SPACE}#{TO}#{SPACE}#{FULL_YEAR}/#{MONTH}/#{DAY})"
|
47
|
+
BetweenDatabaseFormat = "(#{FULL_YEAR}-#{MONTH}-#{DAY}#{SPACE}#{TO}#{SPACE}#{FULL_YEAR}-#{MONTH}-#{DAY})"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
data/lib/scoped_search.rb
CHANGED
@@ -3,7 +3,9 @@ module ScopedSearch
|
|
3
3
|
module ClassMethods
|
4
4
|
|
5
5
|
def self.extended(base)
|
6
|
+
require 'scoped_search/reg_tokens'
|
6
7
|
require 'scoped_search/query_language_parser'
|
8
|
+
require 'scoped_search/query_conditions_builder'
|
7
9
|
end
|
8
10
|
|
9
11
|
# Creates a named scope in the class it was called upon
|
@@ -16,49 +18,19 @@ module ScopedSearch
|
|
16
18
|
# Build a hash that is used for the named_scope search_for.
|
17
19
|
# This function will split the search_string into keywords, and search for all the keywords
|
18
20
|
# in the fields that were provided to searchable_on
|
19
|
-
def build_scoped_search_conditions(search_string)
|
21
|
+
def build_scoped_search_conditions(search_string)
|
20
22
|
if search_string.nil? || search_string.strip.blank?
|
21
|
-
return {
|
22
|
-
else
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
keyword_name = "keyword_#{index}".to_sym
|
28
|
-
query_params[keyword_name] = "%#{search_condition.first}%"
|
29
|
-
|
30
|
-
# a keyword may be found in any of the provided fields, so join the conitions with OR
|
31
|
-
if search_condition.length == 2 && search_condition.last == :not
|
32
|
-
keyword_conditions = self.scoped_search_fields.map do |field|
|
33
|
-
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
34
|
-
"(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
|
35
|
-
end
|
36
|
-
conditions << "(#{keyword_conditions.join(' AND ')})"
|
37
|
-
elsif search_condition.length == 2 && search_condition.last == :or
|
38
|
-
word1, word2 = query_params[keyword_name].split(' OR ')
|
39
|
-
|
40
|
-
query_params.delete(keyword_name)
|
41
|
-
keyword_name_a = "#{keyword_name.to_s}a".to_sym
|
42
|
-
keyword_name_b = "#{keyword_name.to_s}b".to_sym
|
43
|
-
query_params[keyword_name_a] = word1
|
44
|
-
query_params[keyword_name_b] = word2
|
45
|
-
|
46
|
-
keyword_conditions = self.scoped_search_fields.map do |field|
|
47
|
-
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
48
|
-
"(#{field_name} LIKE :#{keyword_name_a.to_s} OR #{field_name} LIKE :#{keyword_name_b.to_s})"
|
49
|
-
end
|
50
|
-
conditions << "(#{keyword_conditions.join(' OR ')})"
|
51
|
-
else
|
52
|
-
keyword_conditions = self.scoped_search_fields.map do |field|
|
53
|
-
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
54
|
-
"#{field_name} LIKE :#{keyword_name.to_s}"
|
55
|
-
end
|
56
|
-
conditions << "(#{keyword_conditions.join(' OR ')})"
|
57
|
-
end
|
23
|
+
return {:conditions => nil}
|
24
|
+
else
|
25
|
+
query_fields = {}
|
26
|
+
self.scoped_search_fields.each do |field|
|
27
|
+
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
28
|
+
query_fields[field_name] = self.columns_hash[field.to_s].type
|
58
29
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
30
|
+
|
31
|
+
search_conditions = QueryLanguageParser.parse(search_string)
|
32
|
+
conditions = QueryConditionsBuilder.build_query(search_conditions, query_fields)
|
33
|
+
return {:conditions => conditions}
|
62
34
|
end
|
63
35
|
end
|
64
36
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class QueryConditionsBuilderTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# change this function if you switch to another query language parser
|
6
|
+
def build_query(search_conditions, query_fields)
|
7
|
+
ScopedSearch::QueryConditionsBuilder.build_query(search_conditions, query_fields)
|
8
|
+
end
|
9
|
+
|
10
|
+
# ** Single query search tests **
|
11
|
+
def test_like_search_condition
|
12
|
+
search_conditions = [["Wes", :like]]
|
13
|
+
query_fields = {'some_table.first_name' => :string}
|
14
|
+
conditions = build_query(search_conditions, query_fields)
|
15
|
+
|
16
|
+
assert_equal '(some_table.first_name LIKE :keyword_0)', conditions.first
|
17
|
+
assert_equal '%Wes%', conditions.last[:keyword_0]
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_not_like_search_condition
|
21
|
+
search_conditions = [["Wes", :not]]
|
22
|
+
query_fields = {'some_table.first_name' => :string}
|
23
|
+
conditions = build_query(search_conditions, query_fields)
|
24
|
+
|
25
|
+
assert_equal '((some_table.first_name NOT LIKE :keyword_0 OR some_table.first_name IS NULL))', conditions.first
|
26
|
+
assert_equal '%Wes%', conditions.last[:keyword_0]
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_or_search_condition
|
30
|
+
search_conditions = [["Wes OR Hays", :or]]
|
31
|
+
query_fields = {'some_table.first_name' => :string}
|
32
|
+
conditions = build_query(search_conditions, query_fields)
|
33
|
+
regExs = build_regex_for_or(['first_name'], 'keyword_0')
|
34
|
+
assert_match /^#{regExs}$/, conditions.first
|
35
|
+
assert_equal '%Wes%', conditions.last[:keyword_0a]
|
36
|
+
assert_equal '%Hays%', conditions.last[:keyword_0b]
|
37
|
+
end
|
38
|
+
|
39
|
+
# def test_date_search_condition
|
40
|
+
# search_conditions = [["09/27/1980", :as_of_date]]
|
41
|
+
# query_fields = {'some_table.event_date' => :datetime}
|
42
|
+
# conditions = build_query(search_conditions, query_fields)
|
43
|
+
# regExs = build_regex_for_date(['event_date'], 'keyword_0')
|
44
|
+
# assert_match /^#{regExs}$/, conditions.first
|
45
|
+
# assert_equal '09/27/1980', conditions.last[:keyword_0a]
|
46
|
+
# end
|
47
|
+
|
48
|
+
|
49
|
+
# ** Multi query search tests **
|
50
|
+
def test_like_two_search_condition
|
51
|
+
search_conditions = [["Wes", :like],["Hays", :like]]
|
52
|
+
query_fields = {'some_table.first_name' => :string,'some_table.last_name' => :string}
|
53
|
+
conditions = build_query(search_conditions, query_fields)
|
54
|
+
|
55
|
+
fields = ['first_name','last_name']
|
56
|
+
regExs = [build_regex_for_like(fields,'keyword_0'),
|
57
|
+
build_regex_for_like(fields,'keyword_1')].join('[ ]AND[ ]')
|
58
|
+
|
59
|
+
assert_match /^#{regExs}$/, conditions.first
|
60
|
+
assert_equal '%Wes%', conditions.last[:keyword_0]
|
61
|
+
assert_equal '%Hays%', conditions.last[:keyword_1]
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_like_two_search_conditions_with_one_not
|
65
|
+
search_conditions = [["Wes", :like],["Hays", :not]]
|
66
|
+
query_fields = {'some_table.first_name' => :string,'some_table.last_name' => :string}
|
67
|
+
conditions = build_query(search_conditions, query_fields)
|
68
|
+
|
69
|
+
fields = ['first_name','last_name']
|
70
|
+
regExs = [build_regex_for_like(fields,'keyword_0'),
|
71
|
+
build_regex_for_not_like(fields,'keyword_1')].join('[ ]AND[ ]')
|
72
|
+
|
73
|
+
assert_match /^#{regExs}$/, conditions.first
|
74
|
+
assert_equal '%Wes%', conditions.last[:keyword_0]
|
75
|
+
assert_equal '%Hays%', conditions.last[:keyword_1]
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
# ** Helper methods **
|
81
|
+
def build_regex_for_like(fields,keyword)
|
82
|
+
orFields = fields.join('|')
|
83
|
+
regParts = fields.collect { |field|
|
84
|
+
"some_table.(#{orFields}) LIKE :#{keyword}"
|
85
|
+
}.join('[ ]OR[ ]')
|
86
|
+
"[\(]#{regParts}[\)]"
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_regex_for_not_like(fields,keyword)
|
90
|
+
orFields = fields.join('|')
|
91
|
+
regParts = fields.collect { |field|
|
92
|
+
"[\(]some_table.(#{orFields}) NOT LIKE :#{keyword} OR some_table.(#{orFields}) IS NULL[\)]"
|
93
|
+
}.join('[ ]AND[ ]')
|
94
|
+
|
95
|
+
"[\(]#{regParts}[\)]"
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_regex_for_or(fields,keyword)
|
99
|
+
orFields = fields.join('|')
|
100
|
+
regParts = fields.collect { |field|
|
101
|
+
"[\(]some_table.(#{orFields}) LIKE :#{keyword}a OR some_table.(#{orFields}) LIKE :#{keyword}b[\)]"
|
102
|
+
}.join('[ ]OR[ ]')
|
103
|
+
|
104
|
+
"[\(]#{regParts}[\)]"
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_regex_for_date(fields,keyword)
|
108
|
+
orFields = fields.join('|')
|
109
|
+
regParts = fields.collect { |field|
|
110
|
+
"some_table.(#{orFields}) = :#{keyword}"
|
111
|
+
}.join('[ ]OR[ ]')
|
112
|
+
"[\(]#{regParts}[\)]"
|
113
|
+
end
|
114
|
+
end
|
data/test/query_language_test.rb
CHANGED
@@ -51,7 +51,7 @@ class QueryLanguageTest < Test::Unit::TestCase
|
|
51
51
|
assert_equal 2, parsed.length
|
52
52
|
assert_equal 'hallo', parsed[0].first
|
53
53
|
assert_equal 'willem', parsed[1].first
|
54
|
-
|
54
|
+
|
55
55
|
parsed = parse_query(' "hallo wi"llem"')
|
56
56
|
assert_equal 2, parsed.length
|
57
57
|
assert_equal 'hallo wi', parsed[0].first
|
@@ -108,6 +108,28 @@ class QueryLanguageTest < Test::Unit::TestCase
|
|
108
108
|
assert_equal :or, parsed[0][1]
|
109
109
|
end
|
110
110
|
|
111
|
+
def test_as_of_date
|
112
|
+
# parsed = parse_query('9/27/1980')
|
113
|
+
# assert_equal 1, parsed.length
|
114
|
+
# assert_equal '9/27/1980', parsed[0][0]
|
115
|
+
# assert_equal :as_of, parsed[0][1]
|
116
|
+
|
117
|
+
# parsed = parse_query('"Man made" OR Dogs')
|
118
|
+
# assert_equal 1, parsed.length
|
119
|
+
# assert_equal 'Man made OR Dogs', parsed[0][0]
|
120
|
+
# assert_equal :or, parsed[0][1]
|
121
|
+
#
|
122
|
+
# parsed = parse_query('Cows OR "Frog Toys"')
|
123
|
+
# assert_equal 1, parsed.length
|
124
|
+
# assert_equal 'Cows OR Frog Toys', parsed[0][0]
|
125
|
+
# assert_equal :or, parsed[0][1]
|
126
|
+
#
|
127
|
+
# parsed = parse_query('"Happy cow" OR "Sad Frog"')
|
128
|
+
# assert_equal 1, parsed.length
|
129
|
+
# assert_equal 'Happy cow OR Sad Frog', parsed[0][0]
|
130
|
+
# assert_equal :or, parsed[0][1]
|
131
|
+
end
|
132
|
+
|
111
133
|
def test_long_string
|
112
134
|
str = 'Wes -Hays "Hello World" -"Goodnight Moon" Bob OR Wes "Happy cow" OR "Sad Frog" "Man made" OR Dogs Cows OR "Frog Toys"'
|
113
135
|
parsed = parse_query(str)
|
data/test/search_for_test.rb
CHANGED
@@ -11,31 +11,55 @@ class ScopedSearchTest < Test::Unit::TestCase
|
|
11
11
|
teardown_db
|
12
12
|
end
|
13
13
|
|
14
|
-
def test_enabling
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
14
|
+
# def test_enabling
|
15
|
+
# assert !SearchTestModel.respond_to?(:search_for)
|
16
|
+
# SearchTestModel.searchable_on :string_field, :text_field, :date_field
|
17
|
+
# assert SearchTestModel.respond_to?(:search_for)
|
18
|
+
#
|
19
|
+
# assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
|
20
|
+
# end
|
21
|
+
#
|
23
22
|
def test_search
|
24
|
-
SearchTestModel.searchable_on :string_field, :text_field
|
23
|
+
SearchTestModel.searchable_on :string_field, :text_field, :date_field
|
25
24
|
|
26
|
-
assert_equal
|
25
|
+
assert_equal 16, SearchTestModel.search_for('').count
|
27
26
|
assert_equal 0, SearchTestModel.search_for('456').count
|
28
27
|
assert_equal 2, SearchTestModel.search_for('hays').count
|
29
28
|
assert_equal 1, SearchTestModel.search_for('hay ob').count
|
30
|
-
assert_equal
|
29
|
+
assert_equal 14, SearchTestModel.search_for('o').count
|
31
30
|
assert_equal 2, SearchTestModel.search_for('-o').count
|
32
|
-
assert_equal
|
31
|
+
assert_equal 14, SearchTestModel.search_for('-Jim').count
|
33
32
|
assert_equal 1, SearchTestModel.search_for('Jim -Bush').count
|
34
33
|
assert_equal 1, SearchTestModel.search_for('"Hello World" -"Goodnight Moon"').count
|
35
34
|
assert_equal 2, SearchTestModel.search_for('Wes OR Bob').count
|
36
35
|
assert_equal 3, SearchTestModel.search_for('"Happy cow" OR "Sad Frog"').count
|
37
36
|
assert_equal 3, SearchTestModel.search_for('"Man made" OR Dogs').count
|
38
37
|
assert_equal 2, SearchTestModel.search_for('Cows OR "Frog Toys"').count
|
38
|
+
|
39
|
+
# ** DATES **
|
40
|
+
#
|
41
|
+
# The next two dates are invalid therefore it will be ignored.
|
42
|
+
# Thus it would be the same as searching for an empty string
|
43
|
+
assert_equal 16, SearchTestModel.search_for('2/30/1980').count
|
44
|
+
assert_equal 16, SearchTestModel.search_for('99/99/9999').count
|
45
|
+
|
46
|
+
assert_equal 1, SearchTestModel.search_for('9/27/1980').count
|
47
|
+
assert_equal 1, SearchTestModel.search_for('hays 9/27/1980').count
|
48
|
+
assert_equal 2, SearchTestModel.search_for('hays 2/30/1980').count
|
49
|
+
assert_equal 1, SearchTestModel.search_for('2006/07/15').count
|
50
|
+
|
51
|
+
assert_equal 1, SearchTestModel.search_for('< 12/01/1980').count
|
52
|
+
assert_equal 5, SearchTestModel.search_for('> 1/1/2006').count
|
53
|
+
|
54
|
+
assert_equal 5, SearchTestModel.search_for('< 12/26/2002').count
|
55
|
+
assert_equal 6, SearchTestModel.search_for('<= 12/26/2002').count
|
56
|
+
|
57
|
+
assert_equal 5, SearchTestModel.search_for('> 2/5/2005').count
|
58
|
+
assert_equal 6, SearchTestModel.search_for('>= 2/5/2005').count
|
59
|
+
|
60
|
+
assert_equal 3, SearchTestModel.search_for('1/1/2005 TO 1/1/2007').count
|
61
|
+
|
62
|
+
assert_equal 2, SearchTestModel.search_for('Happy 1/1/2005 TO 1/1/2007').count
|
39
63
|
end
|
40
64
|
|
41
65
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'test/unit'
|
2
2
|
require 'rubygems'
|
3
3
|
require 'active_record'
|
4
|
+
require 'ruby-debug'
|
4
5
|
|
5
6
|
require "#{File.dirname(__FILE__)}/../lib/scoped_search"
|
6
7
|
|
@@ -11,6 +12,7 @@ def setup_db
|
|
11
12
|
t.string :string_field
|
12
13
|
t.text :text_field
|
13
14
|
t.string :ignored_field
|
15
|
+
t.date :date_field
|
14
16
|
t.timestamps
|
15
17
|
end
|
16
18
|
end
|
@@ -22,20 +24,21 @@ end
|
|
22
24
|
|
23
25
|
class SearchTestModel < ActiveRecord::Base
|
24
26
|
def self.create_corpus!
|
25
|
-
create!(:string_field => "Programmer 123", :text_field => nil,
|
26
|
-
create!(:string_field => "Jim", :text_field => "Henson",
|
27
|
-
create!(:string_field => "Jim", :text_field => "Bush",
|
28
|
-
create!(:string_field => "Wes", :text_field => "Hays",
|
29
|
-
create!(:string_field => "Bob", :text_field => "Hays",
|
30
|
-
create!(:string_field => "Dogs", :text_field => "Pit Bull",
|
31
|
-
create!(:string_field => "Dogs", :text_field => "Eskimo",
|
32
|
-
create!(:string_field => "Cows", :text_field => "Farms",
|
33
|
-
create!(:string_field => "Hello World", :text_field => "Hello Moon",
|
34
|
-
create!(:string_field => "Hello World", :text_field => "Goodnight Moon",
|
35
|
-
create!(:string_field => "Happy Cow", :text_field => "Sad Cow",
|
36
|
-
create!(:string_field => "Happy Frog", :text_field => "Sad Frog",
|
37
|
-
create!(:string_field => "Excited Frog", :text_field => "Sad Frog",
|
38
|
-
create!(:string_field => "Man made", :text_field => "Woman made",
|
39
|
-
create!(:string_field => "Cat Toys", :text_field => "Frog Toys",
|
27
|
+
create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456", :date_field => '2000-01-01')
|
28
|
+
create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a", :date_field => '2001-04-15')
|
29
|
+
create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b", :date_field => '2001-04-17')
|
30
|
+
create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c", :date_field => '1980-09-27')
|
31
|
+
create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d", :date_field => '2002-11-09')
|
32
|
+
create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e", :date_field => '2002-12-26')
|
33
|
+
create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f", :date_field => '2003-03-19')
|
34
|
+
create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g", :date_field => '2004-05-01')
|
35
|
+
create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h", :date_field => '2004-07-11')
|
36
|
+
create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i", :date_field => '2004-09-12')
|
37
|
+
create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j", :date_field => '2005-02-05')
|
38
|
+
create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k", :date_field => '2006-03-09')
|
39
|
+
create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l", :date_field => '2006-07-15')
|
40
|
+
create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m", :date_field => '2007-06-13')
|
41
|
+
create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n", :date_field => '2008-03-04')
|
42
|
+
create!(:string_field => "Happy Toys", :text_field => "Sad Toys", :ignored_field => "123456n", :date_field => '2008-05-12')
|
40
43
|
end
|
41
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wvanbergen-scoped_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2008-09-
|
13
|
+
date: 2008-09-21 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -25,6 +25,7 @@ extensions: []
|
|
25
25
|
extra_rdoc_files: []
|
26
26
|
|
27
27
|
files:
|
28
|
+
- CHANGELOG
|
28
29
|
- LICENSE
|
29
30
|
- README.rdoc
|
30
31
|
- Rakefile
|
@@ -33,8 +34,11 @@ files:
|
|
33
34
|
- lib
|
34
35
|
- lib/scoped_search
|
35
36
|
- lib/scoped_search.rb
|
37
|
+
- lib/scoped_search/query_conditions_builder.rb
|
36
38
|
- lib/scoped_search/query_language_parser.rb
|
39
|
+
- lib/scoped_search/reg_tokens.rb
|
37
40
|
- test
|
41
|
+
- test/query_conditions_builder_test.rb
|
38
42
|
- test/query_language_test.rb
|
39
43
|
- test/search_for_test.rb
|
40
44
|
- test/tasks.rake
|
@@ -66,5 +70,6 @@ signing_key:
|
|
66
70
|
specification_version: 2
|
67
71
|
summary: A Rails plugin to search your models using a named_scope
|
68
72
|
test_files:
|
73
|
+
- test/query_conditions_builder_test.rb
|
69
74
|
- test/query_language_test.rb
|
70
75
|
- test/search_for_test.rb
|