hotdog 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 :