sequel 3.14.0 → 3.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG +30 -0
  2. data/README.rdoc +2 -2
  3. data/doc/cheat_sheet.rdoc +1 -1
  4. data/doc/release_notes/3.15.0.txt +78 -0
  5. data/lib/sequel/adapters/do/postgres.rb +1 -1
  6. data/lib/sequel/adapters/jdbc.rb +15 -7
  7. data/lib/sequel/adapters/jdbc/mssql.rb +22 -0
  8. data/lib/sequel/adapters/mysql.rb +2 -6
  9. data/lib/sequel/adapters/mysql2.rb +176 -0
  10. data/lib/sequel/adapters/odbc.rb +1 -1
  11. data/lib/sequel/adapters/postgres.rb +13 -0
  12. data/lib/sequel/adapters/shared/mysql.rb +9 -1
  13. data/lib/sequel/adapters/shared/postgres.rb +1 -1
  14. data/lib/sequel/adapters/shared/sqlite.rb +41 -13
  15. data/lib/sequel/core.rb +8 -0
  16. data/lib/sequel/database/connecting.rb +1 -1
  17. data/lib/sequel/dataset/actions.rb +1 -1
  18. data/lib/sequel/dataset/misc.rb +32 -0
  19. data/lib/sequel/dataset/query.rb +3 -1
  20. data/lib/sequel/extensions/named_timezones.rb +1 -2
  21. data/lib/sequel/model.rb +2 -1
  22. data/lib/sequel/model/associations.rb +3 -1
  23. data/lib/sequel/model/base.rb +44 -4
  24. data/lib/sequel/plugins/active_model.rb +5 -3
  25. data/lib/sequel/plugins/class_table_inheritance.rb +3 -3
  26. data/lib/sequel/version.rb +1 -1
  27. data/spec/adapters/mysql_spec.rb +12 -2
  28. data/spec/adapters/sqlite_spec.rb +31 -1
  29. data/spec/core/dataset_spec.rb +61 -1
  30. data/spec/core/spec_helper.rb +1 -0
  31. data/spec/extensions/active_model_spec.rb +10 -0
  32. data/spec/extensions/lazy_attributes_spec.rb +19 -11
  33. data/spec/extensions/named_timezones_spec.rb +13 -11
  34. data/spec/extensions/spec_helper.rb +1 -5
  35. data/spec/integration/associations_test.rb +23 -0
  36. data/spec/integration/dataset_test.rb +3 -3
  37. data/spec/integration/plugin_test.rb +6 -0
  38. data/spec/integration/schema_test.rb +7 -0
  39. data/spec/integration/timezone_test.rb +2 -2
  40. data/spec/integration/type_test.rb +1 -1
  41. data/spec/model/associations_spec.rb +7 -0
  42. data/spec/model/base_spec.rb +6 -6
  43. data/spec/model/model_spec.rb +38 -3
  44. data/spec/model/record_spec.rb +24 -0
  45. metadata +7 -4
@@ -157,7 +157,7 @@ module Sequel
157
157
 
158
158
  # Methods shared by Database instances that connect to PostgreSQL.
159
159
  module DatabaseMethods
160
- EXCLUDE_SCHEMAS = %w[pg_catalog pg_toast pg_temp_1 pg_toast_temp_1 information_schema]
160
+ EXCLUDE_SCHEMAS = /pg_*|information_schema/i
161
161
  PREPARED_ARG_PLACEHOLDER = LiteralString.new('$').freeze
162
162
  RE_CURRVAL_ERROR = /currval of sequence "(.*)" is not yet defined in this session|relation "(.*)" does not exist/.freeze
163
163
  SYSTEM_TABLE_REGEXP = /^pg|sql/.freeze
@@ -181,6 +181,21 @@ module Sequel
181
181
  generator.is_a?(Schema::Generator) ? super : generator.map{|c| column_definition_sql(c)}.join(', ')
182
182
  end
183
183
 
