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
@@ -2,52 +2,60 @@ require 'sequel_core/adapters/shared/mysql'
2
2
 
3
3
  module Sequel
4
4
  module JDBC
5
+ # Database and Dataset instance methods for MySQL specific
6
+ # support via JDBC.
5
7
  module MySQL
8
+ # Database instance methods for MySQL databases accessed via JDBC.
6
9
  module DatabaseMethods
7
10
  include Sequel::MySQL::DatabaseMethods
8
11
 
12
+ # Return instance of Sequel::JDBC::MySQL::Dataset with the given opts.
9
13
  def dataset(opts=nil)
10
14
  Sequel::JDBC::MySQL::Dataset.new(self, opts)
11
15
  end
12
16
 
13
- def execute_insert(sql)
14
- begin
15
- log_info(sql)
16
- @pool.hold do |conn|
17
- stmt = conn.createStatement
18
- begin
19
- stmt.executeUpdate(sql)
20
- rs = stmt.executeQuery('SELECT LAST_INSERT_ID()')
21
- rs.next
22
- rs.getInt(1)
23
- rescue NativeException, JavaSQL::SQLException => e
24
- raise Error, e.message
25
- ensure
26
- stmt.close
27
- end
28
- end
29
- rescue NativeException, JavaSQL::SQLException => e
30
- raise Error, "#{sql}\r\n#{e.message}"
31
- end
32
- end
33
-
34
17
  private
35
18
 
19
+ # The database name for the given database. Need to parse it out
20
+ # of the connection string, since the JDBC does no parsing on the
21
+ # given connection string by default.
36
22
  def database_name
37
23
  u = URI.parse(uri.sub(/\Ajdbc:/, ''))
38
24
  (m = /\/(.*)/.match(u.path)) && m[1]
39
25
  end
26
+
27
+ # Get the last inserted id using LAST_INSERT_ID().
28
+ def last_insert_id(conn, opts={})
29
+ stmt = conn.createStatement
30
+ begin
31
+ rs = stmt.executeQuery('SELECT LAST_INSERT_ID()')
32
+ rs.next
33
+ rs.getInt(1)
34
+ ensure
35
+ stmt.close
36
+ end
37
+ end
40
38
  end
41
-
39
+
40
+ # Dataset class for MySQL datasets accessed via JDBC.
42
41
  class Dataset < JDBC::Dataset
43
42
  include Sequel::MySQL::DatasetMethods
44
43
 
44
+ # Use execute_insert to execute the insert_sql.
45
45
  def insert(*values)
46
- @db.execute_insert(insert_sql(*values))
46
+ execute_insert(insert_sql(*values))
47
47
  end
48
48
 
49
+ # Use execute_insert to execute the replace_sql.
49
50
  def replace(*args)
50
- @db.execute_insert(replace_sql(*args))
51
+ execute_insert(replace_sql(*args))
52
+ end
53
+
54
+ private
55
+
56
+ # Call execute_insert on the database.
57
+ def execute_insert(sql, opts={})
58
+ @db.execute_insert(sql, {:server=>@opts[:server] || :default}.merge(opts))
51
59
  end
52
60
  end
53
61
  end
@@ -4,12 +4,18 @@ module Sequel
4
4
  Postgres::CONVERTED_EXCEPTIONS << NativeException
5
5
 
6
6
  module JDBC
7
+ # Adapter, Database, and Dataset support for accessing a PostgreSQL
8
+ # database via JDBC.
7
9
  module Postgres
10
+ # Methods to add to the JDBC adapter/connection to allow it to work
11
+ # with the shared PostgreSQL code.
8
12
  module AdapterMethods
9
13
  include Sequel::Postgres::AdapterMethods
10
14
 
11
- def execute(sql, method=:execute)
12
- method = :executeQuery if block_given?
15
+ # Give the JDBC adapter a direct execute method, which creates
16
+ # a statement with the given sql and executes it.
17
+ def execute(sql, args=nil)
18
+ method = block_given? ? :executeQuery : :execute
13
19
  stmt = createStatement
14
20
  begin
15
21
  rows = stmt.send(method, sql)
@@ -21,6 +27,9 @@ module Sequel
21
27
  end
22
28
  end
23
29
 
30
+ private
31
+
32
+ # JDBC specific method of getting specific values from a result set.
24
33
  def result_set_values(r, *vals)
