riddle 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +16 -0
  3. data/Gemfile +6 -0
  4. data/HISTORY +45 -0
  5. data/LICENCE +20 -0
  6. data/README.textile +5 -3
  7. data/Rakefile +23 -0
  8. data/lib/riddle.rb +1 -0
  9. data/lib/riddle/0.9.9/configuration/searchd.rb +10 -8
  10. data/lib/riddle/auto_version.rb +2 -2
  11. data/lib/riddle/client.rb +117 -118
  12. data/lib/riddle/configuration.rb +6 -6
  13. data/lib/riddle/configuration/distributed_index.rb +16 -16
  14. data/lib/riddle/configuration/sql_source.rb +5 -5
  15. data/lib/riddle/controller.rb +28 -25
  16. data/lib/riddle/query.rb +31 -20
  17. data/lib/riddle/query/select.rb +69 -8
  18. data/lib/riddle/version.rb +3 -0
  19. data/riddle.gemspec +25 -0
  20. data/spec/fixtures/.gitignore +2 -0
  21. data/spec/fixtures/data/0.9.9/anchor.bin +0 -0
  22. data/spec/fixtures/data/0.9.9/any.bin +0 -0
  23. data/spec/fixtures/data/0.9.9/boolean.bin +0 -0
  24. data/spec/fixtures/data/0.9.9/comment.bin +0 -0
  25. data/spec/fixtures/data/0.9.9/distinct.bin +0 -0
  26. data/spec/fixtures/data/0.9.9/field_weights.bin +0 -0
  27. data/spec/fixtures/data/0.9.9/filter.bin +0 -0
  28. data/spec/fixtures/data/0.9.9/filter_array.bin +0 -0
  29. data/spec/fixtures/data/0.9.9/filter_array_exclude.bin +0 -0
  30. data/spec/fixtures/data/0.9.9/filter_boolean.bin +0 -0
  31. data/spec/fixtures/data/0.9.9/filter_floats.bin +0 -0
  32. data/spec/fixtures/data/0.9.9/filter_floats_exclude.bin +0 -0
  33. data/spec/fixtures/data/0.9.9/filter_range.bin +0 -0
  34. data/spec/fixtures/data/0.9.9/filter_range_exclude.bin +0 -0
  35. data/spec/fixtures/data/0.9.9/group.bin +0 -0
  36. data/spec/fixtures/data/0.9.9/index.bin +0 -0
  37. data/spec/fixtures/data/0.9.9/index_weights.bin +0 -0
  38. data/spec/fixtures/data/0.9.9/keywords_with_hits.bin +0 -0
  39. data/spec/fixtures/data/0.9.9/keywords_without_hits.bin +0 -0
  40. data/spec/fixtures/data/0.9.9/overrides.bin +0 -0
  41. data/spec/fixtures/data/0.9.9/phrase.bin +0 -0
  42. data/spec/fixtures/data/0.9.9/rank_mode.bin +0 -0
  43. data/spec/fixtures/data/0.9.9/select.bin +0 -0
  44. data/spec/fixtures/data/0.9.9/simple.bin +0 -0
  45. data/spec/fixtures/data/0.9.9/sort.bin +0 -0
  46. data/spec/fixtures/data/0.9.9/update_simple.bin +0 -0
  47. data/spec/fixtures/data/0.9.9/weights.bin +0 -0
  48. data/spec/fixtures/data/1.10/anchor.bin +0 -0
  49. data/spec/fixtures/data/1.10/any.bin +0 -0
  50. data/spec/fixtures/data/1.10/boolean.bin +0 -0
  51. data/spec/fixtures/data/1.10/comment.bin +0 -0
  52. data/spec/fixtures/data/1.10/distinct.bin +0 -0
  53. data/spec/fixtures/data/1.10/field_weights.bin +0 -0
  54. data/spec/fixtures/data/1.10/filter.bin +0 -0
  55. data/spec/fixtures/data/1.10/filter_array.bin +0 -0
  56. data/spec/fixtures/data/1.10/filter_array_exclude.bin +0 -0
  57. data/spec/fixtures/data/1.10/filter_boolean.bin +0 -0
  58. data/spec/fixtures/data/1.10/filter_floats.bin +0 -0
  59. data/spec/fixtures/data/1.10/filter_floats_exclude.bin +0 -0
  60. data/spec/fixtures/data/1.10/filter_range.bin +0 -0
  61. data/spec/fixtures/data/1.10/filter_range_exclude.bin +0 -0
  62. data/spec/fixtures/data/1.10/group.bin +0 -0
  63. data/spec/fixtures/data/1.10/index.bin +0 -0
  64. data/spec/fixtures/data/1.10/index_weights.bin +0 -0
  65. data/spec/fixtures/data/1.10/keywords_with_hits.bin +0 -0
  66. data/spec/fixtures/data/1.10/keywords_without_hits.bin +0 -0
  67. data/spec/fixtures/data/1.10/overrides.bin +0 -0
  68. data/spec/fixtures/data/1.10/phrase.bin +0 -0
  69. data/spec/fixtures/data/1.10/rank_mode.bin +0 -0
  70. data/spec/fixtures/data/1.10/select.bin +0 -0
  71. data/spec/fixtures/data/1.10/simple.bin +0 -0
  72. data/spec/fixtures/data/1.10/sort.bin +0 -0
  73. data/spec/fixtures/data/1.10/update_simple.bin +0 -0
  74. data/spec/fixtures/data/1.10/weights.bin +0 -0
  75. data/spec/fixtures/data/2.0.1/anchor.bin +0 -0
  76. data/spec/fixtures/data/2.0.1/any.bin +0 -0
  77. data/spec/fixtures/data/2.0.1/boolean.bin +0 -0
  78. data/spec/fixtures/data/2.0.1/comment.bin +0 -0
  79. data/spec/fixtures/data/2.0.1/distinct.bin +0 -0
  80. data/spec/fixtures/data/2.0.1/field_weights.bin +0 -0
  81. data/spec/fixtures/data/2.0.1/filter.bin +0 -0
  82. data/spec/fixtures/data/2.0.1/filter_array.bin +0 -0
  83. data/spec/fixtures/data/2.0.1/filter_array_exclude.bin +0 -0
  84. data/spec/fixtures/data/2.0.1/filter_boolean.bin +0 -0
  85. data/spec/fixtures/data/2.0.1/filter_floats.bin +0 -0
  86. data/spec/fixtures/data/2.0.1/filter_floats_exclude.bin +0 -0
  87. data/spec/fixtures/data/2.0.1/filter_range.bin +0 -0
  88. data/spec/fixtures/data/2.0.1/filter_range_exclude.bin +0 -0
  89. data/spec/fixtures/data/2.0.1/group.bin +0 -0
  90. data/spec/fixtures/data/2.0.1/index.bin +0 -0
  91. data/spec/fixtures/data/2.0.1/index_weights.bin +0 -0
  92. data/spec/fixtures/data/2.0.1/keywords_with_hits.bin +0 -0
  93. data/spec/fixtures/data/2.0.1/keywords_without_hits.bin +0 -0
  94. data/spec/fixtures/data/2.0.1/overrides.bin +0 -0
  95. data/spec/fixtures/data/2.0.1/phrase.bin +0 -0
  96. data/spec/fixtures/data/2.0.1/rank_mode.bin +0 -0
  97. data/spec/fixtures/data/2.0.1/select.bin +0 -0
  98. data/spec/fixtures/data/2.0.1/simple.bin +0 -0
  99. data/spec/fixtures/data/2.0.1/sort.bin +0 -0
  100. data/spec/fixtures/data/2.0.1/update_simple.bin +0 -0
  101. data/spec/fixtures/data/2.0.1/weights.bin +0 -0
  102. data/spec/fixtures/data_generator.0.9.8.php +208 -0
  103. data/spec/fixtures/data_generator.0.9.9.php +5 -0
  104. data/spec/fixtures/data_generator.1.10.php +5 -0
  105. data/spec/fixtures/data_generator.2.0.1.php +5 -0
  106. data/spec/fixtures/data_generator.php +223 -0
  107. data/spec/fixtures/sphinxapi.0.9.8.php +1228 -0
  108. data/spec/fixtures/sphinxapi.0.9.9.php +1646 -0
  109. data/spec/fixtures/sphinxapi.1.10.php +1728 -0
  110. data/spec/fixtures/sphinxapi.2.0.1.php +1731 -0
  111. data/spec/fixtures/sql/conf.example.yml +3 -0
  112. data/spec/fixtures/sql/data.sql +25000 -0
  113. data/spec/fixtures/sql/data.tsv +25000 -0
  114. data/spec/fixtures/sql/structure.sql +16 -0
  115. data/spec/functional/connection_spec.rb +10 -12
  116. data/spec/functional/excerpt_spec.rb +1 -1
  117. data/spec/functional/keywords_spec.rb +1 -1
  118. data/spec/functional/persistance_spec.rb +1 -1
  119. data/spec/functional/search_spec.rb +1 -1
  120. data/spec/functional/status_spec.rb +1 -1
  121. data/spec/functional/update_spec.rb +1 -1
  122. data/spec/riddle/auto_version_spec.rb +18 -10
  123. data/spec/riddle/query/select_spec.rb +78 -14
  124. data/spec/riddle/query_spec.rb +5 -3
  125. data/spec/spec_helper.rb +13 -15
  126. data/spec/support/binary_fixtures.rb +18 -0
  127. data/spec/support/sphinx.rb +135 -0
  128. data/spec/unit/client_spec.rb +150 -142
  129. data/spec/unit/configuration/distributed_index_spec.rb +15 -15
  130. data/spec/unit/configuration/searchd_spec.rb +28 -3
  131. data/spec/unit/configuration_spec.rb +6 -6
  132. metadata +254 -68
  133. data/spec/sphinx_helper.rb +0 -96
