hotdog 0.4.1 → 0.5.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.
@@ -6,13 +6,15 @@ require "parslet"
6
6
  module Hotdog
7
7
  module Commands
8
8
  class Search < BaseCommand
9
- def run(args=[])
10
- search_options = {
11
- }
9
+ def define_options(optparse)
10
+ @search_options = @options.merge({
11
+ })
12
12
  optparse.on("-n", "--limit LIMIT", "Limit result set to specified size at most", Integer) do |limit|
13
- search_options[:limit] = limit
13
+ @search_options[:limit] = limit
14
14
  end
15
- args = optparse.parse(args)
15
+ end
16
+
17
+ def run(args=[])
16
18
  expression = args.join(" ").strip
17
19
  if expression.empty?
18
20
  # return everything if given expression is empty
@@ -29,7 +31,7 @@ module Hotdog
29
31
  result = evaluate(node, self)
30
32
  if 0 < result.length
31
33
  _result, fields = get_hosts_with_search_tags(result, node)
32
- result = _result.take(search_options.fetch(:limit, _result.size))
34
+ result = _result.take(@search_options.fetch(:limit, _result.size))
33
35
  STDOUT.print(format(result, fields: fields))
34
36
  if _result.length == result.length
35
37
  logger.info("found %d host(s)." % result.length)
@@ -67,14 +69,22 @@ module Hotdog
67
69
  def parse(expression)
68
70
  parser = ExpressionParser.new
69
71
  parser.parse(expression).tap do |parsed|
70
- logger.debug(JSON.pretty_generate(JSON.load(parsed.to_json)))
72
+ logger.debug {
73
+ begin
74
+ JSON.pretty_generate(JSON.load(parsed.to_json))
75
+ rescue JSON::NestingError => error
76
+ error.message
77
+ end
78
+ }
71
79
  end
72
80
  end
73
81
 
74
82
  def evaluate(data, environment)
75
83
  node = ExpressionTransformer.new.apply(data)
76
84
  optimized = node.optimize.tap do |optimized|
77
- logger.debug(JSON.pretty_generate(optimized.dump))
85
+ logger.debug {
86
+ JSON.pretty_generate(optimized.dump)
87
+ }
78
88
  end
79
89
  optimized.evaluate(environment)
80
90
  end
@@ -133,9 +143,9 @@ module Hotdog
133
143
  }
134
144
  rule(:atom) {
135
145
  ( spacing.maybe >> str('(') >> expression >> str(')') >> spacing.maybe \
136
- | spacing.maybe >> identifier_regexp.as(:identifier_regexp) >> separator.as(:separator) >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
137
- | spacing.maybe >> identifier_regexp.as(:identifier_regexp) >> separator.as(:separator) >> spacing.maybe \
138
- | spacing.maybe >> identifier_regexp.as(:identifier_regexp) >> spacing.maybe \
146
+ | spacing.maybe >> str('/') >> identifier_regexp.as(:identifier_regexp) >> str('/') >> separator.as(:separator) >> str('/') >> attribute_regexp.as(:attribute_regexp) >> str('/') >> spacing.maybe \
147
+ | spacing.maybe >> str('/') >> identifier_regexp.as(:identifier_regexp) >> str('/') >> separator.as(:separator) >> spacing.maybe \
148
+ | spacing.maybe >> str('/') >> identifier_regexp.as(:identifier_regexp) >> str('/') >> spacing.maybe \
139
149
  | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator.as(:separator) >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
140
150
  | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator.as(:separator) >> attribute.as(:attribute) >> spacing.maybe \
141
151
  | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator.as(:separator) >> spacing.maybe \
@@ -144,16 +154,16 @@ module Hotdog
144
154
  | spacing.maybe >> identifier.as(:identifier) >> separator.as(:separator) >> attribute.as(:attribute) >> spacing.maybe \
145
155
  | spacing.maybe >> identifier.as(:identifier) >> separator.as(:separator) >> spacing.maybe \
146
156
  | spacing.maybe >> identifier.as(:identifier) >> spacing.maybe \
147
- | spacing.maybe >> separator.as(:separator) >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
157
+ | spacing.maybe >> separator.as(:separator) >> str('/') >> attribute_regexp.as(:attribute_regexp) >> str('/') >> spacing.maybe \
148
158
  | spacing.maybe >> separator.as(:separator) >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
149
159
  | spacing.maybe >> separator.as(:separator) >> attribute.as(:attribute) >> spacing.maybe \
150
- | spacing.maybe >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
160
+ | spacing.maybe >> str('/') >> attribute_regexp.as(:attribute_regexp) >> str('/') >> spacing.maybe \
151
161
  | spacing.maybe >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
152
162
  | spacing.maybe >> attribute.as(:attribute) >> spacing.maybe \
153
163
  )
154
164
  }
155
165
  rule(:identifier_regexp) {
156
- ( str('/') >> (str('/').absent? >> any).repeat(0) >> str('/') \
166
+ ( (str('/').absent? >> any).repeat(0) \
157
167
  )
158
168
  }
159
169
  rule(:identifier_glob) {
@@ -170,7 +180,7 @@ module Hotdog
170
180
  )
171
181
  }
