sequel 3.26.0 → 3.27.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 (39) hide show
  1. data/CHANGELOG +26 -0
  2. data/Rakefile +2 -3
  3. data/doc/mass_assignment.rdoc +54 -0
  4. data/doc/migration.rdoc +9 -533
  5. data/doc/prepared_statements.rdoc +8 -7
  6. data/doc/release_notes/3.27.0.txt +82 -0
  7. data/doc/schema_modification.rdoc +547 -0
  8. data/doc/testing.rdoc +64 -0
  9. data/lib/sequel/adapters/amalgalite.rb +4 -0
  10. data/lib/sequel/adapters/jdbc.rb +3 -1
  11. data/lib/sequel/adapters/jdbc/h2.rb +11 -5
  12. data/lib/sequel/adapters/mysql.rb +4 -122
  13. data/lib/sequel/adapters/mysql2.rb +4 -13
  14. data/lib/sequel/adapters/odbc.rb +4 -1
  15. data/lib/sequel/adapters/odbc/db2.rb +21 -0
  16. data/lib/sequel/adapters/shared/mysql.rb +12 -0
  17. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +143 -0
  18. data/lib/sequel/adapters/tinytds.rb +122 -3
  19. data/lib/sequel/core.rb +4 -3
  20. data/lib/sequel/database/misc.rb +7 -10
  21. data/lib/sequel/dataset/misc.rb +1 -1
  22. data/lib/sequel/dataset/sql.rb +7 -0
  23. data/lib/sequel/model/associations.rb +2 -2
  24. data/lib/sequel/model/base.rb +60 -10
  25. data/lib/sequel/plugins/prepared_statements_safe.rb +17 -7
  26. data/lib/sequel/sql.rb +5 -0
  27. data/lib/sequel/timezones.rb +12 -3
  28. data/lib/sequel/version.rb +1 -1
  29. data/spec/adapters/mysql_spec.rb +25 -21
  30. data/spec/core/database_spec.rb +200 -0
  31. data/spec/core/dataset_spec.rb +6 -0
  32. data/spec/extensions/prepared_statements_safe_spec.rb +10 -0
  33. data/spec/extensions/schema_dumper_spec.rb +2 -2
  34. data/spec/integration/schema_test.rb +30 -1
  35. data/spec/integration/type_test.rb +10 -3
  36. data/spec/model/base_spec.rb +44 -0
  37. data/spec/model/model_spec.rb +14 -0
  38. data/spec/model/record_spec.rb +131 -12
  39. metadata +14 -4
@@ -32,9 +32,32 @@ module Sequel
32
32
  begin
33
33
  m = opts[:return]
34
34
  r = nil
35
- log_yield(sql) do
36
- r = c.execute(sql)
37
- return r.send(m) if m
35
+ if (args = opts[:arguments]) && !args.empty?
36
+ types = []
37
+ values = []
38
+ args.each_with_index do |(k, v), i|
39
+ v, type = ps_arg_type(v)
40
+ types << "@#{k} #{type}"
41
+ values << "@#{k} = #{v}"
42
+ end
43
+ case m
44
+ when :do
45
+ sql = "#{sql}; SELECT @@ROWCOUNT AS AffectedRows"
46
+ single_value = true
47
+ when :insert
48
+ sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
49
+ single_value = true
50
+ end
51
+ sql = "EXEC sp_executesql N'#{c.escape(sql)}', N'#{c.escape(types.join(', '))}', #{values.join(', ')}"
52
+ log_yield(sql) do
53
+ r = c.execute(sql)
54
+ r.each{|row| return row.values.first} if single_value
55
+ end
56
+ else
57
+ log_yield(sql) do
58
+ r = c.execute(sql)
59
+ return r.send(m) if m
60
+ end
38
61
  end
39
62
  yield(r) if block_given?
40
63
  rescue TinyTds::Error => e
@@ -84,14 +107,98 @@ module Sequel
84
107
  c.close
85
108
  end
86
109
 
110
+ # Return true if the :conn argument is present and not active.
87
111
  def disconnect_error?(e, opts)
88
112
  super || (opts[:conn] && !opts[:conn].active?)
89
113
  end
