hotdog 0.12.0 → 0.13.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.
@@ -16,6 +16,7 @@ module Hotdog
16
16
  default_option(options, :forward_agent, false)
17
17
  default_option(options, :color, :auto)
18
18
  default_option(options, :max_parallelism, Parallel.processor_count)
19
+ default_option(options, :shuffle, false)
19
20
  optparse.on("-o SSH_OPTION", "Passes this string to ssh command through shell. This option may be given multiple times") do |option|
20
21
  options[:options] += [option]
21
22
  end
@@ -43,6 +44,9 @@ module Hotdog
43
44
  optparse.on("--color=WHEN", "--colour=WHEN", "Enable colors") do |color|
44
45
  options[:color] = color
45
46
  end
47
+ optparse.on("--shuffle", "Shuffle result") do |v|
48
+ options[:shuffle] = v
49
+ end
46
50
  end
47
51
 
48
52
  def run(args=[], options={})
@@ -61,6 +65,9 @@ module Hotdog
61
65
 
62
66
  result0 = evaluate(node, self)
63
67
  tuples, fields = get_hosts_with_search_tags(result0, node)
68
+ if options[:shuffle]
69
+ tuples = tuples.shuffle()
70
+ end
64
71
  tuples = filter_hosts(tuples)
65
72
  validate_hosts!(tuples, fields)
