hotdog 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a7e366b9cd85e46e711e2ca82472a2d278db17a
4
- data.tar.gz: 502752fef200c4abf66772c31dc0b8f6e19a3538
3
+ metadata.gz: b2649bee0b66f057008d4276174d397b21ac87dd
4
+ data.tar.gz: 29d8d1620fab0110ac684b0059fc42e095c8c7bc
5
5
  SHA512:
6
- metadata.gz: 28f25b9273cf877a5c7ee381733f5645eec06e8c40ddb743c6e07becca306bcf270fc4f85a1bc4eba691316cc7d11aac67a77dbab8ed421c11137042f3f03bfa
7
- data.tar.gz: 587039692f3cc6cb5f7c7436487e8d1300dc54457468e6d19f2075bd9f1b0b6a3af21774ef6be44bf3bcb528749756531b5a490c17cb52f521b449a62fcb0373
6
+ metadata.gz: c7024f0e5e985792bd9fe1105b8fab9c65291a7bea66c6cd22f0e33fb7c838873649ae8ee3dc3b889f51882c68c3641ad75da37ff857cc3fc5e898d100abbb07
7
+ data.tar.gz: acf1ba39e5aecf2df6a9e2c7312c38ae3cea71a818d82dc681c68f6068a4cf5ab4f40f3ad8e33ccd69aac8b30f143faf0382ce52afb2264ae292a3cc39183965
@@ -50,7 +50,7 @@ module Hotdog
50
50
  exit(1)
51
51
  end
52
52
 
53
- result = evaluate(node, self).sort
53
+ result = evaluate(node, self)
54
54
  result, fields = get_hosts(result)
55
55
 
56
56
  if result.empty?
@@ -26,7 +26,7 @@ module Hotdog
26
26
  exit(1)
27
27
  end
28
28
 
29
- result = evaluate(node, self).sort
29
+ result = evaluate(node, self)
30
30
  if 0 < result.length
31
31
  _result, fields = get_hosts_with_search_tags(result, node)
32
32
  result = _result.take(search_options.fetch(:limit, _result.size))
@@ -73,7 +73,10 @@ module Hotdog
73
73
 
74
74
  def evaluate(data, environment)
75
75
  node = ExpressionTransformer.new.apply(data)
76
- node.optimize.evaluate(environment)
76
+ optimized = node.optimize.tap do |optimized|
77
+ logger.debug(JSON.pretty_generate(optimized.dump))
78
+ end
79
+ optimized.evaluate(environment)
77
80
  end
78
81
 
79
82
  class ExpressionParser < Parslet::Parser
@@ -101,6 +104,7 @@ module Hotdog
101
104
  ( expression4.as(:left) >> spacing.maybe >> str('&&').as(:binary_op) >> spacing.maybe >> expression.as(:right) \
102
105
  | expression4.as(:left) >> spacing.maybe >> str('||').as(:binary_op) >> spacing.maybe >> expression.as(:right) \
103
106
  | expression4.as(:left) >> spacing.maybe >> str('&').as(:binary_op) >> spacing.maybe >> expression.as(:right) \
107
+ | expression4.as(:left) >> spacing.maybe >> str('^').as(:binary_op) >> spacing.maybe >> expression.as(:right) \
104
108
  | expression4.as(:left) >> spacing.maybe >> str('|').as(:binary_op) >> spacing.maybe >> expression.as(:right) \
105
109
  | expression4 \
106
110
  )
@@ -116,8 +120,10 @@ module Hotdog
116
120
  rule(:binary_op) {
117
121
  ( str('AND') \
118
122
  | str('OR') \
123
+ | str('XOR') \
119
124
  | str('and') \
120
125
  | str('or') \
126
+ | str('xor') \
121
127
  )
122
128
  }
123
129
  rule(:unary_op) {
@@ -127,20 +133,20 @@ module Hotdog
127
133
  }
