sequel 3.14.0 → 3.15.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 (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}