wvanbergen-scoped_search 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +48 -32
- data/Rakefile +1 -3
- data/lib/scoped_search.rb +45 -95
- data/lib/scoped_search/adapters.rb +41 -0
- data/lib/scoped_search/definition.rb +122 -0
- data/lib/scoped_search/query_builder.rb +213 -0
- data/lib/scoped_search/query_language.rb +30 -0
- data/lib/scoped_search/query_language/ast.rb +141 -0
- data/lib/scoped_search/query_language/parser.rb +115 -0
- data/lib/scoped_search/query_language/tokenizer.rb +62 -0
- data/{test → spec}/database.yml +0 -0
- data/spec/integration/api_spec.rb +82 -0
- data/spec/integration/ordinal_querying_spec.rb +153 -0
- data/spec/integration/relation_querying_spec.rb +258 -0
- data/spec/integration/string_querying_spec.rb +187 -0
- data/spec/lib/database.rb +44 -0
- data/spec/lib/matchers.rb +40 -0
- data/spec/lib/mocks.rb +19 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/unit/ast_spec.rb +197 -0
- data/spec/unit/definition_spec.rb +24 -0
- data/spec/unit/parser_spec.rb +105 -0
- data/spec/unit/query_builder_spec.rb +5 -0
- data/spec/unit/tokenizer_spec.rb +97 -0
- data/tasks/database_tests.rake +5 -5
- data/tasks/github-gem.rake +8 -3
- metadata +39 -23
- data/lib/scoped_search/query_conditions_builder.rb +0 -209
- data/lib/scoped_search/query_language_parser.rb +0 -117
- data/lib/scoped_search/reg_tokens.rb +0 -51
- data/tasks/documentation.rake +0 -33
- data/test/integration/api_test.rb +0 -53
- data/test/lib/test_models.rb +0 -148
- data/test/lib/test_schema.rb +0 -68
- data/test/test_helper.rb +0 -44
- data/test/unit/query_conditions_builder_test.rb +0 -410
- data/test/unit/query_language_test.rb +0 -155
- data/test/unit/search_for_test.rb +0 -124
@@ -0,0 +1,30 @@
|
|
1
|
+
module ScopedSearch::QueryLanguage
|
2
|
+
|
3
|
+
require 'scoped_search/query_language/ast'
|
4
|
+
require 'scoped_search/query_language/tokenizer'
|
5
|
+
require 'scoped_search/query_language/parser'
|
6
|
+
|
7
|
+
class Compiler
|
8
|
+
|
9
|
+
include Tokenizer
|
10
|
+
include Parser
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
def initialize(str)
|
14
|
+
@str = str
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse(str)
|
18
|
+
compiler = self.new(str)
|
19
|
+
compiler.parse
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.tokenize(str)
|
23
|
+
compiler = self.new(str)
|
24
|
+
compiler.tokenize
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module ScopedSearch::QueryLanguage::AST
|
2
|
+
|
3
|
+
# Constructs an AST from an array notation.
|
4
|
+
def self.from_array(arg)
|
5
|
+
if arg.kind_of?(Array)
|
6
|
+
operator = arg.shift
|
7
|
+
case operator
|
8
|
+
when :and, :or
|
9
|
+
LogicalOperatorNode.new(operator, arg.map { |c| from_array(c) })
|
10
|
+
when Symbol
|
11
|
+
OperatorNode.new(operator, arg.map { |c| from_array(c) })
|
12
|
+
else
|
13
|
+
raise ScopedSearch::Exception, "Not a valid array representation of an AST!"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
return LeafNode.new(arg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Base AST node class. Instances of this class are used to represent an abstract syntax tree.
|
21
|
+
# This syntax tree is created by the ScopedSearch::QueryLanguage parser and visited by the
|
22
|
+
# ScopedSearch::QueryBuilder to create SQL query conditions.
|
23
|
+
class Node
|
24
|
+
|
25
|
+
def inspect # :nodoc
|
26
|
+
"<AST::#{self.class.to_s.split('::').last} #{self.to_a.inspect}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Tree simplification. By default, do nothing and return the node as is.
|
30
|
+
def simplify
|
31
|
+
return self
|
32
|
+
end
|
33
|
+
|
34
|
+
def compatible_with(node) # :nodoc
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# AST lead node. This node represents leafs in the AST and can represent
|
40
|
+
# either a search phrase or a search field name.
|
41
|
+
class LeafNode < Node
|
42
|
+
attr_reader :value
|
43
|
+
|
44
|
+
def initialize(value) # :nodoc
|
45
|
+
@value = value
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return an array representation for the node
|
49
|
+
def to_a
|
50
|
+
value
|
51
|
+
end
|
52
|
+
|
53
|
+
def eql?(node) # :nodoc
|
54
|
+
node.kind_of?(LeafNode) && node.value == value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# AST class for representing operators in the query. An operator node has an operator
|
59
|
+
# and operands that are represented as AST child nodes. Usually, operator nodes have
|
60
|
+
# one or two children.
|
61
|
+
# For logical operators, a distinct subclass exists to implement some tree
|
62
|
+
# simplification rules.
|
63
|
+
class OperatorNode < Node
|
64
|
+
attr_reader :operator
|
65
|
+
attr_reader :children
|
66
|
+
|
67
|
+
def initialize(operator, children) # :nodoc
|
68
|
+
@operator = operator
|
69
|
+
@children = children
|
70
|
+
end
|
71
|
+
|
72
|
+
# Tree simplicication: returns itself after simpifying its children
|
73
|
+
def simplify
|
74
|
+
@children = children.map { |c| c.simplify }
|
75
|
+
return self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return an array representation for the node
|
79
|
+
def to_a
|
80
|
+
[@operator] + @children.map { |c| c.to_a }
|
81
|
+
end
|
82
|
+
|
83
|
+
def eql?(node) # :nodoc
|
84
|
+
node.kind_of?(OperatorNode) && node.operator == operator && node.children.eql?(children)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return the left-hand side (LHS) operand for this operator.
|
88
|
+
def lhs
|
89
|
+
raise ScopedSearch::Exception, "Operator does not have a LHS" if prefix?
|
90
|
+
raise ScopedSearch::Exception, "Operators with more than 2 children do not have LHS/RHS" if children.length > 2
|
91
|
+
children[0]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return the right-hand side (RHS) operand for this operator.
|
95
|
+
def rhs
|
96
|
+
raise ScopedSearch::Exception, "Operators with more than 2 children do not have LHS/RHS" if children.length > 2
|
97
|
+
children.length == 1 ? children[0] : children[1]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if this is an infix operator
|
101
|
+
def infix?
|
102
|
+
children.length > 1
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns true if this is a prefix operator
|
106
|
+
def prefix?
|
107
|
+
children.length == 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns a child node by index, starting with 0.
|
111
|
+
def [](child_nr)
|
112
|
+
children[child_nr]
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# AST class for representing AND or OR constructs.
|
118
|
+
# Logical constructs can be simplified resulting in a less complex AST.
|
119
|
+
class LogicalOperatorNode < OperatorNode
|
120
|
+
|
121
|
+
# Checks whether another node is comparable so that it can be used for tree simplification.
|
122
|
+
# A node can only be simplified if the logical operator is equal.
|
123
|
+
def compatible_with(node)
|
124
|
+
node.kind_of?(LogicalOperatorNode) && node.operator == self.operator
|
125
|
+
end
|
126
|
+
|
127
|
+
# Simplifies nested AND and OR constructs to single constructs with multiple arguments:
|
128
|
+
# e.g. (a AND (b AND c)) -> (a AND b AND c)
|
129
|
+
def simplify
|
130
|
+
if children.length == 1
|
131
|
+
# AND or OR constructs do nothing if they only have one operand
|
132
|
+
# So remove the logal operator from the AST by simply using the opeand
|
133
|
+
return children.first.simplify
|
134
|
+
else
|
135
|
+
# nested AND or OR constructs can be combined into one construct
|
136
|
+
@children = children.map { |c| c.simplify }.map { |c| self.compatible_with(c) ? c.children : c }.flatten
|
137
|
+
return self
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module ScopedSearch::QueryLanguage::Parser
|
2
|
+
|
3
|
+
DEFAULT_SEQUENCE_OPERATOR = :and
|
4
|
+
|
5
|
+
LOGICAL_INFIX_OPERATORS = [:and, :or]
|
6
|
+
LOGICAL_PREFIX_OPERATORS = [:not]
|
7
|
+
NULL_PREFIX_OPERATORS = [:null, :notnull]
|
8
|
+
COMPARISON_OPERATORS = [:eq, :ne, :gt, :gte, :lt, :lte, :like, :unlike]
|
9
|
+
ALL_INFIX_OPERATORS = LOGICAL_INFIX_OPERATORS + COMPARISON_OPERATORS
|
10
|
+
ALL_PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + COMPARISON_OPERATORS + NULL_PREFIX_OPERATORS
|
11
|
+
|
12
|
+
# Start the parsing process by parsing an expression sequence
|
13
|
+
def parse
|
14
|
+
@tokens = tokenize
|
15
|
+
parse_expression_sequence(true).simplify
|
16
|
+
end
|
17
|
+
|
18
|
+
# Parses a sequence of expressions
|
19
|
+
def parse_expression_sequence(initial = false)
|
20
|
+
expressions = []
|
21
|
+
next_token if !initial && peek_token == :lparen # skip staring :lparen
|
22
|
+
expressions << parse_logical_expression until peek_token.nil? || peek_token == :rparen
|
23
|
+
next_token if !initial && peek_token == :rparen # skip final :rparen
|
24
|
+
return ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(DEFAULT_SEQUENCE_OPERATOR, expressions)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Parses a logical expression.
|
29
|
+
def parse_logical_expression
|
30
|
+
lhs = case peek_token
|
31
|
+
when nil; nil
|
32
|
+
when :lparen; parse_expression_sequence
|
33
|
+
when :not; parse_logical_not_expression
|
34
|
+
when :null, :notnull; parse_null_expression
|
35
|
+
else; parse_comparison
|
36
|
+
end
|
37
|
+
|
38
|
+
if LOGICAL_INFIX_OPERATORS.include?(peek_token)
|
39
|
+
operator = next_token
|
40
|
+
rhs = parse_logical_expression
|
41
|
+
ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(operator, [lhs, rhs])
|
42
|
+
else
|
43
|
+
lhs
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Parses a NOT expression
|
48
|
+
def parse_logical_not_expression
|
49
|
+
next_token # = skip NOT operator
|
50
|
+
negated_expression = case peek_token
|
51
|
+
when :not; parse_logical_not_expression
|
52
|
+
when :lparen; parse_expression_sequence
|
53
|
+
else parse_comparison
|
54
|
+
end
|
55
|
+
return ScopedSearch::QueryLanguage::AST::OperatorNode.new(:not, [negated_expression])
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parses a set? or null? expression
|
59
|
+
def parse_null_expression
|
60
|
+
return ScopedSearch::QueryLanguage::AST::OperatorNode.new(next_token, [parse_value])
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parses a comparison
|
64
|
+
def parse_comparison
|
65
|
+
next_token if peek_token == :comma # skip comma
|
66
|
+
return (String === peek_token) ? parse_infix_comparison : parse_prefix_comparison
|
67
|
+
end
|
68
|
+
|
69
|
+
# Parses a prefix comparison, i.e. without an explicit field: <operator> <value>
|
70
|
+
def parse_prefix_comparison
|
71
|
+
return ScopedSearch::QueryLanguage::AST::OperatorNode.new(next_token, [parse_value])
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parses an infix expression, i.e. <field> <operator> <value>
|
75
|
+
def parse_infix_comparison
|
76
|
+
lhs = parse_value
|
77
|
+
return case peek_token
|
78
|
+
when nil
|
79
|
+
lhs
|
80
|
+
when :comma
|
81
|
+
next_token # skip comma
|
82
|
+
lhs
|
83
|
+
else
|
84
|
+
if COMPARISON_OPERATORS.include?(peek_token)
|
85
|
+
comparison_operator = next_token
|
86
|
+
rhs = parse_value
|
87
|
+
ScopedSearch::QueryLanguage::AST::OperatorNode.new(comparison_operator, [lhs, rhs])
|
88
|
+
else
|
89
|
+
lhs
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Parses a single value.
|
95
|
+
# This can either be a constant value or a field name.
|
96
|
+
def parse_value
|
97
|
+
raise ScopedSearch::QueryNotSupported, "Value expected but found #{peek_token.inspect}" unless String === peek_token
|
98
|
+
ScopedSearch::QueryLanguage::AST::LeafNode.new(next_token)
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def current_token
|
104
|
+
@current_token
|
105
|
+
end
|
106
|
+
|
107
|
+
def peek_token(amount = 1)
|
108
|
+
@tokens[amount - 1]
|
109
|
+
end
|
110
|
+
|
111
|
+
def next_token
|
112
|
+
@current_token = @tokens.shift
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ScopedSearch::QueryLanguage::Tokenizer
|
2
|
+
|
3
|
+
KEYWORDS = { 'and' => :and, 'or' => :or, 'not' => :not, 'set?' => :notnull, 'null?' => :null }
|
4
|
+
OPERATORS = { '&' => :and, '|' => :or, '&&' => :and, '||' => :or, '-'=> :not, '!' => :not, '~' => :like, '!~' => :unlike,
|
5
|
+
'=' => :eq, '==' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
|
6
|
+
|
7
|
+
|
8
|
+
def tokenize
|
9
|
+
@current_char_pos = -1
|
10
|
+
to_a
|
11
|
+
end
|
12
|
+
|
13
|
+
def current_char
|
14
|
+
@current_char
|
15
|
+
end
|
16
|
+
|
17
|
+
def peek_char(amount = 1)
|
18
|
+
@str[@current_char_pos + amount, 1]
|
19
|
+
end
|
20
|
+
|
21
|
+
def next_char
|
22
|
+
@current_char_pos += 1
|
23
|
+
@current_char = @str[@current_char_pos, 1]
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_token(&block)
|
27
|
+
while next_char
|
28
|
+
case current_char
|
29
|
+
when /^\s?$/; # ignore
|
30
|
+
when '('; yield(:lparen)
|
31
|
+
when ')'; yield(:rparen)
|
32
|
+
when ','; yield(:comma)
|
33
|
+
when /\&|\||=|<|>|!|~|-/; tokenize_operator(&block)
|
34
|
+
when '"'; tokenize_quoted_keyword(&block)
|
35
|
+
else; tokenize_keyword(&block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def tokenize_operator(&block)
|
41
|
+
operator = current_char
|
42
|
+
operator << next_char if OPERATORS.has_key?(operator + peek_char)
|
43
|
+
yield(OPERATORS[operator])
|
44
|
+
end
|
45
|
+
|
46
|
+
def tokenize_keyword(&block)
|
47
|
+
keyword = current_char
|
48
|
+
keyword << next_char while /[^=<>\s\&\|\)\(,]/ =~ peek_char
|
49
|
+
KEYWORDS.has_key?(keyword.downcase) ? yield(KEYWORDS[keyword.downcase]) : yield(keyword)
|
50
|
+
end
|
51
|
+
|
52
|
+
def tokenize_quoted_keyword(&block)
|
53
|
+
keyword = ""
|
54
|
+
until next_char.nil? || current_char == '"'
|
55
|
+
keyword << (current_char == "\\" ? next_char : current_char)
|
56
|
+
end
|
57
|
+
yield(keyword)
|
58
|
+
end
|
59
|
+
|
60
|
+
alias :each :each_token
|
61
|
+
|
62
|
+
end
|
data/{test → spec}/database.yml
RENAMED
File without changes
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
|
+
|
3
|
+
describe ScopedSearch, "API" do
|
4
|
+
|
5
|
+
# This spec requires the API to be stable, so that projects using
|
6
|
+
# scoped_search do not have to update their code if a new (minor)
|
7
|
+
# version is released.
|
8
|
+
#
|
9
|
+
# API compatibility is only guaranteed for minor version changes;
|
10
|
+
# New major versions may change the API and require code changes
|
11
|
+
# in projects using this plugin.
|
12
|
+
#
|
13
|
+
# Because of the API stability guarantee, these spec's may only
|
14
|
+
# be changed for new major releases.
|
15
|
+
|
16
|
+
before(:all) do
|
17
|
+
ScopedSearch::Spec::Database.establish_connection
|
18
|
+
end
|
19
|
+
|
20
|
+
after(:all) do
|
21
|
+
ScopedSearch::Spec::Database.close_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'for unprepared ActiveRecord model' do
|
25
|
+
|
26
|
+
it "should respond to :scoped_search to setup scoped_search for the model" do
|
27
|
+
Class.new(ActiveRecord::Base).should respond_to(:scoped_search)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'for a prepared ActiveRecord model' do
|
32
|
+
|
33
|
+
before(:all) do
|
34
|
+
@class = ScopedSearch::Spec::Database.create_model(:field => :string) do |klass|
|
35
|
+
klass.scoped_search :on => :field
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
after(:all) do
|
40
|
+
ScopedSearch::Spec::Database.drop_model(@class)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should respond to :search_for to perform searches" do
|
44
|
+
@class.should respond_to(:search_for)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should return an ActiveRecord::NamedScope::Scope when :search_for is called" do
|
48
|
+
@class.search_for('query').class.should eql(ActiveRecord::NamedScope::Scope)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'having backwards compatibility' do
|
53
|
+
|
54
|
+
before(:each) do
|
55
|
+
class Foo < ActiveRecord::Base
|
56
|
+
belongs_to :bar
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
after(:each) do
|
61
|
+
Object.send :remove_const, :Foo
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should respond to :searchable_on" do
|
65
|
+
Foo.should respond_to(:searchable_on)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should create a Field instance for every argument" do
|
69
|
+
ScopedSearch::Definition::Field.should_receive(:new).exactly(3).times
|
70
|
+
Foo.searchable_on(:field_1, :field_2, :field_3)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should create a Field with a valid relation when using the underscore notation" do
|
74
|
+
ScopedSearch::Definition::Field.should_receive(:new).with(
|
75
|
+
instance_of(ScopedSearch::Definition), hash_including(:in => :bar, :on => :related_field))
|
76
|
+
|
77
|
+
Foo.searchable_on(:bar_related_field)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
|
+
|
3
|
+
describe ScopedSearch do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
ScopedSearch::Spec::Database.establish_connection
|
7
|
+
|
8
|
+
@class = ScopedSearch::Spec::Database.create_model(:int => :integer, :timestamp => :datetime, :date => :date, :unindexed => :integer) do |klass|
|
9
|
+
klass.scoped_search :on => [:int, :timestamp]
|
10
|
+
klass.scoped_search :on => :date, :only_explicit => true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:all) do
|
15
|
+
ScopedSearch::Spec::Database.drop_model(@class)
|
16
|
+
ScopedSearch::Spec::Database.close_connection
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'quering numerical fields' do
|
20
|
+
|
21
|
+
before(:all) do
|
22
|
+
@record = @class.create!(:int => 9)
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:all) do
|
26
|
+
@record.destroy
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should find the record with an exact integer match" do
|
30
|
+
@class.search_for('9').should have(1).item
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should find the record with an exact integer match with an explicit operator" do
|
34
|
+
@class.search_for('= 9').should have(1).item
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should find the record with an exact integer match with an explicit field name" do
|
38
|
+
@class.search_for('int = 9').should have(1).item
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should find the record with an exact integer match with an explicit field name" do
|
42
|
+
@class.search_for('int > 8').should have(1).item
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should find the record with a grater than operator and explicit field" do
|
46
|
+
@class.search_for('int > 9').should have(0).item
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should find the record with an >= operator with an implicit field name" do
|
50
|
+
@class.search_for('>= 9').should have(1).item
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should not return the record if only one predicate is true and AND is used (by default)" do
|
54
|
+
@class.search_for('int <= 8, int > 8').should have(0).item
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return the record in only one predicate is true and OR is used as operator" do
|
58
|
+
@class.search_for('int <= 8 || int > 8').should have(1).item
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'querying unindexed fields' do
|
63
|
+
|
64
|
+
before(:all) do
|
65
|
+
@record = @class.create!(:int => 9, :unindexed => 10)
|
66
|
+
end
|
67
|
+
|
68
|
+
after(:all) do
|
69
|
+
@record.destroy
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should raise an error when explicitly searching in the non-indexed column" do
|
73
|
+
lambda { @class.search_for('unindexed = 10') }.should raise_error(ScopedSearch::Exception)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not return records for which the query matches unindex records" do
|
77
|
+
@class.search_for('= 10').should have(0).item
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'querying date and time fields' do
|
82
|
+
|
83
|
+
before(:all) do
|
84
|
+
@record = @class.create!(:timestamp => Time.parse('2009-01-02 14:51:44'), :date => Date.parse('2009-01-02'))
|
85
|
+
@nil_record = @class.create!(:timestamp => nil, :date => nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
after(:all) do
|
89
|
+
@record.destroy
|
90
|
+
@nil_record.destroy
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should accept YYYY-MM-DD as date format" do
|
94
|
+
@class.search_for('date = 2009-01-02').should have(1).item
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should accept YY-MM-DD as date format" do
|
98
|
+
@class.search_for('date = 09-01-02').should have(1).item
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should accept MM/DD/YY as date format" do
|
102
|
+
@class.search_for('date = 01/02/09').should have(1).item
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should accept YYYY/MM/DD as date format" do
|
106
|
+
@class.search_for('date = 2009/01/02').should have(1).item
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should accept MM/DD/YYYY as date format" do
|
110
|
+
@class.search_for('date = 01/02/2009').should have(1).item
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should ignore an invalid date and thus return all records" do
|
114
|
+
@class.search_for('>= 2009-14-57').should have(2).items
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should find the records with a timestamp set some point on the provided date" do
|
118
|
+
@class.search_for('>= 2009-01-02').should have(1).item
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should support full timestamps" do
|
122
|
+
@class.search_for('> "2009-01-02 02:02:02"').should have(1).item
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should find no record with a timestamp in the past" do
|
126
|
+
@class.search_for('< 2009-01-02').should have(0).item
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should find all timestamps on a date if no time is given using the = operator" do
|
130
|
+
@class.search_for('= 2009-01-02').should have(1).item
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should find all timestamps on a date if no time is when no operator is given" do
|
134
|
+
@class.search_for('2009-01-02').should have(1).item
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should find all timestamps not on a date if no time is given using the != operator" do
|
138
|
+
@class.search_for('!= 2009-01-02').should have(0).item
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should find the records when the date part of a timestamp matches a date" do
|
142
|
+
@class.search_for('>= 2009-01-02').should have(1).item
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should find the record with the timestamp today or in the past" do
|
146
|
+
@class.search_for('<= 2009-01-02').should have(1).item
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should find no record with a timestamp later than today" do
|
150
|
+
@class.search_for('> 2009-01-02').should have(0).item
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|