sequel 2.7.1 → 2.8.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 (50) hide show
  1. data/CHANGELOG +56 -0
  2. data/README +1 -0
  3. data/Rakefile +1 -1
  4. data/lib/sequel_core.rb +9 -16
  5. data/lib/sequel_core/adapters/ado.rb +6 -15
  6. data/lib/sequel_core/adapters/db2.rb +8 -10
  7. data/lib/sequel_core/adapters/dbi.rb +6 -4
  8. data/lib/sequel_core/adapters/informix.rb +21 -22
  9. data/lib/sequel_core/adapters/jdbc.rb +69 -10
  10. data/lib/sequel_core/adapters/jdbc/postgresql.rb +1 -0
  11. data/lib/sequel_core/adapters/mysql.rb +81 -13
  12. data/lib/sequel_core/adapters/odbc.rb +32 -4
  13. data/lib/sequel_core/adapters/openbase.rb +6 -5
  14. data/lib/sequel_core/adapters/oracle.rb +23 -7
  15. data/lib/sequel_core/adapters/postgres.rb +42 -32
  16. data/lib/sequel_core/adapters/shared/mssql.rb +37 -62
  17. data/lib/sequel_core/adapters/shared/mysql.rb +22 -7
  18. data/lib/sequel_core/adapters/shared/oracle.rb +27 -48
  19. data/lib/sequel_core/adapters/shared/postgres.rb +64 -43
  20. data/lib/sequel_core/adapters/shared/progress.rb +31 -0
  21. data/lib/sequel_core/adapters/shared/sqlite.rb +15 -4
  22. data/lib/sequel_core/adapters/sqlite.rb +6 -14
  23. data/lib/sequel_core/connection_pool.rb +47 -13
  24. data/lib/sequel_core/database.rb +60 -35
  25. data/lib/sequel_core/database/schema.rb +4 -4
  26. data/lib/sequel_core/dataset.rb +12 -3
  27. data/lib/sequel_core/dataset/convenience.rb +4 -13
  28. data/lib/sequel_core/dataset/prepared_statements.rb +30 -28
  29. data/lib/sequel_core/dataset/sql.rb +144 -85
  30. data/lib/sequel_core/dataset/stored_procedures.rb +75 -0
  31. data/lib/sequel_core/dataset/unsupported.rb +31 -0
  32. data/lib/sequel_core/exceptions.rb +6 -0
  33. data/lib/sequel_core/schema/generator.rb +4 -3
  34. data/lib/sequel_core/schema/sql.rb +41 -23
  35. data/lib/sequel_core/sql.rb +29 -1
  36. data/lib/sequel_model/associations.rb +1 -1
  37. data/lib/sequel_model/record.rb +31 -28
  38. data/spec/adapters/mysql_spec.rb +37 -4
  39. data/spec/adapters/oracle_spec.rb +26 -4
  40. data/spec/adapters/sqlite_spec.rb +7 -0
  41. data/spec/integration/prepared_statement_test.rb +24 -0
  42. data/spec/integration/schema_test.rb +1 -1
  43. data/spec/sequel_core/connection_pool_spec.rb +49 -2
  44. data/spec/sequel_core/core_sql_spec.rb +9 -2
  45. data/spec/sequel_core/database_spec.rb +64 -14
  46. data/spec/sequel_core/dataset_spec.rb +105 -7
  47. data/spec/sequel_core/schema_spec.rb +40 -12
  48. data/spec/sequel_core/spec_helper.rb +1 -0
  49. data/spec/sequel_model/spec_helper.rb +1 -0
  50. metadata +6 -3
