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
@@ -40,6 +40,9 @@ module Sequel
40
40
 
41
41
  # Whether to quote identifiers (columns and tables) for this database
42
42
  attr_writer :quote_identifiers
43
+
44
+ # The prepared statement objects for this database, keyed by name
45
+ attr_reader :prepared_statements
43
46
 
44
47
  # Constructs a new instance of a database connection with the specified
45
48
  # options hash.
@@ -51,8 +54,10 @@ module Sequel
51
54
  @quote_identifiers = opts.include?(:quote_identifiers) ? opts[:quote_identifiers] : @@quote_identifiers
52
55
  @single_threaded = opts.include?(:single_threaded) ? opts[:single_threaded] : @@single_threaded
53
56
  @schemas = nil
57
+ @prepared_statements = {}
58
+ @transactions = []
54
59
  @pool = (@single_threaded ? SingleThreadedPool : ConnectionPool).new(connection_pool_default_options.merge(opts), &block)
55
- @pool.connection_proc = proc {connect} unless block
60
+ @pool.connection_proc = proc{|server| connect(server)} unless block
56
61
 
57
62
  @loggers = Array(opts[:logger]) + Array(opts[:loggers])
58
63
  ::Sequel::DATABASES.push(self)
@@ -191,6 +196,12 @@ module Sequel
191
196
  (String === args.first) ? fetch(*args, &block) : from(*args, &block)
192
197
  end
193
198
 
199
+ # Call the prepared statement with the given name with the given hash
200
+ # of arguments.
201
+ def call(ps_name, hash={})
202
+ prepared_statements[ps_name].call(hash)
203
+ end
204
+
194
205
  # Connects to the database. This method should be overridden by descendants.
195
206
  def connect
196
207
  raise NotImplementedError, "#connect should be overridden by adapters"
@@ -208,20 +219,20 @@ module Sequel
208
219
  end
209
220
 
210
221
  # Executes the given SQL. This method should be overridden in descendants.
211
- def execute(sql)
222
+ def execute(sql, opts={})
212
223
  raise NotImplementedError, "#execute should be overridden by adapters"
213
224
  end
214
225
 
215
226
  # Method that should be used when submitting any DDL (Data Definition
216
227
  # Language) SQL. By default, calls execute_dui.
217
- def execute_ddl(sql)
218
- execute_dui(sql)
228
+ def execute_ddl(sql, opts={}, &block)
229
+ execute_dui(sql, opts, &block)
219
230
  end
220
231
 
221
232
  # Method that should be used when issuing a DELETE, UPDATE, or INSERT
222
233
  # statement. By default, calls execute.
223
- def execute_dui(sql)
224
- execute(sql)
234
+ def execute_dui(sql, opts={}, &block)
235
+ execute(sql, opts, &block)
225
236
  end
226
237
 
227
238
  # Fetches records for an arbitrary SQL statement. If a block is given,
@@ -273,7 +284,8 @@ module Sequel
273
284
 
274
285
  # Log a message at level info to all loggers. All SQL logging
275
286
  # goes through this method.
276
- def log_info(message)
287
+ def log_info(message, args=nil)
288
+ message = "#{message}; #{args.inspect}" if args
277
289
  @loggers.each{|logger| logger.info(message)}
278
290
  end
279
291
 
@@ -319,8 +331,8 @@ module Sequel
319
331
  end
320
332
 
321
333
  # Acquires a database connection, yielding it to the passed block.
322
- def synchronize(&block)
323
- @pool.hold(&block)
334
+ def synchronize(server=nil, &block)
335
+ @pool.hold(server || :default, &block)
324
336
  end
325
337
 
326
338
  # Returns true if a table with the given name exists.
@@ -339,8 +351,8 @@ module Sequel
339
351
 
340
352
  # Attempts to acquire a database connection. Returns true if successful.
341
353
  # Will probably raise an error if unsuccessful.
342
- def test_connection
343
- synchronize{|conn|}
354
+ def test_connection(server=nil)
355
+ synchronize(server){|conn|}
344
356
  true
345
357
  end
346
358
 
@@ -348,12 +360,9 @@ module Sequel
348
360
  # supported - calling #transaction within a transaction will reuse the
349
361
  # current transaction. Should be overridden for databases that support nested
350
362
  # transactions.
351
- def transaction
352
- @pool.hold do |conn|
353
- @transactions ||= []
354
- if @transactions.include? Thread.current
355
- return yield(conn)
356
- end
363
+ def transaction(server=nil)
364
+ synchronize(server) do |conn|
365
+ return yield(conn) if @transactions.include?(Thread.current)
357
366
  log_info(SQL_BEGIN)
358
367
  conn.execute(SQL_BEGIN)