114
+
115
+ # Return a 2 element array with the literal value and type to use
116
+ # in the prepared statement call for the given value and connection.
117
+ def ps_arg_type(v)
118
+ case v
119
+ when Fixnum
120
+ [v, 'int']
121
+ when Bignum
122
+ [v, 'bigint']
123
+ when Float
124
+ [v, 'double precision']
125
+ when Numeric
126
+ [v, 'numeric']
127
+ when SQLTime
128
+ [literal(v), 'time']
129
+ when DateTime, Time
130
+ [literal(v), 'datetime']
131
+ when Date
132
+ [literal(v), 'date']
133
+ when nil
134
+ ['NULL', 'nvarchar(max)']
135
+ when true
136
+ ['1', 'int']
137
+ when false
138
+ ['0', 'int']
139
+ when SQL::Blob
140
+ [literal(v), 'varbinary(max)']
141
+ else
142
+ [literal(v), 'nvarchar(max)']
143
+ end
144
+ end
90
145
  end
91
146
 
92
147
  class Dataset < Sequel::Dataset
93
148
  include Sequel::MSSQL::DatasetMethods
94
149
 
150
+ # SQLite already supports named bind arguments, so use directly.
151
+ module ArgumentMapper
152
+ include Sequel::Dataset::ArgumentMapper
153
+
154
+ protected
155
+
156
+ # Return a hash with the same values as the given hash,
157
+ # but with the keys converted to strings.
158
+ def map_to_prepared_args(hash)
159
+ args = {}
160
+ hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
161
+ args
162
+ end
163
+
164
+ private
165
+
166
+ # SQLite uses a : before the name of the argument for named
167
+ # arguments.
168
+ def prepared_arg(k)
169
+ LiteralString.new("@#{k.to_s.gsub('.', '__')}")
170
+ end
171
+
172
+ # Always assume a prepared argument.
173
+ def prepared_arg?(k)
174
+ true
175
+ end
176
+ end
177
+
178
+ # SQLite prepared statement uses a new prepared statement each time
179
+ # it is called, but it does use the bind arguments.
180
+ module PreparedStatementMethods
181
+ include ArgumentMapper
182
+
183
+ private
184
+
185
+ # Run execute_select on the database with the given SQL and the stored
186
+ # bind arguments.
187
+ def execute(sql, opts={}, &block)
188
+ super(sql, {:arguments=>bind_arguments}.merge(opts), &block)
189
+ end
190
+
191
+ # Same as execute, explicit due to intricacies of alias and super.
192
+ def execute_dui(sql, opts={}, &block)
193
+ super(sql, {:arguments=>bind_arguments}.merge(opts), &block)
194
+ end
195
+
196
+ # Same as execute, explicit due to intricacies of alias and super.
197
+ def execute_insert(sql, opts={}, &block)
198
+ super(sql, {:arguments=>bind_arguments}.merge(opts), &block)
199
+ end
200
+ end
201
+
95
202
  # Yield hashes with symbol keys, attempting to optimize for
96
203
  # various cases.
97
204
  def fetch_rows(sql)
@@ -123,6 +230,18 @@ module Sequel
123
230
  self
124
231
  end
125
232
 
233
+ # Create a named prepared statement that is stored in the
234
+ # database (and connection) for reuse.
235
+ def prepare(type, name=nil, *values)
236
+ ps = to_prepared_statement(type, values)
237
+ ps.extend(PreparedStatementMethods)
238
+ if name
239
+ ps.prepared_statement_name = name
240
+ db.prepared_statements[name] = ps
241
+ end
242
+ ps
243
+ end
244
+
126
245
  private
127
246
 
128
247
  # Properly escape the given string +v+.
data/lib/sequel/core.rb CHANGED
@@ -228,12 +228,13 @@ module Sequel
228
228
  end
229
229
  end
230
230
 
231
- # Converts the given +string+ into a +Time+ object.
231
+ # Converts the given +string+ into a <tt>Sequel::SQLTime</tt> object.
232
232
  #
233
- # Sequel.string_to_time('10:20:30') # Time.parse('10:20:30')
233
+ # v = Sequel.string_to_time('10:20:30') # Sequel::SQLTime.parse('10:20:30')
234
+ # DB.literal(v) # => '10:20:30'
234
235
  def self.string_to_time(string)
235
236
  begin
236
- Time.parse(string)
237
+ SQLTime.parse(string)
237
238
  rescue => e
238
239
  raise convert_exception_class(e, InvalidValue)
239
240
  end
@@ -215,10 +215,10 @@ module Sequel
215
215
  # Typecast the value to a Date
216
216
  def typecast_value_date(value)
217
217
  case value
218
- when Date
219
- value
220
218
  when DateTime, Time
221
219
  Date.new(value.year, value.month, value.day)
