sequel 2.3.0 → 2.4.0

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 (43) hide show
  1. data/CHANGELOG +16 -0
  2. data/README +4 -1
  3. data/Rakefile +17 -19
  4. data/doc/prepared_statements.rdoc +104 -0
  5. data/doc/sharding.rdoc +113 -0
  6. data/lib/sequel_core/adapters/ado.rb +24 -17
  7. data/lib/sequel_core/adapters/db2.rb +30 -33
  8. data/lib/sequel_core/adapters/dbi.rb +15 -13
  9. data/lib/sequel_core/adapters/informix.rb +13 -14
  10. data/lib/sequel_core/adapters/jdbc.rb +243 -60
  11. data/lib/sequel_core/adapters/jdbc/mysql.rb +32 -24
  12. data/lib/sequel_core/adapters/jdbc/postgresql.rb +32 -2
  13. data/lib/sequel_core/adapters/jdbc/sqlite.rb +16 -20
  14. data/lib/sequel_core/adapters/mysql.rb +164 -76
  15. data/lib/sequel_core/adapters/odbc.rb +21 -34
  16. data/lib/sequel_core/adapters/openbase.rb +10 -7
  17. data/lib/sequel_core/adapters/oracle.rb +17 -23
  18. data/lib/sequel_core/adapters/postgres.rb +246 -35
  19. data/lib/sequel_core/adapters/shared/mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/shared/mysql.rb +34 -26
  21. data/lib/sequel_core/adapters/shared/postgres.rb +82 -38
  22. data/lib/sequel_core/adapters/shared/sqlite.rb +48 -16
  23. data/lib/sequel_core/adapters/sqlite.rb +141 -44
  24. data/lib/sequel_core/connection_pool.rb +85 -63
  25. data/lib/sequel_core/database.rb +46 -17
  26. data/lib/sequel_core/dataset.rb +21 -40
  27. data/lib/sequel_core/dataset/convenience.rb +3 -3
  28. data/lib/sequel_core/dataset/prepared_statements.rb +218 -0
  29. data/lib/sequel_core/exceptions.rb +0 -12
  30. data/lib/sequel_model/base.rb +1 -2
  31. data/lib/sequel_model/plugins.rb +1 -1
  32. data/spec/adapters/ado_spec.rb +32 -3
  33. data/spec/adapters/mysql_spec.rb +7 -8
  34. data/spec/integration/prepared_statement_test.rb +106 -0
  35. data/spec/sequel_core/connection_pool_spec.rb +105 -3
  36. data/spec/sequel_core/database_spec.rb +41 -3
  37. data/spec/sequel_core/dataset_spec.rb +117 -7
  38. data/spec/sequel_core/spec_helper.rb +2 -2
  39. data/spec/sequel_model/model_spec.rb +0 -6
  40. data/spec/sequel_model/spec_helper.rb +1 -1
  41. metadata +11 -6
  42. data/lib/sequel_core/adapters/adapter_skeleton.rb +0 -54
  43. data/lib/sequel_core/adapters/odbc_mssql.rb +0 -106
@@ -41,13 +41,14 @@ module Sequel
41
41
 
42
42
  private_class_method :uri_to_options
43
43
 
44
- def connect
45
- dbname = @opts[:database]
44
+ def connect(server)
45
+ opts = server_opts(server)
46
+ dbname = opts[:database]
46
47
  if dbname !~ /^DBI:/ then
47
48
  dbname = "DBI:#{dbname}"
48
- [:host, :port].each{|sym| dbname += ";#{sym}=#{@opts[sym]}" unless @opts[sym].blank?}
49
+ [:host, :port].each{|sym| dbname += ";#{sym}=#{opts[sym]}" unless opts[sym].blank?}
49
50
  end
50
- ::DBI.connect(dbname, @opts[:user], @opts[:password])
51
+ ::DBI.connect(dbname, opts[:user], opts[:password])
51
52
  end
52
53
 
53
54
  def disconnect
@@ -58,18 +59,18 @@ module Sequel
58
59
  DBI::Dataset.new(self, opts)
59
60
  end
60
61
 
61
- def execute(sql)
62
+ def execute(sql, opts={})
62
63
  log_info(sql)
63
- @pool.hold do |conn|
64
- conn.execute(sql)
64
+ synchronize(opts[:server]) do |conn|
65
+ r = conn.execute(sql)
66
+ yield(r) if block_given?
67
+ r
65
68
  end
66
69
  end
67
70
 
68
- def do(sql)
71
+ def do(sql, opts={})
69
72
  log_info(sql)