@@ -0,0 +1,75 @@
1
+ module Sequel
2
+ class Dataset
3
+ module StoredProcedureMethods
4
+ SQL_QUERY_TYPE = Hash.new{|h,k| h[k] = k}
5
+ SQL_QUERY_TYPE[:first] = SQL_QUERY_TYPE[:all] = :select
6
+
7
+ # The name of the stored procedure to call
8
+ attr_accessor :sproc_name
9
+
10
+ # Call the prepared statement
11
+ def call(*args, &block)
12
+ @sproc_args = args
13
+ case @sproc_type
14
+ when :select, :all
15
+ all(&block)
16
+ when :first
17
+ first
18
+ when :insert
19
+ insert
20
+ when :update
21
+ update
22
+ when :delete
23
+ delete
24
+ end
25
+ end
26
+
27
+ # Programmer friendly string showing this is a stored procedure,
28
+ # showing the name of the procedure.
29
+ def inspect
30
+ "<#{self.class.name}/StoredProcedure name=#{@sproc_name}>"
31
+ end
32
+
33
+ # Set the type of the sproc and override the corresponding _sql
34
+ # method to return the empty string (since the result will be
35
+ # ignored anyway).
36
+ def sproc_type=(type)
37
+ @sproc_type = type
38
+ meta_def("#{sql_query_type}_sql"){|*a| ''}
39
+ end
40
+
41
+ private
42
+
43
+ # The type of query (:select, :insert, :delete, :update).
44
+ def sql_query_type
45
+ SQL_QUERY_TYPE[@sproc_type]
46
+ end
47
+ end
48
+
49
+ module StoredProcedures
50
+ # For the given type (:select, :first, :insert, :update, or :delete),
51
+ # run the database stored procedure with the given name with the given
52
+ # arguments.
53
+ def call_sproc(type, name, *args)
54
+ prepare_sproc(type, name).call(*args)
55
+ end
56
+
57
+ # Transform this dataset into a stored procedure that you can call
58
+ # multiple times with new arguments.
59
+ def prepare_sproc(type, name)
60
+ sp = clone
61
+ prepare_extend_sproc(sp)
62
+ sp.sproc_type = type
63
+ sp.sproc_name = name
64
+ sp
65
+ end
66
+
67
+ private
68
+
69
+ # Extend the dataset with the stored procedure methods.
70
+ def prepare_extend_sproc(ds)
71
+ ds.extend(StoredProcedureMethods)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ class Sequel::Dataset
2
+ # This module should be included in the dataset class for all databases that
3
+ # don't support INTERSECT or EXCEPT.
4
+ module UnsupportedIntersectExcept
5
+ # Raise an Error if EXCEPT is used
6
+ def except(ds, all=false)
7
+ raise(Sequel::Error, "EXCEPT not supported")
8
+ end
9
+
10
+ # Raise an Error if INTERSECT is used
11
+ def intersect(ds, all=false)
12
+ raise(Sequel::Error, "INTERSECT not supported")
13
+ end
14
+ end
15
+
16
+ # This module should be included in the dataset class for all databases that
17
+ # don't support INTERSECT ALL or EXCEPT ALL.
18
+ module UnsupportedIntersectExceptAll
19
+ # Raise an Error if EXCEPT is used
20
+ def except(ds, all=false)
21
+ raise(Sequel::Error, "EXCEPT ALL not supported") if all
22
+ super(ds)
23
+ end
24
+
25
+ # Raise an Error if INTERSECT is used
26
+ def intersect(ds, all=false)
27
+ raise(Sequel::Error, "INTERSECT ALL not supported") if all
28
+ super(ds)
29
+ end
30
+ end
31
+ end
@@ -31,4 +31,10 @@ module Sequel
31
31
  # Generic error raised by the database adapters, indicating a
32
32
  # problem originating from the database server.
33
33
  class DatabaseError < Error; end
34
+
35
+ # Error that should be raised by adapters when they determine that the connection
36
+ # to the database has been lost. Instructs the connection pool code to
37
+ # remove that connection from the pool so that other connections can be acquired
38
+ # automatically.
39
+ class DatabaseDisconnectError < DatabaseError; end
34
40
  end
@@ -127,9 +127,10 @@ module Sequel
127
127
  # optional middle argument denotes the type.
128
128
  #
129
129
  # Examples:
130
- # primary_key(:id) primary_key(:name, :text)
130
+ # primary_key(:id)
131
131
  # primary_key(:zip_code, :null => false)
132
132
  # primary_key([:street_number, :house_number])
