command_search 0.8.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/command_search.rb +11 -12
- data/lib/command_search/backends/memory.rb +70 -0
- data/lib/command_search/backends/mongoer.rb +88 -0
- data/lib/command_search/lexer.rb +1 -1
- data/lib/command_search/normalizer.rb +121 -0
- data/lib/command_search/optimizer.rb +19 -46
- data/lib/command_search/parser.rb +1 -2
- metadata +5 -5
- data/lib/command_search/command_dealiaser.rb +0 -48
- data/lib/command_search/memory.rb +0 -125
- data/lib/command_search/mongoer.rb +0 -229
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4560d75eabd25ff56808df06ab6b72d97813fbd75909739d15bfa2e13055557
|
4
|
+
data.tar.gz: 8c48236dd477f98450076128b7737121fabb16398c123e3c567f049bd68cadbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0abdf98f95abba34b3a27d8f805f5183b1089e7291dad6b7f3a67e6cdb8a2413dad6813cf06e7118b733b44132428d79c89545739184022f3fc6d6222e60f8d
|
7
|
+
data.tar.gz: 836f1b245c14396850259c7e326e4ed1451bfcda6df07a594162ed4bbd1725abdf4a4a46c90c75c23eeb3b3a0ed242e7caa02cded714437acc1cc9ce65a63781
|
data/lib/command_search.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
load(__dir__ + '/command_search/aliaser.rb')
|
2
2
|
load(__dir__ + '/command_search/lexer.rb')
|
3
3
|
load(__dir__ + '/command_search/parser.rb')
|
4
|
-
load(__dir__ + '/command_search/
|
4
|
+
load(__dir__ + '/command_search/normalizer.rb')
|
5
5
|
load(__dir__ + '/command_search/optimizer.rb')
|
6
|
-
|
7
|
-
load(__dir__ + '/command_search/memory.rb')
|
6
|
+
|
7
|
+
load(__dir__ + '/command_search/backends/memory.rb')
|
8
|
+
load(__dir__ + '/command_search/backends/mongoer.rb')
|
8
9
|
|
9
10
|
class Boolean; end
|
10
11
|
|
@@ -17,19 +18,17 @@ module CommandSearch
|
|
17
18
|
command_fields = options[:command_fields] || {}
|
18
19
|
|
19
20
|
aliased_query = Aliaser.alias(query, aliases)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
opted = Optimizer.optimize(cleaned)
|
21
|
+
ast = Lexer.lex(aliased_query)
|
22
|
+
Parser.parse!(ast)
|
23
|
+
Optimizer.optimize!(ast)
|
24
|
+
command_fields = Normalizer.normalize!(ast, command_fields)
|
25
25
|
|
26
26
|
if source.respond_to?(:mongo_client) && source.queryable
|
27
|
-
fields = [:
|
28
|
-
mongo_query = Mongoer.build_query(
|
27
|
+
fields = [:__CommandSearch_dummy_key__] if fields.empty?
|
28
|
+
mongo_query = Mongoer.build_query(ast, fields, command_fields)
|
29
29
|
return source.where(mongo_query)
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
source.select(&selector)
|
32
|
+
source.select { |x| Memory.check(x, ast, fields, command_fields) }
|
34
33
|
end
|
35
34
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module CommandSearch
|
2
|
+
module Memory
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def command_check(item, val)
|
6
|
+
cmd = val[0][:value]
|
7
|
+
cmd_search = val[1][:value]
|
8
|
+
item_val = item[cmd.to_sym] || item[cmd]
|
9
|
+
val_type = val[1][:type]
|
10
|
+
val_type = Boolean if val_type == :existence && cmd_search == true
|
11
|
+
if val_type == Boolean
|
12
|
+
!!item_val == cmd_search
|
13
|
+
elsif val_type == :existence
|
14
|
+
item_val == nil
|
15
|
+
elsif !item_val
|
16
|
+
return false
|
17
|
+
elsif val_type == Time
|
18
|
+
item_time = item_val.to_time
|
19
|
+
cmd_search.first <= item_time && item_time < cmd_search.last
|
20
|
+
elsif cmd_search.is_a?(Regexp)
|
21
|
+
item_val[cmd_search]
|
22
|
+
elsif cmd_search == ''
|
23
|
+
item_val == cmd_search
|
24
|
+
else
|
25
|
+
item_val.to_s[/#{Regexp.escape(cmd_search)}/i]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def compare_check(item, node, cmd_types)
|
30
|
+
cmd = node[:value].first
|
31
|
+
cmd_val = cmd[:value]
|
32
|
+
cmd_type = cmd_types[cmd[:value].to_sym]
|
33
|
+
item_val = item[cmd_val.to_sym] || item[cmd_val.to_s]
|
34
|
+
search = node[:value].last
|
35
|
+
val = search[:value]
|
36
|
+
if val.is_a?(Time)
|
37
|
+
item_val = item_val.to_time if item_val
|
38
|
+
elsif search[:type] == :str && cmd_types[val.to_sym]
|
39
|
+
val = item[val.to_sym] || item[val.to_s]
|
40
|
+
end
|
41
|
+
args = [item_val, val]
|
42
|
+
return unless args.all?
|
43
|
+
fn = node[:nest_op].to_sym.to_proc
|
44
|
+
fn.call(*args.map(&:to_f))
|
45
|
+
end
|
46
|
+
|
47
|
+
def check(item, ast, fields, cmd_types)
|
48
|
+
ast.all? do |node|
|
49
|
+
val = node[:value]
|
50
|
+
case node[:nest_type]
|
51
|
+
when nil
|
52
|
+
fields.any? do |x|
|
53
|
+
item_val = item[x.to_sym] || item[x.to_s]
|
54
|
+
item_val.to_s[val] if item_val
|
55
|
+
end
|
56
|
+
when :colon
|
57
|
+
command_check(item, val)
|
58
|
+
when :compare
|
59
|
+
compare_check(item, node, cmd_types)
|
60
|
+
when :minus
|
61
|
+
!val.all? { |v| check(item, [v], fields, cmd_types) }
|
62
|
+
when :pipe
|
63
|
+
val.any? { |v| check(item, [v], fields, cmd_types) }
|
64
|
+
when :paren
|
65
|
+
val.all? { |v| check(item, [v], fields, cmd_types) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module CommandSearch
|
2
|
+
module Mongoer
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def build_search(node, fields, cmd_fields)
|
6
|
+
val = node[:value]
|
7
|
+
forms = fields.map do |field|
|
8
|
+
type = cmd_fields[field.to_sym]
|
9
|
+
if type == Numeric
|
10
|
+
{ field => node[:number_value] }
|
11
|
+
else
|
12
|
+
{ field => val }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
return forms if forms.count < 2
|
16
|
+
{ '$or' => forms }
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_command(node)
|
20
|
+
(field_node, search_node) = node[:value]
|
21
|
+
key = field_node[:value]
|
22
|
+
val = search_node[:value]
|
23
|
+
search_type = search_node[:type]
|
24
|
+
search_type = Boolean if search_type == :existence && val == true
|
25
|
+
if search_type == Boolean
|
26
|
+
# These queries can return true for empty arrays.
|
27
|
+
val = [
|
28
|
+
{ key => { '$exists' => true } },
|
29
|
+
{ key => { '$ne' => !val } }
|
30
|
+
]
|
31
|
+
key = '$and'
|
32
|
+
elsif search_type == :existence
|
33
|
+
val = { '$exists' => false }
|
34
|
+
elsif search_type == Time
|
35
|
+
return [{ CommandSearchNilTime: true }, { CommandSearchNilTime: false }] unless val
|
36
|
+
return [
|
37
|
+
{ key => { '$gte' => val[0] } },
|
38
|
+
{ key => { '$lt' => val[1] } }
|
39
|
+
]
|
40
|
+
end
|
41
|
+
{ key => val }
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_compare(node, cmd_fields)
|
45
|
+
op_map = { '<' => '$lt', '>' => '$gt', '<=' => '$lte', '>=' => '$gte' }
|
46
|
+
op = op_map[node[:nest_op]]
|
47
|
+
key = node[:value][0][:value]
|
48
|
+
val = node[:value][1][:value]
|
49
|
+
if val.class == String && cmd_fields[val.to_sym]
|
50
|
+
val = '$' + val
|
51
|
+
key = '$' + key
|
52
|
+
val = [key, val]
|
53
|
+
key = '$expr'
|
54
|
+
end
|
55
|
+
{ key => { op => val } }
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_searches!(ast, fields, cmd_fields)
|
59
|
+
mongo_types = { paren: '$and', pipe: '$or', minus: '$nor' }
|
60
|
+
ast.map! do |node|
|
61
|
+
type = node[:nest_type]
|
62
|
+
if type == :colon
|
63
|
+
build_command(node)
|
64
|
+
elsif type == :compare
|
65
|
+
build_compare(node, cmd_fields)
|
66
|
+
elsif key = mongo_types[type]
|
67
|
+
build_searches!(node[:value], fields, cmd_fields)
|
68
|
+
val = node[:value]
|
69
|
+
if key == '$nor' && val.count > 1
|
70
|
+
next { key => [{ '$and' => val }] }
|
71
|
+
end
|
72
|
+
val.map! { |x| x['$or'] || x }.flatten! unless key == '$and'
|
73
|
+
{ key => val }
|
74
|
+
else
|
75
|
+
build_search(node, fields, cmd_fields)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
ast.flatten!
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_query(ast, fields, cmd_fields)
|
82
|
+
build_searches!(ast, fields, cmd_fields)
|
83
|
+
return {} if ast == []
|
84
|
+
return ast.first if ast.count == 1
|
85
|
+
{ '$and' => ast }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/command_search/lexer.rb
CHANGED
@@ -0,0 +1,121 @@
|
|
1
|
+
require('chronic')
|
2
|
+
|
3
|
+
module CommandSearch
|
4
|
+
module Normalizer
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def cast_bool(type, node)
|
8
|
+
if type == Boolean
|
9
|
+
node[:type] = Boolean
|
10
|
+
node[:value] = !!node[:value][0][/t/i]
|
11
|
+
return
|
12
|
+
end
|
13
|
+
return unless type.is_a?(Array) && type.include?(:allow_existence_boolean)
|
14
|
+
return unless node[:type] == :str && node[:value][/\Atrue\Z|\Afalse\Z/i]
|
15
|
+
node[:type] = :existence
|
16
|
+
node[:value] = !!node[:value][0][/t/i]
|
17
|
+
end
|
18
|
+
|
19
|
+
def cast_time(node)
|
20
|
+
search_node = node[:value][1]
|
21
|
+
search_node[:type] = Time
|
22
|
+
str = search_node[:value]
|
23
|
+
if str == str.to_i.to_s
|
24
|
+
search_node[:value] = [Time.new(str), Time.new(str.to_i + 1)]
|
25
|
+
else
|
26
|
+
time_str = str.tr('._-', ' ')
|
27
|
+
times = Chronic.parse(time_str, { guess: nil })
|
28
|
+
times ||= Chronic.parse(str, { guess: nil })
|
29
|
+
if times
|
30
|
+
search_node[:value] = [times.first, times.last]
|
31
|
+
else
|
32
|
+
search_node[:value] = nil
|
33
|
+
return
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return unless node[:nest_type] == :compare
|
37
|
+
op = node[:nest_op]
|
38
|
+
if op == '<' || op == '>='
|
39
|
+
search_node[:value] = search_node[:value].first
|
40
|
+
else
|
41
|
+
search_node[:value] = search_node[:value].last
|
42
|
+
search_node[:value] -= 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def cast_regex(node)
|
47
|
+
type = node[:type]
|
48
|
+
return unless type == :str || type == :quoted_str || type == :number
|
49
|
+
raw = node[:value]
|
50
|
+
str = Regexp.escape(raw)
|
51
|
+
return node[:value] = /#{str}/i unless type == :quoted_str
|
52
|
+
return node[:value] = '' if raw == ''
|
53
|
+
return node[:value] = /\b#{str}\b/ unless raw[/(^\W)|(\W$)/]
|
54
|
+
border_a = '(^|\s|[^:+\w])'
|
55
|
+
border_b = '($|\s|[^:+\w])'
|
56
|
+
node[:value] = Regexp.new(border_a + str + border_b)
|
57
|
+
end
|
58
|
+
|
59
|
+
def flip_operator!(node, cmd_fields)
|
60
|
+
val = node[:value]
|
61
|
+
return if cmd_fields[val[0][:value].to_sym]
|
62
|
+
return unless cmd_fields[val[1][:value].to_sym]
|
63
|
+
flip_ops = { '<' => '>', '>' => '<', '<=' => '>=', '>=' => '<=' }
|
64
|
+
node[:nest_op] = flip_ops[node[:nest_op]]
|
65
|
+
node[:value].reverse!
|
66
|
+
end
|
67
|
+
|
68
|
+
def dealias_key(key, cmd_fields)
|
69
|
+
key = cmd_fields[key.to_sym] while cmd_fields[key.to_sym].is_a?(Symbol)
|
70
|
+
key.to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
def dealias!(ast, cmd_fields)
|
74
|
+
ast.map! do |node|
|
75
|
+
nest = node[:nest_type]
|
76
|
+
unless nest
|
77
|
+
node[:number_value] = node[:value] if node[:type] == :number
|
78
|
+
cast_regex(node)
|
79
|
+
next node
|
80
|
+
end
|
81
|
+
unless nest == :colon || nest == :compare
|
82
|
+
dealias!(node[:value], cmd_fields)
|
83
|
+
next node
|
84
|
+
end
|
85
|
+
flip_operator!(node, cmd_fields) if nest == :compare
|
86
|
+
(key_node, search_node) = node[:value]
|
87
|
+
new_key = dealias_key(key_node[:value], cmd_fields)
|
88
|
+
type = cmd_fields[new_key.to_sym]
|
89
|
+
node[:value][0][:value] = new_key
|
90
|
+
if type
|
91
|
+
cast_bool(type, search_node)
|
92
|
+
type = (type - [:allow_existence_boolean]).first if type.is_a?(Array)
|
93
|
+
cast_time(node) if [Time, Date, DateTime].include?(type)
|
94
|
+
cast_regex(search_node) if type == String
|
95
|
+
next node
|
96
|
+
end
|
97
|
+
str_values = "#{new_key}#{node[:nest_op]}#{search_node[:value]}"
|
98
|
+
node = { type: :str, value: str_values }
|
99
|
+
cast_regex(node)
|
100
|
+
node
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def normalize!(ast, cmd_fields)
|
105
|
+
dealias!(ast, cmd_fields)
|
106
|
+
clean = {}
|
107
|
+
cmd_fields.each do |k, v|
|
108
|
+
next if v.is_a?(Symbol)
|
109
|
+
if v.is_a?(Array)
|
110
|
+
clean[k] = (v - [:allow_existence_boolean]).first
|
111
|
+
next
|
112
|
+
end
|
113
|
+
v = Numeric if v == Integer
|
114
|
+
v = Time if v == Date
|
115
|
+
v = Time if v == DateTime
|
116
|
+
next clean[k] = v
|
117
|
+
end
|
118
|
+
clean
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -2,60 +2,33 @@ module CommandSearch
|
|
2
2
|
module Optimizer
|
3
3
|
module_function
|
4
4
|
|
5
|
-
def
|
5
|
+
def denest!(ast, parent_type = :paren)
|
6
6
|
ast.map! do |node|
|
7
|
-
next
|
8
|
-
ands_and_ors!(node[:value])
|
9
|
-
next node[:value].first if node[:value].length < 2
|
10
|
-
next node unless node[:nest_type] == :pipe
|
11
|
-
node[:value].map! do |kid|
|
12
|
-
next kid[:value] if kid[:nest_type] == :pipe
|
13
|
-
kid
|
14
|
-
end
|
15
|
-
node[:value].flatten!
|
16
|
-
node[:value].uniq!
|
17
|
-
node
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def negate_negate!(ast)
|
22
|
-
ast.map! do |node|
|
23
|
-
next node unless node[:nest_type]
|
24
|
-
negate_negate!(node[:value])
|
25
|
-
next [] if node[:value] == []
|
7
|
+
next [] if node[:type] == :quoted_str && node[:value] == '' && [:paren, :pipe, :minus].include?(parent_type)
|
26
8
|
type = node[:nest_type]
|
27
|
-
|
28
|
-
next node unless type == :
|
29
|
-
node[:value]
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
next node unless node[:nest_type]
|
37
|
-
denest_parens!(node[:value], node[:nest_type])
|
38
|
-
# valid_self && (valid_parent || valid_child)
|
39
|
-
if node[:nest_type] == :paren && (parent_type != :pipe || node[:value].count < 2)
|
40
|
-
next node[:value]
|
9
|
+
next node unless type
|
10
|
+
next node unless type == :paren || type == :pipe || type == :minus
|
11
|
+
denest!(node[:value], type)
|
12
|
+
next [] if node[:value] == []
|
13
|
+
if type == :minus
|
14
|
+
only_child = node[:value].count == 1
|
15
|
+
child = node[:value].first
|
16
|
+
next child[:value] if only_child && child[:nest_type] == :minus
|
17
|
+
next node
|
41
18
|
end
|
19
|
+
next node[:value] if node[:value].count == 1
|
20
|
+
next node[:value] if type == parent_type
|
21
|
+
next node[:value] if type == :paren && parent_type == :minus
|
22
|
+
next node if type == :paren
|
23
|
+
denest!(node[:value], type) # type == :pipe, parent_type == :paren
|
24
|
+
node[:value].uniq!
|
42
25
|
node
|
43
26
|
end
|
44
27
|
ast.flatten!
|
45
28
|
end
|
46
29
|
|
47
|
-
def
|
48
|
-
ast
|
49
|
-
remove_empty_strings!(node[:value]) if [:paren, :pipe, :minus].include?(node[:nest_type])
|
50
|
-
node[:type] == :quoted_str && node[:value] == ''
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def optimize(ast)
|
55
|
-
denest_parens!(ast)
|
56
|
-
remove_empty_strings!(ast)
|
57
|
-
negate_negate!(ast)
|
58
|
-
ands_and_ors!(ast)
|
30
|
+
def optimize!(ast)
|
31
|
+
denest!(ast)
|
59
32
|
ast.uniq!
|
60
33
|
ast
|
61
34
|
end
|
@@ -47,7 +47,7 @@ module CommandSearch
|
|
47
47
|
left = input[i][:type]
|
48
48
|
right = input[i + 2][:type]
|
49
49
|
if types.include?(left) && types.include?(right)
|
50
|
-
input.insert(i + 1, input[i + 1])
|
50
|
+
input.insert(i + 1, input[i + 1].clone())
|
51
51
|
end
|
52
52
|
i += 1
|
53
53
|
end
|
@@ -83,7 +83,6 @@ module CommandSearch
|
|
83
83
|
input[i - 1..i] = merge_strs(input[i - 1..i], [1, 0]) if i > 0
|
84
84
|
end
|
85
85
|
|
86
|
-
input.select! { |x| x[:type] != :space }
|
87
86
|
input[-1][:type] = :str if input[-1] && input[-1][:type] == :minus
|
88
87
|
end
|
89
88
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- zumbalogy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chronic
|
@@ -32,10 +32,10 @@ extra_rdoc_files: []
|
|
32
32
|
files:
|
33
33
|
- lib/command_search.rb
|
34
34
|
- lib/command_search/aliaser.rb
|
35
|
-
- lib/command_search/
|
35
|
+
- lib/command_search/backends/memory.rb
|
36
|
+
- lib/command_search/backends/mongoer.rb
|
36
37
|
- lib/command_search/lexer.rb
|
37
|
-
- lib/command_search/
|
38
|
-
- lib/command_search/mongoer.rb
|
38
|
+
- lib/command_search/normalizer.rb
|
39
39
|
- lib/command_search/optimizer.rb
|
40
40
|
- lib/command_search/parser.rb
|
41
41
|
homepage: https://github.com/zumbalogy/command_search
|
@@ -1,48 +0,0 @@
|
|
1
|
-
module CommandSearch
|
2
|
-
module CommandDealiaser
|
3
|
-
module_function
|
4
|
-
|
5
|
-
def dealias_key(key, aliases)
|
6
|
-
key = aliases[key.to_sym] while aliases[key.to_sym].is_a?(Symbol)
|
7
|
-
key.to_s
|
8
|
-
end
|
9
|
-
|
10
|
-
def dealias_values((key_node, search_node), aliases)
|
11
|
-
new_key = dealias_key(key_node[:value], aliases)
|
12
|
-
key_node[:value] = new_key
|
13
|
-
[key_node, search_node]
|
14
|
-
end
|
15
|
-
|
16
|
-
def unnest_unaliased(node, aliases)
|
17
|
-
type = node[:nest_type]
|
18
|
-
if type == :colon
|
19
|
-
val = node[:value][0][:value].to_sym
|
20
|
-
return node if aliases[val]
|
21
|
-
elsif type == :compare
|
22
|
-
return node if node[:value].any? { |child| aliases[child[:value].to_sym] }
|
23
|
-
end
|
24
|
-
values = node[:value].map { |x| x[:value] }
|
25
|
-
str_values = values.join(node[:nest_op])
|
26
|
-
{ type: :str, value: str_values }
|
27
|
-
end
|
28
|
-
|
29
|
-
def dealias(ast, aliases)
|
30
|
-
ast.map! do |x|
|
31
|
-
next x unless x[:nest_type]
|
32
|
-
dealias(x[:value], aliases)
|
33
|
-
next x unless [:colon, :compare].include?(x[:nest_type])
|
34
|
-
x[:value] = dealias_values(x[:value], aliases)
|
35
|
-
x
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def decompose_unaliasable(ast, aliases)
|
40
|
-
ast.map! do |x|
|
41
|
-
next x unless x[:nest_type]
|
42
|
-
decompose_unaliasable(x[:value], aliases)
|
43
|
-
next x unless [:colon, :compare].include?(x[:nest_type])
|
44
|
-
unnest_unaliased(x, aliases)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
@@ -1,125 +0,0 @@
|
|
1
|
-
require('chronic')
|
2
|
-
|
3
|
-
module CommandSearch
|
4
|
-
module Memory
|
5
|
-
module_function
|
6
|
-
|
7
|
-
def command_check(item, val, command_types)
|
8
|
-
cmd = val[0][:value].to_sym
|
9
|
-
cmd_search = val[1][:value]
|
10
|
-
raw_cmd_type = [command_types[cmd]].flatten
|
11
|
-
allow_existence_boolean = raw_cmd_type.include?(:allow_existence_boolean)
|
12
|
-
cmd_type = (raw_cmd_type - [:allow_existence_boolean]).first
|
13
|
-
if cmd_type == Boolean
|
14
|
-
if cmd_search[/true/i]
|
15
|
-
item[cmd]
|
16
|
-
else
|
17
|
-
item[cmd] == false
|
18
|
-
end
|
19
|
-
elsif allow_existence_boolean && (cmd_search[/true/i] || cmd_search[/false/i])
|
20
|
-
if cmd_search[/true/i]
|
21
|
-
item[cmd]
|
22
|
-
else
|
23
|
-
item[cmd] == nil
|
24
|
-
end
|
25
|
-
elsif !item.key?(cmd)
|
26
|
-
return false
|
27
|
-
elsif [Date, Time, DateTime].include?(cmd_type)
|
28
|
-
item_time = item[cmd].to_time
|
29
|
-
if cmd_search == cmd_search.to_i.to_s
|
30
|
-
Time.new(cmd_search) <= item_time && item_time < Time.new(cmd_search.to_i + 1)
|
31
|
-
else
|
32
|
-
time_str = cmd_search.gsub(/[\._-]/, ' ')
|
33
|
-
input_times = Chronic.parse(time_str, { guess: nil }) || Chronic.parse(cmd_search, { guess: nil })
|
34
|
-
input_times.first <= item_time && item_time < input_times.last
|
35
|
-
end
|
36
|
-
elsif val[1][:type] == :quoted_str
|
37
|
-
regex = /\b#{Regexp.escape(cmd_search)}\b/
|
38
|
-
regex = /\A\Z/ if cmd_search == ''
|
39
|
-
if cmd_search[/(^\W)|(\W$)/]
|
40
|
-
head_border = '(?<=^|[^:+\w])'
|
41
|
-
tail_border = '(?=$|[^:+\w])'
|
42
|
-
regex = Regexp.new(head_border + Regexp.escape(cmd_search) + tail_border)
|
43
|
-
end
|
44
|
-
item[cmd][regex]
|
45
|
-
else
|
46
|
-
item[cmd].to_s[/#{Regexp.escape(cmd_search)}/i]
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def compare_check(item, node, command_types)
|
51
|
-
children = node[:value]
|
52
|
-
cmd = children.find { |c| command_types[c[:value].to_sym] }
|
53
|
-
raw_cmd_type = [command_types[cmd[:value].to_sym]].flatten
|
54
|
-
cmd_type = (raw_cmd_type - [:allow_existence_boolean]).first
|
55
|
-
|
56
|
-
args = children.map do |child|
|
57
|
-
child_val = child[:value]
|
58
|
-
item_val = item[child_val.to_s] || item[child_val.to_sym]
|
59
|
-
item_val ||= child_val unless child == cmd
|
60
|
-
next unless item_val
|
61
|
-
next item_val.to_time if child == cmd && [Date, Time, DateTime].include?(cmd_type)
|
62
|
-
next item_val if child == cmd
|
63
|
-
if [Date, Time, DateTime].include?(cmd_type)
|
64
|
-
date_start_map = {
|
65
|
-
'<' => :start,
|
66
|
-
'>' => :end,
|
67
|
-
'<=' => :end,
|
68
|
-
'>=' => :start
|
69
|
-
}
|
70
|
-
date_pick = date_start_map[node[:nest_op]]
|
71
|
-
time_str = item_val.gsub(/[\._-]/, ' ')
|
72
|
-
next Time.new(time_str) if time_str == time_str.to_i.to_s
|
73
|
-
|
74
|
-
date = Chronic.parse(time_str, { guess: nil }) || Chronic.parse(item_val, { guess: nil })
|
75
|
-
if date_pick == :start
|
76
|
-
date.first
|
77
|
-
else
|
78
|
-
date.last
|
79
|
-
end
|
80
|
-
else
|
81
|
-
item_val
|
82
|
-
end
|
83
|
-
end
|
84
|
-
return unless args.all?
|
85
|
-
fn = node[:nest_op].to_sym.to_proc
|
86
|
-
fn.call(*args.map(&:to_f))
|
87
|
-
end
|
88
|
-
|
89
|
-
def check(item, ast, fields, command_types)
|
90
|
-
field_vals = fields.map { |x| item[x] || item[x.to_s] || item[x.to_sym] }.compact
|
91
|
-
ast_array = ast.is_a?(Array) ? ast : [ast]
|
92
|
-
ast_array.all? do |node|
|
93
|
-
val = node[:value]
|
94
|
-
case node[:nest_type]
|
95
|
-
when nil
|
96
|
-
if node[:type] == :quoted_str
|
97
|
-
regex = /\b#{Regexp.escape(val)}\b/
|
98
|
-
if val[/(^\W)|(\W$)/]
|
99
|
-
head_border = '(?<=^|[^:+\w])'
|
100
|
-
tail_border = '(?=$|[^:+\w])'
|
101
|
-
regex = Regexp.new(head_border + Regexp.escape(val) + tail_border)
|
102
|
-
end
|
103
|
-
field_vals.any? { |x| x.to_s[regex] }
|
104
|
-
else
|
105
|
-
field_vals.any? { |x| x.to_s[/#{Regexp.escape(val)}/i] }
|
106
|
-
end
|
107
|
-
when :colon
|
108
|
-
command_check(item, val, command_types)
|
109
|
-
when :compare
|
110
|
-
compare_check(item, node, command_types)
|
111
|
-
when :pipe
|
112
|
-
val.any? { |v| check(item, v, fields, command_types) }
|
113
|
-
when :minus
|
114
|
-
!val.all? { |v| check(item, v, fields, command_types) }
|
115
|
-
when :paren
|
116
|
-
val.all? { |v| check(item, v, fields, command_types) }
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def build_query(ast, fields, command_types = {})
|
122
|
-
proc { |x| check(x, ast, fields, command_types) }
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
@@ -1,229 +0,0 @@
|
|
1
|
-
require('chronic')
|
2
|
-
|
3
|
-
module CommandSearch
|
4
|
-
module Mongoer
|
5
|
-
module_function
|
6
|
-
|
7
|
-
def numeric_field?(field, command_types)
|
8
|
-
type = command_types[field.to_sym]
|
9
|
-
if type.is_a?(Array)
|
10
|
-
type = (type - [:allow_existence_boolean]).first
|
11
|
-
end
|
12
|
-
[Numeric, Integer].include?(type)
|
13
|
-
end
|
14
|
-
|
15
|
-
def build_str_regex(raw, type)
|
16
|
-
str = Regexp.escape(raw)
|
17
|
-
return /#{str}/i unless type == :quoted_str
|
18
|
-
return '' if raw == ''
|
19
|
-
return /\b#{str}\b/ unless raw[/(^\W)|(\W$)/]
|
20
|
-
border_a = '(^|\s|[^:+\w])'
|
21
|
-
border_b = '($|\s|[^:+\w])'
|
22
|
-
Regexp.new(border_a + str + border_b)
|
23
|
-
end
|
24
|
-
|
25
|
-
def build_search(ast_node, fields, command_types)
|
26
|
-
str = ast_node[:value] || ''
|
27
|
-
fields = [fields] unless fields.is_a?(Array)
|
28
|
-
regex = build_str_regex(str, ast_node[:type])
|
29
|
-
|
30
|
-
forms = fields.map do |field|
|
31
|
-
if numeric_field?(field, command_types)
|
32
|
-
{ field => str }
|
33
|
-
else
|
34
|
-
{ field => regex }
|
35
|
-
end
|
36
|
-
end
|
37
|
-
return forms if forms.count < 2
|
38
|
-
{ '$or' => forms }
|
39
|
-
end
|
40
|
-
|
41
|
-
def is_bool_str?(str, search_type)
|
42
|
-
search_type != :quoted_str && str[/\Atrue\Z|\Afalse\Z/i]
|
43
|
-
end
|
44
|
-
|
45
|
-
def make_boolean(str)
|
46
|
-
str[0] == 't'
|
47
|
-
end
|
48
|
-
|
49
|
-
def build_time_command(key, val)
|
50
|
-
time_str = val.tr('_.-', ' ')
|
51
|
-
if time_str == time_str.to_i.to_s
|
52
|
-
date_a = Time.new(time_str)
|
53
|
-
date_b = Time.new(time_str.to_i + 1).yesterday
|
54
|
-
else
|
55
|
-
date = Chronic.parse(time_str, guess: nil) || Chronic.parse(val, guess: nil)
|
56
|
-
return [{ CommandSeachDummyDate: true }, { CommandSeachDummyDate: false }] unless date
|
57
|
-
date_a = date.begin
|
58
|
-
date_b = date.end
|
59
|
-
end
|
60
|
-
[
|
61
|
-
{ key => { '$gte' => date_a } },
|
62
|
-
{ key => { '$lte' => date_b } }
|
63
|
-
]
|
64
|
-
end
|
65
|
-
|
66
|
-
def build_command(ast_node, command_types)
|
67
|
-
(field_node, search_node) = ast_node[:value]
|
68
|
-
key = field_node[:value]
|
69
|
-
raw_type = command_types[key.to_sym]
|
70
|
-
type = raw_type
|
71
|
-
|
72
|
-
raw_val = search_node[:value]
|
73
|
-
search_type = search_node[:type]
|
74
|
-
|
75
|
-
if raw_type.is_a?(Array)
|
76
|
-
type = (raw_type - [:allow_existence_boolean]).first
|
77
|
-
is_bool = raw_type.include?(:allow_existence_boolean) && is_bool_str?(raw_val, search_type)
|
78
|
-
else
|
79
|
-
type = raw_type
|
80
|
-
is_bool = false
|
81
|
-
end
|
82
|
-
|
83
|
-
if type == Boolean
|
84
|
-
bool = make_boolean(raw_val)
|
85
|
-
val = [
|
86
|
-
{ key => { '$exists' => true } },
|
87
|
-
{ key => { '$ne' => !bool } }
|
88
|
-
]
|
89
|
-
key = '$and'
|
90
|
-
elsif is_bool
|
91
|
-
# These queries return true for empty arrays.
|
92
|
-
bool = make_boolean(raw_val)
|
93
|
-
if bool
|
94
|
-
val = [
|
95
|
-
{ key => { '$exists' => true } },
|
96
|
-
{ key => { '$ne' => false } }
|
97
|
-
]
|
98
|
-
key = '$and'
|
99
|
-
else
|
100
|
-
val = { '$exists' => false }
|
101
|
-
end
|
102
|
-
elsif type == String
|
103
|
-
val = build_str_regex(raw_val, search_type)
|
104
|
-
elsif [Numeric, Integer].include?(type)
|
105
|
-
val = raw_val
|
106
|
-
elsif [Date, Time, DateTime].include?(type)
|
107
|
-
return build_time_command(key, raw_val)
|
108
|
-
end
|
109
|
-
{ key => val }
|
110
|
-
end
|
111
|
-
|
112
|
-
def build_compare(ast_node, command_types)
|
113
|
-
flip_ops = {
|
114
|
-
'<' => '>',
|
115
|
-
'>' => '<',
|
116
|
-
'<=' => '>=',
|
117
|
-
'>=' => '<='
|
118
|
-
}
|
119
|
-
mongo_op_map = {
|
120
|
-
'<' => '$lt',
|
121
|
-
'>' => '$gt',
|
122
|
-
'<=' => '$lte',
|
123
|
-
'>=' => '$gte'
|
124
|
-
}
|
125
|
-
|
126
|
-
keys = command_types.keys
|
127
|
-
(first_node, last_node) = ast_node[:value]
|
128
|
-
key = first_node[:value]
|
129
|
-
val = last_node[:value]
|
130
|
-
op = ast_node[:nest_op]
|
131
|
-
|
132
|
-
if keys.include?(val.to_sym)
|
133
|
-
(key, val) = [val, key]
|
134
|
-
op = flip_ops[op]
|
135
|
-
end
|
136
|
-
|
137
|
-
mongo_op = mongo_op_map[op]
|
138
|
-
raw_type = command_types[key.to_sym]
|
139
|
-
|
140
|
-
if raw_type.is_a?(Array)
|
141
|
-
type = (raw_type - [:allow_existence_boolean]).first
|
142
|
-
else
|
143
|
-
type = raw_type
|
144
|
-
end
|
145
|
-
|
146
|
-
if command_types[val.to_sym]
|
147
|
-
val = '$' + val
|
148
|
-
key = '$' + key
|
149
|
-
val = [key, val]
|
150
|
-
key = '$expr'
|
151
|
-
elsif [Date, Time, DateTime].include?(type)
|
152
|
-
# foo < day | day.start
|
153
|
-
# foo <= day | day.end
|
154
|
-
# foo > day | day.end
|
155
|
-
# foo >= day | day.start
|
156
|
-
date_start_map = {
|
157
|
-
'<' => :start,
|
158
|
-
'>' => :end,
|
159
|
-
'<=' => :end,
|
160
|
-
'>=' => :start
|
161
|
-
}
|
162
|
-
date_pick = date_start_map[op]
|
163
|
-
time_str = val.tr('_.-', ' ')
|
164
|
-
|
165
|
-
if time_str == time_str.to_i.to_s
|
166
|
-
date = [Time.new(time_str), Time.new(time_str.to_i + 1).yesterday]
|
167
|
-
else
|
168
|
-
date = Chronic.parse(time_str, guess: nil) || Chronic.parse(val, guess: nil)
|
169
|
-
end
|
170
|
-
|
171
|
-
date = date || []
|
172
|
-
|
173
|
-
if date_pick == :start
|
174
|
-
val = date.first
|
175
|
-
elsif date_pick == :end
|
176
|
-
val = date.last
|
177
|
-
end
|
178
|
-
end
|
179
|
-
{ key => { mongo_op => val } }
|
180
|
-
end
|
181
|
-
|
182
|
-
def build_searches!(ast, fields, command_types)
|
183
|
-
ast.map! do |x|
|
184
|
-
type = x[:nest_type]
|
185
|
-
if type == :colon
|
186
|
-
build_command(x, command_types)
|
187
|
-
elsif type == :compare
|
188
|
-
build_compare(x, command_types)
|
189
|
-
elsif [:paren, :pipe, :minus].include?(type)
|
190
|
-
build_searches!(x[:value], fields, command_types)
|
191
|
-
x
|
192
|
-
else
|
193
|
-
build_search(x, fields, command_types)
|
194
|
-
end
|
195
|
-
end
|
196
|
-
ast.flatten!
|
197
|
-
end
|
198
|
-
|
199
|
-
def build_tree!(ast)
|
200
|
-
mongo_types = { paren: '$and', pipe: '$or', minus: '$nor' }
|
201
|
-
ast.each do |node|
|
202
|
-
next node unless node[:nest_type]
|
203
|
-
build_tree!(node[:value])
|
204
|
-
key = mongo_types[node[:nest_type]]
|
205
|
-
if key == '$nor' && node[:value].count > 1
|
206
|
-
node[key] = [{ '$and' => node[:value] }]
|
207
|
-
else
|
208
|
-
node[key] = node[:value]
|
209
|
-
end
|
210
|
-
node['$or'].map! { |x| x['$or'] || x }.flatten! if node['$or']
|
211
|
-
node['$nor'].map! { |x| x['$or'] || x }.flatten! if node['$nor']
|
212
|
-
node.delete(:nest_type)
|
213
|
-
node.delete(:nest_op)
|
214
|
-
node.delete(:value)
|
215
|
-
node.delete(:type)
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
def build_query(ast, fields, command_types)
|
220
|
-
out = ast
|
221
|
-
build_searches!(out, fields, command_types)
|
222
|
-
build_tree!(out)
|
223
|
-
out = {} if out == []
|
224
|
-
out = out.first if out.count == 1
|
225
|
-
out = { '$and' => out } if out.count > 1
|
226
|
-
out
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|