184
+ # Array of PRAGMA SQL statements based on the Database options that should be applied to
185
+ # new connections.
186
+ def connection_pragmas
187
+ ps = []
188
+ v = typecast_value_boolean(opts.fetch(:foreign_keys, 1))
189
+ ps << "PRAGMA foreign_keys = #{v ? 1 : 0}"
190
+ [[:auto_vacuum, AUTO_VACUUM], [:synchronous, SYNCHRONOUS], [:temp_store, TEMP_STORE]].each do |prag, con|
191
+ if v = opts[prag]
192
+ raise(Error, "Value for PRAGMA #{prag} not supported, should be one of #{con.join(', ')}") unless v = con.index(v.to_sym)
193
+ ps << "PRAGMA #{prag} = #{v}"
194
+ end
195
+ end
196
+ ps
197
+ end
198
+
184
199
  # The array of column schema hashes, except for the ones given in opts[:except]
185
200
  def defined_columns_for(table, opts={})
186
201
  cols = parse_pragma(table, {})
@@ -192,9 +207,20 @@ module Sequel
192
207
  nono= Array(opts[:except]).compact.map{|n| n.to_s}
193
208
  cols.reject!{|c| nono.include? c[:name] }
194
209
  end
210
+
211
+ if foreign_keys
212
+ metadata_dataset.with_sql("PRAGMA foreign_key_list(?)", input_identifier_meth.call(table)).each do |row|
213
+ c = cols.find {|co| co[:name] == row[:from] } or next
214
+ c[:table] = row[:table]
215
+ c[:key] = row[:to]
216
+ c[:on_update] = on_delete_sql_to_sym(row[:on_update])
217
+ c[:on_delete] = on_delete_sql_to_sym(row[:on_delete])
218
+ # is there any way to get deferrable status?
219
+ end
220
+ end
195
221
  cols
196
222
  end
197
-
223
+
198
224
  # Duplicate an existing table by creating a new table, copying all records
199
225
  # from the existing table into the new table, deleting the existing table
200
226
  # and renaming the new table to the existing table's name.
@@ -229,19 +255,21 @@ module Sequel
229
255
  nil
230
256
  end
231
257
 
232
- # Array of PRAGMA SQL statements based on the Database options that should be applied to
233
- # new connections.
234
- def connection_pragmas
235
- ps = []
236
- v = typecast_value_boolean(opts.fetch(:foreign_keys, 1))
237
- ps << "PRAGMA foreign_keys = #{v ? 1 : 0}"
238
- [[:auto_vacuum, AUTO_VACUUM], [:synchronous, SYNCHRONOUS], [:temp_store, TEMP_STORE]].each do |prag, con|
239
- if v = opts[prag]
240
- raise(Error, "Value for PRAGMA #{prag} not supported, should be one of #{con.join(', ')}") unless v = con.index(v.to_sym)
241
- ps << "PRAGMA #{prag} = #{v}"
242
- end
258
+ # Does the reverse of on_delete_clause, eg. converts strings like +'SET NULL'+
259
+ # to symbols +:set_null+.
260
+ def on_delete_sql_to_sym str
261
+ case str
262
+ when 'RESTRICT'
263
+ :restrict
264
+ when 'CASCADE'
265
+ :cascade
266
+ when 'SET NULL'
267
+ :set_null
268
+ when 'SET DEFAULT'
269
+ :set_default
270
+ when 'NO ACTION'
271
+ :no_action
243
272
  end
244
- ps
245
273
  end
246
274
 
247
275
  # Parse the output of the table_info pragma
data/lib/sequel/core.rb CHANGED
@@ -44,6 +44,14 @@ module Sequel
44
44
  # database. It defaults to +Time+. To change it to +DateTime+:
45
45
  #
46
46
  # Sequel.datetime_class = DateTime
47
+ #
48
+ # For ruby versions less than 1.9.2, +Time+ has a limited range (1901 to
49
+ # 2038), so if you use datetimes out of that range, you need to switch
50
+ # to +DateTime+. Also, before 1.9.2, +Time+ can only handle local and UTC
51
+ # times, not other timezones. Note that +Time+ and +DateTime+ objects
52
+ # have a different API, and in cases where they implement the same methods,
53
+ # they often implement them differently (e.g. + using seconds on +Time+ and
54
+ # days on +DateTime+).
47
55
  attr_accessor :datetime_class