359
368
  begin
@@ -461,6 +470,26 @@ module Sequel
461
470
  alias_method :url, :uri
462
471
 
463
472
  private
473
+
474
+ # Return the options for the given server by merging the generic
475
+ # options for all server with the specific options for the given
476
+ # server specified in the :servers option.
477
+ def server_opts(server)
478
+ opts = if @opts[:servers] && server_options = @opts[:servers][server]
479
+ case server_options
480
+ when Hash
481
+ @opts.merge(server_options)
482
+ when Proc
483
+ @opts.merge(server_options.call(self))
484
+ else
485
+ raise Error, 'Server opts should be a hash or proc'
486
+ end
487
+ else
488
+ @opts.dup
489
+ end
490
+ opts.delete(:servers)
491
+ opts
492
+ end
464
493
 
465
494
  # The default options for the connection pool.
466
495
  def connection_pool_default_options
@@ -1,4 +1,4 @@
1
- %w'callback convenience pagination query schema sql'.each do |f|
1
+ %w'callback convenience pagination prepared_statements query schema sql'.each do |f|
2
2
  require "sequel_core/dataset/#{f}"
3
3
  end
4
4
 
@@ -26,42 +26,6 @@ module Sequel
26
26
  # Datasets are Enumerable objects, so they can be manipulated using any
27
27
  # of the Enumerable methods, such as map, inject, etc.
28
28
  #
29
- # === The Dataset Adapter Interface
30
- #
31
- # Each adapter should define its own dataset class as a descendant of
32
- # Sequel::Dataset. The following methods should be overridden by the adapter
33
- # Dataset class (each method with the stock implementation):
34
- #
35
- # # Iterate over the results of the SQL query and call the supplied
36
- # # block with each record (as a hash).
37
- # def fetch_rows(sql, &block)
38
- # @db.synchronize do
39
- # r = @db.execute(sql)
40
- # r.each(&block)
41
- # end
42
- # end
43
- #
44
- # # Insert records.
45
- # def insert(*values)
46
- # @db.synchronize do
47
- # @db.execute(insert_sql(*values)).last_insert_id
48
- # end
49
- # end
50
- #
51
- # # Update records.
52
- # def update(*args)
53
- # @db.synchronize do
54
- # @db.execute(update_sql(*args)).affected_rows
55
- # end
56
- # end
57
- #
58
- # # Delete records.
59
- # def delete(opts = nil)
60
- # @db.synchronize do
61
- # @db.execute(delete_sql(opts)).affected_rows
62
- # end
63
- # end
64
- #
65
29
  # === Methods added via metaprogramming
66
30
  #
67
31
  # Some methods are added via metaprogramming:
@@ -221,7 +185,7 @@ module Sequel
221
185
 
222
186
  # Deletes the records in the dataset. Adapters should override this method.
223
187
  def delete(*args)
224
- @db.execute_dui(delete_sql(*args))
188
+ execute_dui(delete_sql(*args))
225
189
  end
226
190
 
227
191
  # Iterates over the records in the dataset.
@@ -249,7 +213,7 @@ module Sequel
249
213
  # Inserts values into the associated table. Adapters should override this
250
214
  # method.
251
215
  def insert(*values)
252
- @db.execute_dui(insert_sql(*values))
216
+ execute_dui(insert_sql(*values))
253
217
  end
254
218
 
255
219
  # Returns a string representation of the dataset including the class name
@@ -282,6 +246,13 @@ module Sequel
282
246
  def quote_identifiers?
283
247
  @quote_identifiers
284
248
  end
249
+
250
+ # Set the server for this dataset to use. Used to pick a specific database
251
+ # shard to run a query against, or to override the default SELECT uses
252
+ # :read_only database and all other queries use the :default database.
253
+ def server(servr)
254
+ clone(:server=>servr)
255
+ end
285
256
 
286
257
  # Alias for set, but not aliased directly so subclasses
287
258
  # don't have to override both methods.
@@ -430,7 +401,7 @@ module Sequel
430
401
 
431
402
  # Updates values for the dataset. Adapters should override this method.
432
403
  def update(*args)
433
- @db.execute_dui(update_sql(*args))
404
+ execute_dui(update_sql(*args))
434
405
  end
435
406
 
436
407
  # Add the mutation methods via metaprogramming
@@ -444,6 +415,16 @@ module Sequel
444
415
  end
445
416
 
446
417
  private
418
+
419
+ # Execute the given SQL on the database using execute.
420
+ def execute(sql, opts={}, &block)
421
+ @db.execute(sql, {:server=>@opts[:server] || :read_only}.merge(opts), &block)
422
+ end
423
+
424
+ # Execute the given SQL on the database using execute_dui.
425
+ def execute_dui(sql, opts={}, &block)
426
+ @db.execute_dui(sql, {:server=>@opts[:server] || :default}.merge(opts), &block)
427
+ end
447
428
 
