command_search 0.10.0 → 0.12.0

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