48
56
 
49
57
  # For backwards compatibility, has no effect.
@@ -6,7 +6,7 @@ module Sequel
6
6
  # ---------------------
7
7
 
8
8
  # Array of supported database adapters
9
- ADAPTERS = %w'ado amalgalite db2 dbi do firebird informix jdbc mysql odbc openbase oracle postgres sqlite'.collect{|x| x.to_sym}
9
+ ADAPTERS = %w'ado amalgalite db2 dbi do firebird informix jdbc mysql mysql2 odbc openbase oracle postgres sqlite'.collect{|x| x.to_sym}
10
10
 
11
11
  # Whether to use the single threaded connection pool by default
12
12
  @@single_threaded = false
@@ -587,7 +587,7 @@ module Sequel
587
587
  case c
588
588
  when Symbol
589
589
  c_table, column, _ = split_symbol(c)
590
- c_table ? column.to_sym.qualify(c_table) : column.to_sym
590
+ c_table ? SQL::QualifiedIdentifier.new(c_table, column.to_sym) : column.to_sym
591
591
  when SQL::AliasedExpression
592
592
  c.expression
593
593
  else
@@ -34,6 +34,17 @@ module Sequel
34
34
  @row_proc = nil
35
35
  end
36
36
 
37
+ # Define a hash value such that datasets with the same DB, opts, and SQL
38
+ # will be consider equal.
39
+ def ==(o)
40
+ o.is_a?(self.class) && db == o.db && opts == o.opts && sql == o.sql
41
+ end
42
+
43
+ # Alias for ==
44
+ def eql?(o)
45
+ self == o
46
+ end
47
+
37
48
  # Return the dataset as an aliased expression with the given alias. You can
38
49
  # use this as a FROM or JOIN dataset, or as a column if this dataset
39
50
  # returns a single row and column.
@@ -105,6 +116,12 @@ module Sequel
105
116
  s
106
117
  end
107
118
  end
119
+
120
+ # Define a hash value such that datasets with the same DB, opts, and SQL
121
+ # will have the same hash value
122
+ def hash
123
+ [db, opts.sort_by{|k| k.to_s}, sql].hash
124
+ end
108
125
 
109
126
  # Returns a string representation of the dataset including the class name
110
127
  # and the corresponding SQL select statement.
@@ -112,6 +129,21 @@ module Sequel
112
129
  "#<#{self.class}: #{sql.inspect}>"
113
130
  end
114
131
 
132
+ # Splits a possible implicit alias in C, handling both SQL::AliasedExpressions
133
+ # and Symbols. Returns an array of two elements, with the first being the
134
+ # main expression, and the second being the alias.
135
+ def split_alias(c)
136
+ case c
137
+ when Symbol
138
+ c_table, column, aliaz = split_symbol(c)
139
+ [c_table ? SQL::QualifiedIdentifier.new(c_table, column.to_sym) : column.to_sym, aliaz]
140
+ when SQL::AliasedExpression
141
+ [c.expression, c.aliaz]
142
+ else
143
+ [c, nil]
144
+ end
145
+ end
146
+
115
147
  # Creates a unique table alias that hasn't already been used in the dataset.
116
148
  # table_alias can be any type of object accepted by alias_symbol.
117
149
  # The symbol returned will be the implicit alias in the argument,
@@ -413,6 +413,8 @@ module Sequel
413
413
  table_name = table_alias
414
414
  else
415
415
  table = table.table_name if table.respond_to?(:table_name)
416
+ table, implicit_table_alias = split_alias(table)
417
+ table_alias ||= implicit_table_alias
416
418
  table_name = table_alias || table
417
419
  end
418
420
 
@@ -816,7 +818,7 @@ module Sequel
816
818
  # Whether this dataset is a simple SELECT * FROM table.
817
819
  def simple_select_all?
818
820
  o = @opts.reject{|k,v| v.nil? || NON_SQL_OPTIONS.include?(k)}
819
- o.length == 1 && (f = o[:from]) && f.length == 1 && f.first.is_a?(Symbol)
821
+ o.length == 1 && (f = o[:from]) && f.length == 1 && (f.first.is_a?(Symbol) || f.first.is_a?(SQL::AliasedExpression))
820
822
  end