@@ -14,20 +14,20 @@ module Riddle
14
14
  class Configuration
15
15
  class ConfigurationError < StandardError #:nodoc:
16
16
  end
17
-
18
- attr_reader :indexes, :searchd
17
+
18
+ attr_reader :indices, :searchd
19
19
  attr_accessor :indexer
20
-
20
+
21
21
  def initialize
22
22
  @indexer = Riddle::Configuration::Indexer.new
23
23
  @searchd = Riddle::Configuration::Searchd.new
24
- @indexes = []
24
+ @indices = []
25
25
  end
26
-
26
+
27
27
  def render
28
28
  (
29
29
  [@indexer.render, @searchd.render] +
30
- @indexes.collect { |index| index.render }
30
+ @indices.collect { |index| index.render }
31
31
  ).join("\n")
32
32
  end
33
33
  end
@@ -2,51 +2,51 @@ module Riddle
2
2
  class Configuration
3
3
  class DistributedIndex < Riddle::Configuration::Section
4
4
  def self.settings
5
- [
5
+ [
6
6
  :type, :local, :agent, :agent_blackhole,
7
7
  :agent_connect_timeout, :agent_query_timeout
8
8
  ]
9
9
  end
10
-
11
- attr_accessor :name, :local_indexes, :remote_indexes, :agent_blackhole,
10
+
11
+ attr_accessor :name, :local_indices, :remote_indices, :agent_blackhole,
12
12
  :agent_connect_timeout, :agent_query_timeout
13
-
13
+
14
14
  def initialize(name)
15
15
  @name = name
16
- @local_indexes = []
17
- @remote_indexes = []
16
+ @local_indices = []
17
+ @remote_indices = []
18
18
  @agent_blackhole = []
19
19
  end
20
-
20
+
21
21
  def type
22
22
  "distributed"
23
23
  end
24
-
24
+
25
25
  def local
26
- self.local_indexes
26
+ self.local_indices
27
27
  end
28
-
28
+
29
29
  def agent
30
- agents = remote_indexes.collect { |index| index.remote }.uniq
30
+ agents = remote_indices.collect { |index| index.remote }.uniq
31
31
  agents.collect { |agent|
32
- agent + ":" + remote_indexes.select { |index|
32
+ agent + ":" + remote_indices.select { |index|
33
33
  index.remote == agent
34
34
  }.collect { |index| index.name }.join(",")
35
35
  }
36
36
  end
37
-
37
+
38
38
  def render
39
39
  raise ConfigurationError unless valid?
40
-
40
+
41
41
  (
42
42
  ["index #{name}", "{"] +
43
43
  settings_body +
44
44
  ["}", ""]
45
45
  ).join("\n")
46
46
  end
47
-
47
+
48
48
  def valid?
49
- @local_indexes.length > 0 || @remote_indexes.length > 0
49
+ @local_indices.length > 0 || @remote_indices.length > 0
50
50
  end
51
51
  end
52
52
  end
@@ -13,16 +13,16 @@ module Riddle
13
13
  :sql_column_buffers, :sql_field_string, :sql_field_str2wordcount,
14
14
  :sql_query_post, :sql_query_post_index, :sql_ranged_throttle,
15
15
  :sql_query_info, :mssql_winauth, :mssql_unicode, :unpack_zlib,
16
- :unpack_mysqlcompress, :unpack_mysqlcompress_maxsize
16
+ :unpack_mysqlcompress, :unpack_mysqlcompress_maxsize
17
17
  ]
18
18
  end
19
-
19
+
20
20
  attr_accessor *self.settings
21
-
21
+
22
22
  def initialize(name, type)
23
23
  @name = name
24
24
  @type = type
25
-
25
+
26
26
  @sql_query_pre = []
27
27
  @sql_joined_field = []
28
28
  @sql_file_field = []
@@ -42,7 +42,7 @@ module Riddle
42
42
  @unpack_zlib = []
43
43
  @unpack_mysqlcompress = []
44
44
  end
45
-
45
+
46
46
  def valid?
47
47
  super && (!( @sql_host.nil? || @sql_user.nil? || @sql_db.nil? ||
48
48
  @sql_query.nil? ) || !@parent.nil?)
@@ -1,58 +1,61 @@
1
1
  module Riddle
2
2
  class Controller
3
3
  attr_accessor :path, :bin_path, :searchd_binary_name, :indexer_binary_name
4
-
4
+
5
5
  def initialize(configuration, path)
6
6
  @configuration = configuration
7
7
  @path = path
8
-
8
+
9
9
  @bin_path = ''
10
10
  @searchd_binary_name = 'searchd'
11
11
  @indexer_binary_name = 'indexer'
12
12
  end
13
-
13
+
14
14
  def sphinx_version
15
15
  `#{indexer} 2>&1`[/Sphinx (\d+\.\d+(\.\d+|(?:-dev|(\-id64)?\-beta)))/, 1]
16
16
  rescue
17
17
  nil
18
18
  end
19
-
20
- def index(*indexes)
21
- options = indexes.last.is_a?(Hash) ? indexes.pop : {}
22
- indexes << '--all' if indexes.empty?
23
-
24
- cmd = "#{indexer} --config \"#{@path}\" #{indexes.join(' ')}"
19
+
20
+ def index(*indices)
21
+ options = indices.last.is_a?(Hash) ? indices.pop : {}
22
+ indices << '--all' if indices.empty?
23
+
24
+ cmd = "#{indexer} --config \"#{@path}\" #{indices.join(' ')}"
25
25
  cmd << " --rotate" if running?
26
26
  options[:verbose] ? system(cmd) : `#{cmd}`
27
27
  end
28
-
29
- def start
28
+
29
+ def start(options={})
30
30
  return if running?
31
31
  check_for_configuration_file
32
-
32
+
33
33
  cmd = "#{searchd} --pidfile --config \"#{@path}\""
34
-
35
- if RUBY_PLATFORM =~ /mswin|mingw/
34
+ cmd << " --nodetach" if options[:nodetach]
35
+
36
+ if options[:nodetach]
37
+ exec(cmd)
38
+ elsif RUBY_PLATFORM =~ /mswin|mingw/
36
39
  system("start /B #{cmd} 1> NUL 2>&1")
37
40
  else
38
41
  `#{cmd}`
39
42
  end
40
-
43
+
41
44
  sleep(1)
42
-
45
+
43
46
  unless running?
44
47
  puts "Failed to start searchd daemon. Check #{@configuration.searchd.log}."
45
48
  end
46
49
  end
47
-
50
+
48
51
  def stop
49
52
  return true unless running?
50
53
  check_for_configuration_file
51
-
54
+
52
55
  stop_flag = 'stopwait'
53
56
  stop_flag = 'stop' if Riddle.loaded_version.split('.').first == '0'
54
57
  cmd = %(#{searchd} --pidfile --config "#{@path}" --#{stop_flag})
55
-
58
+
56
59
  if RUBY_PLATFORM =~ /mswin|mingw/
57
60
  system("start /B #{cmd} 1> NUL 2>&1")
58
61
  else
@@ -61,7 +64,7 @@ module Riddle
61
64
  ensure
62
65
  return !running?
63
66
  end
64
-
67
+
65
68
  def pid
66
69
  if File.exists?(@configuration.searchd.pid_file)
67
70
  File.read(@configuration.searchd.pid_file)[/\d+/]
@@ -69,23 +72,23 @@ module Riddle
69
72
  nil
70
73
  end
71
74
  end
72
-
75
+
73
76
  def running?
74
77
  !!pid && !!Process.kill(0, pid.to_i)
75
78
  rescue
76
79
  false
77
80
  end
78
-
81
+
79
82
  private
80
-
83
+
81
84
  def indexer
82
85
  "#{bin_path}#{indexer_binary_name}"
83
86
  end
84
-
87
+
85
88
  def searchd
86
89
  "#{bin_path}#{searchd_binary_name}"
87
90
  end
88
-
91
+
89
92
  def check_for_configuration_file
90
93
  return if File.exist?(@path)
91
94
  raise "Configuration file '#{@path}' does not exist"
@@ -1,86 +1,97 @@
1
1
  module Riddle::Query
2
2
  def self.connection(address = '127.0.0.1', port = 9312)
3
3
  require 'mysql2'
4
-
4
+
5
5
  # If you use localhost, MySQL insists on a socket connection, but Sphinx
6
6
  # requires a TCP connection. Using 127.0.0.1 fixes that.
7
7
  address = '127.0.0.1' if address == 'localhost'
8
-
8
+
9
9
  Mysql2::Client.new(
10
10
  :host => address,
11
11
  :port => port
12
12
  )
13
13
  end
14
-
14
+
15
15
  def self.meta
16
16
  'SHOW META'
17
17
  end
18
-
18
+
19
19
  def self.warnings
20
20
  'SHOW WARNINGS'
21
21
  end
22
-
22
+
23
23
  def self.status
24
24
  'SHOW STATUS'
25
25
  end
26
-
26
+
27
27
  def self.tables
28
28
  'SHOW TABLES'
29
29
  end
30
-
30
+
31
31
  def self.variables
32
32
  'SHOW VARIABLES'
33
33
  end
34
-
34
+
35
35
  def self.collation
36
36
  'SHOW COLLATION'
37
37
  end
38
-
38
+
39
39
  def self.describe(index)
40
40
  "DESCRIBE #{index}"
41
41
  end
42
-
42
+
43
43
  def self.begin
44
44
  'BEGIN'
45
45
  end
46
-
46
+
47
47
  def self.commit
48
48
  'COMMIT'
49
49
  end
50
-
50
+
51
51
  def self.rollback
52
52
  'ROLLBACK'
53
53
  end
54
-
54
+
55
55
  def self.set(variable, values, global = true)
56
56
  values = "(#{values.join(', ')})" if values.is_a?(Array)
57
57
  "SET#{ ' GLOBAL' if global } #{variable} = #{values}"
58
58
  end
59
-
59
+
60
60
  def self.snippets(data, index, query, options = nil)
61
61
  options = ', ' + options.keys.collect { |key|
62
62
  "#{options[key]} AS #{key}"
63
63
  }.join(', ') unless options.nil?
64
-
64
+
65
65
  "CALL SNIPPETS('#{data}', '#{index}', '#{query}'#{options})"
66
66
  end
67
-
67
+
68
68
  def self.create_function(name, type, file)
69
69
  type = type.to_s.upcase
70
70
  "CREATE FUNCTION #{name} RETURNS #{type} SONAME '#{file}'"
71
71
  end
72
-
72
+
73
73
  def self.drop_function(name)
74
74
  "DROP FUNCTION #{name}"
75
75
  end
76
-
76
+
77
77
  def self.update(index, id, values = {})
78
78
  values = values.keys.collect { |key|
79
- "#{key} = #{values[key]}"
79
+ "#{key} = #{translate_value values[key]}"
80
80
  }.join(', ')
81
-
81
+
82
82
  "UPDATE #{index} SET #{values} WHERE id = #{id}"
83
83
  end
84
+
85
+ def self.translate_value(value)
86
+ case value
87
+ when TrueClass
88
+ 1
89
+ when FalseClass
90
+ 0
91
+ else
92
+ value
93
+ end
94
+ end
84
95
  end
85
96
 
86
97
  require 'riddle/query/delete'
@@ -1,8 +1,10 @@
1
1
  class Riddle::Query::Select
2
2
  def initialize
3
+ @values = ['*']
3
4
  @indices = []
4
5
  @matching = nil
5
6
  @wheres = {}
7
+ @where_nots = {}
6
8
  @group_by = nil
7
9
  @order_by = nil
8
10
  @order_within_group_by = nil
@@ -11,6 +13,11 @@ class Riddle::Query::Select
11
13
  @options = {}
12
14
  end
13
15
 
16
+ def values(*values)
17
+ @values += values
18
+ self
19
+ end
20
+
14
21
  def from(*indices)
15
22
  @indices += indices
16
23
  self
@@ -26,6 +33,11 @@ class Riddle::Query::Select
26
33
  self
27
34
  end
28
35
 
36
+ def where_not(filters = {})
37
+ @where_nots.merge!(filters)
38
+ self
39
+ end
40
+
29
41
  def group_by(attribute)
30
42
  @group_by = attribute
31
43
  self
@@ -57,7 +69,7 @@ class Riddle::Query::Select
57
69
  end
58
70
 
59
71
  def to_sql
60
- sql = "SELECT * FROM #{ @indices.join(', ') }"
72
+ sql = "SELECT #{ @values.join(', ') } FROM #{ @indices.join(', ') }"
61
73
  sql << " WHERE #{ combined_wheres }" if wheres?
62
74
  sql << " GROUP BY #{@group_by}" if !@group_by.nil?
63
75
  sql << " ORDER BY #{@order_by}" if !@order_by.nil?
@@ -66,20 +78,20 @@ class Riddle::Query::Select
66
78
  end
67
79
  sql << " #{limit_clause}" unless @limit.nil? && @offset.nil?
68
80
  sql << " #{options_clause}" unless @options.empty?
69
-
81
+
70
82
  sql
71
83
  end
72
84
 
73
85
  private
74
86
 
75
87
  def wheres?
76
- !(@wheres.empty? && @matching.nil?)
88
+ !(@wheres.empty? && @where_nots.empty? && @matching.nil?)
77
89
  end
78
90
 
79
91
  def combined_wheres
80
92
  if @matching.nil?
81
93
  wheres_to_s
82
- elsif @wheres.empty?
94
+ elsif @wheres.empty? && @where_nots.empty?
83
95
  "MATCH('#{@matching}')"
84
96
  else
85
97
  "MATCH('#{@matching}') AND #{wheres_to_s}"
@@ -87,9 +99,49 @@ class Riddle::Query::Select
87
99
  end
88
100
 
89
101
  def wheres_to_s
90
- @wheres.keys.collect { |key|
91
- "#{key} = #{@wheres[key]}"
92
- }.join(' AND ')
102
+ (
103
+ @wheres.keys.collect { |key|
104
+ filter_comparison_and_value key, @wheres[key]
105
+ } +
106
+ @where_nots.keys.collect { |key|
107
+ exclusive_filter_comparison_and_value key, @where_nots[key]
108
+ }
109
+ ).join(' AND ')
110
+ end
111
+
112
+ def filter_comparison_and_value(attribute, value)
113
+ case value
114
+ when Array
115
+ "#{attribute} IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
116
+ when Range
117
+ "#{attribute} BETWEEN #{filter_value(value.first)} AND #{filter_value(value.last)}"
118
+ else
119
+ "#{attribute} = #{filter_value(value)}"
120
+ end
121
+ end
122
+
123
+ def exclusive_filter_comparison_and_value(attribute, value)
124
+ case value
125
+ when Array
126
+ "#{attribute} NOT IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
127
+ when Range
128
+ "#{attribute} < #{filter_value(value.first)} OR #{attribute} > #{filter_value(value.last)}"
129
+ else
130
+ "#{attribute} <> #{filter_value(value)}"
131
+ end
132
+ end
133
+
134
+ def filter_value(value)
135
+ case value
136
+ when TrueClass
137
+ 1
138
+ when FalseClass
139
+ 0
140
+ when Time
141
+ value.to_i
142
+ else
143
+ value
144
+ end
93
145
  end
94
146
 
95
147
  def limit_clause
@@ -102,7 +154,16 @@ class Riddle::Query::Select
102
154
 
103
155
  def options_clause
104
156
  'OPTION ' + @options.keys.collect { |key|
105
- "#{key}=#{@options[key]}"
157
+ "#{key}=#{option_value @options[key]}"
106
158
  }.join(', ')
107
159
  end
160
+
161
+ def option_value(value)
162
+ case value
163
+ when Hash
164
+ '(' + value.collect { |key, value| "#{key}=#{value}" }.join(', ') + ')'
165
+ else
166
+ value
167
+ end
168
+ end
108
169
  end