133
+ # primary_key(:id, :string, :auto_increment => false)
133
134
  def primary_key(name, *args)
134
135
  return composite_primary_key(name, *args) if name.is_a?(Array)
135
136
  @primary_key = @db.serial_primary_key_options.merge({:name => name})
@@ -276,8 +277,8 @@ module Sequel
276
277
  end
277
278
 
278
279
  # Modify a column's type in the DDL for the table.
279
- def set_column_type(name, type)
280
- @operations << {:op => :set_column_type, :name => name, :type => type}
280
+ def set_column_type(name, type, opts={})
281
+ @operations << {:op => :set_column_type, :name => name, :type => type}.merge(opts)
281
282
  end
282
283
 
283
284
  # Modify a column's NOT NULL constraint.
@@ -29,7 +29,7 @@ module Sequel
29
29
  when :rename_column
30
30
  "RENAME COLUMN #{quoted_name} TO #{quote_identifier(op[:new_name])}"
31
31
  when :set_column_type
32
- "ALTER COLUMN #{quoted_name} TYPE #{op[:type]}"
32
+ "ALTER COLUMN #{quoted_name} TYPE #{type_literal(op)}"
33
33
  when :set_column_default
34
34
  "ALTER COLUMN #{quoted_name} SET DEFAULT #{literal(op[:default])}"
35
35
  when :set_column_null
@@ -187,9 +187,9 @@ module Sequel
187
187
  end
188
188
  end
189
189
 
190
+ # Proxy the quote_schema_table method to the dataset
190
191
  def quote_schema_table(table)
191
- schema, table = schema_and_table(table)
192
- "#{"#{quote_identifier(schema)}." if schema}#{quote_identifier(table)}"
192
+ schema_utility_dataset.quote_schema_table(table)
193
193
  end
194
194
 
195
195
  # Proxy the quote_identifier method to the dataset, used for quoting tables and columns.
@@ -202,7 +202,7 @@ module Sequel
202
202
  "ALTER TABLE #{quote_schema_table(name)} RENAME TO #{quote_schema_table(new_name)}"
203
203
  end
204
204
 
205
- # Parse the schema from the database using the SQL standard INFORMATION_SCHEMA.
205
+ # Parse the schema from the database.
206
206
  # If the table_name is not given, returns the schema for all tables as a hash.
207
207
  # If the table_name is given, returns the schema for a single table as an
208
208
  # array with all members being arrays of length 2. Available options are:
@@ -212,11 +212,17 @@ module Sequel
212
212
  # unless you are sure that schema has not been called before with a
213
213
  # table_name, otherwise you may only getting the schemas for tables
214
214
  # that have been requested explicitly.
215
- def schema(table_name = nil, opts={})
216
- table_name = table_name.to_sym if table_name
215
+ # * :schema - An explicit schema to use. It may also be implicitly provided
216
+ # via the table name.
217
+ def schema(table = nil, opts={})
218
+ if table
219
+ sch, table_name = schema_and_table(table)
220
+ quoted_name = quote_schema_table(table)
221
+ end
222
+ opts = opts.merge(:schema=>sch) if sch && !opts.include?(:schema)
217
223
  if opts[:reload] && @schemas
218
224
  if table_name
219
- @schemas.delete(table_name)
225
+ @schemas.delete(quoted_name)
220
226
  else
221
227
  @schemas = nil
222
228
  end
@@ -224,26 +230,31 @@ module Sequel
224
230
 
225
231
  if @schemas
226
232
  if table_name
227
- return @schemas[table_name] if @schemas[table_name]
233
+ return @schemas[quoted_name] if @schemas[quoted_name]
228
234
  else
229
235
  return @schemas
230
236
  end
231
237
  end
238
+
239
+ @schemas ||= Hash.new do |h,k|
240
+ quote_name = quote_schema_table(k)
241
+ h[quote_name] if h.include?(quote_name)
242
+ end
232
243
 
233
244
  if table_name
234
- @schemas ||= {}
235
245
  if respond_to?(:schema_parse_table, true)
236
- @schemas[table_name] ||= schema_parse_table(table_name, opts)
246
+ @schemas[quoted_name] = schema_parse_table(table_name, opts)
237
247
  else
