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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ab70b519f399ddeeba1a8862085f8017d327b60a2cbfddaa4682363e56d2f38
4
- data.tar.gz: 204fc638f22b3ffddef67f3065c551885c9b116b4fcadffb118f52d4d7ccffb2
3
+ metadata.gz: c0a659ab861cfea9badba5aa2c4bdf6e8797c5ac820c468bf87f98519040f1c3
4
+ data.tar.gz: 5d8f4ff47c1b2d7dc917d0e791d27a071943c9d0783968ba831894f67bee9032
5
5
  SHA512:
6
- metadata.gz: 15beca3aa277baf873b57ff539b46c7dc966e357801ed69ed3818b17d89e021a6ead6bc233e119fd227bfc62615e2e0078e954cd92b9a32b216e3414cf6313e4
7
- data.tar.gz: 34da12042bd8b24d8b83bdceb08454a8ec72517cd3cfca155072cc286d0b54c683bfa16fc65f8f579759ea1a8279576c989fbd7599fdd818e0a0d45b2a7cd646
6
+ metadata.gz: 86e941a3b2e15fd637b657c2bf102887b5ea888a783105492edfe5390fc0a908f546f538197b7c3af46cb78e9b838f3a90119c5ac9bbe2abfca795d92dfead86
7
+ data.tar.gz: 334d7bccc459f47d47f445217ae9dc702f5dbc432b3c20367a48678ea2ac2d698cb55257987097b54e483cc0cbd64beadc5a11f9ff8338a9cca5fd8ab9553316
@@ -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, false)
27
+ Normalizer.normalize!(ast, fields)
25
28
  return Postgres.build_query(ast)
26
29
  end
27
- Normalizer.normalize!(ast, fields)
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) && source.queryable
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
@@ -20,6 +20,7 @@ module CommandSearch
20
20
  # item_val.to_s.match?(search)
21
21
  elsif type == Time
22
22
  item_time = item_val.to_time
23
+ return false if search.nil?
23
24
  search.first <= item_time && item_time < search.last
24
25
  else
25
26
  item_val == search
@@ -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 = '(^|\s|[^:+\w])'
16
- tail_border = '($|\s|[^:+\w])'
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 "(#{field} >= '#{val[0]}') AND (#{field} < '#{val[1]}') AND (#{field} IS NOT NULL)"
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 = "'#{build_quoted_regex(val)}'"
48
+ val = "'#{build_quoted_regex(val)}'"
44
49
  elsif type == :str
45
- op = '~*'
46
- val = "'#{quote_string(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
- "(#{field} #{op} #{val}) AND (#{field} IS NOT NULL) AND (#{val} IS NOT NULL)"
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
- search_node[:value] = nil
35
- return
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 = '(^|\s|[^:+\w])'
57
- border_b = '($|\s|[^:+\w])'
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
- def normalize!(ast, fields, cast_all = true)
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!(input)
5
+ def group_parens!(ast)
6
6
  i = 0
7
7
  opening_idxs = []
8
- while i < input.length
9
- next i += 1 unless input[i][:type] == :paren
10
- if input[i][:value] == '('
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
- input.delete_at(i)
12
+ ast.delete_at(i)
13
13
  next
14
14
  end
15
- input.delete_at(i)
15
+ ast.delete_at(i)
16
16
  opening = opening_idxs.pop()
17
17
  next unless opening
18
- val = input.slice(opening, i - opening)
18
+ val = ast.slice(opening, i - opening)
19
19
  if val.count > 1
20
- input[opening..(i - 1)] = { type: :and, value: val }
20
+ ast[opening..(i - 1)] = { type: :and, value: val }
21
21
  i -= val.length
22
22
  next
23
23
  elsif val.count == 1
24
- input[opening] = val.first
24
+ ast[opening] = val.first
25
25
  end
26
26
  end
27
27
  end
28
28
 
29
- def cluster_cmds!(input)
29
+ def cluster_cmds!(ast)
30
30
  i = 1
31
- while i < input.length - 1
32
- type = input[i][:type]
31
+ while i < ast.length - 1
32
+ type = ast[i][:type]
33
33
  next i += 1 unless type == :colon || type == :compare
34
- input[(i - 1)..(i + 1)] = {
34
+ ast[(i - 1)..(i + 1)] = {
35
35
  type: type,
36
- nest_op: input[i][:value],
37
- value: [input[i - 1], input[i + 1]]
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!(input)
42
+ def cluster_or!(ast)
43
43
  i = 0
44
- while i < input.length
45
- type = input[i][:type]
46
- cluster_or!(input[i][:value]) if type == :and || type == :not
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 == input.length - 1
49
- input.delete_at(i)
48
+ if i == 0 || i == ast.length - 1
49
+ ast.delete_at(i)
50
50
  next
51
51
  end
52
- val = [input[i - 1], input[i + 1]]
52
+ val = [ast[i - 1], ast[i + 1]]
53
53
  cluster_or!(val)
54
- input[(i - 1)..(i + 1)] = { type: :or, value: val }
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!(input)
59
- i = input.length
61
+ def cluster_not!(ast)
62
+ i = ast.length
60
63
  while i > 0
61
64
  i -= 1
62
- type = input[i][:type]
63
- cluster_not!(input[i][:value]) if type == :and
65
+ type = ast[i][:type]
66
+ cluster_not!(ast[i][:value]) if type == :and
64
67
  next unless type == :minus
65
- if i == input.length - 1
66
- input.delete_at(i)
68
+ if i == ast.length - 1
69
+ ast.delete_at(i)
67
70
  next
68
71
  end
69
- input[i..(i + 1)] = {
70
- type: :not,
71
- value: [input[i + 1]]
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!(input, types)
77
- i = 0
78
- while i < input.length - 2
79
- left = input[i][:type]
80
- right = input[i + 2][:type]
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 merge_strs(input, (x, y))
89
- if input[y] && input[y][:type] == :str
90
- values = input.map { |x| x[:value] }
91
- { type: :str, value: values.join() }
92
- else
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 clean_ununusable!(input)
99
- i = 1
100
- while i < input.length
101
- next i += 1 unless input[i][:type] == :minus
102
- next i += 1 unless [:compare, :colon].include?(input[i - 1][:type])
103
- input[i..i + 1] = merge_strs(input[i..i + 1], [0, 1])
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
- input[i..i + 1] = merge_strs(input[i..i + 1], [0, 1])
114
- input[i - 1..i] = merge_strs(input[i - 1..i], [1, 0]) if i > 0
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!(input)
119
- clean_ununusable!(input)
120
- unchain!(input, [:colon, :compare])
121
- cluster_cmds!(input)
122
- group_parens!(input)
123
- cluster_not!(input)
124
- cluster_or!(input)
125
- input
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.11.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: 2019-12-14 00:00:00.000000000 Z
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.0.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.