448
429
  # Modify the receiver with the results of sending the meth, args, and block
449
430
  # to the receiver and merging the options of the resulting dataset into
@@ -136,7 +136,7 @@ module Sequel
136
136
  table = @opts[:from].first
137
137
  columns, dataset = *args
138
138
  sql = "INSERT INTO #{quote_identifier(table)} #{literal(columns)} VALUES #{literal(dataset)}"
139
- return @db.transaction {@db.execute_dui sql}
139
+ return @db.transaction{execute_dui(sql)}
140
140
  else
141
141
  # we assume that an array of hashes is given
142
142
  hashes, opts = *args
@@ -153,11 +153,11 @@ module Sequel
153
153
  if slice_size
154
154
  values.each_slice(slice_size) do |slice|
155
155
  statements = multi_insert_sql(columns, slice)
156
- @db.transaction {statements.each {|st| @db.execute_dui(st)}}
156
+ @db.transaction{statements.each{|st| execute_dui(st)}}
157
157
  end
158
158
  else
159
159
  statements = multi_insert_sql(columns, values)
160
- @db.transaction {statements.each {|st| @db.execute_dui(st)}}
160
+ @db.transaction{statements.each{|st| execute_dui(st)}}
161
161
  end
162
162
  end
163
163
  alias_method :import, :multi_insert