25
34
  return if r.nil?
26
35
  r.next
@@ -34,22 +43,43 @@ module Sequel
34
43
  end
35
44
  end
36
45
 
46
+ # Methods to add to Database instances that access PostgreSQL via
47
+ # JDBC.
37
48
  module DatabaseMethods
38
49
  include Sequel::Postgres::DatabaseMethods
39
50
 
51
+ # Return instance of Sequel::JDBC::Postgres::Dataset with the given opts.
40
52
  def dataset(opts=nil)
41
53
  Sequel::JDBC::Postgres::Dataset.new(self, opts)
42
54
  end
43
55
 
56
+ # Run the INSERT sql on the database and return the primary key
57
+ # for the record.
58
+ def execute_insert(sql, opts={})
59
+ super(sql, {:type=>:insert}.merge(opts))
60
+ end
61
+
62
+ private
63
+
64
+ # Extend the adapter with the JDBC PostgreSQL AdapterMethods
44
65
  def setup_connection(conn)
66
+ conn = super(conn)
45
67
  conn.extend(Sequel::JDBC::Postgres::AdapterMethods)
46
68
  conn
47
69
  end
70
+
71
+ # Call insert_result with the table and values specified in the opts.
72
+ def last_insert_id(conn, opts)
73
+ insert_result(conn, opts[:table], opts[:values])
74
+ end
48
75
  end
49
76
 
77
+ # Dataset subclass used for datasets that connect to PostgreSQL via JDBC.
50
78
  class Dataset < JDBC::Dataset
51
79
  include Sequel::Postgres::DatasetMethods
52
80
 
81
+ # Convert Java::JavaSql::Timestamps correctly, and handle SQL::Blobs
82
+ # correctly.
53
83
  def literal(v)
54
84
  case v
55
85
  when SQL::Blob
@@ -2,43 +2,39 @@ require 'sequel_core/adapters/shared/sqlite'
2
2
 
3
3
  module Sequel
4
4
  module JDBC
5
+ # Database and Dataset support for SQLite databases accessed via JDBC.
5
6
  module SQLite
7
+ # Instance methods for SQLite Database objects accessed via JDBC.
6
8
  module DatabaseMethods
7
9
  include Sequel::SQLite::DatabaseMethods
8
10
 
11
+ # Return Sequel::JDBC::SQLite::Dataset object with the given opts.
9
12
  def dataset(opts=nil)
10
13
  Sequel::JDBC::SQLite::Dataset.new(self, opts)
11
14
  end
12
15
 
13
- def execute_insert(sql)
16
+ private
17
+
18
+ # Use last_insert_rowid() to get the last inserted id.
19
+ def last_insert_id(conn, opts={})
20
+ stmt = conn.createStatement
14
21
  begin
15
- log_info(sql)
16
- @pool.hold do |conn|
17
- stmt = conn.createStatement
18
- begin
19
- stmt.executeUpdate(sql)
20
- rs = stmt.executeQuery('SELECT last_insert_rowid()')
21
- rs.next
22
- rs.getInt(1)
23
- rescue NativeException, JavaSQL::SQLException => e
24
- raise Error, e.message
25
- ensure
26
- stmt.close
27
- end
28
- end
29
- rescue NativeException, JavaSQL::SQLException => e
30
- raise Error, "#{sql}\r\n#{e.message}"
22
+ rs = stmt.executeQuery('SELECT last_insert_rowid()')
23
+ rs.next
24
+ rs.getInt(1)
25
+ ensure
26
+ stmt.close
31
27
  end
32
28
  end
33
29
 
34
- private
35
-
30
+ # Default to a single connection for a memory database.
36
31
  def connection_pool_default_options
37
32
  o = super
38
33
  uri == 'jdbc:sqlite::memory:' ? o.merge(:max_connections=>1) : o
39
34
  end
40
35
  end
41
-
36
+
37
+ # Dataset class for SQLite datasets accessed via JDBC.
42
38
  class Dataset < JDBC::Dataset
43
39
  include Sequel::SQLite::DatasetMethods
44
40
  end
@@ -1,8 +1,10 @@
1
1
  require 'mysql'
2
2
  require 'sequel_core/adapters/shared/mysql'
3
3
 