238
248
  raise Error, 'schema parsing is not implemented on this database'
239
249
  end
240
250
  else
241
251
  if respond_to?(:schema_parse_tables, true)
242
- @schemas = schema_parse_tables(opts)
252
+ @schemas.merge!(schema_parse_tables(opts))
243
253
  elsif respond_to?(:schema_parse_table, true) and respond_to?(:tables, true)
244
- tables.each{|t| schema(t, opts)}
254
+ tables.each{|t| @schemas[quote_identifier(t)] = schema_parse_table(t.to_s, opts)}
245
255
  @schemas
246
256
  else
257
+ @schemas = nil
247
258
  raise Error, 'schema parsing is not implemented on this database'
248
259
  end
249
260
  end
@@ -256,30 +267,37 @@ module Sequel
256
267
 
257
268
  private
258
269
 
270
+ # Remove the cached schema for the given schema name
271
+ def remove_cached_schema(table)
272
+ @schemas.delete(quote_schema_table(table)) if @schemas
273
+ end
274
+
259
275
  # Match the database's column type to a ruby type via a
260
276
  # regular expression. The following ruby types are supported:
261
277
  # integer, string, date, datetime, boolean, and float.
262
278
  def schema_column_type(db_type)
263
279
  case db_type
264
- when /\Atinyint/
280
+ when /\Atinyint/io
265
281
  Sequel.convert_tinyint_to_bool ? :boolean : :integer
266
- when /\A(int(eger)?|bigint|smallint)/
267
- :integer
268
- when /\A(character( varying)?|varchar|text)/
282
+ when /\Ainterval\z/io
283
+ :interval
284
+ when /\A(character( varying)?|varchar|text)/io
269
285
  :string
270
- when /\Adate\z/
286
+ when /\A(int(eger)?|bigint|smallint)/io
287
+ :integer
288
+ when /\Adate\z/io
271
289
  :date
272
- when /\A(datetime|timestamp( with(out)? time zone)?)\z/
290
+ when /\A(datetime|timestamp( with(out)? time zone)?)\z/io
273
291
  :datetime
274
- when /\Atime( with(out)? time zone)?\z/
292
+ when /\Atime( with(out)? time zone)?\z/io
275
293
  :time
276
- when "boolean"
294
+ when /\Aboolean\z/io
277
295
  :boolean
278
- when /\A(real|float|double( precision)?)\z/
296
+ when /\A(real|float|double( precision)?)\z/io
279
297
  :float
280
- when /\A(numeric(\(\d+,\d+\))?|decimal|money)\z/
298
+ when /\A(numeric(\(\d+,\d+\))?|decimal|money)\z/io
281
299
  :decimal
282
- when "bytea"
300
+ when /\Abytea\z/io
283
301
  :blob
284
302
  end
285
303
  end
@@ -685,6 +685,34 @@ module Sequel
685
685
  end
686
686
  end
687
687
 
688
+ # Represents a literal string with placeholders and arguments.
689
+ # This is necessary to ensure delayed literalization of the arguments
690
+ # required for the prepared statement support
691
+ class PlaceholderLiteralString < SpecificExpression
692
+ # The arguments that will be subsituted into the placeholders.
693
+ attr_reader :args
694
+
695
+ # The literal string containing placeholders
696
+ attr_reader :str
697
+
698
+ # Whether to surround the expression with parantheses
699
+ attr_reader :parens
700
+
701
+ # Create an object with the given conditions and
702
+ # default value.
703
+ def initialize(str, args, parens=false)
704
+ @str = str
705
+ @args = args
706
+ @parens = parens
707
+ end
708
+
709
+ # Delegate the creation of the resulting SQL to the given dataset,
710
+ # since it may be database dependent.
711
+ def to_s(ds)
712
+ ds.placeholder_literal_string_sql(self)
713
+ end
714
+ end
715
+
688
716
  # Subclass of ComplexExpression where the expression results
689
717
  # in a numeric value in SQL.
690
718
  class NumericExpression < ComplexExpression
