command_search 0.11.0 → 0.12.1
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 +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.
|