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.
- data/.gitignore +2 -0
- data/README.md +235 -44
- data/Rakefile +25 -0
- data/ext/oedipus/extconf.rb +72 -0
- data/ext/oedipus/oedipus.c +239 -0
- data/ext/oedipus/oedipus.h +50 -0
- data/lib/oedipus/comparison/between.rb +26 -0
- data/lib/oedipus/comparison/equal.rb +21 -0
- data/lib/oedipus/comparison/gt.rb +21 -0
- data/lib/oedipus/comparison/gte.rb +21 -0
- data/lib/oedipus/comparison/in.rb +21 -0
- data/lib/oedipus/comparison/lt.rb +21 -0
- data/lib/oedipus/comparison/lte.rb +21 -0
- data/lib/oedipus/comparison/not.rb +25 -0
- data/lib/oedipus/comparison/not_equal.rb +21 -0
- data/lib/oedipus/comparison/not_in.rb +21 -0
- data/lib/oedipus/comparison/outside.rb +26 -0
- data/lib/oedipus/comparison/shortcuts.rb +144 -0
- data/lib/oedipus/comparison.rb +88 -0
- data/lib/oedipus/connection.rb +91 -13
- data/lib/oedipus/connection_error.rb +14 -0
- data/lib/oedipus/index.rb +189 -46
- data/lib/oedipus/query_builder.rb +97 -4
- data/lib/oedipus/version.rb +1 -1
- data/lib/oedipus.rb +24 -7
- data/oedipus.gemspec +4 -5
- data/spec/integration/connection_spec.rb +58 -0
- data/spec/integration/index_spec.rb +353 -0
- data/spec/spec_helper.rb +2 -23
- data/spec/support/test_harness.rb +30 -9
- data/spec/unit/comparison/between_spec.rb +36 -0
- data/spec/unit/comparison/equal_spec.rb +22 -0
- data/spec/unit/comparison/gt_spec.rb +22 -0
- data/spec/unit/comparison/gte_spec.rb +22 -0
- data/spec/unit/comparison/in_spec.rb +22 -0
- data/spec/unit/comparison/lt_spec.rb +22 -0
- data/spec/unit/comparison/lte_spec.rb +22 -0
- data/spec/unit/comparison/not_equal_spec.rb +22 -0
- data/spec/unit/comparison/not_in_spec.rb +22 -0
- data/spec/unit/comparison/not_spec.rb +37 -0
- data/spec/unit/comparison/outside_spec.rb +36 -0
- data/spec/unit/comparison/shortcuts_spec.rb +125 -0
- data/spec/unit/comparison_spec.rb +109 -0
- data/spec/unit/query_builder_spec.rb +150 -0
- metadata +68 -19
- data/lib/oedipus/mysql/client.rb +0 -136
- data/spec/unit/connection_spec.rb +0 -36
- data/spec/unit/index_spec.rb +0 -85
data/.gitignore
CHANGED
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
|
8
|
+
ActiveRecord or DataMapper... though this will follow in separate gems.
|
9
9
|
|
10
|
-
|
11
|
-
|
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.
|
19
|
-
|
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
|
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
|
-
|
27
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
43
|
-
|
76
|
+
```
|
77
|
+
|
78
|
+
### Updating
|
44
79
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# => { id: 7, author_id: 4, views: 103 }
|
80
|
+
``` ruby
|
81
|
+
sphinx[:articles].update(7, views: 103)
|
82
|
+
```
|
49
83
|
|
50
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
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
|
-
#
|
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
|
-
|
84
|
-
popular_farming: ["farming",
|
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
|
-
#
|
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
|
-
|
113
|
-
|
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
|
-
|
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",
|
118
|
-
frogs: "frogs
|
119
|
-
rabbits: ["rabbits
|
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
|
-
|
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
|
-
|
141
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
+
}
|