@@ -831,7 +859,7 @@ module Sequel
831
859
  # LiteralString is used to represent literal SQL expressions. A
832
860
  # LiteralString is copied verbatim into an SQL statement. Instances of
833
861
  # LiteralString can be created by calling String#lit.
834
- # LiteralStrings can use all of the SQL::ColumnMethods and the
862
+ # LiteralStrings can also use all of the SQL::OrderMethods and the
835
863
  # SQL::ComplexExpressionMethods.
836
864
  class LiteralString < ::String
837
865
  include SQL::OrderMethods
@@ -394,7 +394,7 @@ module Sequel::Model::Associations
394
394
  return if old_val and run_association_callbacks(opts, :before_remove, old_val) == false
395
395
  return if o and run_association_callbacks(opts, :before_add, o) == false
396
396
  send(opts._setter_method, o)
397
- @associations[name] = o
397
+ associations[name] = o
398
398
  remove_reciprocal_object(opts, old_val) if old_val
399
399
  add_reciprocal_object(opts, o) if o
400
400
  run_association_callbacks(opts, :after_add, o) if o
@@ -4,15 +4,6 @@ module Sequel
4
4
  # to be called automatically via set.
5
5
  RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_empty_string_to_nil= typecast_on_assignment= strict_param_setting= raise_on_save_failure= raise_on_typecast_failure="
6
6
 
7
- # The current cached associations. A hash with the keys being the
8
- # association name symbols and the values being the associated object
9
- # or nil (many_to_one), or the array of associated objects (*_to_many).
10
- attr_reader :associations
11
-
12
- # The columns that have been updated. This isn't completely accurate,
13
- # see Model#[]=.
14
- attr_reader :changed_columns
15
-
16
7
  # The hash of attribute values. Keys are symbols with the names of the
17
8
  # underlying database columns.
18
9
  attr_reader :values
@@ -32,9 +23,7 @@ module Sequel
32
23
  # string keys will work if from_db is false.
33
24
  # * from_db - should only be set by Model.load, forget it
34
25
  # exists.
35
- def initialize(values = {}, from_db = false, &block)
36
- @associations = {}
37
- @changed_columns = []
26
+ def initialize(values = {}, from_db = false)
38
27
  if from_db
39
28
  @new = false
40
29
  @values = values
@@ -42,8 +31,8 @@ module Sequel
42
31
  @values = {}
43
32
  @new = true
44
33
  set(values)
45
- @changed_columns.clear
46
- yield self if block
34
+ changed_columns.clear
35
+ yield self if block_given?
47
36
  end
48
37
  after_initialize
49
38
  end
@@ -61,7 +50,7 @@ module Sequel
61
50
  # If the column isn't in @values, we can't assume it is
62
51
  # NULL in the database, so assume it has changed.
63
52
  if new? || !@values.include?(column) || value != @values[column]
64
- @changed_columns << column unless @changed_columns.include?(column)
53
+ changed_columns << column unless changed_columns.include?(column)
65
54
  @values[column] = typecast_value(column, value)
66
55
  end
67
56
  end
@@ -84,6 +73,19 @@ module Sequel
84
73
  # self.class.
85
74
  alias_method :model, :class
86
75
 
76
+ # The current cached associations. A hash with the keys being the
77
+ # association name symbols and the values being the associated object
78
+ # or nil (many_to_one), or the array of associated objects (*_to_many).
79
+ def associations
80
+ @associations ||= {}
81
+ end
82
+
83
+ # The columns that have been updated. This isn't completely accurate,
84
+ # see Model#[]=.
85
+ def changed_columns
86
+ @changed_columns ||= []
87
+ end
88
+
87
89
  # Deletes and returns self. Does not run destroy hooks.
88
90
  # Look into using destroy instead.
89
91
  def delete
@@ -171,7 +173,8 @@ module Sequel
171
173
  # exists in the database.
172
174
  def refresh
173
175
  @values = this.first || raise(Error, "Record not found")
174
- @associations.clear
176
+ changed_columns.clear
177
+ associations.clear
175
178
  self
176
179
  end