4
- # Monkey patch Mysql::Result to yield hashes with symbol keys
4
+ # Add methods to get columns, yield hashes with symbol keys, and do
5
+ # type conversion.
5
6
  class Mysql::Result
7
+ # Mapping of type numbers to conversion methods.
6
8
  MYSQL_TYPES = {
7
9
  0 => :to_d, # MYSQL_TYPE_DECIMAL
8
10
  1 => :to_i, # MYSQL_TYPE_TINY
@@ -32,21 +34,8 @@ class Mysql::Result
32
34
  # 254 => :to_s, # MYSQL_TYPE_STRING
33
35
  # 255 => :to_s # MYSQL_TYPE_GEOMETRY
34
36
  }
35
-
36
- def convert_type(v, type)
37
- if v
38
- if type == 1 && Sequel.convert_tinyint_to_bool
39
- # We special case tinyint here to avoid adding
40
- # a method to an ancestor of Fixnum
41
- v.to_i == 0 ? false : true
42
- else
43
- (t = MYSQL_TYPES[type]) ? v.send(t) : v
44
- end
45
- else
46
- nil
47
- end
48
- end
49
-
37
+
38
+ # Return an array of column name symbols for this result set.
50
39
  def columns(with_table = nil)
51
40
  unless @columns
52
41
  @column_types = []
@@ -58,18 +47,7 @@ class Mysql::Result
58
47
  @columns
59
48
  end
60
49
 
61
- def each_array(with_table = nil)
62
- c = columns
63
- while row = fetch_row
64
- c.each_with_index do |f, i|
65
- if (t = MYSQL_TYPES[@column_types[i]]) && (v = row[i])
66
- row[i] = v.send(t)
67
- end
68
- end
69
- yield row
70
- end
71
- end
72
-
50
+ # yield a hash with symbol keys and type converted values.
73
51
  def sequel_each_hash(with_table = nil)
74
52
  c = columns
75
53
  while row = fetch_row
@@ -79,86 +57,104 @@ class Mysql::Result
79
57
  end
80
58
  end
81
59
 
60
+ private
61
+
62
+ # Convert the type of v using the method in MYSQL_TYPES[type].
63
+ def convert_type(v, type)
64
+ if v
65
+ if type == 1 && Sequel.convert_tinyint_to_bool
66
+ # We special case tinyint here to avoid adding
67
+ # a method to an ancestor of Fixnum
68
+ v.to_i == 0 ? false : true
69
+ else
70
+ (t = MYSQL_TYPES[type]) ? v.send(t) : v
71
+ end
72
+ else
73
+ nil
74
+ end
75
+ end
76
+
82
77
  end
83
78
 
84
79
  module Sequel
85
- module MySQL
80
+ # Module for holding all MySQL-related classes and modules for Sequel.
81
+ module MySQL
82
+ # Database class for MySQL databases used with Sequel.
86
83
  class Database < Sequel::Database
87
84
  include Sequel::MySQL::DatabaseMethods
88
85
 
89
86
  set_adapter_scheme :mysql
90
-
91
- def connect
87
+
88
+ # Connect to the database. In addition to the usual database options,
89
+ # the following options have effect:
90
+ #
91
+ # * :encoding, :charset - Set all the related character sets for this
92
+ # connection (connection, client, database, server, and results).
93
+ # * :socket - Use a unix socket file instead of connecting via TCP/IP.
94
+ def connect(server)
95
+ opts = server_opts(server)
92
96
  conn = Mysql.init
93
97
  conn.options(Mysql::OPT_LOCAL_INFILE, "client")
94
98
  conn.real_connect(
95
- @opts[:host] || 'localhost',
96
- @opts[:user],
97
- @opts[:password],
98
- @opts[:database],
99
- @opts[:port],
100
- @opts[:socket],
99
+ opts[:host] || 'localhost',
100
+ opts[:user],
101
+ opts[:password],
102
+ opts[:database],
103
+ opts[:port],
104
+ opts[:socket],
101
105
  Mysql::CLIENT_MULTI_RESULTS +
102
106
  Mysql::CLIENT_MULTI_STATEMENTS +
103
107
  Mysql::CLIENT_COMPRESS
104
108
  )
105
109
  conn.query_with_result = false
106
- if encoding = @opts[:encoding] || @opts[:charset]
110
+ if encoding = opts[:encoding] || opts[:charset]
107
111
  conn.query("set character_set_connection = '#{encoding}'")
108
112
  conn.query("set character_set_client = '#{encoding}'")
109
113
  conn.query("set character_set_database = '#{encoding}'")
110
114
  conn.query("set character_set_server = '#{encoding}'")
111
115
  conn.query("set character_set_results = '#{encoding}'")
112
116
  end
117
+ conn.meta_eval{attr_accessor :prepared_statements}
118
+ conn.prepared_statements = {}
113
119
  conn.reconnect = true
114
120
  conn
115
121
  end
116
122
 
123
+ # Returns instance of Sequel::MySQL::Dataset with the given options.
117
124
  def dataset(opts = nil)
118
125
  MySQL::Dataset.new(self, opts)
119
126
  end
120
-
127
+
128
+ # Closes all database connections.
121
129
  def disconnect
122
130
  @pool.disconnect {|c| c.close}
123
131
  end
124
-
125
- def execute(sql, &block)
132
+
133
+ # Executes the given SQL using an available connection, yielding the
134
+ # connection if the block is given.
135
+ def execute(sql, opts={}, &block)
136
+ return execute_prepared_statement(sql, opts, &block) if Symbol === sql
126
137
  begin
127
- log_info(sql)
128
- @pool.hold do |conn|
129
- conn.query(sql)
130
- block[conn] if block
131
- end
138
+ synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)}
132
139
  rescue Mysql::Error => e
133
140
  raise Error.new(e.message)
134
141
  end
135
142
  end
136
-
137
- def execute_select(sql, &block)
138
- execute(sql) do |c|
139
- r = c.use_result
140
- begin
141
- block[r]
142
- ensure
143
- r.free
144
- end
145
- end
146
- end
147
143
 
148
- def server_version
149
- @server_version ||= (synchronize{|conn| conn.server_version if conn.respond_to?(:server_version)} || super)
144
+ # Return the version of the MySQL server two which we are connecting.
145
+ def server_version(server=nil)
146
+ @server_version ||= (synchronize(server){|conn| conn.server_version if conn.respond_to?(:server_version)} || super)
150
147
  end
151
148
 
152
- def tables
153
- @pool.hold do |conn|
154
- conn.list_tables.map {|t| t.to_sym}
155
- end
149
+ # Return an array of symbols specifying table names in the current database.
150
+ def tables(server=nil)
151
+ synchronize(server){|conn| conn.list_tables.map {|t| t.to_sym}}
156
152
  end
157
153
 
158
- def transaction
159
- @pool.hold do |conn|
160
- @transactions ||= []
161
- return yield(conn) if @transactions.include? Thread.current
154
+ # Support single level transactions on MySQL.
155
+ def transaction(server=nil)
156
+ synchronize(server) do |conn|
157
+ return yield(conn) if @transactions.include?(Thread.current)
162
158
  log_info(SQL_BEGIN)
163
159
  conn.query(SQL_BEGIN)
164
160
  begin
@@ -180,34 +176,103 @@ module Sequel
180
176
 
181
177
  private
182
178
 
179
+ # Execute the given SQL on the given connection. If the :type
180
+ # option is :select, yield the result of the query, otherwise
181
+ # yield the connection if a block is given.
182
+ def _execute(conn, sql, opts)
183
+ log_info(sql)
184
+ conn.query(sql)
185
+ if opts[:type] == :select
186
+ r = conn.use_result
187
+ begin
188
+ yield r
189
+ ensure
190
+ r.free
191
+ end
192
+ else
193
+ yield conn if block_given?
194
+ end
195
+ end
196
+
197
+ # MySQL doesn't need the connection pool to convert exceptions.
183
198
  def connection_pool_default_options
184
199
  super.merge(:pool_convert_exceptions=>false)
185
200
  end
186
201
 
202
+ # The database name when using the native adapter is always stored in
203
+ # the :database option.
187
204
  def database_name
188
205
  @opts[:database]
189
206
  end
207
+
208
+ # Executes a prepared statement on an available connection. If the
209
+ # prepared statement already exists for the connection and has the same
210
+ # SQL, reuse it, otherwise, prepare the new statement. Because of the
211
+ # usual MySQL stupidity, we are forced to name arguments via separate
212
+ # SET queries. Use @sequel_arg_N (for N starting at 1) for these
213
+ # arguments.
214
+ def execute_prepared_statement(ps_name, opts, &block)
215
+ args = opts[:arguments]
216
+ ps = prepared_statements[ps_name]
217
+ sql = ps.prepared_sql
218
+ synchronize(opts[:server]) do |conn|
219
+ unless conn.prepared_statements[ps_name] == sql
220
+ conn.prepared_statements[ps_name] = sql
221
+ s = "PREPARE #{ps_name} FROM '#{::Mysql.quote(sql)}'"
222
+ log_info(s)
223
+ conn.query(s)
224
+ end
225
+ i = 0
226
+ args.each do |arg|
227
+ s = "SET @sequel_arg_#{i+=1} = #{literal(arg)}"
228
+ log_info(s)
229
+ conn.query(s)
230
+ end
231
+ _execute(conn, "EXECUTE #{ps_name}#{" USING #{(1..i).map{|j| "@sequel_arg_#{j}"}.join(', ')}" unless i == 0}", opts, &block)
232
+ end
233
+ end
190
234
  end
191
-
235
+
236
+ # Dataset class for MySQL datasets accessed via the native driver.
192
237
  class Dataset < Sequel::Dataset
193
238
  include Sequel::MySQL::DatasetMethods
194
-
239
+
240
+ # Methods for MySQL prepared statements using the native driver.
241
+ module PreparedStatementMethods
242
+ include Sequel::Dataset::UnnumberedArgumentMapper
243
+
244
+ # Execute the prepared statement with the bind arguments instead of
245
+ # the given SQL.
246
+ def execute(sql, opts={}, &block)
247
+ super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
248
+ end
249
+
250
+ # Same as execute, explicit due to intricacies of alias and super.
251
+ def execute_dui(sql, opts={}, &block)
252
+ super(prepared_statement_name, {:arguments=>bind_arguments}.merge(opts), &block)
253
+ end
254
+ end
255
+
256
+ # Delete rows matching this dataset
195
257
  def delete(opts = nil)
196
- @db.execute(delete_sql(opts)) {|c| c.affected_rows}
258
+ execute_dui(delete_sql(opts)){|c| c.affected_rows}
197
259
  end
198
-
260
+
261
+ # Yield all rows matching this dataset
199
262
  def fetch_rows(sql)
200
- @db.execute_select(sql) do |r|
263
+ execute(sql) do |r|
201
264
  @columns = r.columns
202
265
  r.sequel_each_hash {|row| yield row}
203
266
  end
204
267
  self
205
268
  end
206
-
269
+
270
+ # Insert a new value into this dataset
207
271
  def insert(*values)
208
- @db.execute(insert_sql(*values)) {|c| c.insert_id}
272
+ execute_dui(insert_sql(*values)){|c| c.insert_id}
209
273
  end
210
274
 
275
+ # Handle correct quoting of strings using ::MySQL.quote.
211
276
  def literal(v)
212
277
  case v
213
278
  when LiteralString
@@ -219,12 +284,35 @@ module Sequel
219
284
  end
220
285
  end
221
286
 
287
+ # Store the given type of prepared statement in the associated database
288
+ # with the given name.
289
+ def prepare(type, name, values=nil)
290
+ ps = to_prepared_statement(type, values)
291
+ ps.extend(PreparedStatementMethods)
292
+ ps.prepared_statement_name = name
293
+ db.prepared_statements[name] = ps
294
+ end
295
+
296
+ # Replace (update or insert) the matching row.
222
297
  def replace(*args)
223
- @db.execute(replace_sql(*args)) {|c| c.insert_id}
298
+ execute_dui(replace_sql(*args)){|c| c.insert_id}
224
299
  end
225
300
 
301
+ # Update the matching rows.
226
302
  def update(*args)
227
- @db.execute(update_sql(*args)) {|c| c.affected_rows}
303
+ execute_dui(update_sql(*args)){|c| c.affected_rows}
304
+ end
305
+
306
+ private
307
+
308
+ # Set the :type option to :select if it hasn't been set.
309
+ def execute(sql, opts={}, &block)
310
+ super(sql, {:type=>:select}.merge(opts), &block)
311
+ end
312
+
313
+ # Set the :type option to :dui if it hasn't been set.
314
+ def execute_dui(sql, opts={}, &block)
315
+ super(sql, {:type=>:dui}.merge(opts), &block)
228
316
  end
229
317
  end
230
318
  end