821
823
 
822
824
  private
@@ -50,8 +50,7 @@ module Sequel
50
50
  (v - local_offset).new_offset(local_offset)
51
51
  end
52
52
 
53
- # Convert the timezone setter argument. Returns argument given by default,
54
- # exists for easier overriding in extensions.
53
+ # Returns TZInfo::Timezone instance if given a String.
55
54
  def convert_timezone_setter_arg(tz)
56
55
  tz.is_a?(String) ? TZInfo::Timezone.get(tz) : super
57
56
  end
data/lib/sequel/model.rb CHANGED
@@ -96,7 +96,7 @@ module Sequel
96
96
  :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
97
97
  :@simple_pk=>nil, :@simple_table=>nil, :@strict_param_setting=>nil,
98
98
  :@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil,
99
- :@raise_on_typecast_failure=>nil, :@plugins=>:dup}
99
+ :@raise_on_typecast_failure=>nil, :@plugins=>:dup, :@setter_methods=>nil}
100
100
 
101
101
  # Regular expression that determines if a method name is normal in the sense that
102
102
  # it could be used literally in ruby code without using send. Used to
@@ -120,6 +120,7 @@ module Sequel
120
120
  @require_modification = nil
121
121
  @restrict_primary_key = true
122
122
  @restricted_columns = nil
123
+ @setter_methods = nil
123
124
  @simple_pk = nil
124
125
  @simple_table = nil
125
126
  @strict_param_setting = true
@@ -307,7 +307,9 @@ module Sequel
307
307
 
308
308
  # The table containing the column to use for the associated key when eagerly loading
309
309
  def associated_key_table
310
- self[:join_table]
310
+ self[:associated_key_table] ||= (
311
+ syms = associated_class.dataset.split_alias(self[:join_table]);
312
+ syms[1] || syms[0])
311
313
  end
312
314
 
313
315
  # Alias of right_primary_keys
@@ -100,6 +100,11 @@ module Sequel
100
100
  args.is_a?(Hash) ? dataset[args] : primary_key_lookup(args)
101
101
  end
102
102
 
103
+ # Clear the setter_methods cache
104
+ def clear_setter_methods_cache
105
+ @setter_methods = nil
106
+ end
107
+
103
108
  # Returns the columns in the result set in their original order.
104
109
  # Generally, this will use the columns determined via the database
105
110
  # schema, but in certain cases (e.g. models that are based on a joined
@@ -286,6 +291,12 @@ module Sequel
286
291
  def load(values)
287
292
  new(values, true)
288
293
  end
294
+
295
+ # Clear the setter_methods cache when a setter method is added
296
+ def method_added(meth)
297
+ clear_setter_methods_cache if meth.to_s =~ SETTER_METHOD_REGEXP
298
+ super
299
+ end
289
300
 
290
301
  # Mark the model as not having a primary key. Not having a primary key
291
302
  # can cause issues, among which is that you won't be able to update records.
@@ -294,6 +305,7 @@ module Sequel
294
305
  # Artist.no_primary_key
295
306
  # Artist.primary_key # => nil
296
307
  def no_primary_key
308
+ clear_setter_methods_cache
297
309
  @simple_pk = @primary_key = nil
298
310
  end
299
311
 
@@ -334,6 +346,7 @@ module Sequel
334
346
  # this is the default, this only make sense to use in a subclass where the
335
347
  # parent class has used +unrestrict_primary_key+.
336
348
  def restrict_primary_key
349
+ clear_setter_methods_cache
337
350
  @restrict_primary_key = true
338
351
  end
339
352
 
@@ -355,6 +368,7 @@ module Sequel
355
368
  # Artist.set(:name=>'Bob', :hometown=>'Sactown') # No Error
356
369
  # Artist.set(:name=>'Bob', :records_sold=>30000) # Error
357
370
  def set_allowed_columns(*cols)
371
+ clear_setter_methods_cache
358
372
  @allowed_columns = cols
359
373
  end
360
374
 
