sequel 3.26.0 → 3.27.0

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