70
- @pool.hold do |conn|
71
- conn.do(sql)
72
- end
73
+ synchronize(opts[:server]){|conn| conn.do(sql)}
73
74
  end
74
75
  alias_method :execute_dui, :do
75
76
 
@@ -92,8 +93,7 @@ module Sequel
92
93
  end
93
94
 
94
95
  def fetch_rows(sql, &block)
95
- @db.synchronize do
96
- s = @db.execute sql
96
+ execute(sql) do |s|
97
97
  begin
98
98
  @columns = s.column_names.map do |c|
99
99
  @db.lowercase ? c.downcase.to_sym : c.to_sym
@@ -106,6 +106,8 @@ module Sequel
106
106
  self
107
107
  end
108
108
 
109
+ private
110
+
109
111
  def hash_row(stmt, row)
110
112
  @columns.inject({}) do |m, c|
111
113
  m[c] = row.shift
@@ -11,12 +11,13 @@ module Sequel
11
11
  # AUTO_INCREMENT
12
12
  # end
13
13
 
14
- def connect
15
- ::Informix.connect(@opts[:database], @opts[:user], @opts[:password])
14
+ def connect(server)
15
+ opts = server_opts(server)
16
+ ::Informix.connect(opts[:database], opts[:user], opts[:password])
16
17
  end
17
18
 
18
19
  def disconnect
19
- @pool.disconnect {|c| c.close}
20
+ @pool.disconnect{|c| c.close}
20
21
  end
21
22
 
22
23
  def dataset(opts = nil)
@@ -24,15 +25,15 @@ module Sequel
24
25
  end
25
26
 
26
27
  # Returns number of rows affected
27
- def execute_dui(sql)
28
+ def execute_dui(sql, opts={})
28
29
  log_info(sql)
29
- @pool.hold {|c| c.immediate(sql)}
30
+ synchronize(opts[:server]){|c| c.immediate(sql)}
30
31
  end
31
32
  alias_method :do, :execute_dui
32
33
 
33
- def execute(sql, &block)
34
+ def execute(sql, opts={})
34
35
  log_info(sql)
35
- @pool.hold {|c| block[c.cursor(sql)]}
36
+ synchronize(opts[:server]){|c| yield c.cursor(sql)}
36
37
  end
37
38
  alias_method :query, :execute
38
39
  end
@@ -62,13 +63,11 @@ module Sequel
62
63
  end
63
64
 
64
65
  def fetch_rows(sql, &block)
65
- @db.synchronize do
66
- @db.execute(sql) do |cursor|
67
- begin
68
- cursor.open.each_hash(&block)
69
- ensure
70
- cursor.drop
71
- end
66
+ execute(sql) do |cursor|
67
+ begin
68
+ cursor.open.each_hash(&block)
69
+ ensure
70
+ cursor.drop
72
71
  end
73
72
  end
74
73
  self
@@ -1,9 +1,38 @@
1
1
  require 'java'
2
2
 
3
3
  module Sequel
4
+ # Houses Sequel's JDBC support when running on JRuby.
5
+ # Support for individual database types is done using sub adapters.
6
+ # PostgreSQL, MySQL, SQLite, and MSSQL all have relatively good support,
7
+ # close the the level supported by the native adapter.
8
+ # PostgreSQL, MySQL, SQLite can load necessary support using
9
+ # the jdbc-* gem, if it is installed, though they will work if you
10
+ # have the correct .jar in your CLASSPATH. Oracle and MSSQL should
11
+ # load the necessary support if you have the .jar in your CLASSPATH.
12
+ # For all other databases, the Java class should be loaded manually
13
+ # before calling Sequel.connect.
14
+ #
15
+ # Note that when using a JDBC adapter, the best way to use Sequel
16
+ # is via Sequel.connect, NOT Sequel.jdbc. Use the JDBC connection
17
+ # string when connecting, which will be in a different format than
18
+ # the native connection string. The connection string should start
19
+ # with 'jdbc:'. For PostgreSQL, use 'jdbc:postgresql:', and for
20
+ # SQLite you do not need 2 preceding slashes for the database name
21
+ # (use no preceding slashes for a relative path, and one preceding
22
+ # slash for an absolute path).
4
23
  module JDBC