220
+ when Date
221
+ value
222
222
  when String
223
223
  Sequel.string_to_date(value)
224
224
  when Hash
@@ -230,12 +230,7 @@ module Sequel
230
230
 
231
231
  # Typecast the value to a DateTime or Time depending on Sequel.datetime_class
232
232
  def typecast_value_datetime(value)
233
- klass = Sequel.datetime_class
234
- if value.is_a?(Hash)
235
- klass.send(klass == Time ? :mktime : :new, *[:year, :month, :day, :hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
236
- else
237
- Sequel.typecast_to_application_timestamp(value)
238
- end
233
+ Sequel.typecast_to_application_timestamp(value)
239
234
  end
240
235
 
241
236
  # Typecast the value to a BigDecimal
@@ -280,13 +275,15 @@ module Sequel
280
275
  # Typecast the value to a Time
281
276
  def typecast_value_time(value)
282
277
  case value
283
- when Time
278
+ when SQLTime
284
279
  value
280
+ when Time
281
+ SQLTime.local(value.year, value.month, value.day, value.hour, value.min, value.sec, value.respond_to?(:nsec) ? value.nsec : value.usec)
285
282
  when String
286
283
  Sequel.string_to_time(value)
287
284
  when Hash
288
285
  t = Time.now
289
- Time.mktime(t.year, t.month, t.day, *[:hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
286
+ SQLTime.local(t.year, t.month, t.day, *[:hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
290
287
  else
291
288
  raise Sequel::InvalidValue, "invalid value for Time: #{value.inspect}"
292
289
  end
@@ -107,7 +107,7 @@ module Sequel
107
107
  # Define a hash value such that datasets with the same DB, opts, and SQL
108
108
  # will have the same hash value
109
109
  def hash
110
- [db, opts.sort_by{|k| k.to_s}, sql].hash
110
+ [db, opts.sort_by{|k, v| k.to_s}, sql].hash
111
111
  end
112
112
 
113
113
  # The String instance method to call on identifiers before sending them to
@@ -100,6 +100,8 @@ module Sequel
100
100
  literal_false
101
101
  when Array
102
102
  literal_array(v)
103
+ when SQLTime
104
+ literal_sqltime(v)
103
105
  when Time
104
106
  literal_time(v)
105
107
  when DateTime
@@ -723,6 +725,11 @@ module Sequel
723
725
  end
724
726
  end
725
727
 
728
+ # SQL fragment for Sequel::SQLTime, containing just the time part
729
+ def literal_sqltime(v)
730
+ v.strftime("'%H:%M:%S#{format_timestamp_usec(v.usec) if supports_timestamp_usecs?}'")
731
+ end
732
+
726
733
  # SQL fragment for String. Doubles \ and ' by default.
727
734
  def literal_string(v)
728
735
  "'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
@@ -1037,8 +1037,8 @@ module Sequel
1037
1037
  @associations ||= {}
1038
1038
  end
1039
1039
 
1040
- # Used internally by the associations code, like pk but doesn't raise
1041
- # an Error if the model has no primary key.
1040
+ # Formally used internally by the associations code, like pk but doesn't raise
1041
+ # an Error if the model has no primary key. Not used any longer, deprecated.
1042
1042
  def pk_or_nil
1043
1043
  key = primary_key
1044
1044
  key.is_a?(Array) ? key.map{|k| @values[k]} : @values[key]
@@ -148,6 +148,26 @@ module Sequel
148
148
  def dataset=(ds)
149
149
  set_dataset(ds)
150
150
  end
151
+
152
+ # Extend the dataset with an anonymous module, similar to adding
153
+ # a plugin with the methods defined in DatasetMethods. If a block
154
+ # is given, it is module_evaled.
155
+ #
156
+ # Artist.dataset_module do
157
+ # def foo
158
+ # :bar
159
+ # end
160
+ # end
161
+ # Artist.dataset.foo
162
+ # # => :bar
163
+ # Artist.foo
164
+ # # => :bar
165
+ def dataset_module
166
+ @dataset_module ||= Module.new
167
+ @dataset_module.module_eval(&Proc.new) if block_given?
168
+ dataset_extend(@dataset_module)
169
+ @dataset_module
170
+ end
151
171
 
152
172
  # Returns the database associated with the Model class.
153
173
  # If this model doesn't have a database associated with it,
@@ -248,6 +268,13 @@ module Sequel
248
268
  find(cond) || create(cond, &block)
249
269
  end
250
270
 
271
+ # Clear the setter_methods cache when a module is included, as it
272
+ # may contain setter methods.
273
+ def include(mod)
274
+ clear_setter_methods_cache
275
+ super
276
+ end
277
+
251
278
  # If possible, set the dataset for the model subclass as soon as it
252
279
  # is created. Also, make sure the inherited class instance variables
253
280
  # are copied into the subclass.
@@ -328,12 +355,7 @@ module Sequel
328
355
  m.apply(self, *args, &blk) if m.respond_to?(:apply)
329
356
  include(m::InstanceMethods) if plugin_module_defined?(m, :InstanceMethods)
330
357
  extend(m::ClassMethods)if plugin_module_defined?(m, :ClassMethods)
331
- if plugin_module_defined?(m, :DatasetMethods)
332
- dataset.extend(m::DatasetMethods) if @dataset
333
- dataset_method_modules << m::DatasetMethods
334
- meths = m::DatasetMethods.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
335
- def_dataset_method(*meths) unless meths.empty?
336
- end
358
+ dataset_extend(m::DatasetMethods) if plugin_module_defined?(m, :DatasetMethods)
337
359
  end
338
360
  m.configure(self, *args, &blk) if m.respond_to?(:configure)
339
361
  end
@@ -553,6 +575,16 @@ module Sequel
553
575
  end
554
576
  end
555
577
 
578
+ # Add the module to the class's dataset_method_modules. Extend the dataset with the
579
+ # module if the model has a dataset. Add dataset methods to the class for all
580
+ # public dataset methods.
581
+ def dataset_extend(mod)
582
+ dataset.extend(mod) if @dataset
583
+ dataset_method_modules << mod
584
+ meths = mod.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
585
+ def_dataset_method(*meths) unless meths.empty?
586
+ end
587
+
556
588
  # Create a column accessor for a column with a method name that is hard to use in ruby code.
557
589
  def def_bad_column_accessor(column)
558
590
  overridable_methods_module.module_eval do
@@ -705,7 +737,7 @@ module Sequel
705
737
  # a model object, Sequel will call +around_destory+, which will call +before_destroy+, do
706
738
  # the destroy, and then call +after_destroy+.
707
739
  # * The following instance_methods all call the class method of the same
708
- # name: columns, dataset, db, primary_key, db_schema.
740
+ # name: columns, db, primary_key, db_schema.
709
741
  # * All of the methods in +BOOLEAN_SETTINGS+ create attr_writers allowing you
710
742
  # to set values for the attribute. It also creates instnace getters returning
711
743
  # the value of the setting. If the value has not yet been set, it
@@ -889,14 +921,25 @@ module Sequel
889
921
 
890
922
  # Returns true when current instance exists, false otherwise.
891
923
  # Generally an object that isn't new will exist unless it has
892
- # been deleted. Uses a database query to check for existence.
924
+ # been deleted. Uses a database query to check for existence,
925
+ # unless the model object is new, in which case this is always
926
+ # false.
893
927
  #
894
928
  # Artist[1].exists? # SELECT 1 FROM artists WHERE (id = 1)
895
929
  # # => true
930
+ # Artist.new.exists?
931
+ # # => false
896
932
  def exists?
897
- !this.get(1).nil?
933
+ new? ? false : !this.get(1).nil?
898
934
  end
899
935
 
936
+ # Ignore the model's setter method cache when this instances extends a module, as the
937
+ # module may contain setter methods.
938
+ def extend(mod)
939
+ @singleton_setter_added = true
940
+ super
941
+ end
942
+
900
943
  # Value that should be unique for objects with the same class and pk (if pk is not nil), or
901
944
  # the same class and values (if pk is nil).
902
945
  #
@@ -905,7 +948,14 @@ module Sequel
905
948
  # Artist.new.hash == Artist.new.hash # true
906
949
  # Artist.new(:name=>'Bob').hash == Artist.new.hash # false
907
950
  def hash
908
- [model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
951
+ case primary_key
952
+ when Array
953
+ [model, !pk.all? ? @values.sort_by{|k,v| k.to_s} : pk].hash
954
+ when Symbol
955
+ [model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
956
+ else
957
+ [model, @values.sort_by{|k,v| k.to_s}].hash
958
+ end
909
959
  end
910
960
 
911
961
  # Returns value for the :id attribute, even if the primary key is
@@ -33,10 +33,16 @@ module Sequel
33
33
  # that can be created is 2^N (where N is the number of free columns).
34
34
  attr_reader :prepared_statements_column_defaults
35
35
 
36
- # Set the column defaults to use when creating on the subclass.
37
36
  def inherited(subclass)
38
37
  super
39
- subclass.send(:set_prepared_statements_column_defaults)
38
+ subclass.instance_variable_set(:@prepared_statements_column_defaults, @prepared_statements_column_defaults) if @prepared_statements_column_defaults && !subclass.prepared_statements_column_defaults
39
+ end
40
+
41
+ # Set the column defaults to use when creating on the subclass.
42
+ def set_dataset(*)
43
+ x = super
44
+ set_prepared_statements_column_defaults
45
+ x
40
46
  end
41
47
 
42
48
  private
@@ -45,11 +51,13 @@ module Sequel
45
51
  # are set to a default value unless they are a primary key column or
46
52
  # they don't have a parseable default.
47
53
  def set_prepared_statements_column_defaults
48
- h = {}
49
- db_schema.each do |k, v|
50
- h[k] = v[:ruby_default] if (v[:ruby_default] || !v[:default]) && !v[:primary_key]
54
+ if db_schema
55
+ h = {}
56
+ db_schema.each do |k, v|
57
+ h[k] = v[:ruby_default] if (v[:ruby_default] || !v[:default]) && !v[:primary_key]
58
+ end
59
+ @prepared_statements_column_defaults = h
51
60
  end
52
- @prepared_statements_column_defaults = h
53
61
  end
54
62
  end
55
63
 
@@ -57,7 +65,9 @@ module Sequel
57
65
  # Merge the current values into the default values to reduce the number
58
66
  # of free columns.
59
67
  def before_create
60
- set_values(model.prepared_statements_column_defaults.merge(values))
68
+ if v = model.prepared_statements_column_defaults
69
+ set_values(v.merge(values))
70
+ end
61
71
  super
62
72
  end
63
73
 
data/lib/sequel/sql.rb CHANGED
@@ -38,6 +38,11 @@ module Sequel
38
38
  class LiteralString < ::String
39
39
  end
40
40
 
41
+ # Time subclass that gets literalized with only the time value, so it operates
42
+ # like a standard SQL time type.
43
+ class SQLTime < ::Time
44
+ end
45
+
41
46
  # The SQL module holds classes whose instances represent SQL fragments.
42
47
  # It also holds modules that are included in core ruby classes that
43
48
  # make Sequel a friendly DSL.
@@ -109,6 +109,8 @@ module Sequel
109
109
  else
110
110
  Time.send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s)
111
111
  end
112
+ when Hash
113
+ convert_input_timestamp([:year, :month, :day, :hour, :minute, :second].map{|x| (v[x] || v[x.to_s]).to_i}, input_timezone)
112
114
  when Time
113
115
  if datetime_class == DateTime
114
116
  v.respond_to?(:to_datetime) ? v.to_datetime : string_to_datetime(v.iso8601)
@@ -121,8 +123,6 @@ module Sequel
121
123
  else
122
124
  v.respond_to?(:to_time) ? v.to_time : string_to_datetime(v.to_s)
123
125
  end
124
- when Date
125
- convert_input_timestamp(v.to_s, input_timezone)
126
126
  else
127
127
  raise InvalidValue, "Invalid convert_input_timestamp type: #{v.inspect}"
128
128
  end
@@ -160,7 +160,16 @@ module Sequel
160
160
  # +convert_output_timestamp+.
161
161
  def convert_timestamp(v, input_timezone)
162
162
  begin
163
- convert_output_timestamp(convert_input_timestamp(v, input_timezone), Sequel.application_timezone)
163
+ if v.is_a?(Date) && !v.is_a?(DateTime)
164
+ # Dates handled specially as they are assumed to already be in the application_timezone
165
+ if datetime_class == DateTime
166
+ DateTime.civil(v.year, v.month, v.day, 0, 0, 0, application_timezone == :local ? (defined?(Rational) ? Rational(Time.local(v.year, v.month, v.day).utc_offset, 86400) : Time.local(v.year, v.month, v.day).utc_offset/86400.0) : 0)
167
+ else
168
+ Time.send(application_timezone == :utc ? :utc : :local, v.year, v.month, v.day)
169
+ end
170
+ else
171
+ convert_output_timestamp(convert_input_timestamp(v, input_timezone), application_timezone)
172
+ end
164
173
  rescue InvalidValue
165
174
  raise
166
175
  rescue => e