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
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
+ }