5
- module JavaLang; include_package 'java.lang'; end
6
- module JavaSQL; include_package 'java.sql'; end
24
+ # Make it accesing the java.lang hierarchy more ruby friendly.
25
+ module JavaLang
26
+ include_package 'java.lang'
27
+ end
28
+
29
+ # Make it accesing the java.sql hierarchy more ruby friendly.
30
+ module JavaSQL
31
+ include_package 'java.sql'
32
+ end
33
+
34
+ # Contains procs keyed on sub adapter type that extend the
35
+ # given database object so it supports the correct database type.
7
36
  DATABASE_SETUP = {:postgresql=>proc do |db|
8
37
  require 'sequel_core/adapters/jdbc/postgresql'
9
38
  db.extend(Sequel::JDBC::Postgres::DatabaseMethods)
@@ -23,9 +52,15 @@ module Sequel
23
52
  org.sqlite.JDBC
24
53
  end,
25
54
  :oracle=>proc{oracle.jdbc.driver.OracleDriver},
26
- :sqlserver=>proc{com.microsoft.sqlserver.jdbc.SQLServerDriver}
55
+ :sqlserver=>proc do |db|
56
+ require 'sequel_core/adapters/shared/mssql'
57
+ db.extend(Sequel::MSSQL::DatabaseMethods)
58
+ com.microsoft.sqlserver.jdbc.SQLServerDriver
59
+ end
27
60
  }
28
61
 
62
+ # Allowing loading the necessary JDBC support via a gem, which
63
+ # works for PostgreSQL, MySQL, and SQLite.
29
64
  def self.load_gem(name)
30
65
  begin
31
66
  require "jdbc/#{name}"
@@ -34,12 +69,18 @@ module Sequel
34
69
  end
35
70
  end
36
71
 
72
+ # JDBC Databases offer a fairly uniform interface that does not change
73
+ # much based on the sub adapter.
37
74
  class Database < Sequel::Database
38
75
  set_adapter_scheme :jdbc
39
76
 
40
77
  # The type of database we are connecting to
41
78
  attr_reader :database_type
42
79
 
80
+ # Call the DATABASE_SETUP proc directly after initialization,
81
+ # so the object always uses sub adapter specific code. Also,
82
+ # raise an error immediately if the connection doesn't have a
83
+ # uri, since JDBC requires one.
43
84
  def initialize(opts)
44
85
  super(opts)
45
86
  raise(Error, "No connection string specified") unless uri
@@ -48,38 +89,42 @@ module Sequel
48
89
  end
49
90
  end
50
91
 
51
- def connect
52
- setup_connection(JavaSQL::DriverManager.getConnection(uri))
92
+ # Connect to the database using JavaSQL::DriverManager.getConnection.
93
+ def connect(server)
94
+ setup_connection(JavaSQL::DriverManager.getConnection(uri(server_opts(server))))
53
95
  end
54
96
 
97
+ # Return instances of JDBC::Dataset with the given opts.
55
98
  def dataset(opts = nil)
56
99
  JDBC::Dataset.new(self, opts)
57
100
  end
58
101
 
102
+ # Close all adapter connections
59
103
  def disconnect
60
104
  @pool.disconnect {|c| c.close}
61
105
  end
62
106
 
63
- def execute(sql)
64
- log_info(sql)
65
- @pool.hold do |conn|
66
- stmt = conn.createStatement
67
- begin
68
- yield stmt.executeQuery(sql)
69
- rescue NativeException, JavaSQL::SQLException => e
70
- raise Error, e.message
71
- ensure
72
- stmt.close
73
- end
74
- end
75
- end
76
-
77
- def execute_ddl(sql)
107
+ # Execute the given SQL. If a block is given, if should be a SELECT
108
+ # statement or something else that returns rows.
109
+ def execute(sql, opts={}, &block)
110
+ return execute_prepared_statement(sql, opts, &block) if sql.is_one_of?(Symbol, Dataset)
78
111
  log_info(sql)
79
- @pool.hold do |conn|
112
+ synchronize(opts[:server]) do |conn|
80
113
  stmt = conn.createStatement
81
114
  begin
82
- stmt.execute(sql)
115
+ if block_given?
116
+ yield stmt.executeQuery(sql)
117
+ else
118
+ case opts[:type]
119
+ when :ddl
120
+ stmt.execute(sql)
121
+ when :insert
122
+ stmt.executeUpdate(sql)
123
+ last_insert_id(conn, opts)
124
+ else
125
+ stmt.executeUpdate(sql)
126
+ end
127
+ end
83
128
  rescue NativeException, JavaSQL::SQLException => e
84
129
  raise Error, e.message
85
130
  ensure
@@ -87,28 +132,25 @@ module Sequel
87
132
  end
88
133
  end
89
134
  end
135
+ alias execute_dui execute
90
136
 
