oedipus 0.0.1.pre1 → 0.0.1.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +2 -0
  2. data/README.md +235 -44
  3. data/Rakefile +25 -0
  4. data/ext/oedipus/extconf.rb +72 -0
  5. data/ext/oedipus/oedipus.c +239 -0
  6. data/ext/oedipus/oedipus.h +50 -0
  7. data/lib/oedipus/comparison/between.rb +26 -0
  8. data/lib/oedipus/comparison/equal.rb +21 -0
  9. data/lib/oedipus/comparison/gt.rb +21 -0
  10. data/lib/oedipus/comparison/gte.rb +21 -0
  11. data/lib/oedipus/comparison/in.rb +21 -0
  12. data/lib/oedipus/comparison/lt.rb +21 -0
  13. data/lib/oedipus/comparison/lte.rb +21 -0
  14. data/lib/oedipus/comparison/not.rb +25 -0
  15. data/lib/oedipus/comparison/not_equal.rb +21 -0
  16. data/lib/oedipus/comparison/not_in.rb +21 -0
  17. data/lib/oedipus/comparison/outside.rb +26 -0
  18. data/lib/oedipus/comparison/shortcuts.rb +144 -0
  19. data/lib/oedipus/comparison.rb +88 -0
  20. data/lib/oedipus/connection.rb +91 -13
  21. data/lib/oedipus/connection_error.rb +14 -0
  22. data/lib/oedipus/index.rb +189 -46
  23. data/lib/oedipus/query_builder.rb +97 -4
  24. data/lib/oedipus/version.rb +1 -1
  25. data/lib/oedipus.rb +24 -7
  26. data/oedipus.gemspec +4 -5
  27. data/spec/integration/connection_spec.rb +58 -0
  28. data/spec/integration/index_spec.rb +353 -0
  29. data/spec/spec_helper.rb +2 -23
  30. data/spec/support/test_harness.rb +30 -9
  31. data/spec/unit/comparison/between_spec.rb +36 -0
  32. data/spec/unit/comparison/equal_spec.rb +22 -0
  33. data/spec/unit/comparison/gt_spec.rb +22 -0
  34. data/spec/unit/comparison/gte_spec.rb +22 -0
  35. data/spec/unit/comparison/in_spec.rb +22 -0
  36. data/spec/unit/comparison/lt_spec.rb +22 -0
  37. data/spec/unit/comparison/lte_spec.rb +22 -0
  38. data/spec/unit/comparison/not_equal_spec.rb +22 -0
  39. data/spec/unit/comparison/not_in_spec.rb +22 -0
  40. data/spec/unit/comparison/not_spec.rb +37 -0
  41. data/spec/unit/comparison/outside_spec.rb +36 -0
  42. data/spec/unit/comparison/shortcuts_spec.rb +125 -0
  43. data/spec/unit/comparison_spec.rb +109 -0
  44. data/spec/unit/query_builder_spec.rb +150 -0
  45. metadata +68 -19
  46. data/lib/oedipus/mysql/client.rb +0 -136
  47. data/spec/unit/connection_spec.rb +0 -36
  48. data/spec/unit/index_spec.rb +0 -85