@@ -414,6 +428,7 @@ module Sequel
414
428
  # set_primary_key [:taggable_id, :tag_id]
415
429
  # end
416
430
  def set_primary_key(*key)
431
+ clear_setter_methods_cache
417
432
  key = key.flatten
418
433
  @simple_pk = key.length == 1 ? db.literal(key.first) : nil
419
434
  @primary_key = (key.length == 1) ? key[0] : key
@@ -434,8 +449,21 @@ module Sequel
434
449
  # Artist.set(:name=>'Bob', :hometown=>'Sactown') # No Error
435
450
  # Artist.set(:name=>'Bob', :records_sold=>30000) # Error
436
451
  def set_restricted_columns(*cols)
452
+ clear_setter_methods_cache
437
453
  @restricted_columns = cols
438
454
  end
455
+
456
+ # Cache of setter methods to allow by default, in order to speed up new/set/update instance methods.
457
+ def setter_methods
458
+ @setter_methods ||= if allowed_columns
459
+ allowed_columns.map{|x| "#{x}="}
460
+ else
461
+ meths = instance_methods.collect{|x| x.to_s}.grep(SETTER_METHOD_REGEXP) - RESTRICTED_SETTER_METHODS
462
+ meths -= Array(primary_key).map{|x| "#{x}="} if primary_key && restrict_primary_key?
463
+ meths -= restricted_columns.map{|x| "#{x}="} if restricted_columns
464
+ meths
465
+ end
466
+ end
439
467
 
440
468
  # Shortcut for +def_dataset_method+ that is restricted to modifying the
441
469
  # dataset's filter. Sometimes thought of as a scope, and like most dataset methods,
@@ -475,6 +503,7 @@ module Sequel
475
503
  # Artist.unrestrict_primary_key
476
504
  # Artist.set(:id=>1) # No Error
477
505
  def unrestrict_primary_key
506
+ clear_setter_methods_cache
478
507
  @restrict_primary_key = false
479
508
  end
480
509
 
@@ -502,6 +531,7 @@ module Sequel
502
531
  # Create the column accessors. For columns that can be used as method names directly in ruby code,
503
532
  # use a string to define the method for speed. For other columns names, use a block.
504
533
  def def_column_accessor(*columns)
534
+ clear_setter_methods_cache
505
535
  columns, bad_columns = columns.partition{|x| NORMAL_METHOD_NAME_REGEXP.match(x.to_s)}
506
536
  bad_columns.each{|x| def_bad_column_accessor(x)}
507
537
  im = instance_methods.collect{|x| x.to_s}
@@ -526,10 +556,10 @@ module Sequel
526
556
  if single_table && (schema_array = (db.schema(table_name, :reload=>reload) rescue nil))
527
557
  schema_array.each{|k,v| schema_hash[k] = v}
528
558
  if ds_opts.include?(:select)
529
- # Dataset only selects certain columns, delete the other
530
- # columns from the schema
559
+ # We don't remove the columns from the schema_hash,
560
+ # as it's possible they will be used for typecasting
561
+ # even if they are not selected.
531
562
  cols = get_columns.call
532
- schema_hash.delete_if{|k,v| !cols.include?(k)}
533
563
  cols.each{|c| schema_hash[c] ||= {}}
534
564
  else
535
565
  # Dataset is for a single table with all columns,
@@ -1029,6 +1059,12 @@ module Sequel
1029
1059
  set_restricted(hash, only.flatten, false)
1030
1060
  end
1031
1061
 
1062
+ # Clear the setter_methods cache when a method is added
1063
+ def singleton_method_added(meth)
1064
+ @singleton_setter_added = true if meth.to_s =~ SETTER_METHOD_REGEXP
1065
+ super
1066
+ end
1067
+
1032
1068
  # Returns (naked) dataset that should return only this instance.
1033
1069
  #
1034
1070
  # Artist[1].this
@@ -1282,7 +1318,11 @@ module Sequel
1282
1318
 
1283
1319
  # Set the columns, filtered by the only and except arrays.
1284
1320
  def set_restricted(hash, only, except)