66
73
  run_main(tuples.map {|tuple| tuple.first }, options)
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "parslet"
4
+
5
+ # Monkey patch to prevent `NoMethodError` after some parse error in parselet
6
+ module Parslet
7
+ class Cause
8
+ def cause
9
+ self
10
+ end
11
+
12
+ def backtrace
13
+ []
14
+ end
15
+ end
16
+ end
17
+
18
+ require "hotdog/expression/semantics"
19
+ require "hotdog/expression/syntax"
20
+
21
+ module Hotdog
22
+ module Expression
23
+ class ExpressionTransformer < Parslet::Transform
24
+ rule(float: simple(:float)) {
25
+ float.to_f
26
+ }
27
+ rule(integer: simple(:integer)) {
28
+ integer.to_i
29
+ }
30
+ rule(string: simple(:string)) {
31
+ case string
32
+ when /\A"(.*)"\z/
33
+ $1
34
+ when /\A'(.*)'\z/
35
+ $1
36
+ else
37
+ string
38
+ end
39
+ }
40
+ rule(regexp: simple(:regexp)) {
41
+ case regexp
42
+ when /\A\/(.*)\/\z/
43
+ $1
44
+ else
45
+ regexp
46
+ end
47
+ }
48
+ rule(funcall_args_head: simple(:funcall_args_head), funcall_args_tail: sequence(:funcall_args_tail)) {
49
+ [funcall_args_head] + funcall_args_tail
50
+ }
51
+ rule(funcall_args_head: simple(:funcall_args_head)) {
52
+ [funcall_args_head]
53
+ }
54
+ rule(funcall: simple(:funcall), funcall_args: sequence(:funcall_args)) {
55
+ FuncallNode.new(funcall, funcall_args)
56
+ }
57
+ rule(funcall: simple(:funcall)) {
58
+ FuncallNode.new(funcall, [])
59
+ }
60
+ rule(binary_op: simple(:binary_op), left: simple(:left), right: simple(:right)) {
61
+ BinaryExpressionNode.new(binary_op, left, right)
62
+ }
63
+ rule(unary_op: simple(:unary_op), expression: simple(:expression)) {
64
+ UnaryExpressionNode.new(unary_op, expression)
65
+ }
66
+ rule(tag_name_regexp: simple(:tag_name_regexp), separator: simple(:separator), tag_value_regexp: simple(:tag_value_regexp)) {
67
+ if "/host/" == tag_name_regexp
68
+ RegexpHostNode.new(tag_value_regexp, separator)
69
+ else
70
+ RegexpTagNode.new(tag_name_regexp, tag_value_regexp, separator)
71
+ end
72
+ }
73
+ rule(tag_name_regexp: simple(:tag_name_regexp), separator: simple(:separator)) {
74
+ if "/host/" == tag_name_regexp
75
+ EverythingNode.new()
76
+ else
77
+ RegexpTagNameNode.new(tag_name_regexp, separator)
78
+ end
79
+ }
80
+ rule(tag_name_regexp: simple(:tag_name_regexp)) {
81
+ if "/host/" == tag_name_regexp
82
+ EverythingNode.new()
83
+ else
84
+ RegexpNode.new(tag_name_regexp)
85
+ end
86
+ }
87
+ rule(tag_name_glob: simple(:tag_name_glob), separator: simple(:separator), tag_value_glob: simple(:tag_value_glob)) {
88
+ if "host" == tag_name_glob
89
+ GlobHostNode.new(tag_value_glob, separator)
90
+ else
91
+ GlobTagNode.new(tag_name_glob, tag_value_glob, separator)
92
+ end
93
+ }
94
+ rule(tag_name_glob: simple(:tag_name_glob), separator: simple(:separator), tag_value: simple(:tag_value)) {
95
+ if "host" == tag_name_glob
96
+ GlobHostNode.new(tag_value, separator)
97
+ else
98
+ GlobTagNode.new(tag_name_glob, tag_value, separator)
99
+ end
100
+ }
101
+ rule(tag_name_glob: simple(:tag_name_glob), separator: simple(:separator)) {
102
+ if "host" == tag_name_glob
103
+ EverythingNode.new()
104
+ else
105
+ GlobTagNameNode.new(tag_name_glob, separator)
106
+ end
107
+ }
108
+ rule(tag_name_glob: simple(:tag_name_glob)) {
109
+ if "host" == tag_name_glob
110
+ EverythingNode.new()
111
+ else
112
+ GlobNode.new(tag_name_glob)
113
+ end
114
+ }
115
+ rule(tag_name: simple(:tag_name), separator: simple(:separator), tag_value_glob: simple(:tag_value_glob)) {
116
+ if "host" == tag_name
117
+ GlobHostNode.new(tag_value_glob, separator)
118
+ else
119
+ GlobTagNode.new(tag_name, tag_value_glob, separator)
120
+ end
121
+ }
122
+ rule(tag_name: simple(:tag_name), separator: simple(:separator), tag_value: simple(:tag_value)) {
123
+ if "host" == tag_name
124
+ StringHostNode.new(tag_value, separator)
125
+ else
126
+ StringTagNode.new(tag_name, tag_value, separator)
127
+ end
128
+ }
129
+ rule(tag_name: simple(:tag_name), separator: simple(:separator)) {
130
+ if "host" == tag_name
131
+ EverythingNode.new()
132
+ else
133
+ StringTagNameNode.new(tag_name, separator)
134
+ end
135
+ }
136
+ rule(tag_name: simple(:tag_name)) {
137
+ if "host" == tag_name
138
+ EverythingNode.new()
139
+ else
140
+ StringNode.new(tag_name)
141
+ end
142
+ }
143
+ rule(separator: simple(:separator), tag_value_regexp: simple(:tag_value_regexp)) {
144
+ RegexpTagValueNode.new(tag_value_regexp, separator)
145
+ }
146
+ rule(tag_value_regexp: simple(:tag_value_regexp)) {
147
+ RegexpTagValueNode.new(tag_value_regexp)
148
+ }
149
+ rule(separator: simple(:separator), tag_value_glob: simple(:tag_value_glob)) {
150
+ GlobTagValueNode.new(tag_value_glob, separator)
151
+ }
152
+ rule(tag_value_glob: simple(:tag_value_glob)) {
153
+ GlobTagValueNode.new(tag_value_glob)
154
+ }
155
+ rule(separator: simple(:separator), tag_value: simple(:tag_value)) {
156
+ StringTagValueNode.new(tag_value, separator)
157
+ }
158
+ rule(tag_value: simple(:tag_value)) {
159
+ StringTagValueNode.new(tag_value)
160
+ }
161
+ end
162
+ end
163
+ end
164
+
165
+ # vim:set ft=ruby :
@@ -0,0 +1,1098 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Hotdog
4
+ module Expression
5
+ class ExpressionNode
6
+ def evaluate(environment, options={})
7
+ raise(NotImplementedError.new("must be overridden"))
8
+ end
9
+
10
+ def optimize(options={})
11
+ self
12
+ end
13
+
14
+ def dump(options={})
15
+ {}
16
+ end
17
+ end
18
+
19
+ class UnaryExpressionNode < ExpressionNode
20
+ attr_reader :op, :expression
21
+
22
+ def initialize(op, expression)
23
+ case (op || "not").to_s
24
+ when "!", "~", "NOT", "not"
25
+ @op = :NOT
26
+ else
27
+ raise(SyntaxError.new("unknown unary operator: #{op.inspect}"))
28
+ end
29
+ @expression = expression
30
+ end
31
+
32
+ def evaluate(environment, options={})
33
+ case @op
34
+ when :NOT
35
+ values = @expression.evaluate(environment, options).tap do |values|
36
+ environment.logger.debug("expr: #{values.length} value(s)")
37
+ end
38
+ if values.empty?
39
+ EverythingNode.new().evaluate(environment, options).tap do |values|
40
+ environment.logger.debug("NOT expr: #{values.length} value(s)")
41
+ end
42
+ else
43
+ # workaround for "too many terms in compound SELECT"
44
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
45
+ (min / (SQLITE_LIMIT_COMPOUND_SELECT - 2)).upto(max / (SQLITE_LIMIT_COMPOUND_SELECT - 2)).flat_map { |i|
46
+ range = ((SQLITE_LIMIT_COMPOUND_SELECT - 2) * i)...((SQLITE_LIMIT_COMPOUND_SELECT - 2) * (i + 1))
47
+ selected = values.select { |n| range === n }
48
+ q = "SELECT id FROM hosts " \
49
+ "WHERE ? <= id AND id < ? AND id NOT IN (%s);"
50
+ environment.execute(q % selected.map { "?" }.join(", "), [range.first, range.last] + selected).map { |row| row.first }
51
+ }.tap do |values|
52
+ environment.logger.debug("NOT expr: #{values.length} value(s)")
53
+ end
54
+ end
55
+ else
56
+ []
57
+ end
58
+ end
59
+
60
+ def optimize(options={})
61
+ @expression = @expression.optimize(options)
62
+ case op
63
+ when :NOT
64
+ case expression
65
+ when EverythingNode
66
+ NothingNode.new(options)
67
+ when NothingNode
68
+ EverythingNode.new(options)
69
+ else
70
+ optimize1(options)
71
+ end
72
+ else
73
+ self
74
+ end
75
+ end
76
+
77
+ def ==(other)
78
+ self.class === other and @op == other.op and @expression == other.expression
79
+ end
80
+
81
+ def dump(options={})
82
+ {unary_op: @op.to_s, expression: @expression.dump(options)}
83
+ end
84
+
85
+ private
86
+ def optimize1(options={})
87
+ case op
88
+ when :NOT
89
+ if UnaryExpressionNode === expression and expression.op == :NOT
90
+ expression.expression
91
+ else
92
+ case expression
93
+ when QueryExpressionNode
94
+ q = expression.query
95
+ v = expression.values
96
+ if q and v.length <= SQLITE_LIMIT_COMPOUND_SELECT
97
+ QueryExpressionNode.new("SELECT id AS host_id FROM hosts EXCEPT #{q.sub(/\s*;\s*\z/, "")};", v)
98
+ else
99
+ self
100
+ end
101
+ when TagExpressionNode
102
+ q = expression.maybe_query(options)
103
+ v = expression.condition_values(options)
104
+ if q and v.length <= SQLITE_LIMIT_COMPOUND_SELECT
105
+ QueryExpressionNode.new("SELECT id AS host_id FROM hosts EXCEPT #{q.sub(/\s*;\s*\z/, "")};", v)
106
+ else
107
+ self
108
+ end
109
+ else
110
+ self
111
+ end
112
+ end
113
+ else
114
+ self
115
+ end
116
+ end
117
+ end
118
+
119
+ class BinaryExpressionNode < ExpressionNode
120
+ attr_reader :op, :left, :right
121
+
122
+ def initialize(op, left, right)
123
+ case (op || "or").to_s
124
+ when "&&", "&", "AND", "and"
125
+ @op = :AND
126
+ when ",", "||", "|", "OR", "or"
127
+ @op = :OR
128
+ when "^", "XOR", "xor"
129
+ @op = :XOR
130
+ else
131
+ raise(SyntaxError.new("unknown binary operator: #{op.inspect}"))
132
+ end
133
+ @left = left
134
+ @right = right
135
+ end
136
+
137
+ def evaluate(environment, options={})
138
+ case @op
139
+ when :AND
140
+ left_values = @left.evaluate(environment, options).tap do |values|
141
+ environment.logger.debug("lhs: #{values.length} value(s)")
142
+ end
143
+ if left_values.empty?
144
+ []
145
+ else
146
+ right_values = @right.evaluate(environment, options).tap do |values|
147
+ environment.logger.debug("rhs: #{values.length} value(s)")
148
+ end
149
+ if right_values.empty?
150
+ []
151
+ else
152
+ # workaround for "too many terms in compound SELECT"
153
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
154
+ (min / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).upto(max / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).flat_map { |i|
155
+ range = (((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * i)...(((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * (i + 1))
156
+ left_selected = left_values.select { |n| range === n }
157
+ right_selected = right_values.select { |n| range === n }
158
+ q = "SELECT id FROM hosts " \
159
+ "WHERE ? <= id AND id < ? AND ( id IN (%s) AND id IN (%s) );"
160
+ environment.execute(q % [left_selected.map { "?" }.join(", "), right_selected.map { "?" }.join(", ")], [range.first, range.last] + left_selected + right_selected).map { |row| row.first }
161
+ }.tap do |values|
162
+ environment.logger.debug("lhs AND rhs: #{values.length} value(s)")
163
+ end
164
+ end
165
+ end
166
+ when :OR
167
+ left_values = @left.evaluate(environment, options).tap do |values|
168
+ environment.logger.debug("lhs: #{values.length} value(s)")
169
+ end
170
+ right_values = @right.evaluate(environment, options).tap do |values|
171
+ environment.logger.debug("rhs: #{values.length} value(s)")
172
+ end
173
+ if left_values.empty?
174
+ right_values
175
+ else
176
+ if right_values.empty?
177
+ []
178
+ else
179
+ # workaround for "too many terms in compound SELECT"
180
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
181
+ (min / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).upto(max / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).flat_map { |i|
182
+ range = (((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * i)...(((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * (i + 1))
183
+ left_selected = left_values.select { |n| range === n }
184
+ right_selected = right_values.select { |n| range === n }
185
+ q = "SELECT id FROM hosts " \
186
+ "WHERE ? <= id AND id < ? AND ( id IN (%s) OR id IN (%s) );"
187
+ environment.execute(q % [left_selected.map { "?" }.join(", "), right_selected.map { "?" }.join(", ")], [range.first, range.last] + left_selected + right_selected).map { |row| row.first }
188
+ }.tap do |values|
189
+ environment.logger.debug("lhs OR rhs: #{values.length} value(s)")
190
+ end
191
+ end
192
+ end
193
+ when :XOR
194
+ left_values = @left.evaluate(environment, options).tap do |values|
195
+ environment.logger.debug("lhs: #{values.length} value(s)")
196
+ end
197
+ right_values = @right.evaluate(environment, options).tap do |values|
198
+ environment.logger.debug("rhs: #{values.length} value(s)")
199
+ end
200
+ if left_values.empty?
201
+ right_values
202
+ else
203
+ if right_values.empty?
204
+ []
205
+ else
206
+ # workaround for "too many terms in compound SELECT"
207
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
208
+ (min / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4)).upto(max / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4)).flat_map { |i|
209
+ range = (((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4) * i)...(((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4) * (i + 1))
210
+ left_selected = left_values.select { |n| range === n }
211
+ right_selected = right_values.select { |n| range === n }
212
+ q = "SELECT id FROM hosts " \
213
+ "WHERE ? <= id AND id < ? AND NOT (id IN (%s) AND id IN (%s)) AND ( id IN (%s) OR id IN (%s) );"
214
+ lq = left_selected.map { "?" }.join(", ")
215
+ rq = right_selected.map { "?" }.join(", ")
216
+ environment.execute(q % [lq, rq, lq, rq], [range.first, range.last] + left_selected + right_selected + left_selected + right_selected).map { |row| row.first }
217
+ }.tap do |values|
218
+ environment.logger.debug("lhs XOR rhs: #{values.length} value(s)")
219
+ end
220
+ end
221
+ end
222
+ else
223
+ []
224
+ end
225
+ end
226
+
227
+ def optimize(options={})
228
+ @left = @left.optimize(options)
229
+ @right = @right.optimize(options)
230
+ case op
231
+ when :AND
232
+ case left
233
+ when EverythingNode
234
+ right
235
+ when NothingNode
236
+ left
237
+ else
238
+ if left == right
239
+ left
240
+ else
241
+ optimize1(options)
242
+ end
243
+ end
244
+ when :OR
245
+ case left
246
+ when EverythingNode
247
+ left
248
+ when NothingNode
249
+ right
250
+ else
251
+ if left == right
252
+ left
253
+ else
254
+ if MultinaryExpressionNode === left
255
+ if left.op == op
256
+ left.merge(right, fallback: self)
257
+ else
258
+ optimize1(options)
259
+ end
260
+ else
261
+ if MultinaryExpressionNode === right
262
+ if right.op == op
263
+ right.merge(left, fallback: self)
264
+ else
265
+ optimize1(options)
266
+ end
267
+ else
268
+ MultinaryExpressionNode.new(op, [left, right], fallback: self)
269
+ end
270
+ end
271
+ end
272
+ end
273
+ when :XOR
274
+ if left == right
275
+ []
276
+ else
277
+ optimize1(options)
278
+ end
279
+ else
280
+ self
281
+ end
282
+ end
283
+
284
+ def ==(other)
285
+ self.class === other and @op == other.op and @left == other.left and @right == other.right
286
+ end
287
+
288
+ def dump(options={})
289
+ {left: @left.dump(options), binary_op: @op.to_s, right: @right.dump(options)}
290
+ end
291
+
292
+ private
293
+ def optimize1(options)
294
+ if TagExpressionNode === left and TagExpressionNode === right
295
+ lq = left.maybe_query(options)
296
+ lv = left.condition_values(options)
297
+ rq = right.maybe_query(options)
298
+ rv = right.condition_values(options)
299
+ if lq and rq and lv.length + rv.length <= SQLITE_LIMIT_COMPOUND_SELECT
300
+ case op
301
+ when :AND
302
+ q = "#{lq.sub(/\s*;\s*\z/, "")} INTERSECT #{rq.sub(/\s*;\s*\z/, "")};"
303
+ QueryExpressionNode.new(q, lv + rv, fallback: self)
304
+ when :OR
305
+ q = "#{lq.sub(/\s*;\s*\z/, "")} UNION #{rq.sub(/\s*;\s*\z/, "")};"
306
+ QueryExpressionNode.new(q, lv + rv, fallback: self)
307
+ when :XOR
308
+ q = "#{lq.sub(/\s*;\s*\z/, "")} UNION #{rq.sub(/\s*;\s*\z/, "")} " \
309
+ "EXCEPT #{lq.sub(/\s*;\s*\z/, "")} " \
310
+ "INTERSECT #{rq.sub(/\s*;\s*\z/, "")};"
311
+ QueryExpressionNode.new(q, lv + rv, fallback: self)
312
+ else
313
+ self
314
+ end
315
+ else
316
+ self
317
+ end
318
+ else
319
+ self
320
+ end
321
+ end
322
+ end
323
+
324
+ class MultinaryExpressionNode < ExpressionNode
325
+ attr_reader :op, :expressions
326
+
327
+ def initialize(op, expressions, options={})
328
+ case (op || "or").to_s
329
+ when ",", "||", "|", "OR", "or"
330
+ @op = :OR
331
+ else
332
+ raise(SyntaxError.new("unknown multinary operator: #{op.inspect}"))
333
+ end
334
+ if SQLITE_LIMIT_COMPOUND_SELECT < expressions.length
335
+ raise(ArgumentError.new("expressions limit exceeded: #{expressions.length} for #{SQLITE_LIMIT_COMPOUND_SELECT}"))
336
+ end
337
+ @expressions = expressions
338
+ @fallback = options[:fallback]
339
+ end
340
+
341
+ def merge(other, options={})
342
+ if MultinaryExpressionNode === other and op == other.op
343
+ MultinaryExpressionNode.new(op, expressions + other.expressions, options)
344
+ else
345
+ MultinaryExpressionNode.new(op, expressions + [other], options)
346
+ end
347
+ end
348
+
349
+ def evaluate(environment, options={})
350
+ case @op
351
+ when :OR
352
+ if expressions.all? { |expression| TagExpressionNode === expression }
353
+ values = expressions.group_by { |expression| expression.class }.values.flat_map { |expressions|
354
+ query_without_condition = expressions.first.maybe_query_without_condition(options)
355
+ if query_without_condition
356
+ condition_length = expressions.map { |expression| expression.condition_values(options).length }.max
357
+ expressions.each_slice(SQLITE_LIMIT_COMPOUND_SELECT / condition_length).flat_map { |expressions|
358
+ q = query_without_condition.sub(/\s*;\s*\z/, "") + " WHERE " + expressions.map { |expression| "( %s )" % expression.condition(options) }.join(" OR ") + ";"
359
+ environment.execute(q, expressions.flat_map { |expression| expression.condition_values(options) }).map { |row| row.first }
360
+ }
361
+ else
362
+ []
363
+ end
364
+ }
365
+ else
366
+ values = []
367
+ end
368
+ else
369
+ values = []
370
+ end
371
+ if values.empty?
372
+ if @fallback
373
+ @fallback.evaluate(environment, options={})
374
+ else
375
+ []
376
+ end
377
+ else
378
+ values
379
+ end
380
+ end
381
+
382
+ def dump(options={})
383
+ {multinary_op: @op.to_s, expressions: expressions.map { |expression| expression.dump(options) }}
384
+ end
385
+ end
386
+
387
+ class QueryExpressionNode < ExpressionNode
388
+ def initialize(query, values=[], options={})
389
+ @query = query
390
+ @values = values
391
+ @fallback = options[:fallback]
392
+ end
393
+ attr_reader :query
394
+ attr_reader :values
395
+
396
+ def evaluate(environment, options={})
397
+ values = environment.execute(@query, @values).map { |row| row.first }
398
+ if values.empty? and @fallback
399
+ @fallback.evaluate(environment, options)
400
+ else
401
+ values
402
+ end
403
+ end
404
+
405
+ def dump(options={})
406
+ data = {query: @query, values: @values}
407
+ data[:fallback] = @fallback.dump(options) if @fallback
408
+ data
409
+ end
410
+ end
411
+
412
+ class FuncallNode < ExpressionNode
413
+ attr_reader :function, :args
414
+
415
+ def initialize(function, args)
416
+ # FIXME: smart argument handling (e.g. arity & type checking)
417
+ case function.to_s
418
+ when "HEAD", "head"
419
+ @function = :HEAD
420
+ when "GROUP_BY", "group_by"
421
+ @function = :GROUP_BY
422
+ when "LIMIT", "limit"
423
+ @function = :HEAD
424
+ when "ORDER_BY", "order_by"
425
+ @function = :ORDER_BY
426
+ when "REVERSE", "reverse"
427
+ @function = :REVERSE
428
+ when "SHUFFLE", "shuffle"
429
+ @function = :SHUFFLE
430
+ when "SORT", "sort"
431
+ @function = :ORDER_BY
432
+ when "TAIL", "tail"
433
+ @function = :TAIL
434
+ else
435
+ raise(SyntaxError.new("unknown function call: #{function}"))
436
+ end
437
+ @args = args
438
+ end
439
+
440
+ def optimize(options={})
441
+ self
442
+ end
443
+
444
+ def dump(options={})
445
+ args = @args.map { |arg|
446
+ if ExpressionNode === arg
447
+ arg.dump(options)
448
+ else
449
+ arg
450
+ end
451
+ }
452
+ {funcall: @function.to_s, args: args}
453
+ end
454
+
455
+ def evaluate(environment, options={})
456
+ case function
457
+ when :HEAD
458
+ args[0].evaluate(environment, options).take(args[1] || 1)
459
+ when :GROUP_BY
460
+ intermediate = args[0].evaluate(environment, options)
461
+ q = "SELECT hosts_tags.host_id FROM hosts_tags " \
462
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
463
+ "WHERE tags.name = ? AND hosts_tags.host_id IN (%s) " \
464
+ "GROUP BY tags.value;" % intermediate.map { "?" }.join(", ")
465
+ QueryExpressionNode.new(q, [args[1]] + intermediate, fallback: nil).evaluate(environment, options)
466
+ when :ORDER_BY
467
+ intermediate = args[0].evaluate(environment, options)
468
+ if args[1]
469
+ q = "SELECT hosts_tags.host_id FROM hosts_tags " \
470
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
471
+ "WHERE tags.name = ? AND hosts_tags.host_id IN (%s) " \
472
+ "ORDER BY tags.value;" % intermediate.map { "?" }.join(", ")
473
+ QueryExpressionNode.new(q, [args[1]] + intermediate, fallback: nil).evaluate(environment, options)
474
+ else
475
+ q = "SELECT hosts_tags.host_id FROM hosts_tags " \
476
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
477
+ "WHERE hosts_tags.host_id IN (%s) " \
478
+ "ORDER BY hosts_tags.host_id;" % intermediate.map { "?" }.join(", ")
479
+ QueryExpressionNode.new(q, intermediate, fallback: nil).evaluate(environment, options)
480
+ end
481
+ when :REVERSE
482
+ args[0].evaluate(environment, options).reverse()
483
+ when :SHUFFLE
484
+ args[0].evaluate(environment, options).shuffle()
485
+ when :TAIL
486
+ args[0].evaluate(environment, options).last(args[1] || 1)
487
+ else
488
+ []
489
+ end
490
+ end
491
+ end
492
+
493
+ class EverythingNode < QueryExpressionNode
494
+ def initialize(options={})
495
+ super("SELECT id AS host_id FROM hosts", [], options)
496
+ end
497
+ end
498
+
499
+ class NothingNode < QueryExpressionNode
500
+ def initialize(options={})
501
+ super("SELECT NULL AS host_id WHERE host_id NOT NULL", [], options)
502
+ end
503
+
504
+ def evaluate(environment, options={})
505
+ if @fallback
506
+ @fallback.evaluate(environment, options)
507
+ else
508
+ []
509
+ end
510
+ end
511
+ end
512
+
513
+ class TagExpressionNode < ExpressionNode
514
+ def initialize(tag_name, tag_value, separator=nil)
515
+ @tag_name = tag_name
516
+ @tag_value = tag_value
517
+ @separator = separator
518
+ @fallback = nil
519
+ end
520
+ attr_reader :tag_name
521
+ attr_reader :tag_value
522
+ attr_reader :separator
523
+
524
+ def tag_name?
525
+ !(tag_name.nil? or tag_name.to_s.empty?)
526
+ end
527
+
528
+ def tag_value?
529
+ !(tag_value.nil? or tag_value.to_s.empty?)
530
+ end
531
+
532
+ def separator?
533
+ !(separator.nil? or separator.to_s.empty?)
534
+ end
535
+
536
+ def maybe_query(options={})
537
+ query_without_condition = maybe_query_without_condition(options)
538
+ if query_without_condition
539
+ query_without_condition.sub(/\s*;\s*\z/, "") + " WHERE " + condition(options) + ";"
540
+ else
541
+ nil
542
+ end
543
+ end
544
+
545
+ def maybe_query_without_condition(options={})
546
+ tables = condition_tables(options)
547
+ if tables.empty?
548
+ nil
549
+ else
550
+ case tables
551
+ when [:hosts]
552
+ "SELECT hosts.id AS host_id FROM hosts;"
553
+ when [:hosts, :tags]
554
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags INNER JOIN hosts ON hosts_tags.host_id = hosts.id INNER JOIN tags ON hosts_tags.tag_id = tags.id;"
555
+ when [:tags]
556
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags INNER JOIN tags ON hosts_tags.tag_id = tags.id;"
557
+ else
558
+ raise(NotImplementedError.new("unknown tables: #{tables.join(", ")}"))
559
+ end
560
+ end
561
+ end
562
+
563
+ def condition(options={})
564
+ raise(NotImplementedError.new("must be overridden"))
565
+ end
566
+
567
+ def condition_tables(options={})
568
+ raise(NotImplementedError.new("must be overridden"))
569
+ end
570
+
571
+ def condition_values(options={})
572
+ raise(NotImplementedError.new("must be overridden"))
573
+ end
574
+
575
+ def evaluate(environment, options={})
576
+ q = maybe_query(options)
577
+ if q
578
+ values = environment.execute(q, condition_values(options)).map { |row| row.first }
579
+ if values.empty?
580
+ if options[:did_fallback]
581
+ []
582
+ else
583
+ if not environment.fixed_string? and @fallback
584
+ # avoid optimizing @fallback to prevent infinite recursion
585
+ values = @fallback.evaluate(environment, options.merge(did_fallback: true))
586
+ if values.empty?
587
+ if reload(environment, options)
588
+ evaluate(environment, options).tap do |values|
589
+ if values.empty?
590
+ environment.logger.info("no result: #{self.dump.inspect}")
591
+ end
592
+ end
593
+ else
594
+ []
595
+ end
596
+ else
597
+ values
598
+ end
599
+ else
600
+ if reload(environment, options)
601
+ evaluate(environment, options).tap do |values|
602
+ if values.empty?
603
+ environment.logger.info("no result: #{self.dump.inspect}")
604
+ end
605
+ end
606
+ else
607
+ []
608
+ end
609
+ end
610
+ end
611
+ else
612
+ values
613
+ end
614
+ else
615
+ []
616
+ end
617
+ end
618
+
619
+ def ==(other)
620
+ self.class == other.class and @tag_name == other.tag_name and @tag_value == other.tag_value
621
+ end
622
+
623
+ def optimize(options={})
624
+ # fallback to glob expression
625
+ @fallback = maybe_fallback(options)
626
+ self
627
+ end
628
+
629
+ def to_glob(s)
630
+ (s.start_with?("*") ? "" : "*") + s.gsub(/[-.\/_]/, "?") + (s.end_with?("*") ? "" : "*")
631
+ end
632
+
633
+ def maybe_glob(s)
634
+ s ? to_glob(s.to_s) : nil
635
+ end
636
+
637
+ def reload(environment, options={})
638
+ $did_reload ||= false
639
+ if $did_reload
640
+ false
641
+ else
642
+ $did_reload = true
643
+ environment.logger.info("force reloading all hosts and tags.")
644
+ environment.reload(force: true)
645
+ true
646
+ end
647
+ end
648
+
649
+ def dump(options={})
650
+ data = {}
651
+ data[:tag_name] = tag_name.to_s if tag_name
652
+ data[:separator] = separator.to_s if separator
653
+ data[:tag_value] = tag_value.to_s if tag_value
654
+ data[:fallback ] = @fallback.dump(options) if @fallback
655
+ data
656
+ end
657
+
658
+ def maybe_fallback(options={})
659
+ nil
660
+ end
661
+ end
662
+
663
+ class AnyHostNode < TagExpressionNode
664
+ def initialize(separator=nil)
665
+ super("host", nil, separator)
666
+ end
667
+
668
+ def condition(options={})
669
+ "1"
670
+ end
671
+
672
+ def condition_tables(options={})
673
+ [:hosts]
674
+ end
675
+
676
+ def condition_values(options={})
677
+ []
678
+ end
679
+ end
680
+
681
+ class StringExpressionNode < TagExpressionNode
682
+ end
683
+
684
+ class StringHostNode < StringExpressionNode
685
+ def initialize(tag_value, separator=nil)
686
+ super("host", tag_value.to_s, separator)
687
+ end
688
+
689
+ def condition(options={})
690
+ "hosts.name = ?"
691
+ end
692
+
693
+ def condition_tables(options={})
694
+ [:hosts]
695
+ end
696
+
697
+ def condition_values(options={})
698
+ [tag_value]
699
+ end
700
+
701
+ def maybe_fallback(options={})
702
+ fallback = GlobHostNode.new(to_glob(tag_value), separator)
703
+ query = fallback.maybe_query(options)
704
+ if query
705
+ QueryExpressionNode.new(query, fallback.condition_values(options))
706
+ else
707
+ nil
708
+ end
709
+ end
710
+ end
711
+
712
+ class StringTagNode < StringExpressionNode
713
+ def initialize(tag_name, tag_value, separator=nil)
714
+ super(tag_name.to_s, tag_value.to_s, separator)
715
+ end
716
+
717
+ def condition(options={})
718
+ "tags.name = ? AND tags.value = ?"
719
+ end
720
+
721
+ def condition_tables(options={})
722
+ [:tags]
723
+ end
724
+
725
+ def condition_values(options={})
726
+ [tag_name, tag_value]
727
+ end
728
+
729
+ def maybe_fallback(options={})
730
+ fallback = GlobTagNode.new(to_glob(tag_name), to_glob(tag_value), separator)
731
+ query = fallback.maybe_query(options)
732
+ if query
733
+ QueryExpressionNode.new(query, fallback.condition_values(options))
734
+ else
735
+ nil
736
+ end
737
+ end
738
+ end
739
+
740
+ class StringTagNameNode < StringExpressionNode
741
+ def initialize(tag_name, separator=nil)
742
+ super(tag_name.to_s, nil, separator)
743
+ end
744
+
745
+ def condition(options={})
746
+ "tags.name = ?"
747
+ end
748
+
749
+ def condition_tables(options={})
750
+ [:tags]
751
+ end
752
+
753
+ def condition_values(options={})
754
+ [tag_name]
755
+ end
756
+
757
+ def maybe_fallback(options={})
758
+ fallback = GlobTagNameNode.new(to_glob(tag_name), separator)
759
+ query = fallback.maybe_query(options)
760
+ if query
761
+ QueryExpressionNode.new(query, fallback.condition_values(options))
762
+ else
763
+ nil
764
+ end
765
+ end
766
+ end
767
+
768
+ class StringTagValueNode < StringExpressionNode
769
+ def initialize(tag_value, separator=nil)
770
+ super(nil, tag_value.to_s, separator)
771
+ end
772
+
773
+ def condition(options={})
774
+ "hosts.name = ? OR tags.value = ?"
775
+ end
776
+
777
+ def condition_tables(options={})
778
+ [:hosts, :tags]
779
+ end
780
+
781
+ def condition_values(options={})
782
+ [tag_value, tag_value]
783
+ end
784
+
785
+ def maybe_fallback(options={})
786
+ fallback = GlobTagValueNode.new(to_glob(tag_value), separator)
787
+ query = fallback.maybe_query(options)
788
+ if query
789
+ QueryExpressionNode.new(query, fallback.condition_values(options))
790
+ else
791
+ nil
792
+ end
793
+ end
794
+ end
795
+
796
+ class StringNode < StringExpressionNode
797
+ def initialize(tag_name, separator=nil)
798
+ super(tag_name.to_s, nil, separator)
799
+ end
800
+
801
+ def condition(options={})
802
+ "hosts.name = ? OR tags.name = ? OR tags.value = ?"
803
+ end
804
+
805
+ def condition_tables(options={})
806
+ [:hosts, :tags]
807
+ end
808
+
809
+ def condition_values(options={})
810
+ [tag_name, tag_name, tag_name]
811
+ end
812
+
813
+ def maybe_fallback(options={})
814
+ fallback = GlobNode.new(to_glob(tag_name), separator)
815
+ query = fallback.maybe_query(options)
816
+ if query
817
+ QueryExpressionNode.new(query, fallback.condition_values(options))
818
+ else
819
+ nil
820
+ end
821
+ end
822
+ end
823
+
824
+ class GlobExpressionNode < TagExpressionNode
825
+ def dump(options={})
826
+ data = {}
827
+ data[:tag_name_glob] = tag_name.to_s if tag_name
828
+ data[:separator] = separator.to_s if separator
829
+ data[:tag_value_glob] = tag_value.to_s if tag_value
830
+ data[:fallback] = @fallback.dump(options) if @fallback
831
+ data
832
+ end
833
+ end
834
+
835
+ class GlobHostNode < GlobExpressionNode
836
+ def initialize(tag_value, separator=nil)
837
+ super("host", tag_value.to_s, separator)
838
+ end
839
+
840
+ def condition(options={})
841
+ "LOWER(hosts.name) GLOB LOWER(?)"
842
+ end
843
+
844
+ def condition_tables(options={})
845
+ [:hosts]
846
+ end
847
+
848
+ def condition_values(options={})
849
+ [tag_value]
850
+ end
851
+
852
+ def maybe_fallback(options={})
853
+ fallback = GlobHostNode.new(to_glob(tag_value), separator)
854
+ query = fallback.maybe_query(options)
855
+ if query
856
+ QueryExpressionNode.new(query, fallback.condition_values(options))
857
+ else
858
+ nil
859
+ end
860
+ end
861
+ end
862
+
863
+ class GlobTagNode < GlobExpressionNode
864
+ def initialize(tag_name, tag_value, separator=nil)
865
+ super(tag_name.to_s, tag_value.to_s, separator)
866
+ end
867
+
868
+ def condition(options={})
869
+ "LOWER(tags.name) GLOB LOWER(?) AND LOWER(tags.value) GLOB LOWER(?)"
870
+ end
871
+
872
+ def condition_tables(options={})
873
+ [:tags]
874
+ end
875
+
876
+ def condition_values(options={})
877
+ [tag_name, tag_value]
878
+ end
879
+
880
+ def maybe_fallback(options={})
881
+ fallback = GlobTagNode.new(to_glob(tag_name), to_glob(tag_value), separator)
882
+ query = fallback.maybe_query(options)
883
+ if query
884
+ QueryExpressionNode.new(query, fallback.condition_values(options))
885
+ else
886
+ nil
887
+ end
888
+ end
889
+ end
890
+
891
+ class GlobTagNameNode < GlobExpressionNode
892
+ def initialize(tag_name, separator=nil)
893
+ super(tag_name.to_s, nil, separator)
894
+ end
895
+
896
+ def condition(options={})
897
+ "LOWER(tags.name) GLOB LOWER(?)"
898
+ end
899
+
900
+ def condition_tables(options={})
901
+ [:tags]
902
+ end
903
+
904
+ def condition_values(options={})
905
+ [tag_name]
906
+ end
907
+
908
+ def maybe_fallback(options={})
909
+ fallback = GlobTagNameNode.new(to_glob(tag_name), separator)
910
+ query = fallback.maybe_query(options)
911
+ if query
912
+ QueryExpressionNode.new(query, fallback.condition_values(options))
913
+ else
914
+ nil
915
+ end
916
+ end
917
+ end
918
+
919
+ class GlobTagValueNode < GlobExpressionNode
920
+ def initialize(tag_value, separator=nil)
921
+ super(nil, tag_value.to_s, separator)
922
+ end
923
+
924
+ def condition(options={})
925
+ "LOWER(hosts.name) GLOB LOWER(?) OR LOWER(tags.value) GLOB LOWER(?)"
926
+ end
927
+
928
+ def condition_tables(options={})
929
+ [:hosts, :tags]
930
+ end
931
+
932
+ def condition_values(options={})
933
+ [tag_value, tag_value]
934
+ end
935
+
936
+ def maybe_fallback(options={})
937
+ fallback = GlobTagValueNode.new(to_glob(tag_value), separator)
938
+ query = fallback.maybe_query(options)
939
+ if query
940
+ QueryExpressionNode.new(query, fallback.condition_values(options))
941
+ else
942
+ nil
943
+ end
944
+ end
945
+ end
946
+
947
+ class GlobNode < GlobExpressionNode
948
+ def initialize(tag_name, separator=nil)
949
+ super(tag_name.to_s, nil, separator)
950
+ end
951
+
952
+ def condition(options={})
953
+ "LOWER(hosts.name) GLOB LOWER(?) OR LOWER(tags.name) GLOB LOWER(?) OR LOWER(tags.value) GLOB LOWER(?)"
954
+ end
955
+
956
+ def condition_tables(options={})
957
+ [:hosts, :tags]
958
+ end
959
+
960
+ def condition_values(options={})
961
+ [tag_name, tag_name, tag_name]
962
+ end
963
+
964
+ def maybe_fallback(options={})
965
+ fallback = GlobNode.new(to_glob(tag_name), separator)
966
+ query = fallback.maybe_query(options)
967
+ if query
968
+ QueryExpressionNode.new(query, fallback.condition_values(options))
969
+ else
970
+ nil
971
+ end
972
+ end
973
+ end
974
+
975
+ class RegexpExpressionNode < TagExpressionNode
976
+ def dump(options={})
977
+ data = {}
978
+ data[:tag_name_regexp] = tag_name.to_s if tag_name
979
+ data[:separator] = separator.to_s if separator
980
+ data[:tag_value_regexp] = tag_value.to_s if tag_value
981
+ data[:fallback] = @fallback.dump(options) if @fallback
982
+ data
983
+ end
984
+ end
985
+
986
+ class RegexpHostNode < RegexpExpressionNode
987
+ def initialize(tag_value, separator=nil)
988
+ case tag_value
989
+ when /\A\/(.*)\/\z/
990
+ tag_value = $1
991
+ end
992
+ super("host", tag_value, separator)
993
+ end
994
+
995
+ def condition(options={})
996
+ "hosts.name REGEXP ?"
997
+ end
998
+
999
+ def condition_tables(options={})
1000
+ [:hosts]
1001
+ end
1002
+
1003
+ def condition_values(options={})
1004
+ [tag_value]
1005
+ end
1006
+ end
1007
+
1008
+ class RegexpTagNode < RegexpExpressionNode
1009
+ def initialize(tag_name, tag_value, separator=nil)
1010
+ case tag_name
1011
+ when /\A\/(.*)\/\z/
1012
+ tag_name = $1
1013
+ end
1014
+ case tag_value
1015
+ when /\A\/(.*)\/\z/
1016
+ tag_value = $1
1017
+ end
1018
+ super(tag_name, tag_value, separator)
1019
+ end
1020
+
1021
+ def condition(options={})
1022
+ "tags.name REGEXP ? AND tags.value REGEXP ?"
1023
+ end
1024
+
1025
+ def condition_tables(options={})
1026
+ [:tags]
1027
+ end
1028
+
1029
+ def condition_values(options={})
1030
+ [tag_name, tag_value]
1031
+ end
1032
+ end
1033
+
1034
+ class RegexpTagNameNode < RegexpExpressionNode
1035
+ def initialize(tag_name, separator=nil)
1036
+ case tag_name
1037
+ when /\A\/(.*)\/\z/
1038
+ tag_name = $1
1039
+ end
1040
+ super(tag_name.to_s, nil, separator)
1041
+ end
1042
+
1043
+ def condition(options={})
1044
+ "tags.name REGEXP ?"
1045
+ end
1046
+
1047
+ def condition_tables(options={})
1048
+ [:tags]
1049
+ end
1050
+
1051
+ def condition_values(options={})
1052
+ [tag_name]
1053
+ end
1054
+ end
1055
+
1056
+ class RegexpTagValueNode < RegexpExpressionNode
1057
+ def initialize(tag_value, separator=nil)
1058
+ case tag_value
1059
+ when /\A\/(.*)\/\z/
1060
+ tag_value = $1
1061
+ end
1062
+ super(nil, tag_value.to_s, separator)
1063
+ end
1064
+
1065
+ def condition(options={})
1066
+ "hosts.name REGEXP ? OR tags.value REGEXP ?"
1067
+ end
1068
+
1069
+ def condition_tables(options={})
1070
+ [:hosts, :tags]
1071
+ end
1072
+
1073
+ def condition_values(options={})
1074
+ [tag_value, tag_value]
1075
+ end
1076
+ end
1077
+
1078
+ class RegexpNode < RegexpExpressionNode
1079
+ def initialize(tag_name, separator=nil)
1080
+ super(tag_name, separator)
1081
+ end
1082
+
1083
+ def condition(options={})
1084
+ "hosts.name REGEXP ? OR tags.name REGEXP ? OR tags.value REGEXP ?"
1085
+ end
1086
+
1087
+ def condition_tables(options={})
1088
+ [:hosts, :tags]
1089
+ end
1090
+
1091
+ def condition_values(options={})
1092
+ [tag_name, tag_name, tag_name]
1093
+ end
1094
+ end
1095
+ end
1096
+ end
1097
+
1098
+ # vim:set ft=ruby :