177
180
  alias_method :reload, :refresh
@@ -220,12 +223,12 @@ module Sequel
220
223
  else
221
224
  return save_failure(:update) if before_update == false
222
225
  if columns.empty?
223
- vals = opts[:changed] ? @values.reject{|k,v| !@changed_columns.include?(k)} : @values
226
+ vals = opts[:changed] ? @values.reject{|k,v| !changed_columns.include?(k)} : @values
224
227
  this.update(vals)
225
- @changed_columns = []
228
+ changed_columns.clear
226
229
  else # update only the specified columns
227
- this.update(@values.reject {|k, v| !columns.include?(k)})
228
- @changed_columns.reject! {|c| columns.include?(c)}
230
+ this.update(@values.reject{|k, v| !columns.include?(k)})
231
+ changed_columns.reject!{|c| columns.include?(c)}
229
232
  end
230
233
  after_update
231
234
  end
@@ -237,7 +240,7 @@ module Sequel
237
240
  # chanaged. If no columns have been changed, returns nil. If unable to
238
241
  # save, returns false unless raise_on_save_failure is true.
239
242
  def save_changes
240
- save(:changed=>true) || false unless @changed_columns.empty?
243
+ save(:changed=>true) || false unless changed_columns.empty?
241
244
  end
242
245
 
243
246
  # Updates the instance with the supplied values with support for virtual
@@ -357,7 +360,7 @@ module Sequel
357
360
  raise(Sequel::Error, 'associated object does not have a primary key') if opts.need_associated_primary_key? && !o.pk
358
361
  return if run_association_callbacks(opts, :before_add, o) == false
359
362
  send(opts._add_method, o)
360
- @associations[opts[:name]].push(o) if @associations.include?(opts[:name])
363
+ associations[opts[:name]].push(o) if associations.include?(opts[:name])
361
364
  add_reciprocal_object(opts, o)
362
365
  run_association_callbacks(opts, :after_add, o)
363
366
  o
@@ -378,8 +381,8 @@ module Sequel
378
381
  # Load the associated objects using the dataset
379
382
  def load_associated_objects(opts, reload=false)
380
383
  name = opts[:name]
381
- if @associations.include?(name) and !reload
382
- @associations[name]
384
+ if associations.include?(name) and !reload
385
+ associations[name]
383
386
  else
384
387
  objs = if opts.returns_array?
385
388
  send(opts.dataset_method).all
@@ -393,7 +396,7 @@ module Sequel
393
396
  run_association_callbacks(opts, :after_load, objs)
394
397
  # Only one_to_many associations should set the reciprocal object
395
398
  objs.each{|o| add_reciprocal_object(opts, o)} if opts.set_reciprocal_to_self?
396
- @associations[name] = objs
399
+ associations[name] = objs
397
400
  end
398
401
  end
399
402
 
@@ -401,8 +404,8 @@ module Sequel
401
404
  def remove_all_associated_objects(opts)
402
405
  raise(Sequel::Error, 'model object does not have a primary key') unless pk
403
406
  send(opts._remove_all_method)
404
- ret = @associations[opts[:name]].each{|o| remove_reciprocal_object(opts, o)} if @associations.include?(opts[:name])
405
- @associations[opts[:name]] = []
407
+ ret = associations[opts[:name]].each{|o| remove_reciprocal_object(opts, o)} if associations.include?(opts[:name])
408
+ associations[opts[:name]] = []
406
409
  ret
407
410
  end
408
411
 
@@ -412,7 +415,7 @@ module Sequel
412
415
  raise(Sequel::Error, 'associated object does not have a primary key') if opts.need_associated_primary_key? && !o.pk
413
416
  return if run_association_callbacks(opts, :before_remove, o) == false
414
417
  send(opts._remove_method, o)
415
- @associations[opts[:name]].delete_if{|x| o === x} if @associations.include?(opts[:name])
418
+ associations[opts[:name]].delete_if{|x| o === x} if associations.include?(opts[:name])
416
419
  remove_reciprocal_object(opts, o)
417
420
  run_association_callbacks(opts, :after_remove, o)
418
421
  o