172
182
  rule(:attribute_regexp) {
173
- ( str('/') >> (str('/').absent? >> any).repeat(0) >> str('/') \
183
+ ( (str('/').absent? >> any).repeat(0) \
174
184
  )
175
185
  }
176
186
  rule(:attribute_glob) {
@@ -198,55 +208,75 @@ module Hotdog
198
208
  UnaryExpressionNode.new(unary_op, expression)
199
209
  }
200
210
  rule(identifier_regexp: simple(:identifier_regexp), separator: simple(:separator), attribute_regexp: simple(:attribute_regexp)) {
201
- TagRegexpExpressionNode.new(identifier_regexp.to_s, attribute_regexp.to_s, separator)
211
+ if "host" == identifier_regexp
212
+ RegexpHostNode.new(attribute_regexp.to_s, separator)
213
+ else
214
+ RegexpTagNode.new(identifier_regexp.to_s, attribute_regexp.to_s, separator)
215
+ end
202
216
  }
203
217
  rule(identifier_regexp: simple(:identifier_regexp), separator: simple(:separator)) {
204
- TagRegexpExpressionNode.new(identifier_regexp.to_s, nil, nil)
218
+ RegexpTagNameNode.new(identifier_regexp.to_s, separator)
205
219
  }
206
220
  rule(identifier_regexp: simple(:identifier_regexp)) {
207
- TagRegexpExpressionNode.new(identifier_regexp.to_s, nil, nil)
221
+ RegexpNode.new(identifier_regexp.to_s)
208
222
  }
209
223
  rule(identifier_glob: simple(:identifier_glob), separator: simple(:separator), attribute_glob: simple(:attribute_glob)) {
210
- TagGlobExpressionNode.new(identifier_glob.to_s, attribute_glob.to_s, separator)
224
+ if "host" == identifier_glob
225
+ GlobHostNode.new(attribute_glob.to_s, separator)
226
+ else
227
+ GlobTagNode.new(identifier_glob.to_s, attribute_glob.to_s, separator)
228
+ end
211
229
  }
212
230
  rule(identifier_glob: simple(:identifier_glob), separator: simple(:separator), attribute: simple(:attribute)) {
213
- TagGlobExpressionNode.new(identifier_glob.to_s, attribute.to_s, separator)
231
+ if "host" == identifier_glob
232
+ GlobHostNode.new(attribute.to_s, separator)
233
+ else
234
+ GlobTagNode.new(identifier.to_s, attribute.to_s, separator)
235
+ end
214
236
  }
215
237
  rule(identifier_glob: simple(:identifier_glob), separator: simple(:separator)) {
216
- TagGlobExpressionNode.new(identifier_glob.to_s, nil, separator)
238
+ GlobTagNameNode.new(identifier_glob.to_s, separator)
217
239
  }
218
240
  rule(identifier_glob: simple(:identifier_glob)) {
219
- TagGlobExpressionNode.new(identifier_glob.to_s, nil, nil)
241
+ GlobNode.new(identifier_glob.to_s)
220
242
  }
221
243
  rule(identifier: simple(:identifier), separator: simple(:separator), attribute_glob: simple(:attribute_glob)) {
222
- TagGlobExpressionNode.new(identifier.to_s, attribute_glob.to_s, separator)
244
+ if "host" == identifier
245
+ GlobHostNode.new(attribute_glob.to_s, separator)
246
+ else
247
+ GlobTagNode.new(identifier.to_s, attribute_glob.to_s, separator)
248
+ end
223
249
  }
224
250
  rule(identifier: simple(:identifier), separator: simple(:separator), attribute: simple(:attribute)) {
225
- TagExpressionNode.new(identifier.to_s, attribute.to_s, separator)
251
+ if "host" == identifier
252
+ StringHostNode.new(attribute.to_s, separator)
253
+ else
254
+ StringTagNode.new(identifier.to_s, attribute.to_s, separator)
255
+ end
226
256
  }
227
257
  rule(identifier: simple(:identifier), separator: simple(:separator)) {
228
- TagExpressionNode.new(identifier.to_s, nil, separator)
258
+ StringTagNameNode.new(identifier.to_s, separator)
229
259
  }
230
260
  rule(identifier: simple(:identifier)) {
231
- TagExpressionNode.new(identifier.to_s, nil, nil)
261
+ StringNode.new(identifier.to_s)
232
262
  }
233
263
  rule(separator: simple(:separator), attribute_regexp: simple(:attribute_regexp)) {
234
- TagRegexpExpressionNode.new(nil, attribute_regexp.to_s, separator)
264
+ RegexpTagValueNode.new(attribute_regexp.to_s, separator)
235
265
  }
236
266
  rule(attribute_regexp: simple(:attribute_regexp)) {
237
- TagRegexpExpressionNode.new(nil, attribute_regexp.to_s, nil)
267
+ RegexpTagValueNode.new(attribute_regexp.to_s)
238
268
  }
239
269
  rule(separator: simple(:separator), attribute_glob: simple(:attribute_glob)) {
240
- TagGlobExpressionNode.new(nil, attribute_glob.to_s, separator)
270
+ GlobTagValueNode.new(attribute_glob.to_s, separator)
241
271
  }
242
272
  rule(attribute_glob: simple(:attribute_glob)) {
243
- TagGlobExpressionNode.new(nil, attribute_glob.to_s, nil)
273
+ GlobTagValueNode.new(attribute_glob.to_s)
244
274
  }
245
275
  rule(separator: simple(:separator), attribute: simple(:attribute)) {
246
- TagExpressionNode.new(nil, attribute.to_s, separator)
276
+ StringTagValueNode.new(attribute.to_s, separator)
247
277
  }
248
278
  rule(attribute: simple(:attribute)) {
249
- TagExpressionNode.new(nil, attribute.to_s, nil)
279
+ StringTagValueNode.new(attribute.to_s)
250
280
  }
251
281
  end
252
282
 
@@ -262,6 +292,106 @@ module Hotdog
262
292
  def dump(options={})
263
293
  {}
264
294
  end
295
+
296
+ def intermediates()
297
+ []
298
+ end
299
+
300
+ def leafs()
301
+ [self]
302
+ end
303
+ end
304
+
305
+ class UnaryExpressionNode < ExpressionNode
306
+ attr_reader :op, :expression
307
+
308
+ def initialize(op, expression)
309
+ case op
310
+ when "!", "~", /\Anot\z/i
311
+ @op = :NOT
312
+ else
313
+ raise(SyntaxError.new("unknown unary operator: #{@op.inspect}"))
314
+ end
315
+ @expression = expression
316
+ end
317
+
318
+ def evaluate(environment, options={})
319
+ case @op
320
+ when :NOT
321
+ values = @expression.evaluate(environment, options).tap do |values|
322
+ environment.logger.debug("expr: #{values.length} value(s)")
323
+ end
324
+ if values.empty?
325
+ environment.execute("SELECT id FROM hosts").map { |row| row.first }.tap do |values|
326
+ environment.logger.debug("NOT expr: #{values.length} value(s)")
327
+ end
328
+ else
329
+ # workaround for "too many terms in compound SELECT"
330
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
331
+ (min / (SQLITE_LIMIT_COMPOUND_SELECT - 2)).upto(max / (SQLITE_LIMIT_COMPOUND_SELECT - 2)).flat_map { |i|
332
+ range = ((SQLITE_LIMIT_COMPOUND_SELECT - 2) * i)...((SQLITE_LIMIT_COMPOUND_SELECT - 2) * (i + 1))
333
+ selected = values.select { |n| range === n }
334
+ q = "SELECT id FROM hosts " \
335
+ "WHERE ? <= id AND id < ? AND id NOT IN (%s);"
336
+ environment.execute(q % selected.map { "?" }.join(", "), [range.first, range.last] + selected).map { |row| row.first }
337
+ }.tap do |values|
338
+ environment.logger.debug("NOT expr: #{values.length} value(s)")
339
+ end
340
+ end
341
+ else
342
+ []
343
+ end
344
+ end
345
+
346
+ def optimize(options={})
347
+ @expression = @expression.optimize(options)
348
+ case op
349
+ when :NOT
350
+ optimize1(options)
351
+ else
352
+ self
353
+ end
354
+ end
355
+
356
+ def ==(other)
357
+ self.class === other and @op == other.op and @expression == other.expression
358
+ end
359
+
360
+ def dump(options={})
361
+ {unary_op: @op.to_s, expression: @expression.dump(options)}
362
+ end
363
+
364
+ def intermediates()
365
+ [self] + @expression.intermediates
366
+ end
367
+
368
+ def leafs()
369
+ @expression.leafs
370
+ end
371
+
372
+ private
373
+ def optimize1(options={})
374
+ case op
375
+ when :NOT
376
+ if UnaryExpressionNode === expression and expression.op == :NOT
377
+ expression.expression
378
+ else
379
+ if TagExpressionNode === expression
380
+ q = expression.maybe_query(options)
381
+ v = expression.condition_values(options)
382
+ if q and v.length <= SQLITE_LIMIT_COMPOUND_SELECT
383
+ QueryExpressionNode.new("SELECT id AS host_id FROM hosts EXCEPT #{q.sub(/\s*;\s*\z/, "")};", v)
384
+ else
385
+ self
386
+ end
387
+ else
388
+ self
389
+ end
390
+ end
391
+ else
392
+ self
393
+ end
394
+ end
265
395
  end
266
396
 
267
397
  class BinaryExpressionNode < ExpressionNode
@@ -386,10 +516,30 @@ module Hotdog
386
516
  if left == right
387
517
  left
388
518
  else
389
- optimize1(options)
519
+ if MultinaryExpressionNode === left
520
+ if left.op == op
521
+ left.merge(right, fallback: self)
522
+ else
523
+ optimize1(options)
524
+ end
525
+ else
526
+ if MultinaryExpressionNode === right
527
+ if right.op == op
528
+ right.merge(left, fallback: self)
529
+ else
530
+ optimize1(options)
531
+ end
532
+ else
533
+ MultinaryExpressionNode.new(op, [left, right], fallback: self)
534
+ end
535
+ end
390
536
  end
391
537
  when :XOR
392
- optimize1(options)
538
+ if left == right
539
+ []
540
+ else
541
+ optimize1(options)
542
+ end
393
543
  else
394
544
  self
395
545
  end
@@ -400,30 +550,37 @@ module Hotdog
400
550
  end
401
551
 
402
552
  def dump(options={})
403
- {left: @left.dump(options), op: @op.to_s, right: @right.dump(options)}
553
+ {left: @left.dump(options), binary_op: @op.to_s, right: @right.dump(options)}
554
+ end
555
+
556
+ def intermediates()
557
+ [self] + @left.intermediates + @right.intermediates
558
+ end
559
+
560
+ def leafs()
561
+ @left.leafs + @right.leafs
404
562
  end
405
563
 
406
564
  private
407
565
  def optimize1(options)
408
566
  if TagExpressionNode === left and TagExpressionNode === right
409
- lhs = left.plan(options)
410
- rhs = right.plan(options)
411
- if lhs and rhs and lhs[1].length + rhs[1].length <= SQLITE_LIMIT_COMPOUND_SELECT
567
+ lq = left.maybe_query(options)
568
+ lv = left.condition_values(options)
569
+ rq = right.maybe_query(options)
570
+ rv = right.condition_values(options)
571
+ if lq and rq and lv.length + rv.length <= SQLITE_LIMIT_COMPOUND_SELECT
412
572
  case op
413
573
  when :AND
414
- q = "SELECT host_id FROM ( #{lhs[0].sub(/\s*;\s*\z/, "")} ) " \
415
- "INTERSECT #{rhs[0].sub(/\s*;\s*\z/, "")};"
416
- QueryExpressionNode.new(q, lhs[1] + rhs[1], fallback: self)
574
+ q = "#{lq.sub(/\s*;\s*\z/, "")} INTERSECT #{rq.sub(/\s*;\s*\z/, "")};"
575
+ QueryExpressionNode.new(q, lv + rv, fallback: self)
417
576
  when :OR
418
- q = "SELECT host_id FROM ( #{lhs[0].sub(/\s*;\s*\z/, "")} ) " \
419
- "UNION #{rhs[0].sub(/\s*;\s*\z/, "")};"
420
- QueryExpressionNode.new(q, lhs[1] + rhs[1], fallback: self)
577
+ q = "#{lq.sub(/\s*;\s*\z/, "")} UNION #{rq.sub(/\s*;\s*\z/, "")};"
578
+ QueryExpressionNode.new(q, lv + rv, fallback: self)
421
579
  when :XOR
422
- q = "SELECT host_id FROM ( #{lhs[0].sub(/\s*;\s*\z/, "")} ) " \
423
- "UNION #{rhs[0].sub(/\s*;\s*\z/, "")} " \
424
- "EXCEPT SELECT host_id FROM ( #{lhs[0].sub(/\s*;\s*\z/, "")} ) " \
425
- "INTERSECT #{rhs[0].sub(/\s*;\s*\z/, "")};"
426
- QueryExpressionNode.new(q, lhs[1] + rhs[1], fallback: self)
580
+ q = "#{lq.sub(/\s*;\s*\z/, "")} UNION #{rq.sub(/\s*;\s*\z/, "")} " \
581
+ "EXCEPT #{lq.sub(/\s*;\s*\z/, "")} " \
582
+ "INTERSECT #{rq.sub(/\s*;\s*\z/, "")};"
583
+ QueryExpressionNode.new(q, lv + rv, fallback: self)
427
584
  else
428
585
  self
429
586
  end
@@ -436,88 +593,71 @@ module Hotdog
436
593
  end
437
594
  end
438
595
 
439
- class UnaryExpressionNode < ExpressionNode
440
- attr_reader :op, :expression
596
+ class MultinaryExpressionNode < ExpressionNode
597
+ attr_reader :op, :expressions
441
598
 
442
- def initialize(op, expression)
599
+ def initialize(op, expressions, options={})
443
600
  case op
444
- when "!", "~", /\Anot\z/i
445
- @op = :NOT
601
+ when :OR
602
+ @op = :OR
446
603
  else
447
- raise(SyntaxError.new("unknown unary operator: #{@op.inspect}"))
604
+ raise(SyntaxError.new("unknown multinary operator: #{op.inspect}"))
605
+ end
606
+ if SQLITE_LIMIT_COMPOUND_SELECT < expressions.length
607
+ raise(ArgumentError.new("expressions limit exceeded: #{expressions.length} for #{SQLITE_LIMIT_COMPOUND_SELECT}"))
608
+ end
609
+ @expressions = expressions
610
+ @fallback = options[:fallback]
611
+ end
612
+
613
+ def merge(other, options={})
614
+ if MultinaryExpressionNode === other and op == other.op
615
+ MultinaryExpressionNode.new(op, expressions + other.expressions, options)
616
+ else
617
+ MultinaryExpressionNode.new(op, expressions + [other], options)
448
618
  end
449
- @expression = expression
450
619
  end
451
620
 
452
621
  def evaluate(environment, options={})
453
622
  case @op
454
- when :NOT
455
- values = @expression.evaluate(environment, options).tap do |values|
456
- environment.logger.debug("expr: #{values.length} value(s)")
457
- end
458
- if values.empty?
459
- environment.execute("SELECT id FROM hosts").map { |row| row.first }.tap do |values|
460
- environment.logger.debug("NOT expr: #{values.length} value(s)")
623
+ when :OR
624
+ if expressions.all? { |expression| TagExpressionNode === expression }
625
+ if query_without_condition = expressions.first.maybe_query_without_condition(options)
626
+ q = query_without_condition.sub(/\s*;\s*\z/, "") + " WHERE " + expressions.map { |expression| "( %s )" % expression.condition(options) }.join(" OR ") + ";"
627
+ condition_length = expressions.first.condition_values(options).length
628
+ values = expressions.each_slice(SQLITE_LIMIT_COMPOUND_SELECT / condition_length).flat_map { |expressions|
629
+ environment.execute(q, expressions.flat_map { |expression| expression.condition_values(options) }).map { |row| row.first }
630
+ }
631
+ else
632
+ values = []
461
633
  end
462
634
  else
463
- # workaround for "too many terms in compound SELECT"
464
- min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
465
- (min / (SQLITE_LIMIT_COMPOUND_SELECT - 2)).upto(max / (SQLITE_LIMIT_COMPOUND_SELECT - 2)).flat_map { |i|
466
- range = ((SQLITE_LIMIT_COMPOUND_SELECT - 2) * i)...((SQLITE_LIMIT_COMPOUND_SELECT - 2) * (i + 1))
467
- selected = values.select { |n| range === n }
468
- q = "SELECT id FROM hosts " \
469
- "WHERE ? <= id AND id < ? AND id NOT IN (%s);"
470
- environment.execute(q % selected.map { "?" }.join(", "), [range.first, range.last] + selected).map { |row| row.first }
471
- }.tap do |values|
472
- environment.logger.debug("NOT expr: #{values.length} value(s)")
473
- end
635
+ values = []
474
636
  end
475
637
  else
476
- []
638
+ values = []
477
639
  end
478
- end
479
-
480
- def optimize(options={})
481
- @expression = @expression.optimize(options)
482
- case op
483
- when :NOT
484
- optimize1(options)
640
+ if values.empty?
641
+ if @fallback
642
+ @fallback.evaluate(environment, options={})
643
+ else
644
+ []
645
+ end
485
646
  else
486
- self
647
+ values
487
648
  end
488
649
  end
489
650
 
490
- def ==(other)
491
- self.class === other and @op == other.op and @expression == other.expression
651
+ def dump(optinos={})
652
+ {multinary_op: @op.to_s, expressions: expressions.map { |expression| expression.dump }}
492
653
  end
493
654
 
494
- def dump(options={})
495
- {op: @op.to_s, expression: @expression.dump(options)}
655
+ def intermediates()
656
+ [self] + @expression.flat_map { |expression| expression.intermediates }
496
657
  end
497
658
 
498
- private
499
- def optimize1(options={})
500
- case op
501
- when :NOT
502
- if UnaryExpressionNode === expression and expression.op == :NOT
503
- expression.expression
504
- else
505
- if TagExpressionNode === expression
506
- expr = expression.plan(options)
507
- if expr and expr[1].length <= SQLITE_LIMIT_COMPOUND_SELECT
508
- q = "SELECT id AS host_id FROM hosts " \
509
- "EXCEPT #{expr[0].sub(/\s*;\s*\z/, "")};"
510
- QueryExpressionNode.new(q, expr[1])
511
- else
512
- self
513
- end
514
- else
515
- self
516
- end
517
- end
518
- else
519
- self
520
- end
659
+ def leafs()
660
+ @expressions.flat_map { |expression| expression.leafs }
521
661
  end
522
662
  end
523
663
 
@@ -531,7 +671,11 @@ module Hotdog
531
671
  def evaluate(environment, options={})
532
672
  values = environment.execute(@query, @args).map { |row| row.first }
533
673
  if values.empty? and @fallback
534
- @fallback.evaluate(environment, options)
674
+ @fallback.evaluate(environment, options).tap do |values|
675
+ if values.empty?
676
+ environment.logger.info("no result: #{self.dump.inspect}")
677
+ end
678
+ end
535
679
  else
536
680
  values
537
681
  end
@@ -567,49 +711,47 @@ module Hotdog
567
711
  !(separator.nil? or separator.to_s.empty?)
568
712
  end
569
713
 
570
- def plan(options={})
571
- if identifier?
572
- if attribute?
573
- case identifier
574
- when /\Ahost\z/i
575
- q = "SELECT hosts.id AS host_id FROM hosts " \
576
- "WHERE hosts.name = ?;"
577
- [q, [attribute]]
578
- else
579
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
580
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
581
- "WHERE tags.name = ? AND tags.value = ?;"
582
- [q, [identifier, attribute]]
583
- end
584
- else
585
- if separator?
586
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
587
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
588
- "WHERE tags.name = ?;"
589
- [q, [identifier]]
590
- else
591
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
592
- "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
593
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
594
- "WHERE hosts.name = ? OR tags.name = ? OR tags.value = ?;"
595
- [q, [identifier, identifier, identifier]]
596
- end
597
- end
714
+ def maybe_query(options={})
715
+ if query_without_condition = maybe_query_without_condition(options)
716
+ query_without_condition.sub(/\s*;\s*\z/, "") + " WHERE " + condition(options) + ";"
598
717
  else
599
- if attribute?
600
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
601
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
602
- "WHERE tags.value = ?;"
603
- [q, [attribute]]
718
+ nil
719
+ end
720
+ end
721
+
722
+ def maybe_query_without_condition(options={})
723
+ tables = condition_tables(options)
724
+ if tables.empty?
725
+ nil
726
+ else
727
+ case tables.sort
728
+ when [:hosts]
729
+ "SELECT hosts.id AS host_id FROM hosts;"
730
+ when [:hosts, :tags]
731
+ "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;"
732
+ when [:tags]
733
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags INNER JOIN tags ON hosts_tags.tag_id = tags.id;"
604
734
  else
605
- nil
735
+ raise(NotImplementedError.new("unknown tables: #{tables.join(", ")}"))
606
736
  end
607
737
  end
608
738
  end
609
739
 
740
+ def condition(options={})
741
+ raise NotImplementedError
742
+ end
743
+
744
+ def condition_tables(options={})
745
+ raise NotImplementedError
746
+ end
747
+
748
+ def condition_values(options={})
749
+ raise NotImplementedError
750
+ end
751
+
610
752
  def evaluate(environment, options={})
611
- if q = plan(options)
612
- values = environment.execute(*q).map { |row| row.first }
753
+ if q = maybe_query(options)
754
+ values = environment.execute(q, condition_values(options)).map { |row| row.first }
613
755
  if values.empty?
614
756
  if options[:did_fallback]
615
757
  []
@@ -618,12 +760,28 @@ module Hotdog
618
760
  # avoid optimizing @fallback to prevent infinite recursion
619
761
  values = @fallback.evaluate(environment, options.merge(did_fallback: true))
620
762
  if values.empty?
621
- reload(environment, options)
763
+ if reload(environment, options)
764
+ evaluate(environment, options).tap do |values|
765
+ if values.empty?
766
+ environment.logger.info("no result: #{self.dump.inspect}")
767
+ end
768
+ end
769
+ else
770
+ []
771
+ end
622
772
  else
623
773
  values
624
774
  end
625
775
  else
626
- reload(environment, options)
776
+ if reload(environment, options)
777
+ evaluate(environment, options).tap do |values|
778
+ if values.empty?
779
+ environment.logger.info("no result: #{self.dump.inspect}")
780
+ end
781
+ end
782
+ else
783
+ []
784
+ end
627
785
  end
628
786
  end
629
787
  else
@@ -640,158 +798,378 @@ module Hotdog
640
798
 
641
799
  def optimize(options={})
642
800
  # fallback to glob expression
643
- if identifier?
644
- prefix = (identifier.start_with?("*")) ? "" : "*"
645
- suffix = (identifier.end_with?("*")) ? "" : "*"
646
- identifier_glob = prefix + identifier.gsub(/[-.\/_]/, "?") + suffix
647
- else
648
- identifier_glob = nil
649
- end
650
- if attribute?
651
- prefix = (attribute.start_with?("*")) ? "" : "*"
652
- suffix = (attribute.end_with?("*")) ? "" : "*"
653
- attribute_glob = prefix + attribute.gsub(/[-.\/_]/, "?") + suffix
654
- else
655
- attribute_glob = nil
656
- end
657
- if (identifier? and identifier != identifier_glob) or (attribute? and attribute != attribute_glob)
658
- @fallback = TagGlobExpressionNode.new(identifier_glob, attribute_glob, separator)
659
- end
801
+ @fallback = maybe_fallback(options)
660
802
  self
661
803
  end
662
804
 
805
+ def to_glob(s)
806
+ (s.start_with?("*") ? "" : "*") + s.gsub(/[-.\/_]/, "?") + (s.end_with?("*") ? "" : "*")
807
+ end
808
+
809
+ def maybe_glob(s)
810
+ s ? to_glob(s.to_s) : nil
811
+ end
812
+
663
813
  def reload(environment, options={})
664
- ttl = options.fetch(:ttl, 1)
665
- if 0 < ttl
814
+ $did_reload ||= false
815
+ if $did_reload
816
+ false
817
+ else
818
+ $did_reload = true
666
819
  environment.logger.info("force reloading all hosts and tags.")
667
820
  environment.reload(force: true)
668
- self.class.new(identifier, attribute, separator).optimize(options).evaluate(environment, options.merge(ttl: ttl-1))
669
- else
670
- []
821
+ true
671
822
  end
672
823
  end
673
824
 
674
825
  def dump(options={})
675
826
  data = {}
676
- data[:identifier] = @identifier.to_s if @identifier
677
- data[:separator] = @separator.to_s if @separator
678
- data[:attribute] = @attribute.to_s if @attribute
827
+ data[:identifier] = identifier.to_s if identifier
828
+ data[:separator] = separator.to_s if separator
829
+ data[:attribute] = attribute.to_s if attribute
679
830
  data[:fallback ] = @fallback.dump(options) if @fallback
680
831
  data
681
832
  end
833
+
834
+ def maybe_fallback(options={})
835
+ nil
836
+ end
682
837
  end
683
838
 
684
- class TagGlobExpressionNode < TagExpressionNode
685
- def plan(options={})
686
- if identifier?
687
- if attribute?
688
- case identifier
689
- when /\Ahost\z/i
690
- q = "SELECT hosts.id AS host_id FROM hosts " \
691
- "WHERE LOWER(hosts.name) GLOB LOWER(?);"
692
- [q, [attribute]]
693
- else
694
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
695
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
696
- "WHERE LOWER(tags.name) GLOB LOWER(?) AND LOWER(tags.value) GLOB LOWER(?);"
697
- [q, [identifier, attribute]]
698
- end
699
- else
700
- if separator?
701
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
702
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
703
- "WHERE LOWER(tags.name) GLOB LOWER(?);"
704
- [q, [identifier]]
705
- else
706
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
707
- "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
708
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
709
- "WHERE LOWER(hosts.name) GLOB LOWER(?) OR LOWER(tags.name) GLOB LOWER(?) OR LOWER(tags.value) GLOB LOWER(?);"
710
- [q, [identifier, identifier, identifier]]
711
- end
712
- end
713
- else
714
- if attribute?
715
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
716
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
717
- "WHERE LOWER(tags.value) GLOB LOWER(?);"
718
- [q, [attribute]]
719
- else
720
- nil
721
- end
722
- end
839
+ class StringExpressionNode < TagExpressionNode
840
+ end
841
+
842
+ class StringHostNode < StringExpressionNode
843
+ def initialize(attribute, separator=nil)
844
+ super("host", attribute, separator)
845
+ end
846
+
847
+ def condition(options={})
848
+ "hosts.name = ?"
849
+ end
850
+
851
+ def condition_tables(options={})
852
+ [:hosts]
853
+ end
854
+
855
+ def condition_values(options={})
856
+ [attribute]
857
+ end
858
+
859
+ def maybe_fallback(options={})
860
+ GlobHostNode.new(maybe_glob(attribute), separator)
861
+ end
862
+ end
863
+
864
+ class StringTagNode < StringExpressionNode
865
+ def initialize(identifier, attribute, separator=nil)
866
+ super(identifier, attribute, separator)
723
867
  end
724
868
 
869
+ def condition(options={})
870
+ "tags.name = ? AND tags.value = ?"
871
+ end
872
+
873
+ def condition_tables(options={})
874
+ [:tags]
875
+ end
876
+
877
+ def condition_values(options={})
878
+ [identifier, attribute]
879
+ end
880
+
881
+ def maybe_fallback(options={})
882
+ GlobTagNode.new(maybe_glob(identifier), maybe_glob(attribute), separator)
883
+ end
884
+ end
885
+
886
+ class StringTagNameNode < StringExpressionNode
887
+ def initialize(identifier, separator=nil)
888
+ super(identifier, nil, separator)
889
+ end
890
+
891
+ def condition(options={})
892
+ "tags.name = ?"
893
+ end
894
+
895
+ def condition_tables(options={})
896
+ [:tags]
897
+ end
898
+
899
+ def condition_values(options={})
900
+ [identifier]
901
+ end
902
+
903
+ def maybe_fallback(options={})
904
+ GlobTagNameNode.new(maybe_glob(identifier), separator)
905
+ end
906
+ end
907
+
908
+ class StringTagValueNode < StringExpressionNode
909
+ def initialize(attribute, separator=nil)
910
+ super(nil, attribute, separator)
911
+ end
912
+
913
+ def condition(options={})
914
+ "tags.value = ?"
915
+ end
916
+
917
+ def condition_tables(options={})
918
+ [:tags]
919
+ end
920
+
921
+ def condition_values(options={})
922
+ [attribute]
923
+ end
924
+
925
+ def maybe_fallback(options={})
926
+ GlobTagValueNode.new(maybe_glob(attribute), separator)
927
+ end
928
+ end
929
+
930
+ class StringNode < StringExpressionNode
931
+ def initialize(identifier, separator=nil)
932
+ super(identifier, nil, separator)
933
+ end
934
+
935
+ def condition(options={})
936
+ "hosts.name = ? OR tags.name = ? OR tags.value = ?"
937
+ end
938
+
939
+ def condition_tables(options={})
940
+ [:hosts, :tags]
941
+ end
942
+
943
+ def condition_values(options={})
944
+ [identifier, identifier, identifier]
945
+ end
946
+
947
+ def maybe_fallback(options={})
948
+ GlobNode.new(maybe_glob(identifier), separator)
949
+ end
950
+ end
951
+
952
+ class GlobExpressionNode < TagExpressionNode
725
953
  def dump(options={})
726
954
  data = {}
727
- data[:identifier_glob] = @identifier.to_s if @identifier
728
- data[:separator] = @separator.to_s if @separator
729
- data[:attribute_glob] = @attribute.to_s if @attribute
955
+ data[:identifier_glob] = identifier.to_s if identifier
956
+ data[:separator] = separator.to_s if separator
957
+ data[:attribute_glob] = attribute.to_s if attribute
730
958
  data[:fallback] = @fallback.dump(options) if @fallback
731
959
  data
732
960
  end
733
961
  end
734
962
 
735
- class TagRegexpExpressionNode < TagExpressionNode
963
+ class GlobHostNode < GlobExpressionNode
964
+ def initialize(attribute, separator=nil)
965
+ super("host", attribute, separator)
966
+ end
967
+
968
+ def condition(options={})
969
+ "LOWER(hosts.name) GLOB LOWER(?)"
970
+ end
971
+
972
+ def condition_tables(options={})
973
+ [:hosts]
974
+ end
975
+
976
+ def condition_values(options={})
977
+ [attribute]
978
+ end
979
+
980
+ def maybe_fallback(options={})
981
+ GlobHostNode.new(maybe_glob(attribute), separator)
982
+ end
983
+ end
984
+
985
+ class GlobTagNode < GlobExpressionNode
736
986
  def initialize(identifier, attribute, separator=nil)
737
- identifier = identifier.sub(%r{\A/(.*)/\z}) { $1 } if identifier
738
- attribute = attribute.sub(%r{\A/(.*)/\z}) { $1 } if attribute
739
987
  super(identifier, attribute, separator)
740
988
  end
741
989
 
742
- def plan(options={})
743
- if identifier?
744
- if attribute?
745
- case identifier
746
- when /\Ahost\z/i
747
- q = "SELECT hosts.id AS host_id FROM hosts " \
748
- "WHERE hosts.name REGEXP ?;"
749
- [q, [attribute]]
750
- else
751
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
752
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
753
- "WHERE tags.name REGEXP ? AND tags.value REGEXP ?;"
754
- [q, [identifier, attribute]]
755
- end
756
- else
757
- if separator?
758
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
759
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
760
- "WHERE tags.name REGEXP ?;"
761
- [q, [identifier]]
762
- else
763
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
764
- "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
765
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
766
- "WHERE hosts.name REGEXP ? OR tags.name REGEXP ? OR tags.value REGEXP ?;"
767
- [q, [identifier, identifier, identifier]]
768
- end
769
- end
770
- else
771
- if attribute?
772
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
773
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
774
- "WHERE tags.value REGEXP ?;"
775
- [q, [attribute]]
776
- else
777
- nil
778
- end
779
- end
990
+ def condition(options={})
991
+ "LOWER(tags.name) GLOB LOWER(?) AND LOWER(tags.value) GLOB LOWER(?)"
780
992
  end
781
993
 
782
- def optimize(options={})
783
- self # disable fallback
994
+ def condition_tables(options={})
995
+ [:tags]
996
+ end
997
+
998
+ def condition_values(options={})
999
+ [identifier, attribute]
1000
+ end
1001
+
1002
+ def maybe_fallback(options={})
1003
+ GlobTagNode.new(maybe_glob(identifier), maybe_glob(attribute), separator)
1004
+ end
1005
+ end
1006
+
1007
+ class GlobTagNameNode < GlobExpressionNode
1008
+ def initialize(identifier, separator=nil)
1009
+ super(identifier, nil, separator)
1010
+ end
1011
+
1012
+ def condition(options={})
1013
+ "LOWER(tags.name) GLOB LOWER(?)"
1014
+ end
1015
+
1016
+ def condition_tables(options={})
1017
+ [:tags]
784
1018
  end
785
1019
 
1020
+ def condition_values(options={})
1021
+ [identifier]
1022
+ end
1023
+
1024
+ def maybe_fallback(options={})
1025
+ GlobTagNameNode.new(maybe_glob(identifier), separator)
1026
+ end
1027
+ end
1028
+
1029
+ class GlobTagValueNode < GlobExpressionNode
1030
+ def initialize(attribute, separator=nil)
1031
+ super(nil, attribute, separator)
1032
+ end
1033
+
1034
+ def condition(options={})
1035
+ "LOWER(tags.value) GLOB LOWER(?)"
1036
+ end
1037
+
1038
+ def condition_tables(options={})
1039
+ [:tags]
1040
+ end
1041
+
1042
+ def condition_values(options={})
1043
+ [attribute]
1044
+ end
1045
+
1046
+ def maybe_fallback(options={})
1047
+ GlobTagValueNode.new(maybe_glob(attribute), separator)
1048
+ end
1049
+ end
1050
+
1051
+ class GlobNode < GlobExpressionNode
1052
+ def initialize(identifier, separator=nil)
1053
+ super(identifier, nil, separator)
1054
+ end
1055
+
1056
+ def condition(options={})
1057
+ "LOWER(hosts.name) GLOB LOWER(?) OR LOWER(tags.name) GLOB LOWER(?) OR LOWER(tags.value) GLOB LOWER(?)"
1058
+ end
1059
+
1060
+ def condition_tables(options={})
1061
+ [:hosts, :tags]
1062
+ end
1063
+
1064
+ def condition_values(options={})
1065
+ [identifier, identifier, identifier]
1066
+ end
1067
+
1068
+ def maybe_fallback(options={})
1069
+ GlobNode.new(maybe_glob(identifier), separator)
1070
+ end
1071
+ end
1072
+
1073
+ class RegexpExpressionNode < TagExpressionNode
786
1074
  def dump(options={})
787
1075
  data = {}
788
- data[:identifier_regexp] = @identifier.to_s if @identifier
789
- data[:separator] = @separator.to_s if @separator
790
- data[:attribute_regexp] = @attribute.to_s if @attribute
1076
+ data[:identifier_regexp] = identifier.to_s if identifier
1077
+ data[:separator] = separator.to_s if separator
1078
+ data[:attribute_regexp] = attribute.to_s if attribute
791
1079
  data[:fallback] = @fallback.dump(options) if @fallback
792
1080
  data
793
1081
  end
794
1082
  end
1083
+
1084
+ class RegexpHostNode < RegexpExpressionNode
1085
+ def initialize(attribute, separator=nil)
1086
+ super("host", attribute, separator)
1087
+ end
1088
+
1089
+ def condition(options={})
1090
+ "hosts.name REGEXP ?"
1091
+ end
1092
+
1093
+ def condition_tables(options={})
1094
+ [:hosts]
1095
+ end
1096
+
1097
+ def condition_values(options={})
1098
+ [attribute]
1099
+ end
1100
+ end
1101
+
1102
+ class RegexpTagNode < RegexpExpressionNode
1103
+ def initialize(identifier, attribute, separator=nil)
1104
+ super(identifier, attribute, separator)
1105
+ end
1106
+
1107
+ def condition(options={})
1108
+ "tags.name REGEXP ? AND tags.value REGEXP ?"
1109
+ end
1110
+
1111
+ def condition_tables(options={})
1112
+ [:tags]
1113
+ end
1114
+
1115
+ def condition_values(options={})
1116
+ [identifier, attribute]
1117
+ end
1118
+ end
1119
+
1120
+ class RegexpTagNameNode < RegexpExpressionNode
1121
+ def initialize(identifier, separator=nil)
1122
+ super(identifier, nil, separator)
1123
+ end
1124
+
1125
+ def condition(options={})
1126
+ "tags.name REGEXP ?"
1127
+ end
1128
+
1129
+ def condition_tables(options={})
1130
+ [:tags]
1131
+ end
1132
+
1133
+ def condition_values(options={})
1134
+ [identifier]
1135
+ end
1136
+ end
1137
+
1138
+ class RegexpTagValueNode < RegexpExpressionNode
1139
+ def initialize(attribute, separator=nil)
1140
+ super(nil, attribute, separator)
1141
+ end
1142
+
1143
+ def condition(options={})
1144
+ "tags.value REGEXP ?"
1145
+ end
1146
+
1147
+ def condition_tables(options={})
1148
+ [:tags]
1149
+ end
1150
+
1151
+ def condition_values(options={})
1152
+ [attribute]
1153
+ end
1154
+ end
1155
+
1156
+ class RegexpNode < RegexpExpressionNode
1157
+ def initialize(identifier, separator=nil)
1158
+ super(identifier, separator)
1159
+ end
1160
+
1161
+ def condition(options={})
1162
+ "hosts.name REGEXP ? OR tags.name REGEXP ? OR tags.value REGEXP ?"
1163
+ end
1164
+
1165
+ def condition_tables(options={})
1166
+ [:hosts, :tags]
1167
+ end
1168
+
1169
+ def condition_values(options={})
1170
+ [identifier, identifier, identifier]
1171
+ end
1172
+ end
795
1173
  end
796
1174
  end
797
1175
  end