91
- def execute_dui(sql)
92
- log_info(sql)
93
- @pool.hold do |conn|
94
- stmt = conn.createStatement
95
- begin
96
- stmt.executeUpdate(sql)
97
- rescue NativeException, JavaSQL::SQLException => e
98
- raise Error, e.message
99
- ensure
100
- stmt.close
101
- end
102
- end
137
+ # Execute the given DDL SQL, which should not return any
138
+ # values or rows.
139
+ def execute_ddl(sql, opts={})
140
+ execute(sql, {:type=>:ddl}.merge(opts))
103
141
  end
104
142
 
105
- def setup_connection(conn)
106
- conn
143
+ # Execute the given INSERT SQL, returning the last inserted
144
+ # row id.
145
+ def execute_insert(sql, opts={})
146
+ execute(sql, {:type=>:insert}.merge(opts))
107
147
  end
108
148
 
109
- def transaction
110
- @pool.hold do |conn|
111
- @transactions ||= []
149
+ # Default transaction method that should work on most JDBC
150
+ # databases. Does not use the JDBC transaction methods, uses
151
+ # SQL BEGIN/ROLLBACK/COMMIT statements instead.
152
+ def transaction(server=nil)
153
+ synchronize(server) do |conn|
112
154
  return yield(conn) if @transactions.include?(Thread.current)
113
155
  stmt = conn.createStatement
114
156
  begin
@@ -131,55 +173,196 @@ module Sequel
131
173
  end
132
174
  end
133
175
 
134
- def uri
135
- ur = @opts[:uri] || @opts[:url] || @opts[:database]
176
+ # The uri for this connection. You can specify the uri
177
+ # using the :uri, :url, or :database options. You don't
178
+ # need to worry about this if you use Sequel.connect
179
+ # with the JDBC connectrion strings.
180
+ def uri(opts={})
181
+ opts = @opts.merge(opts)
182
+ ur = opts[:uri] || opts[:url] || opts[:database]
136
183
  ur =~ /^\Ajdbc:/ ? ur : "jdbc:#{ur}"
137
184
  end
138
185
  alias url uri
139
186
 
140
187
  private
141
188
 