128
134
  rule(:atom) {
129
135
  ( spacing.maybe >> str('(') >> expression >> str(')') >> spacing.maybe \
130
- | spacing.maybe >> identifier_regexp.as(:identifier_regexp) >> separator >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
131
- | spacing.maybe >> identifier_regexp.as(:identifier_regexp) >> separator >> 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 \
132
138
  | spacing.maybe >> identifier_regexp.as(:identifier_regexp) >> spacing.maybe \
133
- | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
134
- | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator >> attribute.as(:attribute) >> spacing.maybe \
135
- | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator >> spacing.maybe \
139
+ | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator.as(:separator) >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
140
+ | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator.as(:separator) >> attribute.as(:attribute) >> spacing.maybe \
141
+ | spacing.maybe >> identifier_glob.as(:identifier_glob) >> separator.as(:separator) >> spacing.maybe \
136
142
  | spacing.maybe >> identifier_glob.as(:identifier_glob) >> spacing.maybe \
137
- | spacing.maybe >> identifier.as(:identifier) >> separator >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
138
- | spacing.maybe >> identifier.as(:identifier) >> separator >> attribute.as(:attribute) >> spacing.maybe \
139
- | spacing.maybe >> identifier.as(:identifier) >> separator >> spacing.maybe \
143
+ | spacing.maybe >> identifier.as(:identifier) >> separator.as(:separator) >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
144
+ | spacing.maybe >> identifier.as(:identifier) >> separator.as(:separator) >> attribute.as(:attribute) >> spacing.maybe \
145
+ | spacing.maybe >> identifier.as(:identifier) >> separator.as(:separator) >> spacing.maybe \
140
146
  | spacing.maybe >> identifier.as(:identifier) >> spacing.maybe \
141
- | spacing.maybe >> separator >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
142
- | spacing.maybe >> separator >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
143
- | spacing.maybe >> separator >> attribute.as(:attribute) >> spacing.maybe \
147
+ | spacing.maybe >> separator.as(:separator) >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
148
+ | spacing.maybe >> separator.as(:separator) >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
149
+ | spacing.maybe >> separator.as(:separator) >> attribute.as(:attribute) >> spacing.maybe \
144
150
  | spacing.maybe >> attribute_regexp.as(:attribute_regexp) >> spacing.maybe \
145
151
  | spacing.maybe >> attribute_glob.as(:attribute_glob) >> spacing.maybe \
146
152
  | spacing.maybe >> attribute.as(:attribute) >> spacing.maybe \
@@ -185,44 +191,62 @@ module Hotdog
185
191
  end
186
192
 
187
193
  class ExpressionTransformer < Parslet::Transform
188
- rule(:binary_op => simple(:binary_op), :left => simple(:left), :right => simple(:right)) {
194
+ rule(binary_op: simple(:binary_op), left: simple(:left), right: simple(:right)) {
189
195
  BinaryExpressionNode.new(binary_op, left, right)
190
196
  }
191
- rule(:unary_op => simple(:unary_op), :expression => simple(:expression)) {
197
+ rule(unary_op: simple(:unary_op), expression: simple(:expression)) {
192
198
  UnaryExpressionNode.new(unary_op, expression)
193
199
  }
194
- rule(:identifier_regexp => simple(:identifier_regexp), :attribute_regexp => simple(:attribute_regexp)) {
195
- TagRegexpExpressionNode.new(identifier_regexp.to_s, attribute_regexp.to_s)
200
+ 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)
202
+ }
203
+ rule(identifier_regexp: simple(:identifier_regexp), separator: simple(:separator)) {
204
+ TagRegexpExpressionNode.new(identifier_regexp.to_s, nil, nil)
205
+ }
206
+ rule(identifier_regexp: simple(:identifier_regexp)) {
207
+ TagRegexpExpressionNode.new(identifier_regexp.to_s, nil, nil)
208
+ }
209
+ 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)
196
211
  }
197
- rule(:identifier_regexp => simple(:identifier_regexp)) {
198
- TagRegexpExpressionNode.new(identifier_regexp.to_s, nil)
212
+ rule(identifier_glob: simple(:identifier_glob), separator: simple(:separator), attribute: simple(:attribute)) {
213
+ TagGlobExpressionNode.new(identifier_glob.to_s, attribute.to_s, separator)
199
214
  }
200
- rule(:identifier_glob => simple(:identifier_glob), :attribute_glob => simple(:attribute_glob)) {
201
- TagGlobExpressionNode.new(identifier_glob.to_s, attribute_glob.to_s)
215
+ rule(identifier_glob: simple(:identifier_glob), separator: simple(:separator)) {
216
+ TagGlobExpressionNode.new(identifier_glob.to_s, nil, separator)
202
217
  }
203
- rule(:identifier_glob => simple(:identifier_glob), :attribute => simple(:attribute)) {
204
- TagGlobExpressionNode.new(identifier_glob.to_s, attribute.to_s)
218
+ rule(identifier_glob: simple(:identifier_glob)) {
219
+ TagGlobExpressionNode.new(identifier_glob.to_s, nil, nil)
205
220
  }
206
- rule(:identifier_glob => simple(:identifier_glob)) {
207
- TagGlobExpressionNode.new(identifier_glob.to_s, nil)
221
+ rule(identifier: simple(:identifier), separator: simple(:separator), attribute_glob: simple(:attribute_glob)) {
222
+ TagGlobExpressionNode.new(identifier.to_s, attribute_glob.to_s, separator)
208
223
  }
209
- rule(:identifier => simple(:identifier), :attribute_glob => simple(:attribute_glob)) {
210
- TagGlobExpressionNode.new(identifier.to_s, attribute_glob.to_s)
224
+ rule(identifier: simple(:identifier), separator: simple(:separator), attribute: simple(:attribute)) {
225
+ TagExpressionNode.new(identifier.to_s, attribute.to_s, separator)
211
226
  }
212
- rule(:identifier => simple(:identifier), :attribute => simple(:attribute)) {
213
- TagExpressionNode.new(identifier.to_s, attribute.to_s)
227
+ rule(identifier: simple(:identifier), separator: simple(:separator)) {
228
+ TagExpressionNode.new(identifier.to_s, nil, separator)
214
229
  }
215
- rule(:identifier => simple(:identifier)) {
216
- TagExpressionNode.new(identifier.to_s, nil)
230
+ rule(identifier: simple(:identifier)) {
231
+ TagExpressionNode.new(identifier.to_s, nil, nil)
217
232
  }
218
- rule(:attribute_regexp => simple(:attribute_regexp)) {
219
- TagRegexpExpressionNode.new(nil, attribute_regexp.to_s)
233
+ rule(separator: simple(:separator), attribute_regexp: simple(:attribute_regexp)) {
234
+ TagRegexpExpressionNode.new(nil, attribute_regexp.to_s, separator)
220
235
  }
221
- rule(:attribute_glob => simple(:attribute_glob)) {
222
- TagGlobExpressionNode.new(nil, attribute_glob.to_s)
236
+ rule(attribute_regexp: simple(:attribute_regexp)) {
237
+ TagRegexpExpressionNode.new(nil, attribute_regexp.to_s, nil)
223
238
  }
224
- rule(:attribute => simple(:attribute)) {
225
- TagExpressionNode.new(nil, attribute.to_s)
239
+ rule(separator: simple(:separator), attribute_glob: simple(:attribute_glob)) {
240
+ TagGlobExpressionNode.new(nil, attribute_glob.to_s, separator)
241
+ }
242
+ rule(attribute_glob: simple(:attribute_glob)) {
243
+ TagGlobExpressionNode.new(nil, attribute_glob.to_s, nil)
244
+ }
245
+ rule(separator: simple(:separator), attribute: simple(:attribute)) {
246
+ TagExpressionNode.new(nil, attribute.to_s, separator)
247
+ }
248
+ rule(attribute: simple(:attribute)) {
249
+ TagExpressionNode.new(nil, attribute.to_s, nil)
226
250
  }
227
251
  end
228
252
 
@@ -234,6 +258,10 @@ module Hotdog
234
258
  def optimize(options={})
235
259
  self
236
260
  end
261
+
262
+ def dump(options={})
263
+ {}
264
+ end
237
265
  end
238
266
 
239
267
  class BinaryExpressionNode < ExpressionNode
@@ -245,6 +273,8 @@ module Hotdog
245
273
  @op = :AND
246
274
  when "||", "|", /\Aor\z/i
247
275
  @op = :OR
276
+ when "^", /\Axor\z/i
277
+ @op = :XOR
248
278
  else
249
279
  raise(SyntaxError.new("unknown binary operator: #{op.inspect}"))
250
280
  end
@@ -255,24 +285,87 @@ module Hotdog
255
285
  def evaluate(environment, options={})
256
286
  case @op
257
287
  when :AND
258
- left_values = @left.evaluate(environment)
259
- environment.logger.debug("lhs: #{left_values.length} value(s)")
288
+ left_values = @left.evaluate(environment, options).tap do |values|
289
+ environment.logger.debug("lhs: #{values.length} value(s)")
290
+ end
260
291
  if left_values.empty?
261
292
  []
262
293
  else
263
- right_values = @right.evaluate(environment)
264
- environment.logger.debug("rhs: #{right_values.length} value(s)")
265
- (left_values & right_values).tap do |values|
266
- environment.logger.debug("lhs AND rhs: #{values.length} value(s)")
294
+ right_values = @right.evaluate(environment, options).tap do |values|
295
+ environment.logger.debug("rhs: #{values.length} value(s)")
296
+ end
297
+ if right_values.empty?
298
+ []
299
+ else
300
+ # workaround for "too many terms in compound SELECT"
301
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
302
+ (min / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).upto(max / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).flat_map { |i|
303
+ range = (((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * i)...(((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * (i + 1))
304
+ left_selected = left_values.select { |n| range === n }
305
+ right_selected = right_values.select { |n| range === n }
306
+ q = "SELECT id FROM hosts " \
307
+ "WHERE ? <= id AND id < ? AND ( id IN (%s) AND id IN (%s) );"
308
+ environment.execute(q % [left_selected.map { "?" }.join(", "), right_selected.map { "?" }.join(", ")], [range.first, range.last] + left_selected + right_selected).map { |row| row.first }
309
+ }.tap do |values|
310
+ environment.logger.debug("lhs AND rhs: #{values.length} value(s)")
311
+ end
267
312
  end
268
313
  end
269
314
  when :OR
270
- left_values = @left.evaluate(environment)
271
- environment.logger.debug("lhs: #{left_values.length} value(s)")
272
- right_values = @right.evaluate(environment)
273
- environment.logger.debug("rhs: #{right_values.length} value(s)")
274
- (left_values | right_values).uniq.tap do |values|
275
- environment.logger.debug("lhs OR rhs: #{values.length} value(s)")
315
+ left_values = @left.evaluate(environment, options).tap do |values|
316
+ environment.logger.debug("lhs: #{values.length} value(s)")
317
+ end
318
+ right_values = @right.evaluate(environment, options).tap do |values|
319
+ environment.logger.debug("rhs: #{values.length} value(s)")
320
+ end
321
+ if left_values.empty?
322
+ right_values
323
+ else
324
+ if right_values.empty?
325
+ []
326
+ else
327
+ # workaround for "too many terms in compound SELECT"
328
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
329
+ (min / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).upto(max / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2)).flat_map { |i|
330
+ range = (((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * i)...(((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 2) * (i + 1))
331
+ left_selected = left_values.select { |n| range === n }
332
+ right_selected = right_values.select { |n| range === n }
333
+ q = "SELECT id FROM hosts " \
334
+ "WHERE ? <= id AND id < ? AND ( id IN (%s) OR id IN (%s) );"
335
+ environment.execute(q % [left_selected.map { "?" }.join(", "), right_selected.map { "?" }.join(", ")], [range.first, range.last] + left_selected + right_selected).map { |row| row.first }
336
+ }.tap do |values|
337
+ environment.logger.debug("lhs OR rhs: #{values.length} value(s)")
338
+ end
339
+ end
340
+ end
341
+ when :XOR
342
+ left_values = @left.evaluate(environment, options).tap do |values|
343
+ environment.logger.debug("lhs: #{values.length} value(s)")
344
+ end
345
+ right_values = @right.evaluate(environment, options).tap do |values|
346
+ environment.logger.debug("rhs: #{values.length} value(s)")
347
+ end
348
+ if left_values.empty?
349
+ right_values
350
+ else
351
+ if right_values.empty?
352
+ []
353
+ else
354
+ # workaround for "too many terms in compound SELECT"
355
+ min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
356
+ (min / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4)).upto(max / ((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4)).flat_map { |i|
357
+ range = (((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4) * i)...(((SQLITE_LIMIT_COMPOUND_SELECT - 2) / 4) * (i + 1))
358
+ left_selected = left_values.select { |n| range === n }
359
+ right_selected = right_values.select { |n| range === n }
360
+ q = "SELECT id FROM hosts " \
361
+ "WHERE ? <= id AND id < ? AND NOT (id IN (%s) AND id IN (%s)) AND ( id IN (%s) OR id IN (%s) );"
362
+ lq = left_selected.map { "?" }.join(", ")
363
+ rq = right_selected.map { "?" }.join(", ")
364
+ environment.execute(q % [lq, rq, lq, rq], [range.first, range.last] + left_selected + right_selected + left_selected + right_selected).map { |row| row.first }
365
+ }.tap do |values|
366
+ environment.logger.debug("lhs XOR rhs: #{values.length} value(s)")
367
+ end
368
+ end
276
369
  end
277
370
  else
278
371
  []
@@ -282,8 +375,21 @@ module Hotdog
282
375
  def optimize(options={})
283
376
  @left = @left.optimize(options)
284
377
  @right = @right.optimize(options)
285
- if @left == @right
286
- @left
378
+ case op
379
+ when :AND
380
+ if left == right
381
+ left
382
+ else
383
+ optimize1(options)
384
+ end
385
+ when :OR
386
+ if left == right
387
+ left
388
+ else
389
+ optimize1(options)
390
+ end
391
+ when :XOR
392
+ optimize1(options)
287
393
  else
288
394
  self
289
395
  end
@@ -292,6 +398,42 @@ module Hotdog
292
398
  def ==(other)
293
399
  self.class === other and @op == other.op and @left == other.left and @right == other.right
294
400
  end
401
+
402
+ def dump(options={})
403
+ {left: @left.dump(options), op: @op.to_s, right: @right.dump(options)}
404
+ end
405
+
406
+ private
407
+ def optimize1(options)
408
+ 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
412
+ case op
413
+ 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)
417
+ 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)
421
+ 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)
427
+ else
428
+ self
429
+ end
430
+ else
431
+ self
432
+ end
433
+ else
434
+ self
435
+ end
436
+ end
295
437
  end
296
438
 
297
439
  class UnaryExpressionNode < ExpressionNode
@@ -310,8 +452,9 @@ module Hotdog
310
452
  def evaluate(environment, options={})
311
453
  case @op
312
454
  when :NOT
313
- values = @expression.evaluate(environment).sort
314
- environment.logger.debug("expr: #{values.length} value(s)")
455
+ values = @expression.evaluate(environment, options).tap do |values|
456
+ environment.logger.debug("expr: #{values.length} value(s)")
457
+ end
315
458
  if values.empty?
316
459
  environment.execute("SELECT id FROM hosts").map { |row| row.first }.tap do |values|
317
460
  environment.logger.debug("NOT expr: #{values.length} value(s)")
@@ -319,8 +462,8 @@ module Hotdog
319
462
  else
320
463
  # workaround for "too many terms in compound SELECT"
321
464
  min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts ORDER BY id LIMIT 1").first.to_a
322
- (min / SQLITE_LIMIT_COMPOUND_SELECT).upto(max / SQLITE_LIMIT_COMPOUND_SELECT).flat_map { |i|
323
- range = (SQLITE_LIMIT_COMPOUND_SELECT*i)...(SQLITE_LIMIT_COMPOUND_SELECT*(i+1))
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))
324
467
  selected = values.select { |n| range === n }
325
468
  q = "SELECT id FROM hosts " \
326
469
  "WHERE ? <= id AND id < ? AND id NOT IN (%s);"
@@ -336,12 +479,9 @@ module Hotdog
336
479
 
337
480
  def optimize(options={})
338
481
  @expression = @expression.optimize(options)
339
- if UnaryExpressionNode === @expression
340
- if @op == :NOT and @expression.op == :NOT
341
- @expression.expression
342
- else
343
- self
344
- end
482
+ case op
483
+ when :NOT
484
+ optimize1(options)
345
485
  else
346
486
  self
347
487
  end
@@ -350,15 +490,69 @@ module Hotdog
350
490
  def ==(other)
351
491
  self.class === other and @op == other.op and @expression == other.expression
352
492
  end
493
+
494
+ def dump(options={})
495
+ {op: @op.to_s, expression: @expression.dump(options)}
496
+ end
497
+
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
521
+ end
522
+ end
523
+
524
+ class QueryExpressionNode < ExpressionNode
525
+ def initialize(query, args=[], options={})
526
+ @query = query
527
+ @args = args
528
+ @fallback = options[:fallback]
529
+ end
530
+
531
+ def evaluate(environment, options={})
532
+ values = environment.execute(@query, @args).map { |row| row.first }
533
+ if values.empty? and @fallback
534
+ @fallback.evaluate(environment, options)
535
+ else
536
+ values
537
+ end
538
+ end
539
+
540
+ def dump(options={})
541
+ data = {query: @query, arguments: @args}
542
+ data[:fallback] = @fallback.dump(options) if @fallback
543
+ data
544
+ end
353
545
  end
354
546
 
355
547
  class TagExpressionNode < ExpressionNode
356
- def initialize(identifier, attribute)
548
+ def initialize(identifier, attribute, separator=nil)
357
549
  @identifier = identifier
358
550
  @attribute = attribute
551
+ @separator = separator
359
552
  end
360
553
  attr_reader :identifier
361
554
  attr_reader :attribute
555
+ attr_reader :separator
362
556
 
363
557
  def identifier?
364
558
  !(identifier.nil? or identifier.to_s.empty?)
@@ -368,41 +562,60 @@ module Hotdog
368
562
  !(attribute.nil? or attribute.to_s.empty?)
369
563
  end
370
564
 
371
- def evaluate(environment, options={})
565
+ def separator?
566
+ !(separator.nil? or separator.to_s.empty?)
567
+ end
568
+
569
+ def plan(options={})
372
570
  if identifier?
373
571
  if attribute?
374
572
  case identifier
375
573
  when /\Ahost\z/i
376
- q = "SELECT hosts.id FROM hosts " \
574
+ q = "SELECT hosts.id AS host_id FROM hosts " \
377
575
  "WHERE hosts.name = ?;"
378
- values = environment.execute(q, [attribute]).map { |row| row.first }
576
+ [q, [attribute]]
379
577
  else
380
578
  q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
381
579
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
382
580
  "WHERE tags.name = ? AND tags.value = ?;"
383
- values = environment.execute(q, [identifier, attribute]).map { |row| row.first }
581
+ [q, [identifier, attribute]]
384
582
  end
385
583
  else
386
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
387
- "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
388
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
389
- "WHERE hosts.name = ? OR tags.name = ? OR tags.value = ?;"
390
- values = environment.execute(q, [identifier, identifier, identifier]).map { |row| row.first }
584
+ if separator?
585
+ q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
586
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
587
+ "WHERE tags.name = ?;"
588
+ [q, [identifier]]
589
+ else
590
+ q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
591
+ "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
592
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
593
+ "WHERE hosts.name = ? OR tags.name = ? OR tags.value = ?;"
594
+ [q, [identifier, identifier, identifier]]
595
+ end
391
596
  end
392
597
  else
393
598
  if attribute?
394
599
  q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
395
600
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
396
601
  "WHERE tags.value = ?;"
397
- values = environment.execute(q, [attribute]).map { |row| row.first }
602
+ [q, [attribute]]
398
603
  else
399
- return []
604
+ nil
400
605
  end
401
606
  end
402
- if values.empty?
403
- fallback(environment, options)
607
+ end
608
+
609
+ def evaluate(environment, options={})
610
+ if q = plan(options)
611
+ values = environment.execute(*q).map { |row| row.first }
612
+ if values.empty?
613
+ fallback(environment, options)
614
+ else
615
+ values
616
+ end
404
617
  else
405
- values
618
+ return []
406
619
  end
407
620
  end
408
621
 
@@ -415,11 +628,11 @@ module Hotdog
415
628
  []
416
629
  else
417
630
  # fallback to glob expression
418
- identifier_glob = identifier.gsub(/[-.\/_]/, "?") if identifier?
419
- attribute_glob = attribute.gsub(/[-.\/_]/, "?") if attribute?
631
+ identifier_glob = "*#{identifier.gsub(/[-.\/_]/, "?")}*" if identifier?
632
+ attribute_glob = "*#{attribute.gsub(/[-.\/_]/, "?")}*" if attribute?
420
633
  if (identifier? and identifier != identifier_glob) or (attribute? and attribute != attribute_glob)
421
634
  environment.logger.info("fallback to glob expression: %s:%s" % [identifier_glob, attribute_glob])
422
- values = TagGlobExpressionNode.new(identifier_glob, attribute_glob).evaluate(environment, options)
635
+ values = TagGlobExpressionNode.new(identifier_glob, attribute_glob, separator).evaluate(environment, options)
423
636
  if values.empty?
424
637
  reload(environment, options)
425
638
  else
@@ -436,97 +649,138 @@ module Hotdog
436
649
  if 0 < ttl
437
650
  environment.logger.info("force reloading all hosts and tags.")
438
651
  environment.reload(force: true)
439
- self.class.new(identifier, attribute).evaluate(environment, options.merge(ttl: ttl-1))
652
+ self.class.new(identifier, attribute, separator).evaluate(environment, options.merge(ttl: ttl-1))
440
653
  else
441
654
  []
442
655
  end
443
656
  end
657
+
658
+ def dump(options={})
659
+ data = {}
660
+ data[:identifier] = @identifier if @identifier
661
+ data[:separator] = @separator if @separator
662
+ data[:attribute] = @attribute if @attribute
663
+ data
664
+ end
444
665
  end
445
666
 
446
667
  class TagGlobExpressionNode < TagExpressionNode
447
- def evaluate(environment, options={})
668
+ def plan(options={})
448
669
  if identifier?
449
670
  if attribute?
450
671
  case identifier
451
672
  when /\Ahost\z/i
452
- q = "SELECT hosts.id FROM hosts " \
673
+ q = "SELECT hosts.id AS host_id FROM hosts " \
453
674
  "WHERE hosts.name GLOB ?;"
454
- values = environment.execute(q, [attribute]).map { |row| row.first }
675
+ [q, [attribute]]
455
676
  else
456
677
  q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
457
678
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
458
679
  "WHERE tags.name GLOB ? AND tags.value GLOB ?;"
459
- values = environment.execute(q, [identifier, attribute]).map { |row| row.first }
680
+ [q, [identifier, attribute]]
460
681
  end
461
682
  else
462
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
463
- "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
464
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
465
- "WHERE hosts.name GLOB ? OR tags.name GLOB ? OR tags.value GLOB ?;"
466
- values = environment.execute(q, [identifier, identifier, identifier]).map { |row| row.first }
683
+ if separator?
684
+ q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
685
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
686
+ "WHERE tags.name GLOB ?;"
687
+ [q, [identifier]]
688
+ else
689
+ q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
690
+ "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
691
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
692
+ "WHERE hosts.name GLOB ? OR tags.name GLOB ? OR tags.value GLOB ?;"
693
+ [q, [identifier, identifier, identifier]]
694
+ end
467
695
  end
468
696
  else
469
697
  if attribute?
470
698
  q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
471
699
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
472
700
  "WHERE tags.value GLOB ?;"
473
- values = environment.execute(q, [attribute]).map { |row| row.first }
701
+ [q, [attribute]]
474
702
  else
475
- return []
703
+ nil
476
704
  end
477
705
  end
478
- if values.empty?
479
- fallback(environment, options)
480
- else
481
- values
482
- end
706
+ end
707
+
708
+ def dump(options={})
709
+ data = {}
710
+ data[:identifier_glob] = @identifier if @identifier
711
+ data[:separator] = @separator if @separator
712
+ data[:attribute_glob] = @attribute if @attribute
713
+ data
483
714
  end
484
715
  end
485
716
 
486
717
  class TagRegexpExpressionNode < TagExpressionNode
487
- def initialize(identifier, attribute)
718
+ def initialize(identifier, attribute, separator=nil)
488
719
  identifier = identifier.sub(%r{\A/(.*)/\z}) { $1 } if identifier
489
720
  attribute = attribute.sub(%r{\A/(.*)/\z}) { $1 } if attribute
490
- super(identifier, attribute)
721
+ super(identifier, attribute, separator)
491
722
  end
492
723
 
493
- def evaluate(environment, options={})
724
+ def plan(options={})
494
725
  if identifier?
495
726
  if attribute?
496
727
  case identifier
497
728
  when /\Ahost\z/i
498
- q = "SELECT hosts.id FROM hosts " \
729
+ q = "SELECT hosts.id AS host_id FROM hosts " \
499
730
  "WHERE hosts.name REGEXP ?;"
500
- values = environment.execute(q, [attribute]).map { |row| row.first }
731
+ [q, [attribute]]
501
732
  else
502
733
  q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
503
734
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
504
735
  "WHERE tags.name REGEXP ? AND tags.value REGEXP ?;"
505
- values = environment.execute(q, [identifier, attribute]).map { |row| row.first }
736
+ [q, [identifier, attribute]]
506
737
  end
507
738
  else
508
- q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
509
- "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
510
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
511
- "WHERE hosts.name REGEXP ? OR tags.name REGEXP ? OR tags.value REGEXP ?;"
512
- values = environment.execute(q, [identifier, identifier, identifier]).map { |row| row.first }
739
+ if separator?
740
+ q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
741
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
742
+ "WHERE tags.name REGEXP ?;"
743
+ [q, [identifier]]
744
+ else
745
+ q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
746
+ "INNER JOIN hosts ON hosts_tags.host_id = hosts.id " \
747
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
748
+ "WHERE hosts.name REGEXP ? OR tags.name REGEXP ? OR tags.value REGEXP ?;"
749
+ [q, [identifier, identifier, identifier]]
750
+ end
513
751
  end
514
752
  else
515
753
  if attribute?
516
754
  q = "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags " \
517
755
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
518
756
  "WHERE tags.value REGEXP ?;"
519
- values = environment.execute(q, [attribute]).map { |row| row.first }
757
+ [q, [attribute]]
520
758
  else
521
- return []
759
+ nil
522
760
  end
523
761
  end
524
- if values.empty?
525
- reload(environment)
762
+ end
763
+
764
+ def evaluate(environment, options={})
765
+ if q = plan(options)
766
+ values = environment.execute(*q).map { |row| row.first }
767
+ if values.empty?
768
+ reload(environment)
769
+ else
770
+ values
771
+ end
526
772
  else
527
- values
773
+ return []
528
774
  end
529
775
  end
776
+
777
+ def dump(options={})
778
+ data = {}
779
+ data[:identifier_regexp] = @identifier if @identifier
780
+ data[:separator] = @separator if @separator
781
+ data[:attribute_regexp] = @attribute if @attribute
782
+ data
783
+ end
530
784
  end
531
785
  end
532
786
  end
@@ -51,7 +51,7 @@ module Hotdog
51
51
  exit(1)
52
52
  end
53
53
 
54
- result = evaluate(node, self).sort
54
+ result = evaluate(node, self)
55
55
 
56
56
  if result.length == 1
57
57
  host = result[0]
@@ -1,3 +1,3 @@
1
1
  module Hotdog
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -15,15 +15,15 @@ describe "parser" do
15
15
  end
16
16
 
17
17
  it "parses ':foo'" do
18
- expect(cmd.parse(":foo")).to eq({attribute: "foo"})
18
+ expect(cmd.parse(":foo")).to eq({separator: ":", attribute: "foo"})
19
19
  end
20
20
 
21
21
  it "parses ':foo*'" do
22
- expect(cmd.parse(":foo*")).to eq({attribute_glob: "foo*"})
22
+ expect(cmd.parse(":foo*")).to eq({separator: ":", attribute_glob: "foo*"})
23
23
  end
24
24
 
25
25
  it "parses ':/foo/'" do
26
- expect(cmd.parse(":/foo/")).to eq({attribute_regexp: "/foo/"})
26
+ expect(cmd.parse(":/foo/")).to eq({separator: ":", attribute_regexp: "/foo/"})
27
27
  end
28
28
 
29
29
  it "parses 'foo'" do
@@ -31,23 +31,23 @@ describe "parser" do
31
31
  end
32
32
 
33
33
  it "parses 'foo:bar'" do
34
- expect(cmd.parse("foo:bar")).to eq({identifier: "foo", attribute: "bar"})
34
+ expect(cmd.parse("foo:bar")).to eq({identifier: "foo", separator: ":", attribute: "bar"})
35
35
  end
36
36
 
37
37
  it "parses 'foo: bar'" do
38
- expect(cmd.parse("foo:bar")).to eq({identifier: "foo", attribute: "bar"})
38
+ expect(cmd.parse("foo:bar")).to eq({identifier: "foo", separator: ":", attribute: "bar"})
39
39
  end
40
40
 
41
41
  it "parses 'foo :bar'" do
42
- expect(cmd.parse("foo:bar")).to eq({identifier: "foo", attribute: "bar"})
42
+ expect(cmd.parse("foo:bar")).to eq({identifier: "foo", separator: ":", attribute: "bar"})
43
43
  end
44
44
 
45
45
  it "parses 'foo : bar'" do
46
- expect(cmd.parse("foo:bar")).to eq({identifier: "foo", attribute: "bar"})
46
+ expect(cmd.parse("foo:bar")).to eq({identifier: "foo", separator: ":", attribute: "bar"})
47
47
  end
48
48
 
49
49
  it "parses 'foo:bar*'" do
50
- expect(cmd.parse("foo:bar*")).to eq({identifier: "foo", attribute_glob: "bar*"})
50
+ expect(cmd.parse("foo:bar*")).to eq({identifier: "foo", separator: ":", attribute_glob: "bar*"})
51
51
  end
52
52
 
53
53
  it "parses 'foo*'" do
@@ -55,11 +55,11 @@ describe "parser" do
55
55
  end
56
56
 
57
57
  it "parses 'foo*:bar'" do
58
- expect(cmd.parse("foo*:bar")).to eq({identifier_glob: "foo*", attribute: "bar"})
58
+ expect(cmd.parse("foo*:bar")).to eq({identifier_glob: "foo*", separator: ":", attribute: "bar"})
59
59
  end
60
60
 
61
61
  it "parses 'foo*:bar*'" do
62
- expect(cmd.parse("foo*:bar*")).to eq({identifier_glob: "foo*", attribute_glob: "bar*"})
62
+ expect(cmd.parse("foo*:bar*")).to eq({identifier_glob: "foo*", separator: ":", attribute_glob: "bar*"})
63
63
  end
64
64
 
65
65
  it "parses '/foo/'" do
@@ -67,7 +67,7 @@ describe "parser" do
67
67
  end
68
68
 
69
69
  it "parses '/foo/:/bar/'" do
70
- expect(cmd.parse("/foo/:/bar/")).to eq({identifier_regexp: "/foo/", attribute_regexp: "/bar/"})
70
+ expect(cmd.parse("/foo/:/bar/")).to eq({identifier_regexp: "/foo/", separator: ":", attribute_regexp: "/bar/"})
71
71
  end
72
72
 
73
73
  it "parses '(foo)'" do
@@ -10,19 +10,20 @@ describe "tag expression" do
10
10
  }
11
11
 
12
12
  it "interprets tag with host" do
13
- expr = Hotdog::Commands::Search::TagExpressionNode.new("host", "foo")
13
+ expr = Hotdog::Commands::Search::TagExpressionNode.new("host", "foo", ":")
14
14
  q = [
15
- "SELECT hosts.id FROM hosts",
15
+ "SELECT hosts.id AS host_id FROM hosts",
16
16
  "WHERE hosts.name = ?;",
17
17
  ]
18
18
  allow(cmd).to receive(:execute).with(q.join(" "), ["foo"]) {
19
19
  [[1], [2], [3]]
20
20
  }
21
21
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
22
+ expect(expr.dump).to eq({identifier: "host", separator: ":", attribute: "foo"})
22
23
  end
23
24
 
24
25
  it "interprets tag with identifier and attribute" do
25
- expr = Hotdog::Commands::Search::TagExpressionNode.new("foo", "bar")
26
+ expr = Hotdog::Commands::Search::TagExpressionNode.new("foo", "bar", ":")
26
27
  q = [
27
28
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
28
29
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
@@ -32,10 +33,25 @@ describe "tag expression" do
32
33
  [[1], [2], [3]]
33
34
  }
34
35
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
36
+ expect(expr.dump).to eq({identifier: "foo", separator: ":", attribute: "bar"})
35
37
  end
36
38
 
37
- it "interprets tag with identifier" do
38
- expr = Hotdog::Commands::Search::TagExpressionNode.new("foo", nil)
39
+ it "interprets tag with identifier with separator" do
40
+ expr = Hotdog::Commands::Search::TagExpressionNode.new("foo", nil, ":")
41
+ q = [
42
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
43
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
44
+ "WHERE tags.name = ?;",
45
+ ]
46
+ allow(cmd).to receive(:execute).with(q.join(" "), ["foo"]) {
47
+ [[1], [2], [3]]
48
+ }
49
+ expect(expr.evaluate(cmd)).to eq([1, 2, 3])
50
+ expect(expr.dump).to eq({identifier: "foo", separator: ":"})
51
+ end
52
+
53
+ it "interprets tag with identifier without separator" do
54
+ expr = Hotdog::Commands::Search::TagExpressionNode.new("foo", nil, nil)
39
55
  q = [
40
56
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
41
57
  "INNER JOIN hosts ON hosts_tags.host_id = hosts.id",
@@ -46,10 +62,11 @@ describe "tag expression" do
46
62
  [[1], [2], [3]]
47
63
  }
48
64
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
65
+ expect(expr.dump).to eq({identifier: "foo"})
49
66
  end
50
67
 
51
- it "interprets tag with attribute" do
52
- expr = Hotdog::Commands::Search::TagExpressionNode.new(nil, "foo")
68
+ it "interprets tag with attribute with separator" do
69
+ expr = Hotdog::Commands::Search::TagExpressionNode.new(nil, "foo", ":")
53
70
  q = [
54
71
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
55
72
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
@@ -59,5 +76,26 @@ describe "tag expression" do
59
76
  [[1], [2], [3]]
60
77
  }
61
78
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
79
+ expect(expr.dump).to eq({separator: ":", attribute: "foo"})
80
+ end
81
+
82
+ it "interprets tag with attribute without separator" do
83
+ expr = Hotdog::Commands::Search::TagExpressionNode.new(nil, "foo", nil)
84
+ q = [
85
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
86
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
87
+ "WHERE tags.value = ?;",
88
+ ]
89
+ allow(cmd).to receive(:execute).with(q.join(" "), ["foo"]) {
90
+ [[1], [2], [3]]
91
+ }
92
+ expect(expr.evaluate(cmd)).to eq([1, 2, 3])
93
+ expect(expr.dump).to eq({attribute: "foo"})
94
+ end
95
+
96
+ it "empty tag" do
97
+ expr = Hotdog::Commands::Search::TagExpressionNode.new(nil, nil, nil)
98
+ expect(expr.evaluate(cmd)).to eq([])
99
+ expect(expr.dump).to eq({})
62
100
  end
63
101
  end
@@ -10,19 +10,20 @@ describe "tag glob expression" do
10
10
  }
11
11
 
12
12
  it "interprets tag glob with host" do
13
- expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("host", "foo*")
13
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("host", "foo*", ":")
14
14
  q = [
15
- "SELECT hosts.id FROM hosts",
15
+ "SELECT hosts.id AS host_id FROM hosts",
16
16
  "WHERE hosts.name GLOB ?;",
17
17
  ]
18
18
  allow(cmd).to receive(:execute).with(q.join(" "), ["foo*"]) {
19
19
  [[1], [2], [3]]
20
20
  }
21
21
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
22
+ expect(expr.dump).to eq({identifier_glob: "host", separator: ":", attribute_glob: "foo*"})
22
23
  end
23
24
 
24
25
  it "interprets tag glob with identifier and attribute" do
25
- expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("foo*", "bar*")
26
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("foo*", "bar*", ":")
26
27
  q = [
27
28
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
28
29
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
@@ -32,10 +33,25 @@ describe "tag glob expression" do
32
33
  [[1], [2], [3]]
33
34
  }
34
35
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
36
+ expect(expr.dump).to eq({identifier_glob: "foo*", separator: ":", attribute_glob: "bar*"})
35
37
  end
36
38
 
37
- it "interprets tag glob with identifier" do
38
- expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("foo*", nil)
39
+ it "interprets tag glob with identifier with separator" do
40
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("foo*", nil, ":")
41
+ q = [
42
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
43
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
44
+ "WHERE tags.name GLOB ?;",
45
+ ]
46
+ allow(cmd).to receive(:execute).with(q.join(" "), ["foo*"]) {
47
+ [[1], [2], [3]]
48
+ }
49
+ expect(expr.evaluate(cmd)).to eq([1, 2, 3])
50
+ expect(expr.dump).to eq({identifier_glob: "foo*", separator: ":"})
51
+ end
52
+
53
+ it "interprets tag glob with identifier without separator" do
54
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new("foo*", nil, nil)
39
55
  q = [
40
56
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
41
57
  "INNER JOIN hosts ON hosts_tags.host_id = hosts.id",
@@ -46,10 +62,11 @@ describe "tag glob expression" do
46
62
  [[1], [2], [3]]
47
63
  }
48
64
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
65
+ expect(expr.dump).to eq({identifier_glob: "foo*"})
49
66
  end
50
67
 
51
- it "interprets tag glob with attribute" do
52
- expr = Hotdog::Commands::Search::TagGlobExpressionNode.new(nil, "foo*")
68
+ it "interprets tag glob with attribute with separator" do
69
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new(nil, "foo*", ":")
53
70
  q = [
54
71
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
55
72
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
@@ -59,5 +76,26 @@ describe "tag glob expression" do
59
76
  [[1], [2], [3]]
60
77
  }
61
78
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
79
+ expect(expr.dump).to eq({separator: ":", attribute_glob: "foo*"})
80
+ end
81
+
82
+ it "interprets tag glob with attribute without separator" do
83
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new(nil, "foo*", nil)
84
+ q = [
85
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
86
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
87
+ "WHERE tags.value GLOB ?;",
88
+ ]
89
+ allow(cmd).to receive(:execute).with(q.join(" "), ["foo*"]) {
90
+ [[1], [2], [3]]
91
+ }
92
+ expect(expr.evaluate(cmd)).to eq([1, 2, 3])
93
+ expect(expr.dump).to eq({attribute_glob: "foo*"})
94
+ end
95
+
96
+ it "empty tag glob" do
97
+ expr = Hotdog::Commands::Search::TagGlobExpressionNode.new(nil, nil, nil)
98
+ expect(expr.evaluate(cmd)).to eq([])
99
+ expect(expr.dump).to eq({})
62
100
  end
63
101
  end
@@ -10,19 +10,20 @@ describe "tag regexp expression" do
10
10
  }
11
11
 
12
12
  it "interprets tag regexp with host" do
13
- expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("host", "/foo/")
13
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("host", "/foo/", ":")
14
14
  q = [
15
- "SELECT hosts.id FROM hosts",
15
+ "SELECT hosts.id AS host_id FROM hosts",
16
16
  "WHERE hosts.name REGEXP ?;",
17
17
  ]
18
18
  allow(cmd).to receive(:execute).with(q.join(" "), ["foo"]) {
19
19
  [[1], [2], [3]]
20
20
  }
21
21
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
22
+ expect(expr.dump).to eq({identifier_regexp: "host", separator: ":", attribute_regexp: "foo"})
22
23
  end
23
24
 
24
25
  it "interprets tag regexp with identifier and attribute" do
25
- expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("/foo/", "/bar/")
26
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("/foo/", "/bar/", ":")
26
27
  q = [
27
28
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
28
29
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
@@ -32,10 +33,25 @@ describe "tag regexp expression" do
32
33
  [[1], [2], [3]]
33
34
  }
34
35
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
36
+ expect(expr.dump).to eq({identifier_regexp: "foo", separator: ":", attribute_regexp: "bar"})
35
37
  end
36
38
 
37
- it "interprets tag regexp with identifier" do
38
- expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("/foo/", nil)
39
+ it "interprets tag regexp with identifier with separator" do
40
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("/foo/", nil, ":")
41
+ q = [
42
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
43
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
44
+ "WHERE tags.name REGEXP ?;",
45
+ ]
46
+ allow(cmd).to receive(:execute).with(q.join(" "), ["foo"]) {
47
+ [[1], [2], [3]]
48
+ }
49
+ expect(expr.evaluate(cmd)).to eq([1, 2, 3])
50
+ expect(expr.dump).to eq({identifier_regexp: "foo", separator: ":"})
51
+ end
52
+
53
+ it "interprets tag regexp with identifier without separator" do
54
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new("/foo/", nil, nil)
39
55
  q = [
40
56
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
41
57
  "INNER JOIN hosts ON hosts_tags.host_id = hosts.id",
@@ -46,10 +62,11 @@ describe "tag regexp expression" do
46
62
  [[1], [2], [3]]
47
63
  }
48
64
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
65
+ expect(expr.dump).to eq({identifier_regexp: "foo"})
49
66
  end
50
67
 
51
- it "interprets tag regexp with attribute" do
52
- expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new(nil, "/foo/")
68
+ it "interprets tag regexp with attribute with separator" do
69
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new(nil, "/foo/", ":")
53
70
  q = [
54
71
  "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
55
72
  "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
@@ -59,5 +76,26 @@ describe "tag regexp expression" do
59
76
  [[1], [2], [3]]
60
77
  }
61
78
  expect(expr.evaluate(cmd)).to eq([1, 2, 3])
79
+ expect(expr.dump).to eq({separator: ":", attribute_regexp: "foo"})
80
+ end
81
+
82
+ it "interprets tag regexp with attribute without separator" do
83
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new(nil, "/foo/", nil)
84
+ q = [
85
+ "SELECT DISTINCT hosts_tags.host_id FROM hosts_tags",
86
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id",
87
+ "WHERE tags.value REGEXP ?;",
88
+ ]
89
+ allow(cmd).to receive(:execute).with(q.join(" "), ["foo"]) {
90
+ [[1], [2], [3]]
91
+ }
92
+ expect(expr.evaluate(cmd)).to eq([1, 2, 3])
93
+ expect(expr.dump).to eq({attribute_regexp: "foo"})
94
+ end
95
+
96
+ it "empty tag regexp" do
97
+ expr = Hotdog::Commands::Search::TagRegexpExpressionNode.new(nil, nil, nil)
98
+ expect(expr.evaluate(cmd)).to eq([])
99
+ expect(expr.dump).to eq({})
62
100
  end
63
101
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotdog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yamashita Yuu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-16 00:00:00.000000000 Z
11
+ date: 2015-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler