sequel 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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