189
+ # Execute the prepared statement. If the provided name is a
190
+ # dataset, use that as the prepared statement, otherwise use
191
+ # it as a key to look it up in the prepared_statements hash.
192
+ # If the connection we are using has already prepared an identical
193
+ # statement, use that statement instead of creating another.
194
+ # Otherwise, prepare a new statement for the connection, bind the
195
+ # variables, and execute it.
196
+ def execute_prepared_statement(name, opts={})
197
+ args = opts[:arguments]
198
+ if Dataset === name
199
+ ps = name
200
+ name = ps.prepared_statement_name
201
+ else
202
+ ps = prepared_statements[name]
203
+ end
204
+ sql = ps.prepared_sql
205
+ synchronize(opts[:server]) do |conn|
206
+ if name and cps = conn.prepared_statements[name] and cps[0] == sql
207
+ cps = cps[1]
208
+ else
209
+ if cps
210
+ log_info("Closing #{name}")
211
+ cps[1].close
212
+ end
213
+ log_info("Preparing#{" #{name}:" if name} #{sql}")
214
+ cps = conn.prepareStatement(sql)
215
+ conn.prepared_statements[name] = [sql, cps] if name
216
+ end
217
+ i = 0
218
+ args.each{|arg| set_ps_arg(cps, arg, i+=1)}
219
+ log_info("Executing#{" #{name}" if name}", args)
220
+ begin
221
+ if block_given?
222
+ yield cps.executeQuery
223
+ else
224
+ case opts[:type]
225
+ when :ddl
226
+ cps.execute
227
+ when :insert
228
+ cps.executeUpdate
229
+ last_insert_id(conn, opts)
230
+ else
231
+ cps.executeUpdate
232
+ end
233
+ end
234
+ rescue NativeException, JavaSQL::SQLException => e
235
+ raise Error, e.message
236
+ ensure
237
+ cps.close unless name
238
+ end
239
+ end
240
+ end
241
+
242
+ # By default, there is no support for determining the last inserted
243
+ # id, so return nil. This method should be overridden in
244
+ # sub adapters.
245
+ def last_insert_id(conn, opts)
246
+ nil
247
+ end
248
+
249
+ # Java being java, you need to specify the type of each argument
250
+ # for the prepared statement, and bind it individually. This
251
+ # guesses which JDBC method to use, and hopefully JRuby will convert
252
+ # things properly for us.
253
+ def set_ps_arg(cps, arg, i)
254
+ case arg
255
+ when Integer
256
+ cps.setInt(i, arg)
257
+ when String
258
+ cps.setString(i, arg)
259
+ when Date, Java::JavaSql::Date
260
+ cps.setDate(i, arg)
261
+ when Time, DateTime, Java::JavaSql::Timestamp
262
+ cps.setTimestamp(i, arg)
263
+ when Float
264
+ cps.setDouble(i, arg)
265
+ when nil
266
+ cps.setNull(i, JavaSQL::Types::NULL)
267
+ end
268
+ end
269
+
270
+ # Add a prepared_statements accessor to the connection,
271
+ # and set it to an empty hash. This is used to store
272
+ # adapter specific prepared statements.
273
+ def setup_connection(conn)
274
+ conn.meta_eval{attr_accessor :prepared_statements}
275
+ conn.prepared_statements = {}
276
+ conn
277
+ end
278
+
279
+ # The JDBC adapter should not need the pool to convert exceptions.
142
280
  def connection_pool_default_options
143
281
  super.merge(:pool_convert_exceptions=>false)
144
282
  end
145
283
  end
146
284
 
147
285
  class Dataset < Sequel::Dataset
286
+ # Use JDBC PreparedStatements instead of emulated ones. Statements
287
+ # created using #prepare are cached at the connection level to allow
288
+ # reuse. This also supports bind variables by using unnamed
289
+ # prepared statements created using #call.
290
+ module PreparedStatementMethods
291
+ include Sequel::Dataset::UnnumberedArgumentMapper
292
+
293
+ private
294
+
295
+ # Execute the prepared SQL using the stored type and
296
+ # arguments derived from the hash passed to call.
297
+ def execute(sql, opts={}, &block)
298
+ super(self, {:arguments=>bind_arguments, :type=>sql_query_type}.merge(opts), &block)
299
+ end
300
+
301
+ # Same as execute, explicit due to intricacies of alias and super.
302
+ def execute_dui(sql, opts={}, &block)
303
+ super(self, {:arguments=>bind_arguments, :type=>sql_query_type}.merge(opts), &block)
304
+ end
305
+
306
+ # Same as execute, explicit due to intricacies of alias and super.
307
+ def execute_insert(sql, opts={}, &block)
308
+ super(self, {:arguments=>bind_arguments, :type=>sql_query_type}.merge(opts), &block)
309
+ end
310
+ end
311
+
312
+ # Create an unnamed prepared statement and call it. Allows the
313
+ # use of bind variables.
314
+ def call(type, hash, values=nil, &block)
315
+ prepare(type, nil, values).call(hash, &block)
316
+ end
317
+
318
+ # Correctly return rows from the database and return them as hashes.
319
+ def fetch_rows(sql, &block)
320
+ execute(sql) do |result|
321
+ # get column names
322
+ meta = result.getMetaData
323
+ column_count = meta.getColumnCount
324
+ @columns = []
325
+ column_count.times {|i| @columns << meta.getColumnName(i+1).to_sym}
326
+
327
+ # get rows
328
+ while result.next
329
+ row = {}
330
+ @columns.each_with_index {|v, i| row[v] = result.getObject(i+1)}
331
+ yield row
332
+ end
333
+ end
334
+ self
335
+ end
336
+
337
+ # Use the ISO values for dates and times.
148
338
  def literal(v)
149
339
  case v
150
340
  when Time
151
341
  literal(v.iso8601)
152
- when Date, DateTime, Java::JavaSql::Timestamp
342
+ when Date, DateTime, Java::JavaSql::Timestamp, Java::JavaSql::Date
153
343
  literal(v.to_s)
154
344
  else
155
345
  super
156
346
  end
157
347
  end
158
-
159
- def fetch_rows(sql, &block)
160
- @db.synchronize do
161
- @db.execute(sql) do |result|
162
- # get column names
163
- meta = result.getMetaData
164
- column_count = meta.getColumnCount
165
- @columns = []
166
- column_count.times {|i| @columns << meta.getColumnName(i+1).to_sym}
167
-
168
- # get rows
169
- while result.next
170
- row = {}
171
- @columns.each_with_index {|v, i| row[v] = result.getObject(i+1)}
172
- yield row
173
- end
174
- end
348
+
349
+ # Create a named prepared statement that is stored in the
350
+ # database (and connection) for reuse.
351
+ def prepare(type, name, values=nil)
352
+ ps = to_prepared_statement(type, values)
353
+ ps.extend(PreparedStatementMethods)
354
+ if name
355
+ ps.prepared_statement_name = name
356
+ db.prepared_statements[name] = ps
175
357
  end
176
- self
358
+ ps
177
359
  end
178
360
  end
179
361
  end
180
362
  end
181
363
 
182
364
  class Java::JavaSQL::Timestamp
365
+ # Add a usec method in order to emulate Time values.
183
366
  def usec
184
367
  getNanos/1000
185
368
  end