oedipus 0.0.1.pre1 → 0.0.1.pre2

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.
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