1285
- meths = setter_methods(only, except)
1321
+ meths = if only.nil? && except.nil? && !@singleton_setter_added
1322
+ model.setter_methods
1323
+ else
1324
+ setter_methods(only, except)
1325
+ end
1286
1326
  strict = strict_param_setting
1287
1327
  hash.each do |k,v|
1288
1328
  m = "#{k}="
@@ -36,8 +36,10 @@ module Sequel
36
36
 
37
37
  # An array of primary key values, or nil if the object is not persisted.
38
38
  def to_key
39
- if persisted?
40
- primary_key.is_a?(Symbol) ? [pk] : pk
39
+ if primary_key.is_a?(Symbol)
40
+ [pk] if pk
41
+ else
42
+ pk if pk.all?
41
43
  end
42
44
  end
43
45
 
@@ -50,7 +52,7 @@ module Sequel
50
52
  # An string representing the object's primary key. For composite
51
53
  # primary keys, joins them with to_param_joiner.
52
54
  def to_param
53
- if k = to_key
55
+ if persisted? and k = to_key
54
56
  k.join(to_param_joiner)
55
57
  end
56
58
  end
@@ -180,8 +180,8 @@ module Sequel
180
180
  module InstanceMethods
181
181
  # Set the cti_key column to the name of the model.
182
182
  def before_create
183
- return false if super == false
184
183
  send("#{model.cti_key}=", model.name.to_s) if model.cti_key
184
+ super
185
185
  end
186
186
 
187
187
  # Delete the row from all backing tables, starting from the
@@ -200,11 +200,11 @@ module Sequel
200
200
  # in each table.
201
201
  def _insert
202
202
  return super if model == model.cti_base_model
203
- iid = nil
203
+ iid = @values[primary_key]
204
204
  m = model
205
205
  m.cti_tables.each do |table|
206
206
  h = {}
207
- h[m.primary_key] = iid if iid
207
+ h[m.primary_key] ||= iid if iid
208
208
  m.cti_columns[table].each{|c| h[c] = @values[c] if @values.include?(c)}
209
209
  nid = m.db.from(table).insert(h)
210
210
  iid ||= nid
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 14
6
+ MINOR = 15
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -73,6 +73,15 @@ context "MySQL", '#create_table' do
73
73
  end
74
74
  @db.schema(:dolls).map{|k, v| v[:db_type]}.should == %w"blob tinyblob mediumblob longblob blob"
75
75
  end
76
+
77
+ specify "should include an :auto_increment schema attribute if auto incrementing" do
78
+ @db.create_table(:dolls) do
79
+ Integer :n2
80
+ String :n3
81
+ Integer :n4, :auto_increment=>true, :unique=>true
82
+ end
83
+ @db.schema(:dolls).map{|k, v| v[:auto_increment]}.should == [nil, nil, true]
84
+ end
76
85
  end
77
86
 
78
87
  context "A MySQL database" do
@@ -421,7 +430,7 @@ context "A MySQL database with table options" do
421
430
 
422
431
  Sequel::MySQL.default_engine = 'InnoDB'
423
432
  Sequel::MySQL.default_charset = 'utf8'
424
- Sequel::MySQL.default_collate = 'utf8_general_ci'
433
+ Sequel::MySQL.default_collate = 'utf8_general_ci'
425
434
 
426
435
  @db = MYSQL_DB
427
436
  @db.drop_table(:items) rescue nil
@@ -430,6 +439,7 @@ context "A MySQL database with table options" do
430
439
  end
431
440
  after do
432
441
  @db.drop_table(:items) rescue nil
442
+
433
443
  Sequel::MySQL.default_engine = nil
434
444
  Sequel::MySQL.default_charset = nil
435
445
  Sequel::MySQL.default_collate = nil
@@ -895,7 +905,7 @@ context "MySQL::Dataset#complex_expression_sql" do
895
905
  end
896
906
  end
897
907
 
898
- unless MYSQL_DB.adapter_scheme == :do
908
+ unless MYSQL_DB.adapter_scheme == :do or MYSQL_DB.adapter_scheme == :mysql2
899
909
  context "MySQL Stored Procedures" do
900
910
  before do
901
911
  MYSQL_DB.create_table(:items){Integer :id; Integer :value}