hotdog 0.3.1 → 0.4.0

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