command_search 0.11.0 → 0.12.1
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 +26 -3
- data/lib/command_search/backends/memory.rb +1 -0
- 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 +24 -10
- data/lib/command_search/backends/sqlite.rb +109 -0
- data/lib/command_search/normalizer.rb +11 -5
- data/lib/command_search/parser.rb +82 -73
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0a659ab861cfea9badba5aa2c4bdf6e8797c5ac820c468bf87f98519040f1c3
|
4
|
+
data.tar.gz: 5d8f4ff47c1b2d7dc917d0e791d27a071943c9d0783968ba831894f67bee9032
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86e941a3b2e15fd637b657c2bf102887b5ea888a783105492edfe5390fc0a908f546f538197b7c3af46cb78e9b838f3a90119c5ac9bbe2abfca795d92dfead86
|
7
|
+
data.tar.gz: 334d7bccc459f47d47f445217ae9dc702f5dbc432b3c20367a48678ea2ac2d698cb55257987097b54e483cc0cbd64beadc5a11f9ff8338a9cca5fd8ab9553316
|
data/lib/command_search.rb
CHANGED
@@ -7,6 +7,9 @@ load(__dir__ + '/command_search/optimizer.rb')
|
|
7
7
|
load(__dir__ + '/command_search/backends/memory.rb')
|
8
8
|
load(__dir__ + '/command_search/backends/mongoer.rb')
|
9
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')
|
10
13
|
|
11
14
|
class Boolean; end
|
12
15
|
|
@@ -21,16 +24,28 @@ module CommandSearch
|
|
21
24
|
Parser.parse!(ast)
|
22
25
|
Optimizer.optimize!(ast)
|
23
26
|
if type == :postgres
|
24
|
-
Normalizer.normalize!(ast, fields
|
27
|
+
Normalizer.normalize!(ast, fields)
|
25
28
|
return Postgres.build_query(ast)
|
26
29
|
end
|
27
|
-
|
30
|
+
if type == :sqlite
|
31
|
+
Normalizer.normalize!(ast, fields)
|
32
|
+
return Sqlite.build_query(ast)
|
33
|
+
end
|
34
|
+
if type == :mysql
|
35
|
+
Normalizer.normalize!(ast, fields)
|
36
|
+
return Mysql.build_query(ast)
|
37
|
+
end
|
38
|
+
if type == :mysqlV5
|
39
|
+
Normalizer.normalize!(ast, fields)
|
40
|
+
return MysqlV5.build_query(ast)
|
41
|
+
end
|
42
|
+
Normalizer.normalize!(ast, fields, true)
|
28
43
|
return Mongoer.build_query(ast) if type == :mongo
|
29
44
|
ast
|
30
45
|
end
|
31
46
|
|
32
47
|
def search(source, query, options)
|
33
|
-
if source.respond_to?(:mongo_client)
|
48
|
+
if source.respond_to?(:mongo_client)
|
34
49
|
ast = CommandSearch.build(:mongo, query, options)
|
35
50
|
return source.where(ast)
|
36
51
|
end
|
@@ -38,6 +53,14 @@ module CommandSearch
|
|
38
53
|
ast = CommandSearch.build(:postgres, query, options)
|
39
54
|
return source.where(ast)
|
40
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
|
41
64
|
ast = CommandSearch.build(:other, query, options)
|
42
65
|
source.select { |x| CommandSearch::Memory.check(x, ast) }
|
43
66
|
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
|
@@ -4,16 +4,15 @@ module CommandSearch
|
|
4
4
|
|
5
5
|
def quote_string(str)
|
6
6
|
# activerecord/lib/active_record/connection_adapters/abstract/quoting.rb:62
|
7
|
-
str.gsub
|
8
|
-
str.gsub!("'", "''")
|
9
|
-
Regexp.escape(str)
|
7
|
+
str.gsub('\\', '\&\&').gsub("'", "''")
|
10
8
|
end
|
11
9
|
|
12
10
|
def build_quoted_regex(input)
|
13
11
|
str = quote_string(input)
|
12
|
+
str = Regexp.escape(str)
|
14
13
|
if str[/(^\W)|(\W$)/]
|
15
|
-
head_border = '(
|
16
|
-
tail_border = '(
|
14
|
+
head_border = '(^|[^:+\w])'
|
15
|
+
tail_border = '($|[^:+\w])'
|
17
16
|
return head_border + str + tail_border
|
18
17
|
end
|
19
18
|
'\m' + str + '\y'
|
@@ -36,14 +35,23 @@ module CommandSearch
|
|
36
35
|
end
|
37
36
|
if type == Time
|
38
37
|
return '0 = 1' unless val
|
39
|
-
return "
|
38
|
+
return "
|
39
|
+
(
|
40
|
+
(#{field} >= '#{val[0]}') AND
|
41
|
+
(#{field} < '#{val[1]}') AND
|
42
|
+
(#{field} IS NOT NULL)
|
43
|
+
)
|
44
|
+
"
|
40
45
|
end
|
41
46
|
if type == :quote
|
42
47
|
op = '~'
|
43
|
-
val =
|
48
|
+
val = "'#{build_quoted_regex(val)}'"
|
44
49
|
elsif type == :str
|
45
|
-
op = '
|
46
|
-
val =
|
50
|
+
op = '~~*'
|
51
|
+
val = quote_string(val)
|
52
|
+
val.gsub!('%', '\%')
|
53
|
+
val.gsub!('_', '\_')
|
54
|
+
val = "'%#{val}%'"
|
47
55
|
elsif type == :number
|
48
56
|
op = '='
|
49
57
|
end
|
@@ -58,7 +66,13 @@ module CommandSearch
|
|
58
66
|
type = search_node[:type]
|
59
67
|
op = node[:nest_op]
|
60
68
|
if node[:compare_across_fields]
|
61
|
-
"
|
69
|
+
"
|
70
|
+
(
|
71
|
+
(#{field} #{op} #{val}) AND
|
72
|
+
(#{field} IS NOT NULL) AND
|
73
|
+
(#{val} IS NOT NULL)
|
74
|
+
)
|
75
|
+
"
|
62
76
|
elsif type == Time && val
|
63
77
|
"(#{field} #{op} '#{val}') AND (#{field} IS NOT NULL)"
|
64
78
|
elsif val.is_a?(Numeric) || val == val.to_i.to_s || val == val.to_f.to_s
|
@@ -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
|
@@ -31,8 +31,13 @@ module CommandSearch
|
|
31
31
|
if times
|
32
32
|
search_node[:value] = [times.first, times.last]
|
33
33
|
else
|
34
|
-
|
35
|
-
|
34
|
+
time_parsed = Time.parse(str) rescue nil
|
35
|
+
if time_parsed
|
36
|
+
search_node[:value] = [time_parsed, time_parsed + 1]
|
37
|
+
else
|
38
|
+
search_node[:value] = nil
|
39
|
+
return
|
40
|
+
end
|
36
41
|
end
|
37
42
|
end
|
38
43
|
return unless node[:type] == :compare
|
@@ -53,8 +58,8 @@ module CommandSearch
|
|
53
58
|
str = Regexp.escape(raw)
|
54
59
|
return node[:value] = /#{str}/i unless type == :quote
|
55
60
|
return node[:value] = /\b#{str}\b/ unless raw[/(\A\W)|(\W\Z)/]
|
56
|
-
border_a = '(
|
57
|
-
border_b = '(
|
61
|
+
border_a = '(^|[^:+\w])'
|
62
|
+
border_b = '($|[^:+\w])'
|
58
63
|
node[:value] = Regexp.new(border_a + str + border_b)
|
59
64
|
end
|
60
65
|
|
@@ -114,7 +119,8 @@ module CommandSearch
|
|
114
119
|
end
|
115
120
|
end
|
116
121
|
|
117
|
-
|
122
|
+
# TODO: default to false
|
123
|
+
def normalize!(ast, fields, cast_all = false)
|
118
124
|
ast.map! do |node|
|
119
125
|
if node[:type] == :and || node[:type] == :or || node[:type] == :not
|
120
126
|
normalize!(node[:value], fields, cast_all)
|
@@ -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.1
|
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: 2021-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chronic
|
@@ -34,7 +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
|
37
39
|
- lib/command_search/backends/postgres.rb
|
40
|
+
- lib/command_search/backends/sqlite.rb
|
38
41
|
- lib/command_search/lexer.rb
|
39
42
|
- lib/command_search/normalizer.rb
|
40
43
|
- lib/command_search/optimizer.rb
|
@@ -58,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
61
|
- !ruby/object:Gem::Version
|
59
62
|
version: '0'
|
60
63
|
requirements: []
|
61
|
-
rubygems_version: 3.
|
64
|
+
rubygems_version: 3.1.4
|
62
65
|
signing_key:
|
63
66
|
specification_version: 4
|
64
67
|
summary: Let users query collections with ease.
|