@@ -0,0 +1,50 @@
1
+ /*-- encoding: utf-8 --*/
2
+
3
+ /*
4
+ * Oedipus Sphinx 2 Search.
5
+ * Copyright © 2012 Chris Corbyn.
6
+ *
7
+ * See LICENSE file for details.
8
+ */
9
+
10
+ #include <ruby.h>
11
+ #include <mysql.h>
12
+
13
+ /*! Internal struct used to reference a mysql connection */
14
+ typedef struct {
15
+ /*! Boolean representing the connected state */
16
+ int connected;
17
+ /*! The actual pointer allocated by mysql_init() */
18
+ MYSQL * ptr;
19
+ } OdpMysql;
20
+
21
+ /* -- Public methods -- */
22
+
23
+ /*! Allocate and initialize a new mysql client */
24
+ static VALUE odp_new(VALUE klass, VALUE host, VALUE port);
25
+
26
+ /*! Initialize a new mysql client */
27
+ static VALUE odp_initialize(VALUE self, VALUE host, VALUE port);
28
+
29
+ /*! Connect, or reconnect to mysql */
30
+ static VALUE odp_open(VALUE self);
31
+
32
+ /*! Disconnect from mysql */
33
+ static VALUE odp_close(VALUE self);
34
+
35
+ /*! Execute an SQL non-read query and return the number of rows affected */
36
+ static VALUE odp_execute(VALUE self, VALUE sql);
37
+
38
+ /*! Execute several SQL read queries and return the result sets */
39
+ static VALUE odp_query(VALUE self, VALUE sql);
40
+
41
+ /*! Cast the given field to a ruby data type */
42
+ static VALUE odp_cast_value(MYSQL_FIELD f, char * v, unsigned long len);
43
+
44
+ /* -- Internal methods -- */
45
+
46
+ /*! Generic method to raise a connection error */
47
+ static void odp_raise(VALUE self, const char *msg);
48
+
49
+ /*! Free memory allocated to mysql */
50
+ static void odp_free(OdpMysql *conn);
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Between comparison of range.
12
+ class Comparison::Between < Comparison
13
+ def to_s
14
+ [
15
+ "BETWEEN",
16
+ Connection.quote(v.first),
17
+ "AND",
18
+ Connection.quote(v.exclude_end? ? v.end - 1 : v.end)
19
+ ].join(" ")
20
+ end
21
+
22
+ def inverse
23
+ Comparison::Outside.new(v)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Equality comparison of value.
12
+ class Comparison::Equal < Comparison
13
+ def to_s
14
+ "= #{Connection.quote(v)}"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::NotEqual.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Greater than comparison of +v+.
12
+ class Comparison::GT < Comparison
13
+ def to_s
14
+ "> #{Connection.quote(v)}"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::LTE.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Greater than or equal comparison of +v+.
12
+ class Comparison::GTE < Comparison
13
+ def to_s
14
+ ">= #{Connection.quote(v)}"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::LT.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # IN comparison of +v+.
12
+ class Comparison::In < Comparison
13
+ def to_s
14
+ "IN (#{v.map { |o| Connection.quote(o)}.join(', ')})"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::NotIn.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Less than comparison of +v+.
12
+ class Comparison::LT < Comparison
13
+ def to_s
14
+ "< #{Connection.quote(v)}"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::GTE.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Less than or equal comparison of +v+.
12
+ class Comparison::LTE < Comparison
13
+ def to_s
14
+ "<= #{Connection.quote(v)}"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::GT.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Negation comparison of value.
12
+ class Comparison::Not < Comparison
13
+ def initialize(v)
14
+ super(Comparison.of(v))
15
+ end
16
+
17
+ def to_s
18
+ v.inverse.to_s
19
+ end
20
+
21
+ def inverse
22
+ v
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Negation comparison of value.
12
+ class Comparison::NotEqual < Comparison
13
+ def to_s
14
+ "!= #{Connection.quote(v)}"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::Equal.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # NOT IN comparison of +v+.
12
+ class Comparison::NotIn < Comparison
13
+ def to_s
14
+ "NOT IN (#{v.map { |o| Connection.quote(o)}.join(', ')})"
15
+ end
16
+
17
+ def inverse
18
+ Comparison::In.new(v)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Outside comparison of range.
12
+ class Comparison::Outside < Comparison
13
+ def to_s
14
+ [
15
+ "NOT BETWEEN",
16
+ Connection.quote(v.first),
17
+ "AND",
18
+ Connection.quote(v.exclude_end? ? v.end - 1 : v.end)
19
+ ].join(" ")
20
+ end
21
+
22
+ def inverse
23
+ Comparison::Between.new(v)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,144 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ class Comparison
12
+ # Provides short methods for casting values to Comparisons.
13
+ module Shortcuts
14
+ extend self
15
+
16
+ # Return the Comparison for equality of +v+.
17
+ #
18
+ # @param [Object] v
19
+ # any ruby object to compare in a query
20
+ #
21
+ # @return [Comparison::Equal]
22
+ # an equality comparison of v
23
+ def eq(v)
24
+ Equal.new(v)
25
+ end
26
+
27
+ # Return the Comparison for inequality of +v+.
28
+ #
29
+ # @param [Object] v
30
+ # any ruby object to compare in a query
31
+ #
32
+ # @return [Comparison::Equal]
33
+ # an inequality comparison of v
34
+ def neq(v)
35
+ NotEqual.new(v)
36
+ end
37
+
38
+ # Return the Comparison for negation of +v+.
39
+ #
40
+ # @param [Object] v
41
+ # any ruby object to compare in a query
42
+ #
43
+ # @return [Comparison::Not]
44
+ # an negated comparison of v
45
+ def not(v)
46
+ Not.new(v)
47
+ end
48
+
49
+ # Return the Comparison for the range a..b.
50
+ #
51
+ # @param [Object] v
52
+ # either a Range, or a number
53
+ #
54
+ # @param [Fixnum] b
55
+ # if the first argument was a number, the other bound
56
+ #
57
+ # @return [Comparison::Between]
58
+ # an between comparison of a..b
59
+ def between(a, b = nil)
60
+ Between.new(a.kind_of?(Range) ? a : a..b)
61
+ end
62
+
63
+ # Return the Comparison to exclude the range a..b.
64
+ #
65
+ # @param [Object] v
66
+ # either a Range, or a number
67
+ #
68
+ # @param [Fixnum] b
69
+ # if the first argument was a number, the other bound
70
+ #
71
+ # @return [Comparison::Outside]
72
+ # an outside comparison of a..b
73
+ def outside(a, b = nil)
74
+ Outside.new(a.kind_of?(Range) ? a : a..b)
75
+ end
76
+
77
+ # Return the Comparison for any value in the set +v+.
78
+ #
79
+ # @param [Object] v
80
+ # any ruby object to compare
81
+ #
82
+ # @return [Comparison::In]
83
+ # the IN comparison for the values in v
84
+ def in(*v)
85
+ In.new(v.map { |el| el.respond_to?(:to_a) ? el.to_a : el }.flatten)
86
+ end
87
+
88
+ # Return the Comparison for any value NOT in the set +v+.
89
+ #
90
+ # @param [Object] v
91
+ # any ruby object to compare
92
+ #
93
+ # @return [Comparison::NotIn]
94
+ # the NOT IN comparison for the values in v
95
+ def not_in(*v)
96
+ NotIn.new(v.map { |el| el.respond_to?(:to_a) ? el.to_a : el }.flatten)
97
+ end
98
+
99
+ # Return the Comparison for >= +v+.
100
+ #
101
+ # @param [Object] v
102
+ # a number to compare
103
+ #
104
+ # @return [Comparison::GTE]
105
+ # a greater than or equal comparison for v
106
+ def gte(v)
107
+ GTE.new(v)
108
+ end
109
+
110
+ # Return the Comparison for > +v+.
111
+ #
112
+ # @param [Object] v
113
+ # a number to compare
114
+ #
115
+ # @return [Comparison::GT]
116
+ # a greater than comparison for v
117
+ def gt(v)
118
+ GT.new(v)
119
+ end
120
+
121
+ # Return the Comparison for <= +v+.
122
+ #
123
+ # @param [Object] v
124
+ # a number to compare
125
+ #
126
+ # @return [Comparison::LTE]
127
+ # a less than or equal comparison for v
128
+ def lte(v)
129
+ LTE.new(v)
130
+ end
131
+
132
+ # Return the Comparison for < +v+.
133
+ #
134
+ # @param [Object] v
135
+ # a number to compare
136
+ #
137
+ # @return [Comparison::LT]
138
+ # a less than comparison for v
139
+ def lt(v)
140
+ LT.new(v)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Represents a comparison operator and value.
12
+ class Comparison
13
+ class << self
14
+ # Return a suitable comparison object for +v+.
15
+ #
16
+ # The conversions are:
17
+ #
18
+ # - leave Comparison objects unchanged
19
+ # - convert real ranges to Between comparisons
20
+ # - convert Infinity-bounded exclusive ranges to GT/LT comparisons
21
+ # - convert Infinity-bounded inclusive ranges to GTE/LTE comparisons.
22
+ # - convert everything else to an Equal comparison
23
+ #
24
+ # @param [Object] v
25
+ # a ruby object to be compared
26
+ #
27
+ # @param [Comparison]
28
+ # a comparison suitable for comparing the input
29
+ def of(v)
30
+ case v
31
+ when Comparison
32
+ v
33
+ when Range
34
+ if v.end == Float::INFINITY
35
+ v.exclude_end? ? Shortcuts.gt(v.first) : Shortcuts.gte(v.first)
36
+ elsif v.first == -Float::INFINITY
37
+ v.exclude_end? ? Shortcuts.lt(v.end) : Shortcuts.lte(v.end)
38
+ else
39
+ Shortcuts.between(v)
40
+ end
41
+ when Enumerable
42
+ Shortcuts.in(v)
43
+ else
44
+ Shortcuts.eq(v)
45
+ end
46
+ end
47
+ end
48
+
49
+ attr_reader :v
50
+
51
+ # Initialize a new Comparison for +v+.
52
+ #
53
+ # @param [Object] v
54
+ # any ruby object to compare
55
+ def initialize(v)
56
+ @v = v
57
+ end
58
+
59
+ # Compare two comparisons for equality.
60
+ #
61
+ # @param [Comparison] other
62
+ # another comparison to check
63
+ #
64
+ # @return [Boolean]
65
+ # true if the comparisons are the same
66
+ def ==(other)
67
+ other.class == self.class && other.v == v
68
+ end
69
+
70
+ alias_method :eql?, :==
71
+
72
+ # Return the exact inverse of this comparison.
73
+ #
74
+ # @return [Comparison]
75
+ # the inverse of the current comparison
76
+ def inverse
77
+ raise NotImplementedError, "Comparison#inverse must be defined by subclasses"
78
+ end
79
+
80
+ # Represent the comparison as a string.
81
+ #
82
+ # @return [String]
83
+ # an expression to compare a LHS against v
84
+ def to_s
85
+ raise NotImplementedError, "Comparison#to_s must be defined by subclasses"
86
+ end
87
+ end
88
+ end
@@ -7,22 +7,69 @@
7
7
  # See LICENSE file for details.