@@ -0,0 +1,218 @@
1
+ module Sequel
2
+ class Dataset
3
+ PREPARED_ARG_PLACEHOLDER = '?'.lit.freeze
4
+
5
+ # Default implementation of the argument mapper to allow
6
+ # native database support for bind variables and prepared
7
+ # statements (as opposed to the emulated ones used by default).
8
+ module ArgumentMapper
9
+ SQL_QUERY_TYPE = Hash.new{|h,k| h[k] = k}
10
+ SQL_QUERY_TYPE[:first] = SQL_QUERY_TYPE[:all] = :select
11
+
12
+ # The name of the prepared statement, if any.
13
+ attr_accessor :prepared_statement_name
14
+
15
+ # The bind arguments to use for running this prepared statement
16
+ attr_accessor :bind_arguments
17
+
18
+ # Set the bind arguments based on the hash and call super.
19
+ def call(hash, &block)
20
+ ds = clone
21
+ ds.prepared_sql
22
+ ds.bind_arguments = ds.map_to_prepared_args(hash)
23
+ ds.prepared_args = hash
24
+ ds.run(&block)
25
+ end
26
+
27
+ # Override the given *_sql method based on the type, and
28
+ # cache the result of the sql. This requires that the object
29
+ # that includes this module implement prepared_args_hash.
30
+ def prepared_sql
31
+ return @prepared_sql if @prepared_sql
32
+ @prepared_args = prepared_args_hash
33
+ @prepared_sql = super
34
+ meta_def("#{sql_query_type}_sql"){|*args| prepared_sql}
35
+ @prepared_sql
36
+ end
37
+
38
+ private
39
+
40
+ def sql_query_type
41
+ SQL_QUERY_TYPE[@prepared_type]
42
+ end
43
+ end
44
+
45
+ # Backbone of the prepared statement support. Grafts bind variable
46
+ # support into datasets by hijacking #literal and using placeholders.
47
+ # By default, emulates prepared statements and bind variables by
48
+ # taking the hash of bind variables and directly substituting them
49
+ # into the query, which works on all databases, as it is no different
50
+ # from using the dataset without bind variables.
51
+ module PreparedStatementMethods
52
+ PLACEHOLDER_RE = /\A\$(.*)\z/
53
+
54
+ # The type of prepared statement, should be one of :select,
55
+ # :insert, :update, or :delete
56
+ attr_accessor :prepared_type
57
+
58
+ # The bind variable hash to use when substituting
59
+ attr_accessor :prepared_args
60
+
61
+ # The argument to supply to insert and update, which may use
62
+ # placeholders specified by prepared_args
63
+ attr_accessor :prepared_modify_values
64
+
65
+ # Sets the prepared_args to the given hash and runs the
66
+ # prepared statement.
67
+ def call(hash, &block)
68
+ ds = clone
69
+ ds.prepared_args = hash
70
+ ds.run(&block)
71
+ end
72
+
73
+ # Returns the SQL for the prepared statement, depending on
74
+ # the type of the statement and the prepared_modify_values.
75
+ def prepared_sql
76
+ case @prepared_type
77
+ when :select, :all
78
+ select_sql
79
+ when :first
80
+ select_sql(:limit=>1)
81
+ when :insert
82
+ insert_sql(@prepared_modify_values)
83
+ when :update
84
+ update_sql(@prepared_modify_values)
85
+ when :delete
86
+ delete_sql
87
+ end
88
+ end
89
+
90
+ # Changes the values of symbols if they start with $ and
91
+ # prepared_args is present. If so, they are considered placeholders,
92
+ # and they are substituted using prepared_arg.
93
+ def literal(v)
94
+ case v
95
+ when Symbol
96
+ if match = PLACEHOLDER_RE.match(v.to_s) and @prepared_args
97
+ super(prepared_arg(match[1].to_sym))
98
+ else
99
+ super
100
+ end
101
+ else
102
+ super
103
+ end
104
+ end
105
+
106
+ # Programmer friendly string showing this is a prepared statement,
107
+ # with the prepared SQL it represents (which in general won't have
108
+ # substituted variables).
109
+ def inspect
110
+ "<#{self.class.name}/PreparedStatement #{prepared_sql.inspect}>"
111
+ end
112
+
113
+ protected
114
+
115
+ # Run the method based on the type of prepared statement, with
116
+ # :select running #all to get all of the rows, and the other
117
+ # types running the method with the same name as the type.
118
+ def run(&block)
119
+ case @prepared_type
120
+ when :select, :all
121
+ all(&block)
122
+ when :first
123
+ first
124
+ when :insert
125
+ insert(@prepared_modify_values)
126
+ when :update
127
+ update(@prepared_modify_values)
128
+ when :delete
129
+ delete
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ # Returns the value of the prepared_args hash for the given key.
136
+ def prepared_arg(k)
137
+ @prepared_args[k]
138
+ end
139
+ end
140
+
141
+ # Default implementation for an argument mapper that uses
142
+ # unnumbered SQL placeholder arguments. Keeps track of which
143
+ # arguments have been used, and allows arguments to
144
+ # be used more than once.
145
+ module UnnumberedArgumentMapper
146
+ include ArgumentMapper
147
+
148
+ protected
149
+
150
+ # Returns a single output array mapping the values of the input hash.
151
+ # Keys in the input hash that are used more than once in the query
152
+ # have multiple entries in the output array.
153
+ def map_to_prepared_args(hash)
154
+ array = []
155
+ @prepared_args.each{|k,vs| vs.each{|v| array[v] = hash[k]}}
156
+ array
157
+ end
158
+
159
+ private
160
+
161
+ # Uses a separate array of each key, holding the positions
162
+ # in the output array (necessary to support arguments
163
+ # that are used more than once).
164
+ def prepared_args_hash
165
+ Hash.new{|h,k| h[k] = Array.new}
166
+ end
167
+
168
+ # Associates the argument with name k with the next position in
169
+ # the output array.
170
+ def prepared_arg(k)
171
+ @max_prepared_arg ||= 0
172
+ @prepared_args[k] << @max_prepared_arg
173
+ @max_prepared_arg += 1
174
+ prepared_arg_placeholder
175
+ end
176
+ end
177
+
178
+ # For the given type (:select, :insert, :update, or :delete),
179
+ # run the sql with the bind variables
180
+ # specified in the hash. values is a hash of passed to
181
+ # insert or update (if one of those types is used),
182
+ # which may contain placeholders.
183
+ def call(type, bind_variables={}, values=nil)
184
+ to_prepared_statement(type, values).call(bind_variables)
185
+ end
186
+
187
+ # Prepare an SQL statement for later execution. This returns
188
+ # a clone of the dataset extended with PreparedStatementMethods,
189
+ # on which you can call call with the hash of bind variables to
190
+ # do substitution. The prepared statement is also stored in
191
+ # the associated database. The following usage is identical:
192
+ #
193
+ # ps = prepare(:select, :select_by_name)
194
+ # ps.call(:name=>'Blah')
195
+ # db.call(:select_by_name, :name=>'Blah')
196
+ def prepare(type, name, values=nil)
197
+ db.prepared_statements[name] = to_prepared_statement(type, values)
198
+ end
199
+
200
+ private
201
+
202
+ # The argument placeholder. Most databases used unnumbered
203
+ # arguments with question marks, so that is the default.
204
+ def prepared_arg_placeholder
205
+ PREPARED_ARG_PLACEHOLDER
206
+ end
207
+
208
+ # Return a cloned copy of the current dataset extended with
209
+ # PreparedStatementMethods, setting the type and modify values.
210
+ def to_prepared_statement(type, values=nil)
211
+ ps = clone
212
+ ps.extend(PreparedStatementMethods)
213
+ ps.prepared_type = type
214
+ ps.prepared_modify_values = values
215
+ ps
216
+ end
217
+ end
218
+ end