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 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.