8
8
  ##
9
9
 
10
- require "mysql"
11
-
12
- # SphinxQL greets with charset number 0
13
- Mysql::Charset::NUMBER_TO_CHARSET[0] = Mysql::Charset::COLLATION_TO_CHARSET["utf8_general_ci"]
14
-
15
10
  module Oedipus
16
11
  # Provides an interface for talking to SphinxQL.
12
+ #
13
+ # Currently this class wraps a native mysql extension.
17
14
  class Connection
15
+ class << self
16
+ # Quote a value (of any type) for use in SphinxQL.
17
+ #
18
+ # @param [Object] v
19
+ # the value to quote
20
+ #
21
+ # @return [Object]
22
+ # the safe value
23
+ #
24
+ # Note that single quotes are added to strings.
25
+ def quote(v)
26
+ require "bigdecimal"
27
+ case v
28
+ when BigDecimal, Rational, Complex
29
+ v.to_f
30
+ when Numeric
31
+ v
32
+ when NilClass
33
+ "NULL"
34
+ else
35
+ "'#{escape_str(v.to_s)}'"
36
+ end
37
+ end
38
+
39
+ # Escape a string, without adding enclosing quotes.
40
+ #
41
+ # @param [String] str
42
+ # the unsafe input string
43
+ #
44
+ # @return [String]
45
+ # a safe string for use in SphinxQL
46
+ def escape_str(str)
47
+ str.gsub(/[\0\n\r\\\'\"\x1a]/) do |s|
48
+ case s
49
+ when "\0" then "\\0"
50
+ when "\n" then "\\n"
51
+ when "\r" then "\\r"
52
+ when "\x1a" then "\\Z"
53
+ else "\\#{s}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+
18
59
  # Instantiate a new Connection to a SphinxQL host.
19
60
  #
20
- # @param [Hash]
61
+ # @param [String] server
62
+ # a 'hostname:port' string
63
+ #
64
+ # @param [Hash] options
21
65
  # a Hash containing :host and :port
22
66
  #
23
67
  # The connection will be established on initialization.
24
68
  def initialize(options)
25
- @conn = ::Mysql.new(options[:host], nil, nil, nil, options[:port], nil, ::Mysql::CLIENT_MULTI_STATEMENTS | ::Mysql::CLIENT_MULTI_RESULTS)
69
+ options = options.kind_of?(String) ?
70
+ Hash[ [:host, :port].zip(options.split(":")) ] :
71
+ options
72
+ @conn = Oedipus::Mysql.new(options[:host], options[:port].to_i)
26
73
  end
27
74
 
28
75
  # Acess a specific index for querying.
@@ -36,15 +83,46 @@ module Oedipus
36
83
  Index.new(index_name, self)
37
84
  end
38
85
 
39
- def execute(sql)
86
+ # Execute one or more queries in a batch.
87
+ #
88
+ # Queries should be separated by semicolons.
89
+ # Results are returned in a 2-dimensional array.
90
+ #
91
+ # @param [String] sql
92
+ # one or more SphinxQL statements, separated by semicolons
93
+ #
94
+ # @return [Array]
95
+ # an array of arrays, containing the returned records
96
+ #
97
+ # Note that SphinxQL does not support prepared statements.
98
+ def multi_query(sql)
40
99
  @conn.query(sql)
41
100
  end
42
101
 
43
- def quote(v)
44
- case v
45
- when Numeric then v
46
- else "'#{::Mysql.quote(v.to_s)}'"
47
- end
102
+ # Execute a single read query.
103
+ #
104
+ # @param [String] sql
105
+ # a single SphinxQL statement
106
+ #
107
+ # @return [Array]
108
+ # an array of Hashes containing the matched records
109
+ #
110
+ # Note that SphinxQL does not support prepared statements.
111
+ def query(sql)
112
+ @conn.query(sql).first
113
+ end
114
+
115
+ # Execute a non-read query.
116
+ #
117
+ # @param [String] sql
118
+ # a SphinxQL query, such as INSERT or REPLACE
119
+ #
120
+ # @return [Fixnum]
121
+ # the number of affected rows
122
+ #
123
+ # Note that SphinxQL does not support prepared statements.
124
+ def execute(sql)
125
+ @conn.execute(sql)
48
126
  end
49
127
  end
50
128
  end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ module Oedipus
11
+ # Raised on any error coming from Sphinx.
12
+ class ConnectionError < RuntimeError
13
+ end
14
+ end