command_search 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/command_search.rb +42 -8
- data/lib/command_search/backends/mysql.rb +110 -0
- data/lib/command_search/backends/mysql_v5.rb +108 -0
- data/lib/command_search/backends/postgres.rb +108 -0
- data/lib/command_search/backends/sqlite.rb +109 -0
- data/lib/command_search/normalizer.rb +15 -9
- data/lib/command_search/parser.rb +82 -73
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1d615f992c4521f18821e9b3ee2d96fb800095abeadf549f59b8c24fb6419b6
|
4
|
+
data.tar.gz: df4378e2ab56dc1ee3299582f8a9aed11d4d64d5dd1d3d6d38a0d208a0d48f1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5707ed405d3f6133a6437821b5df18a076b67c299334a226639d8d9e62486f3f385a50907a2bcc57c63a86280125745f3a00975b880086539df899b1106a5acb
|
7
|
+
data.tar.gz: 312e172cc413d7f0d2efb127fd7858454b383b6eed72d1102a416f8d408eefaa831247caf5b27c67ae4959b7ac50871ccf5b05d6eb4abaa6cc6dea40d61425c7
|
data/lib/command_search.rb
CHANGED
@@ -6,28 +6,62 @@ load(__dir__ + '/command_search/optimizer.rb')
|
|
6
6
|
|
7
7
|
load(__dir__ + '/command_search/backends/memory.rb')
|
8
8
|
load(__dir__ + '/command_search/backends/mongoer.rb')
|
9
|
+
load(__dir__ + '/command_search/backends/postgres.rb')
|
10
|
+
load(__dir__ + '/command_search/backends/sqlite.rb')
|
11
|
+
load(__dir__ + '/command_search/backends/mysql.rb')
|
12
|
+
load(__dir__ + '/command_search/backends/mysql_v5.rb')
|
9
13
|
|
10
14
|
class Boolean; end
|
11
15
|
|
12
16
|
module CommandSearch
|
13
17
|
module_function
|
14
18
|
|
15
|
-
def
|
19
|
+
def build(type, query, options)
|
16
20
|
aliases = options[:aliases] || {}
|
17
21
|
fields = options[:fields] || {}
|
18
|
-
|
19
22
|
aliased_query = Aliaser.alias(query, aliases)
|
20
23
|
ast = Lexer.lex(aliased_query)
|
21
|
-
|
22
24
|
Parser.parse!(ast)
|
23
25
|
Optimizer.optimize!(ast)
|
26
|
+
if type == :postgres
|
27
|
+
Normalizer.normalize!(ast, fields, false)
|
28
|
+
return Postgres.build_query(ast)
|
29
|
+
end
|
30
|
+
if type == :sqlite
|
31
|
+
Normalizer.normalize!(ast, fields, false)
|
32
|
+
return Sqlite.build_query(ast)
|
33
|
+
end
|
34
|
+
if type == :mysql
|
35
|
+
Normalizer.normalize!(ast, fields, false)
|
36
|
+
return Mysql.build_query(ast)
|
37
|
+
end
|
38
|
+
if type == :mysqlV5
|
39
|
+
Normalizer.normalize!(ast, fields, false)
|
40
|
+
return MysqlV5.build_query(ast)
|
41
|
+
end
|
24
42
|
Normalizer.normalize!(ast, fields)
|
43
|
+
return Mongoer.build_query(ast) if type == :mongo
|
44
|
+
ast
|
45
|
+
end
|
25
46
|
|
26
|
-
|
27
|
-
|
28
|
-
|
47
|
+
def search(source, query, options)
|
48
|
+
if source.respond_to?(:mongo_client)
|
49
|
+
ast = CommandSearch.build(:mongo, query, options)
|
50
|
+
return source.where(ast)
|
29
51
|
end
|
30
|
-
|
31
|
-
|
52
|
+
if source.respond_to?(:postgresql_connection)
|
53
|
+
ast = CommandSearch.build(:postgres, query, options)
|
54
|
+
return source.where(ast)
|
55
|
+
end
|
56
|
+
if source.respond_to?(:sqlite3_connection)
|
57
|
+
ast = CommandSearch.build(:sqlite, query, options)
|
58
|
+
return source.where(ast)
|
59
|
+
end
|
60
|
+
if source.respond_to?(:mysql2_connection)
|
61
|
+
ast = CommandSearch.build(:mysql, query, options)
|
62
|
+
return source.where(ast)
|
63
|
+
end
|
64
|
+
ast = CommandSearch.build(:other, query, options)
|
65
|
+
source.select { |x| CommandSearch::Memory.check(x, ast) }
|
32
66
|
end
|
33
67
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# NOTE: This module supports does not support MariaDB or eariler Mysql versions than 8.0, due to
|
2
|
+
# changes in how word breaks are handled in regexes.
|
3
|
+
|
4
|
+
module CommandSearch
|
5
|
+
module Mysql
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def quote_string(str)
|
9
|
+
# activerecord/lib/active_record/connection_adapters/abstract/quoting.rb:62
|
10
|
+
str.gsub('\\', '\&\&').gsub("'", "''")
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_quoted_regex(str)
|
14
|
+
str = Regexp.escape(str)
|
15
|
+
str = quote_string(str)
|
16
|
+
if str[/(^\W)|(\W$)/]
|
17
|
+
head_border = '(^|[^:+[[:alnum:]]])'
|
18
|
+
tail_border = '($|[^:+[[:alnum:]]])'
|
19
|
+
return head_border + str + tail_border
|
20
|
+
end
|
21
|
+
'\\\\b' + str + '\\\\b'
|
22
|
+
end
|
23
|
+
|
24
|
+
def command_search(node)
|
25
|
+
field_node = node[:value].first
|
26
|
+
field = field_node[:value]
|
27
|
+
search_node = node[:value].last
|
28
|
+
val = search_node[:value]
|
29
|
+
type = search_node[:type]
|
30
|
+
return '0 = 1' if field == '__CommandSearch_dummy_key__'
|
31
|
+
if type == Boolean || type == :existence
|
32
|
+
if val
|
33
|
+
return "(NOT ((#{field} IS NULL) OR (#{field} LIKE '0')))"
|
34
|
+
end
|
35
|
+
return "((#{field} IS NULL) OR (#{field} LIKE '0'))"
|
36
|
+
end
|
37
|
+
if type == Time
|
38
|
+
return '0 = 1' unless val
|
39
|
+
return "
|
40
|
+
(
|
41
|
+
(#{field} > '#{val[0] - 1}') AND
|
42
|
+
(#{field} <= '#{val[1] - 1}') AND
|
43
|
+
(#{field} IS NOT NULL)
|
44
|
+
)
|
45
|
+
"
|
46
|
+
end
|
47
|
+
if type == :quote
|
48
|
+
val = build_quoted_regex(val)
|
49
|
+
return "(REGEXP_LIKE(#{field}, '#{val}', 'c') AND (#{field} IS NOT NULL))"
|
50
|
+
end
|
51
|
+
if type == :number
|
52
|
+
op = '='
|
53
|
+
else # type == :str
|
54
|
+
op = 'LIKE'
|
55
|
+
val = quote_string(val)
|
56
|
+
val.gsub!('%', '\%')
|
57
|
+
val.gsub!('_', '\_')
|
58
|
+
val = "'%#{val}%'"
|
59
|
+
end
|
60
|
+
"((#{field} #{op} #{val}) AND (#{field} IS NOT NULL))"
|
61
|
+
end
|
62
|
+
|
63
|
+
def compare_search(node)
|
64
|
+
field_node = node[:value].first
|
65
|
+
field = field_node[:value]
|
66
|
+
search_node = node[:value].last
|
67
|
+
val = search_node[:value]
|
68
|
+
type = search_node[:type]
|
69
|
+
op = node[:nest_op]
|
70
|
+
if node[:compare_across_fields]
|
71
|
+
"
|
72
|
+
(
|
73
|
+
(#{field} #{op} #{val}) AND
|
74
|
+
(#{field} IS NOT NULL) AND
|
75
|
+
(#{val} IS NOT NULL)
|
76
|
+
)
|
77
|
+
"
|
78
|
+
elsif type == Time && val
|
79
|
+
"(#{field} #{op} '#{val}') AND (#{field} IS NOT NULL)"
|
80
|
+
elsif val.is_a?(Numeric) || val == val.to_i.to_s || val == val.to_f.to_s
|
81
|
+
"(#{field} #{op} #{val}) AND (#{field} IS NOT NULL)"
|
82
|
+
else
|
83
|
+
'0 = 1'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_query(ast)
|
88
|
+
out = []
|
89
|
+
ast = [ast] unless ast.is_a?(Array)
|
90
|
+
ast.each do |node|
|
91
|
+
type = node[:type]
|
92
|
+
if type == :colon
|
93
|
+
out.push(command_search(node))
|
94
|
+
elsif type == :compare
|
95
|
+
out.push(compare_search(node))
|
96
|
+
elsif type == :and
|
97
|
+
out.push(build_query(node[:value]))
|
98
|
+
elsif type == :or
|
99
|
+
clauses = node[:value].map { |x| build_query(x) }
|
100
|
+
clause = clauses.join(' OR ')
|
101
|
+
out.push("(#{clause})")
|
102
|
+
elsif type == :not
|
103
|
+
clause = build_query(node[:value])
|
104
|
+
out.push("NOT (#{clause})")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
out.join(' AND ')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# NOTE: This module supports MariaDB and MySql 5 with its distinct word break regex rules.
|
2
|
+
|
3
|
+
module CommandSearch
|
4
|
+
module MysqlV5
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def quote_string(str)
|
8
|
+
# activerecord/lib/active_record/connection_adapters/abstract/quoting.rb:62
|
9
|
+
str.gsub('\\', '\&\&').gsub("'", "''")
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_quoted_regex(str)
|
13
|
+
str = Regexp.escape(str)
|
14
|
+
str = quote_string(str)
|
15
|
+
if str[/(^\W)|(\W$)/]
|
16
|
+
head_border = '(^|[[:<:]]|\\\\()'
|
17
|
+
tail_border = '($|[[:>:]]|\\\\))'
|
18
|
+
return head_border + str + tail_border
|
19
|
+
end
|
20
|
+
'[[:<:]]' + str + '[[:>:]]'
|
21
|
+
end
|
22
|
+
|
23
|
+
def command_search(node)
|
24
|
+
field_node = node[:value].first
|
25
|
+
field = field_node[:value]
|
26
|
+
search_node = node[:value].last
|
27
|
+
val = search_node[:value]
|
28
|
+
type = search_node[:type]
|
29
|
+
return '0 = 1' if field == '__CommandSearch_dummy_key__'
|
30
|
+
if type == Boolean || type == :existence
|
31
|
+
if val
|
32
|
+
return "(NOT ((#{field} IS NULL) OR (#{field} LIKE '0')))"
|
33
|
+
end
|
34
|
+
return "((#{field} IS NULL) OR (#{field} LIKE '0'))"
|
35
|
+
end
|
36
|
+
if type == Time
|
37
|
+
return '0 = 1' unless val
|
38
|
+
return "
|
39
|
+
(
|
40
|
+
(#{field} > '#{val[0] - 1}') AND
|
41
|
+
(#{field} <= '#{val[1] - 1}') AND
|
42
|
+
(#{field} IS NOT NULL)
|
43
|
+
)
|
44
|
+
"
|
45
|
+
end
|
46
|
+
if type == :quote
|
47
|
+
op = 'RLIKE BINARY'
|
48
|
+
val = "'#{build_quoted_regex(val)}'"
|
49
|
+
elsif type == :number
|
50
|
+
op = '='
|
51
|
+
else # type == :str
|
52
|
+
op = 'LIKE'
|
53
|
+
val = quote_string(val)
|
54
|
+
val.gsub!('%', '\%')
|
55
|
+
val.gsub!('_', '\_')
|
56
|
+
val = "'%#{val}%'"
|
57
|
+
end
|
58
|
+
"((#{field} #{op} #{val}) AND (#{field} IS NOT NULL))"
|
59
|
+
end
|
60
|
+
|
61
|
+
def compare_search(node)
|
62
|
+
field_node = node[:value].first
|
63
|
+
field = field_node[:value]
|
64
|
+
search_node = node[:value].last
|
65
|
+
val = search_node[:value]
|
66
|
+
type = search_node[:type]
|
67
|
+
op = node[:nest_op]
|
68
|
+
if node[:compare_across_fields]
|
69
|
+
"
|
70
|
+
(
|
71
|
+
(#{field} #{op} #{val}) AND
|
72
|
+
(#{field} IS NOT NULL) AND
|
73
|
+
(#{val} IS NOT NULL)
|
74
|
+
)
|
75
|
+
"
|
76
|
+
elsif type == Time && val
|
77
|
+
"(#{field} #{op} '#{val}') AND (#{field} IS NOT NULL)"
|
78
|
+
elsif val.is_a?(Numeric) || val == val.to_i.to_s || val == val.to_f.to_s
|
79
|
+
"(#{field} #{op} #{val}) AND (#{field} IS NOT NULL)"
|
80
|
+
else
|
81
|
+
'0 = 1'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_query(ast)
|
86
|
+
out = []
|
87
|
+
ast = [ast] unless ast.is_a?(Array)
|
88
|
+
ast.each do |node|
|
89
|
+
type = node[:type]
|
90
|
+
if type == :colon
|
91
|
+
out.push(command_search(node))
|
92
|
+
elsif type == :compare
|
93
|
+
out.push(compare_search(node))
|
94
|
+
elsif type == :and
|
95
|
+
out.push(build_query(node[:value]))
|
96
|
+
elsif type == :or
|
97
|
+
clauses = node[:value].map { |x| build_query(x) }
|
98
|
+
clause = clauses.join(' OR ')
|
99
|
+
out.push("(#{clause})")
|
100
|
+
elsif type == :not
|
101
|
+
clause = build_query(node[:value])
|
102
|
+
out.push("NOT (#{clause})")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
out.join(' AND ')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module CommandSearch
|
2
|
+
module Postgres
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def quote_string(str)
|
6
|
+
# activerecord/lib/active_record/connection_adapters/abstract/quoting.rb:62
|
7
|
+
str.gsub('\\', '\&\&').gsub("'", "''")
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_quoted_regex(input)
|
11
|
+
str = quote_string(input)
|
12
|
+
str = Regexp.escape(str)
|
13
|
+
if str[/(^\W)|(\W$)/]
|
14
|
+
head_border = '(^|[^:+\w])'
|
15
|
+
tail_border = '($|[^:+\w])'
|
16
|
+
return head_border + str + tail_border
|
17
|
+
end
|
18
|
+
'\m' + str + '\y'
|
19
|
+
end
|
20
|
+
|
21
|
+
def command_search(node)
|
22
|
+
field_node = node[:value].first
|
23
|
+
field = field_node[:value]
|
24
|
+
search_node = node[:value].last
|
25
|
+
val = search_node[:value]
|
26
|
+
type = search_node[:type]
|
27
|
+
return '0 = 1' if field == '__CommandSearch_dummy_key__'
|
28
|
+
if type == Boolean || type == :existence
|
29
|
+
false_val = "'f'"
|
30
|
+
false_val = 0 if field_node[:field_type] == Numeric
|
31
|
+
if val
|
32
|
+
return "NOT ((#{field} = #{false_val}) OR (#{field} IS NULL))"
|
33
|
+
end
|
34
|
+
return "((#{field} = #{false_val}) OR (#{field} IS NULL))"
|
35
|
+
end
|
36
|
+
if type == Time
|
37
|
+
return '0 = 1' unless val
|
38
|
+
return "
|
39
|
+
(
|
40
|
+
(#{field} >= '#{val[0]}') AND
|
41
|
+
(#{field} < '#{val[1]}') AND
|
42
|
+
(#{field} IS NOT NULL)
|
43
|
+
)
|
44
|
+
"
|
45
|
+
end
|
46
|
+
if type == :quote
|
47
|
+
op = '~'
|
48
|
+
val = "'#{build_quoted_regex(val)}'"
|
49
|
+
elsif type == :str
|
50
|
+
op = '~~*'
|
51
|
+
val = quote_string(val)
|
52
|
+
val.gsub!('%', '\%')
|
53
|
+
val.gsub!('_', '\_')
|
54
|
+
val = "'%#{val}%'"
|
55
|
+
elsif type == :number
|
56
|
+
op = '='
|
57
|
+
end
|
58
|
+
"(#{field} #{op} #{val}) AND (#{field} IS NOT NULL)"
|
59
|
+
end
|
60
|
+
|
61
|
+
def compare_search(node)
|
62
|
+
field_node = node[:value].first
|
63
|
+
field = field_node[:value]
|
64
|
+
search_node = node[:value].last
|
65
|
+
val = search_node[:value]
|
66
|
+
type = search_node[:type]
|
67
|
+
op = node[:nest_op]
|
68
|
+
if node[:compare_across_fields]
|
69
|
+
"
|
70
|
+
(
|
71
|
+
(#{field} #{op} #{val}) AND
|
72
|
+
(#{field} IS NOT NULL) AND
|
73
|
+
(#{val} IS NOT NULL)
|
74
|
+
)
|
75
|
+
"
|
76
|
+
elsif type == Time && val
|
77
|
+
"(#{field} #{op} '#{val}') AND (#{field} IS NOT NULL)"
|
78
|
+
elsif val.is_a?(Numeric) || val == val.to_i.to_s || val == val.to_f.to_s
|
79
|
+
"(#{field} #{op} #{val}) AND (#{field} IS NOT NULL)"
|
80
|
+
else
|
81
|
+
'0 = 1'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_query(ast)
|
86
|
+
out = []
|
87
|
+
ast = [ast] unless ast.is_a?(Array)
|
88
|
+
ast.each do |node|
|
89
|
+
type = node[:type]
|
90
|
+
if type == :colon
|
91
|
+
out.push(command_search(node))
|
92
|
+
elsif type == :compare
|
93
|
+
out.push(compare_search(node))
|
94
|
+
elsif type == :and
|
95
|
+
out.push(build_query(node[:value]))
|
96
|
+
elsif type == :or
|
97
|
+
clauses = node[:value].map { |x| build_query(x) }
|
98
|
+
clause = clauses.join(' OR ')
|
99
|
+
out.push("(#{clause})")
|
100
|
+
elsif type == :not
|
101
|
+
clause = build_query(node[:value])
|
102
|
+
out.push("NOT (#{clause})")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
out.join(' AND ')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module CommandSearch
|
2
|
+
module Sqlite
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def quote_string(str)
|
6
|
+
# activerecord/lib/active_record/connection_adapters/abstract/quoting.rb:62
|
7
|
+
str.gsub('\\', '\&\&').gsub("'", "''")
|
8
|
+
end
|
9
|
+
|
10
|
+
def command_search(node)
|
11
|
+
field_node = node[:value].first
|
12
|
+
field = field_node[:value]
|
13
|
+
search_node = node[:value].last
|
14
|
+
val = search_node[:value]
|
15
|
+
type = search_node[:type]
|
16
|
+
return '0 = 1' if field == '__CommandSearch_dummy_key__'
|
17
|
+
if type == Boolean || type == :existence
|
18
|
+
if val
|
19
|
+
return "NOT ((#{field} = 0) OR (#{field} IS NULL))"
|
20
|
+
end
|
21
|
+
return "((#{field} = 0) OR (#{field} IS NULL))"
|
22
|
+
end
|
23
|
+
if type == Time
|
24
|
+
return '0 = 1' unless val
|
25
|
+
return "
|
26
|
+
(
|
27
|
+
(#{field} > '#{val[0] - 1}') AND
|
28
|
+
(#{field} <= '#{val[1] - 1}') AND
|
29
|
+
(#{field} IS NOT NULL)
|
30
|
+
)
|
31
|
+
"
|
32
|
+
end
|
33
|
+
if type == :quote
|
34
|
+
val = quote_string(val)
|
35
|
+
val.gsub!('[', '[[]')
|
36
|
+
val.gsub!('*', '[*]')
|
37
|
+
val.gsub!('?', '[?]')
|
38
|
+
border = '[ .,()?"\'\']'
|
39
|
+
return "
|
40
|
+
(
|
41
|
+
(#{field} IS NOT NULL) AND
|
42
|
+
(
|
43
|
+
(#{field} GLOB '#{val}') OR
|
44
|
+
(#{field} GLOB '*#{border}#{val}#{border}*') OR
|
45
|
+
(#{field} GLOB '*#{border}#{val}') OR
|
46
|
+
(#{field} GLOB '#{val}#{border}*')
|
47
|
+
)
|
48
|
+
)
|
49
|
+
"
|
50
|
+
elsif type == :str
|
51
|
+
op = 'LIKE'
|
52
|
+
val = quote_string(val)
|
53
|
+
val.gsub!('%', '\%')
|
54
|
+
val.gsub!('_', '\_')
|
55
|
+
val = "'%#{val}%' ESCAPE '\\'"
|
56
|
+
elsif type == :number
|
57
|
+
op = '='
|
58
|
+
end
|
59
|
+
"((#{field} #{op} #{val}) AND (#{field} IS NOT NULL))"
|
60
|
+
end
|
61
|
+
|
62
|
+
def compare_search(node)
|
63
|
+
field_node = node[:value].first
|
64
|
+
field = field_node[:value]
|
65
|
+
search_node = node[:value].last
|
66
|
+
val = search_node[:value]
|
67
|
+
type = search_node[:type]
|
68
|
+
op = node[:nest_op]
|
69
|
+
if node[:compare_across_fields]
|
70
|
+
"
|
71
|
+
(
|
72
|
+
(#{field} #{op} #{val}) AND
|
73
|
+
(#{field} IS NOT NULL) AND
|
74
|
+
(#{val} IS NOT NULL)
|
75
|
+
)
|
76
|
+
"
|
77
|
+
elsif type == Time && val
|
78
|
+
"(#{field} #{op} '#{val}') AND (#{field} IS NOT NULL)"
|
79
|
+
elsif val.is_a?(Numeric) || val == val.to_i.to_s || val == val.to_f.to_s
|
80
|
+
"(#{field} #{op} #{val}) AND (#{field} IS NOT NULL)"
|
81
|
+
else
|
82
|
+
'0 = 1'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_query(ast)
|
87
|
+
out = []
|
88
|
+
ast = [ast] unless ast.is_a?(Array)
|
89
|
+
ast.each do |node|
|
90
|
+
type = node[:type]
|
91
|
+
if type == :colon
|
92
|
+
out.push(command_search(node))
|
93
|
+
elsif type == :compare
|
94
|
+
out.push(compare_search(node))
|
95
|
+
elsif type == :and
|
96
|
+
out.push(build_query(node[:value]))
|
97
|
+
elsif type == :or
|
98
|
+
clauses = node[:value].map { |x| build_query(x) }
|
99
|
+
clause = clauses.join(' OR ')
|
100
|
+
out.push("(#{clause})")
|
101
|
+
elsif type == :not
|
102
|
+
clause = build_query(node[:value])
|
103
|
+
out.push("NOT (#{clause})")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
out.join(' AND ')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -53,8 +53,8 @@ module CommandSearch
|
|
53
53
|
str = Regexp.escape(raw)
|
54
54
|
return node[:value] = /#{str}/i unless type == :quote
|
55
55
|
return node[:value] = /\b#{str}\b/ unless raw[/(\A\W)|(\W\Z)/]
|
56
|
-
border_a = '(
|
57
|
-
border_b = '(
|
56
|
+
border_a = '(^|[^:+\w])'
|
57
|
+
border_b = '($|[^:+\w])'
|
58
58
|
node[:value] = Regexp.new(border_a + str + border_b)
|
59
59
|
end
|
60
60
|
|
@@ -96,22 +96,28 @@ module CommandSearch
|
|
96
96
|
{ type: :or, value: new_val }
|
97
97
|
end
|
98
98
|
|
99
|
-
def type_cast!(node, fields)
|
99
|
+
def type_cast!(node, fields, cast_all)
|
100
100
|
(key_node, search_node) = node[:value]
|
101
101
|
key = key_node[:value]
|
102
102
|
field = fields[key.to_sym] || fields[key.to_s]
|
103
103
|
return unless field
|
104
104
|
type = field.is_a?(Class) ? field : field[:type]
|
105
|
+
type = Numeric if type == Integer
|
106
|
+
key_node[:field_type] = type
|
105
107
|
cast_bool!(field, search_node)
|
106
108
|
return cast_time!(node) if [Time, Date, DateTime].include?(type)
|
107
|
-
return cast_numeric!(search_node) if
|
108
|
-
|
109
|
+
return cast_numeric!(search_node) if Numeric == type
|
110
|
+
if cast_all
|
111
|
+
cast_regex!(search_node)
|
112
|
+
else
|
113
|
+
search_node[:type] = :str if search_node[:type] == :number
|
114
|
+
end
|
109
115
|
end
|
110
116
|
|
111
|
-
def normalize!(ast, fields)
|
117
|
+
def normalize!(ast, fields, cast_all = true)
|
112
118
|
ast.map! do |node|
|
113
119
|
if node[:type] == :and || node[:type] == :or || node[:type] == :not
|
114
|
-
normalize!(node[:value], fields)
|
120
|
+
normalize!(node[:value], fields, cast_all)
|
115
121
|
next node
|
116
122
|
end
|
117
123
|
if node[:type] == :colon || node[:type] == :compare
|
@@ -127,9 +133,9 @@ module CommandSearch
|
|
127
133
|
node = split_general_fields(node, fields)
|
128
134
|
end
|
129
135
|
if node[:type] == :or
|
130
|
-
node[:value].each { |x| type_cast!(x, fields) }
|
136
|
+
node[:value].each { |x| type_cast!(x, fields, cast_all) }
|
131
137
|
else
|
132
|
-
type_cast!(node, fields)
|
138
|
+
type_cast!(node, fields, cast_all)
|
133
139
|
end
|
134
140
|
node
|
135
141
|
end
|
@@ -2,127 +2,136 @@ module CommandSearch
|
|
2
2
|
module Parser
|
3
3
|
module_function
|
4
4
|
|
5
|
-
def group_parens!(
|
5
|
+
def group_parens!(ast)
|
6
6
|
i = 0
|
7
7
|
opening_idxs = []
|
8
|
-
while i <
|
9
|
-
next i += 1 unless
|
10
|
-
if
|
8
|
+
while i < ast.length
|
9
|
+
next i += 1 unless ast[i][:type] == :paren
|
10
|
+
if ast[i][:value] == '('
|
11
11
|
opening_idxs.push(i)
|
12
|
-
|
12
|
+
ast.delete_at(i)
|
13
13
|
next
|
14
14
|
end
|
15
|
-
|
15
|
+
ast.delete_at(i)
|
16
16
|
opening = opening_idxs.pop()
|
17
17
|
next unless opening
|
18
|
-
val =
|
18
|
+
val = ast.slice(opening, i - opening)
|
19
19
|
if val.count > 1
|
20
|
-
|
20
|
+
ast[opening..(i - 1)] = { type: :and, value: val }
|
21
21
|
i -= val.length
|
22
22
|
next
|
23
23
|
elsif val.count == 1
|
24
|
-
|
24
|
+
ast[opening] = val.first
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
def cluster_cmds!(
|
29
|
+
def cluster_cmds!(ast)
|
30
30
|
i = 1
|
31
|
-
while i <
|
32
|
-
type =
|
31
|
+
while i < ast.length - 1
|
32
|
+
type = ast[i][:type]
|
33
33
|
next i += 1 unless type == :colon || type == :compare
|
34
|
-
|
34
|
+
ast[(i - 1)..(i + 1)] = {
|
35
35
|
type: type,
|
36
|
-
nest_op:
|
37
|
-
value: [
|
36
|
+
nest_op: ast[i][:value],
|
37
|
+
value: [ast[i - 1], ast[i + 1]]
|
38
38
|
}
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
def cluster_or!(
|
42
|
+
def cluster_or!(ast)
|
43
43
|
i = 0
|
44
|
-
while i <
|
45
|
-
type =
|
46
|
-
cluster_or!(
|
44
|
+
while i < ast.length
|
45
|
+
type = ast[i][:type]
|
46
|
+
cluster_or!(ast[i][:value]) if type == :and || type == :not
|
47
47
|
next i += 1 unless type == :pipe
|
48
|
-
if i == 0 || i ==
|
49
|
-
|
48
|
+
if i == 0 || i == ast.length - 1
|
49
|
+
ast.delete_at(i)
|
50
50
|
next
|
51
51
|
end
|
52
|
-
val = [
|
52
|
+
val = [ast[i - 1], ast[i + 1]]
|
53
53
|
cluster_or!(val)
|
54
|
-
|
54
|
+
ast[i][:type] = :or
|
55
|
+
ast[i][:value] = val
|
56
|
+
ast.delete_at(i + 1)
|
57
|
+
ast.delete_at(i - 1)
|
55
58
|
end
|
56
59
|
end
|
57
60
|
|
58
|
-
def cluster_not!(
|
59
|
-
i =
|
61
|
+
def cluster_not!(ast)
|
62
|
+
i = ast.length
|
60
63
|
while i > 0
|
61
64
|
i -= 1
|
62
|
-
type =
|
63
|
-
cluster_not!(
|
65
|
+
type = ast[i][:type]
|
66
|
+
cluster_not!(ast[i][:value]) if type == :and
|
64
67
|
next unless type == :minus
|
65
|
-
if i ==
|
66
|
-
|
68
|
+
if i == ast.length - 1
|
69
|
+
ast.delete_at(i)
|
67
70
|
next
|
68
71
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
}
|
72
|
+
ast[i][:type] = :not
|
73
|
+
ast[i][:value] = [ast[i + 1]]
|
74
|
+
ast.delete_at(i + 1)
|
73
75
|
end
|
74
76
|
end
|
75
77
|
|
76
|
-
def unchain!(
|
77
|
-
i =
|
78
|
-
while i <
|
79
|
-
left =
|
80
|
-
right =
|
81
|
-
if types.include?(left) && types.include?(right)
|
82
|
-
input.insert(i + 1, input[i + 1].clone())
|
83
|
-
end
|
78
|
+
def unchain!(ast)
|
79
|
+
i = 1
|
80
|
+
while i < ast.length - 3
|
81
|
+
left = ast[i][:type]
|
82
|
+
right = ast[i + 2][:type]
|
84
83
|
i += 1
|
84
|
+
next unless left == :colon || left == :compare
|
85
|
+
next unless right == :colon || right == :compare
|
86
|
+
ast.insert(i, ast[i].clone())
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
input[x][:type] = :str
|
94
|
-
input
|
95
|
-
end
|
90
|
+
def r_merge!(ast, i)
|
91
|
+
ast[i][:type] = :str
|
92
|
+
return unless ast[i + 1] && ast[i + 1][:type] == :str
|
93
|
+
ast[i][:value] = ast[i][:value] + ast[i + 1][:value]
|
94
|
+
ast.delete_at(i + 1)
|
96
95
|
end
|
97
96
|
|
98
|
-
def
|
99
|
-
i =
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
i = 0
|
106
|
-
while i < input.length
|
107
|
-
next i += 1 if ![:compare, :colon].include?(input[i][:type])
|
108
|
-
next i += 1 if i > 0 &&
|
109
|
-
(i < input.count - 1) &&
|
110
|
-
[:str, :number, :quote].include?(input[i - 1][:type]) &&
|
111
|
-
[:str, :number, :quote].include?(input[i + 1][:type])
|
97
|
+
def l_merge!(ast, i)
|
98
|
+
ast[i][:type] = :str
|
99
|
+
return unless ast[i - 1] && ast[i - 1][:type] == :str
|
100
|
+
ast[i][:value] = ast[i - 1][:value] + ast[i][:value]
|
101
|
+
ast.delete_at(i - 1)
|
102
|
+
end
|
112
103
|
|
113
|
-
|
114
|
-
|
104
|
+
def clean!(ast)
|
105
|
+
return unless ast.any?
|
106
|
+
if ast[0][:type] == :colon || ast[0][:type] == :compare
|
107
|
+
r_merge!(ast, 0)
|
108
|
+
end
|
109
|
+
if ast[-1][:type] == :colon || ast[-1][:type] == :compare
|
110
|
+
l_merge!(ast, ast.length - 1)
|
111
|
+
end
|
112
|
+
i = 1
|
113
|
+
while i < ast.length - 1
|
114
|
+
next i += 1 unless ast[i][:type] == :colon || ast[i][:type] == :compare
|
115
|
+
if ast[i + 1][:type] == :minus
|
116
|
+
r_merge!(ast, i + 1)
|
117
|
+
elsif ![:str, :number, :quote].include?(ast[i - 1][:type])
|
118
|
+
r_merge!(ast, i)
|
119
|
+
elsif ![:str, :number, :quote].include?(ast[i + 1][:type])
|
120
|
+
l_merge!(ast, i)
|
121
|
+
else
|
122
|
+
i += 1
|
123
|
+
end
|
115
124
|
end
|
116
125
|
end
|
117
126
|
|
118
|
-
def parse!(
|
119
|
-
|
120
|
-
unchain!(
|
121
|
-
cluster_cmds!(
|
122
|
-
group_parens!(
|
123
|
-
cluster_not!(
|
124
|
-
cluster_or!(
|
125
|
-
|
127
|
+
def parse!(ast)
|
128
|
+
clean!(ast)
|
129
|
+
unchain!(ast)
|
130
|
+
cluster_cmds!(ast)
|
131
|
+
group_parens!(ast)
|
132
|
+
cluster_not!(ast)
|
133
|
+
cluster_or!(ast)
|
134
|
+
ast
|
126
135
|
end
|
127
136
|
end
|
128
137
|
end
|
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.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- zumbalogy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chronic
|
@@ -34,6 +34,10 @@ files:
|
|
34
34
|
- lib/command_search/aliaser.rb
|
35
35
|
- lib/command_search/backends/memory.rb
|
36
36
|
- lib/command_search/backends/mongoer.rb
|
37
|
+
- lib/command_search/backends/mysql.rb
|
38
|
+
- lib/command_search/backends/mysql_v5.rb
|
39
|
+
- lib/command_search/backends/postgres.rb
|
40
|
+
- lib/command_search/backends/sqlite.rb
|
37
41
|
- lib/command_search/lexer.rb
|
38
42
|
- lib/command_search/normalizer.rb
|
39
43
|
- lib/command_search/optimizer.rb
|
@@ -57,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
61
|
- !ruby/object:Gem::Version
|
58
62
|
version: '0'
|
59
63
|
requirements: []
|
60
|
-
rubygems_version: 3.0.
|
64
|
+
rubygems_version: 3.0.8
|
61
65
|
signing_key:
|
62
66
|
specification_version: 4
|
63
67
|
summary: Let users query collections with ease.
|