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
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ spec/data/index/*
6
6
  spec/data/binlog/*
7
7
  spec/data/searchd.*
8
8
  spec/data/sphinx.*
9
+ lib/oedipus/oedipus.so
10
+ tmp/
data/README.md CHANGED
@@ -5,53 +5,108 @@ real-time indexes and multi and/or faceted searches.
5
5
 
6
6
  It is not a clone of the PHP API, rather it is written from the ground up,
7
7
  wrapping the SphinxQL API offered by searchd. Nor is it a plugin for
8
- ActiveRecord or DataMapper... though this is planned in separate gems.
8
+ ActiveRecord or DataMapper... though this will follow in separate gems.
9
9
 
10
- It will provide some higher level of abstraction in terms of the ease with
11
- which faceted search may be implemented, though it will remain light and simple.
10
+ Oedipus provides a level of abstraction in terms of the ease with which faceted
11
+ search may be implemented, while remaining light and simple.
12
+
13
+ Data structures are managed using core ruby data type (Array and Hash), ensuring
14
+ simplicity and flexibilty.
12
15
 
13
16
  ## Current Status
14
17
 
15
18
  This gem is in development. It is not ready for production use. I work for
16
19
  a company called Flippa.com, which currently implements faceted search in a PHP
17
20
  part of the website, using a slightly older version of Sphinx with lesser
18
- support for SphinxQL. Once a month the developers at Flippa are given three days
19
- to work on a project of their own choice. This is my 'Triple Time' project.
21
+ support for SphinxQL. We want to move this search across to the ruby codebase
22
+ of the website, but are held back by ruby's lack of support for Sphinx 2.
23
+
24
+ Once a month the developers at Flippa are given three days to work on a project of
25
+ their own choice. This is my 'Triple Time' project.
20
26
 
21
- I anticipate another week or two of development before I can consider this project
27
+ I anticipate another week or so of development before I can consider this project
22
28
  production-ready.
23
29
 
30
+ ## Dependencies
31
+
32
+ * ruby (>= 1.9)
33
+ * sphinx (>= 2.0.2)
34
+ * mysql.h / client development libraries (>= 4.1)
35
+
36
+ The gem builds a small (tiny) native extension for interfacing with mysql, as
37
+ existing gems either did not support multi-queries, or were too flaky
38
+ (i.e. ruby-mysql). I was also concerned about potential conflicts with any
39
+ specific ORMs users may be using. I will add a pure-ruby option in due course
40
+ (it requires implementing a relatively small subset of the mysql 4.1/5.0 protocol).
41
+
24
42
  ## Usage
25
43
 
26
- Not all of the following features are currently implemented, but the interface
27
- style is as follows.
44
+ The following features are all currently implemented, but more are coming.
45
+
46
+ ### Connecting to Sphinx
28
47
 
29
48
  ``` ruby
30
49
  require "oedipus"
31
50
 
32
51
  sphinx = Oedipus.connect('localhost:9306') # sphinxql host
52
+ ```
53
+
54
+ ### Inserting
55
+
56
+ ``` ruby
57
+ sphinx[:articles].insert(
58
+ 7,
59
+ title: "Badgers in the wild",
60
+ body: "A big long wodge of text",
61
+ author_id: 4,
62
+ views: 102
63
+ )
64
+ ```
65
+
66
+ ### Replacing
33
67
 
34
- # insert a record into the 'articles' real-time index
35
- record = sphinx[:articles].insert(
68
+ ``` ruby
69
+ sphinx[:articles].replace(
36
70
  7,
37
71
  title: "Badgers in the wild",
38
72
  body: "A big long wodge of text",
39
73
  author_id: 4,
40
74
  views: 102
41
75
  )
42
- # The attributes (but not the indexed fields) are returned
43
- # => { id: 7, author_id: 4, views: 102 }
76
+ ```
77
+
78
+ ### Updating
44
79
 
45
- # updating a record
46
- record = sphinx[:articles].update(7, views: 103)
47
- # The new attributes (but not the indexed fields) are returned
48
- # => { id: 7, author_id: 4, views: 103 }
80
+ ``` ruby
81
+ sphinx[:articles].update(7, views: 103)
82
+ ```
49
83
 
50
- # deleting a record
84
+ ### Deleting
85
+
86
+ ``` ruby
51
87
  sphinx[:articles].delete(7)
52
88
  # => true
89
+ ```
90
+
91
+ ### Fetching a known document (by ID)
53
92
 
54
- # searching the index
93
+ ``` ruby
94
+ record = sphinx[:articles].fetch(7)
95
+ # => { id: 7, views: 984, author_id: 3 }
96
+ ```
97
+
98
+ ### Fulltext searching
99
+
100
+ You perform queries by invoking `#search` on the index.
101
+
102
+ Oedipus makes no attempt to provide an abstraction layer for the fulltext
103
+ query itself. I believe this would not be flexible enough. Sphinx fulltext
104
+ queries are extremely featureful, very dense and concise; a ruby solution
105
+ would only be lengthier and harder to understand, IMHO. Perhaps such an
106
+ abstraction could be provided by a separate gem.
107
+
108
+
109
+ ``` ruby
55
110
  results = sphinx[:articles].search("badgers", limit: 2)
56
111
 
57
112
  # Meta deta indicates the overall number of matched records, while the ':records'
@@ -67,25 +122,108 @@ results = sphinx[:articles].search("badgers", limit: 2)
67
122
  # { id: 11, author_id: 6, views: 23 }
68
123
  # ]
69
124
  # }
125
+ ```
126
+
127
+ ### Attribute filters
70
128
 
71
- # using attribute filters
72
- results = sphinx[:articles].search(
129
+ Result formatting is the same as for a fulltext search. You can add as many
130
+ filters as you like.
131
+
132
+ ``` ruby
133
+ # equality
134
+ sphinx[:articles].search(
73
135
  "example",
74
136
  author_id: 7
75
137
  )
76
- # => (the same results, filtered by author)
77
138
 
78
- # performing a faceted search
139
+ # less than or equal
140
+ sphinx[:articles].search(
141
+ "example",
142
+ views: -Float::INFINITY..100
143
+ )
144
+
145
+ sphinx[:articles].search(
146
+ "example",
147
+ views: Oedipus.lte(100)
148
+ )
149
+
150
+ # greater than
151
+ sphinx[:articles].search(
152
+ "example",
153
+ views: 100...Float::INFINITY
154
+ )
155
+
156
+ sphinx[:articles].search(
157
+ "example",
158
+ views: Oedipus.gt(100)
159
+ )
160
+
161
+ # not equal
162
+ sphinx[:articles].search(
163
+ "example",
164
+ author_id: Oedipus.not(7)
165
+ )
166
+
167
+ # between
168
+ sphinx[:articles].search(
169
+ "example",
170
+ views: 50..100
171
+ )
172
+
173
+ sphinx[:articles].search(
174
+ "example",
175
+ views: 50...100
176
+ )
177
+
178
+ # not between
179
+ sphinx[:articles].search(
180
+ "example",
181
+ views: Oedipus.not(50..100)
182
+ )
183
+
184
+ sphinx[:articles].search(
185
+ "example",
186
+ views: Oedipus.not(50...100)
187
+ )
188
+
189
+ # IN( ... )
190
+ sphinx[:articles].search(
191
+ "example",
192
+ author_id: [7, 22]
193
+ )
194
+
195
+ # NOT IN( ... )
196
+ sphinx[:articles].search(
197
+ "example",
198
+ author_id: Oedipus.not([7, 22])
199
+ )
200
+ ```
201
+
202
+ ### Faceted searching
203
+
204
+ A faceted search takes a base query and a set of additional queries that are
205
+ variations on it. Oedipus makes this simple by allowing your facets to inherit
206
+ from the base query.
207
+
208
+ Oedipus allows you to replace '%{query}' in your facets with whatever was in the
209
+ original query. This can be useful if you want to provide facets that only
210
+ perform the search in the title of the document (`"@title (%{query})"`) for example.
211
+
212
+ Each facet is given a name, which is used to reference them in the results.
213
+
214
+ Sphinx optimizes the queries by figuring out what the common parts are. Currently
215
+ it does two optimizations, though in future this will likely improve further, so
216
+ using this technique to do your faceted searches is a good idea.
217
+
218
+ ``` ruby
79
219
  results = sphinx[:articles].facted_search(
80
220
  "badgers",
81
221
  facets: {
82
222
  popular: { views: 100..10000 },
83
- farming: "farming",
84
- popular_farming: ["farming", { views: 100..10000 } ]
223
+ also_farming: "%{query} & farming",
224
+ popular_farming: ["%{query} & farming", views: 100..10000 ]
85
225
  }
86
226
  )
87
- # The main results are returned in the ':records' array, and all the facets in
88
- # the ':facets' Hash.
89
227
  # => {
90
228
  # total_found: 987,
91
229
  # time: 0.000,
@@ -96,7 +234,7 @@ results = sphinx[:articles].facted_search(
96
234
  # time: 0.000,
97
235
  # records: [ ... ]
98
236
  # },
99
- # farming: {
237
+ # also_farming: {
100
238
  # total_found: 123,
101
239
  # time: 0.000,
102
240
  # records: [ ... ]
@@ -108,17 +246,21 @@ results = sphinx[:articles].facted_search(
108
246
  # }
109
247
  # }
110
248
  # }
111
- #
112
- # When performing a faceted search, the primary search is used as the basis for
113
- # each facet, so they can be considered refinements.
249
+ ```
250
+
251
+ ### General purpose multi-search
252
+
253
+ If you want to execute multiple queries in a batch that are not related to each
254
+ other (which would be a faceted search), then you can use `#multi_search`.
114
255
 
115
- # performing a mutli-search
256
+ You pass a Hash of named queries and get a Hash of named resultsets.
257
+
258
+ ``` ruby
116
259
  results = sphinx[:articles].multi_search(
117
- badgers: ["badgers", { limit: 30 }],
118
- frogs: "frogs AND wetlands",
119
- rabbits: ["rabbits OR burrows", { view_count: 20..100 }]
260
+ badgers: ["badgers", limit: 30],
261
+ frogs: "frogs & wetlands",
262
+ rabbits: ["rabbits | burrows", view_count: 20..100]
120
263
  )
121
- # The results are returned in a 2-dimensional Hash, keyed as sent in the query
122
264
  # => {
123
265
  # badgers: {
124
266
  # ...
@@ -130,19 +272,68 @@ results = sphinx[:articles].multi_search(
130
272
  # ...
131
273
  # }
132
274
  # }
133
- #
134
- # Unlike with a faceted search, the queries in a multi-search do not have to be
135
- # related to one another.
136
275
  ```
137
276
 
138
- ## Future Plans
277
+ ### Limits and offsets
278
+
279
+ Note that Sphinx applies a limit of 20 by default, so you probably want to specify
280
+ a limit yourself. You are bound by your `max_matches` setting in sphinx.conf.
281
+
282
+ Note that the meta data will still indicate the actual number of results that matched;
283
+ you simply get a smaller collection of materialized records.
284
+
285
+ ``` ruby
286
+ sphinx[:articles].search("bobcats", limit: 50)
287
+ sphinx[:articles].search("bobcats", limit: 50, offset: 150)
288
+ ```
289
+
290
+ ### Ordering
291
+
292
+ ``` ruby
293
+ sphinx[:articles].search("badgers", order: { views: :asc })
294
+ ```
295
+
296
+ ## Running the specs
139
297
 
140
- I plan to release gems for integration with DataMapper and ActiveRecord. DataMapper
141
- first, since that's what we use.
298
+ There are both unit tests and integration tests in the specs/ directory. By default they
299
+ will both run, but in order for the integration specs to work, you need a locally
300
+ installed copy of Sphinx [1]. You then execute the specs as follows:
301
+
302
+ SEARCHD=/path/to/bin/searchd bundle exec rake spec
303
+
304
+ If you don't have Sphinx installed locally, you cannot run the integration specs (they need
305
+ to write config files and start and stop sphinx internally).
306
+
307
+ To run the unit tests alone, without the need for Sphinx:
308
+
309
+ bundle exec rake spec:unit
310
+
311
+ If you have made changes to the C extension, those changes will be compiled and installed
312
+ (to the lib/ directory) before the specs are run.
313
+
314
+ You may also compile the C extension and run the specs separately, if you prefer:
315
+
316
+ bundle exec rake compile
317
+ bundle exec rspec spec/unit/
318
+
319
+ ### Footnotes
320
+
321
+ [1] You can build a local copy of sphinx without installing it on the system:
322
+
323
+ cd sphinx-2.0.4/
324
+ ./configure
325
+ make
326
+
327
+ The searchd binary will be found in /path/to/sphinx-2.0.4/src/searchd.
328
+
329
+ ## Future Plans
142
330
 
143
- I also intend to remove ruby-mysql from the dependencies, as it doesn't perfectly fit
144
- the needs of SphinxQL. I will be implementing the limited subset of the MySQL protocol
145
- by hand (which is not as big a deal as it sounds).
331
+ * Integration with DataMapper and ActiveRecord (DataMapper first)
332
+ * Distributed index support (sharding writes between indexes)
333
+ * Make C extension optional and provide an implementation in pure-ruby
334
+ * N-dimensional faceted search (facets inside of facets)
335
+ * Query translation layer for Google-style AND/OR/NOT interpretation
336
+ * Fulltext query sanitization for unsafe user input (e.g. @missing field)
146
337
 
147
338
  ## Copyright and Licensing
148
339
 
data/Rakefile CHANGED
@@ -1 +1,26 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rake/extensiontask"
4
+
5
+ Rake::ExtensionTask.new('oedipus') do |ext|
6
+ ext.lib_dir = File.join('lib', 'oedipus')
7
+ end
8
+
9
+ desc "Run the full RSpec suite (requires SEARCHD environment variable)"
10
+ RSpec::Core::RakeTask.new('spec') do |t|
11
+ t.pattern = 'spec/'
12
+ end
13
+
14
+ desc "Run the RSpec unit tests alone"
15
+ RSpec::Core::RakeTask.new('spec:unit') do |t|
16
+ t.pattern = 'spec/unit/'
17
+ end
18
+
19
+ desc "Run the integration tests (requires SEARCHD environment variable)"
20
+ RSpec::Core::RakeTask.new('spec:integration') do |t|
21
+ t.pattern = 'spec/integration/'
22
+ end
23
+
24
+ Rake::Task['spec'].prerequisites << :compile
25
+ Rake::Task['spec:unit'].prerequisites << :compile
26
+ Rake::Task['spec:integration'].prerequisites << :compile
@@ -0,0 +1,72 @@
1
+ # encoding: UTF-8
2
+ require 'mkmf'
3
+
4
+ # borrowed from https://github.com/brianmario/mysql2/master/ext/mysql2/extconf.rb
5
+
6
+ def asplode lib
7
+ abort "-----\n#{lib} is missing. please check your installation of mysql and try again.\n-----"
8
+ end
9
+
10
+ # borrowed from mysqlplus
11
+ # http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb
12
+ dirs = ENV['PATH'].split(File::PATH_SEPARATOR) + %w[
13
+ /opt
14
+ /opt/local
15
+ /opt/local/mysql
16
+ /opt/local/lib/mysql5
17
+ /usr
18
+ /usr/mysql
19
+ /usr/local
20
+ /usr/local/mysql
21
+ /usr/local/mysql-*
22
+ /usr/local/lib/mysql5
23
+ ].map{|dir| "#{dir}/bin" }
24
+
25
+ GLOB = "{#{dirs.join(',')}}/{mysql_config,mysql_config5}"
26
+
27
+ if RUBY_PLATFORM =~ /mswin|mingw/
28
+ inc, lib = dir_config('mysql')
29
+ exit 1 unless have_library("libmysql")
30
+ elsif mc = (with_config('mysql-config') || Dir[GLOB].first) then
31
+ mc = Dir[GLOB].first if mc == true
32
+ cflags = `#{mc} --cflags`.chomp
33
+ exit 1 if $? != 0
34
+ libs = `#{mc} --libs_r`.chomp
35
+ if libs.empty?
36
+ libs = `#{mc} --libs`.chomp
37
+ end
38
+ exit 1 if $? != 0
39
+ $CPPFLAGS += ' ' + cflags
40
+ $libs = libs + " " + $libs
41
+ else
42
+ inc, lib = dir_config('mysql', '/usr/local')
43
+ libs = ['m', 'z', 'socket', 'nsl', 'mygcc']
44
+ while not find_library('mysqlclient', 'mysql_query', lib, "#{lib}/mysql") do
45
+ exit 1 if libs.empty?
46
+ have_library(libs.shift)
47
+ end
48
+ end
49
+
50
+ if have_header('mysql.h') then
51
+ prefix = nil
52
+ elsif have_header('mysql/mysql.h') then
53
+ prefix = 'mysql'
54
+ else
55
+ asplode 'mysql.h'
56
+ end
57
+
58
+ %w{ errmsg.h mysqld_error.h }.each do |h|
59
+ header = [prefix, h].compact.join '/'
60
+ asplode h unless have_header h
61
+ end
62
+
63
+ # GCC specific flags
64
+ if RbConfig::MAKEFILE_CONFIG['CC'] =~ /gcc/
65
+ $CFLAGS << ' -Wall -funroll-loops'
66
+
67
+ if hard_mysql_path = $libs[%r{-L(/[^ ]+)}, 1]
68
+ $LDFLAGS << " -Wl,-rpath,#{hard_mysql_path}"
69
+ end
70
+ end
71
+
72
+ create_makefile('oedipus/oedipus')
@@ -0,0 +1,239 @@
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 "oedipus.h"
11
+
12
+ /* -- Public methods -- */
13
+
14
+ static VALUE odp_new(VALUE klass, VALUE host, VALUE port) {
15
+ OdpMysql * conn;
16
+ VALUE self;
17
+ VALUE args[2];
18
+
19
+ conn = malloc(sizeof(OdpMysql));
20
+ conn->connected = 0;
21
+
22
+ self = Data_Wrap_Struct(klass, 0, odp_free, conn);
23
+
24
+ args[0] = host;
25
+ args[1] = port;
26
+
27
+ rb_obj_call_init(self, 2, args);
28
+
29
+ return self;
30
+ }
31
+
32
+ static VALUE odp_initialize(VALUE self, VALUE host, VALUE port) {
33
+ Check_Type(host, T_STRING);
34
+ Check_Type(port, T_FIXNUM);
35
+
36
+ rb_iv_set(self, "@host", host);
37
+ rb_iv_set(self, "@port", port);
38
+
39
+ odp_open(self);
40
+
41
+ return self;
42
+ }
43
+
44
+ static VALUE odp_open(VALUE self) {
45
+ OdpMysql * conn;
46
+
47
+ Data_Get_Struct(self, OdpMysql, conn);
48
+
49
+ if (conn->connected) {
50
+ return Qfalse;
51
+ }
52
+
53
+ if ((conn->ptr = mysql_init(NULL)) == NULL) {
54
+ odp_raise(self, "Unable to initialize mysql");
55
+ }
56
+
57
+ if (mysql_real_connect(conn->ptr,
58
+ RSTRING_PTR(rb_iv_get(self, "@host")),
59
+ "",
60
+ "",
61
+ NULL,
62
+ NUM2UINT(rb_iv_get(self, "@port")),
63
+ NULL,
64
+ CLIENT_MULTI_STATEMENTS) == NULL) {
65
+ odp_raise(self, "Unable to connect to mysql");
66
+ }
67
+
68
+ conn->connected = 1;
69
+
70
+ return Qtrue;
71
+ }
72
+
73
+ static VALUE odp_close(VALUE self) {
74
+ OdpMysql * conn;
75
+
76
+ Data_Get_Struct(self, OdpMysql, conn);
77
+
78
+ if (!conn->connected) {
79
+ return Qfalse;
80
+ }
81
+
82
+ mysql_close(conn->ptr);
83
+ conn->connected = 0;
84
+
85
+ return Qtrue;
86
+ }
87
+
88
+ static VALUE odp_execute(VALUE self, VALUE sql) {
89
+ OdpMysql * conn;
90
+
91
+ Check_Type(sql, T_STRING);
92
+
93
+ Data_Get_Struct(self, OdpMysql, conn);
94
+
95
+ if (!conn->connected) {
96
+ odp_raise(self, "Cannot execute query on a closed connection");
97
+ }
98
+
99
+ if (mysql_query(conn->ptr, RSTRING_PTR(sql))) {
100
+ odp_raise(self, "Failed to execute statement(s)");
101
+ }
102
+
103
+ return INT2NUM(mysql_affected_rows(conn->ptr));
104
+ }
105
+
106
+ static VALUE odp_query(VALUE self, VALUE sql) {
107
+ OdpMysql * conn;
108
+ MYSQL_RES * rs;
109
+ int status;
110
+ int num_fields;
111
+ MYSQL_ROW row;
112
+ MYSQL_FIELD * fields;
113
+ unsigned long * lengths;
114
+ int i;
115
+ VALUE rows;
116
+ VALUE hash;
117
+ VALUE results;
118
+
119
+ Check_Type(sql, T_STRING);
120
+
121
+ Data_Get_Struct(self, OdpMysql, conn);
122
+
123
+ if (!conn->connected) {
124
+ odp_raise(self, "Cannot execute query on a closed connection");
125
+ }
126
+
127
+ if (mysql_query(conn->ptr, RSTRING_PTR(sql))) {
128
+ odp_raise(self, "Failed to execute statement(s)");
129
+ }
130
+
131
+ results = rb_ary_new();
132
+
133
+ do {
134
+ if ((rs = mysql_store_result(conn->ptr)) != NULL) {
135
+ rb_ary_push(results, (rows = rb_ary_new()));
136
+
137
+ num_fields = mysql_num_fields(rs);
138
+ fields = mysql_fetch_fields(rs);
139
+
140
+ while ((row = mysql_fetch_row(rs))) {
141
+ lengths = mysql_fetch_lengths(rs);
142
+ rb_ary_push(rows, (hash = rb_hash_new()));
143
+ for (i = 0; i < num_fields; ++i) {
144
+ rb_hash_aset(hash,
145
+ rb_str_new2(fields[i].name),
146
+ odp_cast_value(fields[i], row[i], lengths[i]));
147
+ }
148
+ }
149
+
150
+ mysql_free_result(rs);
151
+ }
152
+
153
+ if ((status = mysql_next_result(conn->ptr)) > 0) {
154
+ odp_raise(self, "Query execution failed");
155
+ }
156
+ } while (status == 0);
157
+
158
+ return results;
159
+ }
160
+
161
+ /* -- Internal functions -- */
162
+
163
+ static void odp_raise(VALUE self, const char *msg) {
164
+ OdpMysql * conn;
165
+
166
+ Data_Get_Struct(self, OdpMysql, conn);
167
+ rb_raise(rb_path2class("Oedipus::ConnectionError"),
168
+ "%s. Error %u: %s", msg, mysql_errno(conn->ptr), mysql_error(conn->ptr));
169
+ }
170
+
171
+ static void odp_free(OdpMysql *conn) {
172
+ if (conn->connected) {
173
+ mysql_close(conn->ptr);
174
+ }
175
+ free(conn);
176
+ }
177
+
178
+ static VALUE odp_cast_value(MYSQL_FIELD f, char * v, unsigned long len) {
179
+ short s;
180
+ int i;
181
+ long l;
182
+ double d;
183
+
184
+ // FIXME: Add the DATETIME, TIMESTAMP, TIME, DATE and YEAR conversions
185
+ switch (f.type) {
186
+ case MYSQL_TYPE_NULL:
187
+ return Qnil;
188
+
189
+ case MYSQL_TYPE_TINY:
190
+ case MYSQL_TYPE_SHORT:
191
+ sscanf(v, "%hd", &s);
192
+ return INT2NUM(s);
193
+
194
+ case MYSQL_TYPE_LONG:
195
+ sscanf(v, "%d", &i);
196
+ return INT2NUM(i);
197
+
198
+ case MYSQL_TYPE_INT24:
199
+ case MYSQL_TYPE_LONGLONG:
200
+ sscanf(v, "%ld", &l);
201
+ return INT2NUM(l);
202
+
203
+ case MYSQL_TYPE_DECIMAL:
204
+ case MYSQL_TYPE_NEWDECIMAL:
205
+ rb_require("bigdecimal");
206
+ return rb_funcall(rb_path2class("BigDecimal"),
207
+ rb_intern("new"),
208
+ 1,
209
+ rb_str_new(v, len));
210
+
211
+ case MYSQL_TYPE_DOUBLE:
212
+ case MYSQL_TYPE_FLOAT:
213
+ sscanf(v, "%lf", &d);
214
+ return DBL2NUM(d);
215
+
216
+ case MYSQL_TYPE_STRING:
217
+ case MYSQL_TYPE_VAR_STRING:
218
+ case MYSQL_TYPE_BLOB:
219
+ case MYSQL_TYPE_SET:
220
+ case MYSQL_TYPE_ENUM:
221
+ default:
222
+ return rb_str_new(v, len);
223
+ }
224
+ }
225
+
226
+ /* -- Extension initialization -- */
227
+
228
+ void Init_oedipus(void) {
229
+ VALUE mOedipus = rb_define_module("Oedipus");
230
+ VALUE cMysql = rb_define_class_under(mOedipus, "Mysql", rb_cObject);
231
+
232
+ rb_define_method(cMysql, "initialize", odp_initialize, 2);
233
+ rb_define_method(cMysql, "open", odp_open, 0);
234
+ rb_define_method(cMysql, "close", odp_close, 0);
235
+ rb_define_method(cMysql, "execute", odp_execute, 1);
236
+ rb_define_method(cMysql, "query", odp_query, 1);
237
+
238
+ rb_define_singleton_method(cMysql, "new", odp_new, 2);
239
+ }