sequel 5.34.0 → 5.39.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +82 -0
  3. data/README.rdoc +2 -2
  4. data/doc/association_basics.rdoc +7 -2
  5. data/doc/cheat_sheet.rdoc +5 -5
  6. data/doc/code_order.rdoc +0 -12
  7. data/doc/dataset_filtering.rdoc +2 -2
  8. data/doc/fork_safety.rdoc +84 -0
  9. data/doc/model_plugins.rdoc +1 -1
  10. data/doc/opening_databases.rdoc +5 -1
  11. data/doc/postgresql.rdoc +1 -1
  12. data/doc/querying.rdoc +3 -3
  13. data/doc/release_notes/5.35.0.txt +56 -0
  14. data/doc/release_notes/5.36.0.txt +60 -0
  15. data/doc/release_notes/5.37.0.txt +30 -0
  16. data/doc/release_notes/5.38.0.txt +28 -0
  17. data/doc/release_notes/5.39.0.txt +19 -0
  18. data/doc/transactions.rdoc +0 -8
  19. data/doc/validations.rdoc +1 -1
  20. data/lib/sequel/adapters/jdbc.rb +13 -1
  21. data/lib/sequel/adapters/jdbc/mysql.rb +4 -4
  22. data/lib/sequel/adapters/odbc.rb +4 -6
  23. data/lib/sequel/adapters/oracle.rb +2 -1
  24. data/lib/sequel/adapters/shared/mssql.rb +35 -5
  25. data/lib/sequel/adapters/shared/oracle.rb +13 -7
  26. data/lib/sequel/adapters/shared/postgres.rb +40 -2
  27. data/lib/sequel/adapters/shared/sqlite.rb +8 -2
  28. data/lib/sequel/adapters/tinytds.rb +1 -0
  29. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -0
  30. data/lib/sequel/core.rb +5 -6
  31. data/lib/sequel/database/connecting.rb +0 -1
  32. data/lib/sequel/database/misc.rb +14 -0
  33. data/lib/sequel/database/schema_generator.rb +6 -0
  34. data/lib/sequel/database/schema_methods.rb +16 -6
  35. data/lib/sequel/database/transactions.rb +2 -2
  36. data/lib/sequel/dataset/actions.rb +10 -6
  37. data/lib/sequel/dataset/query.rb +1 -1
  38. data/lib/sequel/deprecated.rb +1 -1
  39. data/lib/sequel/extensions/_pretty_table.rb +1 -2
  40. data/lib/sequel/extensions/columns_introspection.rb +1 -2
  41. data/lib/sequel/extensions/core_refinements.rb +2 -0
  42. data/lib/sequel/extensions/duplicate_columns_handler.rb +2 -0
  43. data/lib/sequel/extensions/migration.rb +8 -2
  44. data/lib/sequel/extensions/pg_array_ops.rb +4 -0
  45. data/lib/sequel/extensions/pg_enum.rb +2 -0
  46. data/lib/sequel/extensions/pg_extended_date_support.rb +1 -1
  47. data/lib/sequel/extensions/pg_hstore_ops.rb +2 -0
  48. data/lib/sequel/extensions/pg_inet.rb +2 -0
  49. data/lib/sequel/extensions/pg_json_ops.rb +46 -2
  50. data/lib/sequel/extensions/pg_range.rb +3 -7
  51. data/lib/sequel/extensions/pg_range_ops.rb +2 -0
  52. data/lib/sequel/extensions/pg_row.rb +0 -1
  53. data/lib/sequel/extensions/pg_row_ops.rb +24 -0
  54. data/lib/sequel/extensions/query.rb +1 -0
  55. data/lib/sequel/extensions/run_transaction_hooks.rb +1 -1
  56. data/lib/sequel/extensions/s.rb +2 -0
  57. data/lib/sequel/extensions/schema_dumper.rb +3 -3
  58. data/lib/sequel/extensions/symbol_aref_refinement.rb +2 -0
  59. data/lib/sequel/extensions/symbol_as_refinement.rb +2 -0
  60. data/lib/sequel/extensions/to_dot.rb +9 -3
  61. data/lib/sequel/model.rb +1 -1
  62. data/lib/sequel/model/associations.rb +24 -7
  63. data/lib/sequel/model/base.rb +9 -3
  64. data/lib/sequel/model/plugins.rb +1 -0
  65. data/lib/sequel/plugins/association_pks.rb +3 -2
  66. data/lib/sequel/plugins/association_proxies.rb +1 -0
  67. data/lib/sequel/plugins/blacklist_security.rb +1 -2
  68. data/lib/sequel/plugins/class_table_inheritance.rb +3 -8
  69. data/lib/sequel/plugins/csv_serializer.rb +2 -0
  70. data/lib/sequel/plugins/dirty.rb +44 -0
  71. data/lib/sequel/plugins/forbid_lazy_load.rb +2 -0
  72. data/lib/sequel/plugins/instance_specific_default.rb +113 -0
  73. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  74. data/lib/sequel/plugins/pg_array_associations.rb +2 -3
  75. data/lib/sequel/plugins/prepared_statements.rb +5 -11
  76. data/lib/sequel/plugins/prepared_statements_safe.rb +1 -3
  77. data/lib/sequel/plugins/rcte_tree.rb +8 -14
  78. data/lib/sequel/plugins/single_table_inheritance.rb +7 -0
  79. data/lib/sequel/plugins/string_stripper.rb +1 -1
  80. data/lib/sequel/plugins/tree.rb +9 -4
  81. data/lib/sequel/plugins/validation_class_methods.rb +5 -1
  82. data/lib/sequel/timezones.rb +8 -3
  83. data/lib/sequel/version.rb +1 -1
  84. metadata +16 -3
@@ -116,7 +116,9 @@ module Sequel
116
116
  end
117
117
  end
118
118
 
119
+ # :nocov:
119
120
  if defined?(PGRange)
121
+ # :nocov:
120
122
  class PGRange
121
123
  # Wrap the PGRange instance in an RangeOp, allowing you to easily use
122
124
  # the PostgreSQL range functions and operators with literal ranges.
@@ -222,7 +222,6 @@ module Sequel
222
222
  # Split the stored string into an array of strings, handling
223
223
  # the different types of quoting.
224
224
  def parse
225
- return @result if @result
226
225
  values = []
227
226
  skip(/\(/)
228
227
  if skip(/\)/)
@@ -158,6 +158,30 @@ module Sequel
158
158
  end
159
159
  end
160
160
  end
161
+
162
+ # :nocov:
163
+ if defined?(PGRow::ArrayRow)
164
+ # :nocov:
165
+ class PGRow::ArrayRow
166
+ # Wrap the PGRow::ArrayRow instance in an PGRowOp, allowing you to easily use
167
+ # the PostgreSQL row functions and operators with literal rows.
168
+ def op
169
+ Sequel.pg_row_op(self)
170
+ end
171
+ end
172
+ end
173
+
174
+ # :nocov:
175
+ if defined?(PGRow::HashRow)
176
+ # :nocov:
177
+ class PGRow::HashRow
178
+ # Wrap the PGRow::ArrayRow instance in an PGRowOp, allowing you to easily use
179
+ # the PostgreSQL row functions and operators with literal rows.
180
+ def op
181
+ Sequel.pg_row_op(self)
182
+ end
183
+ end
184
+ end
161
185
  end
162
186
 
163
187
  module SQL::Builders
@@ -74,6 +74,7 @@ module Sequel
74
74
  raise(Sequel::Error, "method #{method.inspect} did not return a dataset") unless @dataset.is_a?(Dataset)
75
75
  self
76
76
  end
77
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
77
78
  end
78
79
  end
79
80
 
@@ -48,7 +48,7 @@ class Sequel::Database
48
48
  def _run_transaction_hooks(type, opts)
49
49
  synchronize(opts[:server]) do |conn|
50
50
  unless h = _trans(conn)
51
- raise Error, "Cannot call run_#{type}_hooks outside of a transaction"
51
+ raise Sequel::Error, "Cannot call run_#{type}_hooks outside of a transaction"
52
52
  end
53
53
 
54
54
  if hooks = h[type]
@@ -49,7 +49,9 @@ module Sequel::S
49
49
  Sequel.expr(*a, &block)
50
50
  end
51
51
 
52
+ # :nocov:
52
53
  if RUBY_VERSION >= '2.0.0'
54
+ # :nocov:
53
55
  refine Object do
54
56
  include Sequel::S
55
57
  end
@@ -37,7 +37,7 @@ module Sequel
37
37
  {:type =>schema[:type] == :boolean ? TrueClass : Integer}
38
38
  when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/
39
39
  {:type=>:Bignum}
40
- when /\A(?:real|float(?: unsigned)?|double(?: precision)?|double\(\d+,\d+\)(?: unsigned)?)\z/
40
+ when /\A(?:real|float|double(?: precision)?|double\(\d+,\d+\))(?: unsigned)?\z/
41
41
  {:type=>Float}
42
42
  when 'boolean', 'bit', 'bool'
43
43
  {:type=>TrueClass}
@@ -57,7 +57,7 @@ module Sequel
57
57
  {:type=>String, :size=>($1.to_i if $1)}
58
58
  when /\A(?:small)?money\z/
59
59
  {:type=>BigDecimal, :size=>[19,2]}
60
- when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/
60
+ when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?(?: unsigned)?\z/
61
61
  s = [($1.to_i if $1), ($2.to_i if $2)].compact
62
62
  {:type=>BigDecimal, :size=>(s.empty? ? nil : s)}
63
63
  when /\A(?:bytea|(?:tiny|medium|long)?blob|(?:var)?binary)(?:\((\d+)\))?\z/
@@ -218,7 +218,7 @@ END_MIG
218
218
  gen.foreign_key(name, table, col_opts)
219
219
  else
220
220
  gen.column(name, type, col_opts)
221
- if [Integer, :Bignum, Float].include?(type) && schema[:db_type] =~ / unsigned\z/io
221
+ if [Integer, :Bignum, Float, BigDecimal].include?(type) && schema[:db_type] =~ / unsigned\z/io
222
222
  gen.check(Sequel::SQL::Identifier.new(name) >= 0)
223
223
  end
224
224
  end
@@ -25,7 +25,9 @@
25
25
  #
26
26
  # Related module: Sequel::SymbolAref
27
27
 
28
+ # :nocov:
28
29
  raise(Sequel::Error, "Refinements require ruby 2.0.0 or greater") unless RUBY_VERSION >= '2.0.0'
30
+ # :nocov:
29
31
 
30
32
  module Sequel::SymbolAref
31
33
  refine Symbol do
@@ -23,7 +23,9 @@
23
23
  #
24
24
  # Related module: Sequel::SymbolAs
25
25
 
26
+ # :nocov:
26
27
  raise(Sequel::Error, "Refinements require ruby 2.0.0 or greater") unless RUBY_VERSION >= '2.0.0'
28
+ # :nocov:
27
29
 
28
30
  module Sequel::SymbolAs
29
31
  refine Symbol do
@@ -53,7 +53,13 @@ module Sequel
53
53
  # is given, it is used directly as the node or transition. Otherwise
54
54
  # a node is created for the current object.
55
55
  def dot(label, j=nil)
56
- @dot << "#{j||@i} [label=#{label.to_s.inspect}];"
56
+ label = case label
57
+ when nil
58
+ "<nil>"
59
+ else
60
+ label.to_s
61
+ end
62
+ @dot << "#{j||@i} [label=#{label.inspect}];"
57
63
  end
58
64
 
59
65
  # Recursive method that parses all of Sequel's internal datastructures,
@@ -61,7 +67,7 @@ module Sequel
61
67
  # structure.
62
68
  def v(e, l)
63
69
  @i += 1
64
- dot(l, "#{@stack.last} -> #{@i}") if l
70
+ dot(l, "#{@stack.last} -> #{@i}")
65
71
  @stack.push(@i)
66
72
  case e
67
73
  when LiteralString
@@ -144,7 +150,7 @@ module Sequel
144
150
  dot "Dataset"
145
151
  TO_DOT_OPTIONS.each do |k|
146
152
  if val = e.opts[k]
147
- v(val, k.to_s)
153
+ v(val, k)
148
154
  end
149
155
  end
150
156
  else
@@ -79,7 +79,7 @@ module Sequel
79
79
  def_Model(::Sequel)
80
80
 
81
81
  # The setter methods (methods ending with =) that are never allowed
82
- # to be called automatically via +set+/+update+/+new+/etc..
82
+ # to be called automatically via +set+/+update+/+new+/etc.
83
83
  RESTRICTED_SETTER_METHODS = instance_methods.map(&:to_s).select{|l| l.end_with?('=')}.freeze
84
84
  end
85
85
  end
@@ -356,7 +356,7 @@ module Sequel
356
356
  def finalize
357
357
  return unless cache = self[:cache]
358
358
 
359
- finalize_settings.each do |meth, key|
359
+ finalizer = proc do |meth, key|
360
360
  next if has_key?(key)
361
361
 
362
362
  # Allow calling private methods to make sure caching is done appropriately
@@ -364,6 +364,13 @@ module Sequel
364
364
  self[key] = cache.delete(key) if cache.has_key?(key)
365
365
  end
366
366
 
367
+ finalize_settings.each(&finalizer)
368
+
369
+ unless self[:instance_specific]
370
+ finalizer.call(:associated_eager_dataset, :associated_eager_dataset)
371
+ finalizer.call(:filter_by_associations_conditions_dataset, :filter_by_associations_conditions_dataset)
372
+ end
373
+
367
374
  nil
368
375
  end
369
376
 
@@ -371,9 +378,7 @@ module Sequel
371
378
  FINALIZE_SETTINGS = {
372
379
  :associated_class=>:class,
373
380
  :associated_dataset=>:_dataset,
374
- :associated_eager_dataset=>:associated_eager_dataset,
375
381
  :eager_limit_strategy=>:_eager_limit_strategy,
376
- :filter_by_associations_conditions_dataset=>:filter_by_associations_conditions_dataset,
377
382
  :placeholder_loader=>:placeholder_loader,
378
383
  :predicate_key=>:predicate_key,
379
384
  :predicate_keys=>:predicate_keys,
@@ -432,7 +437,11 @@ module Sequel
432
437
  if use_placeholder_loader?
433
438
  cached_fetch(:placeholder_loader) do
434
439
  Sequel::Dataset::PlaceholderLiteralizer.loader(associated_dataset) do |pl, ds|
435
- ds.where(Sequel.&(*predicate_keys.map{|k| SQL::BooleanExpression.new(:'=', k, pl.arg)}))
440
+ ds = ds.where(Sequel.&(*predicate_keys.map{|k| SQL::BooleanExpression.new(:'=', k, pl.arg)}))
441
+ if self[:block]
442
+ ds = self[:block].call(ds)
443
+ end
444
+ ds
436
445
  end
437
446
  end
438
447
  end
@@ -796,7 +805,7 @@ module Sequel
796
805
 
797
806
  # Whether the placeholder loader can be used to load the association.
798
807
  def use_placeholder_loader?
799
- !self[:instance_specific] && !self[:eager_graph]
808
+ self[:use_placeholder_loader]
800
809
  end
801
810
  end
802
811
 
@@ -1793,11 +1802,12 @@ module Sequel
1793
1802
  opts.merge!(:type => type, :name => name, :cache=>({} if cache_associations), :model => self)
1794
1803
 
1795
1804
  opts[:block] = block if block
1796
- if !opts.has_key?(:instance_specific) && (block || orig_opts[:block] || orig_opts[:dataset])
1805
+ opts[:instance_specific] = true if orig_opts[:dataset]
1806
+ if !opts.has_key?(:instance_specific) && (block || orig_opts[:block])
1797
1807
  # It's possible the association is instance specific, in that it depends on
1798
1808
  # values other than the foreign key value. This needs to be checked for
1799
1809
  # in certain places to disable optimizations.
1800
- opts[:instance_specific] = true
1810
+ opts[:instance_specific] = _association_instance_specific_default(name)
1801
1811
  end
1802
1812
  opts = assoc_class.new.merge!(opts)
1803
1813
 
@@ -1805,6 +1815,7 @@ module Sequel
1805
1815
  raise(Error, "cannot clone an association to an association of different type (association #{name} with type #{type} cloning #{opts[:clone]} with type #{cloned_assoc[:type]})")
1806
1816
  end
1807
1817
 
1818
+ opts[:use_placeholder_loader] = !opts[:instance_specific] && !opts[:eager_graph]
1808
1819
  opts[:eager_block] = opts[:block] unless opts.include?(:eager_block)
1809
1820
  opts[:graph_join_type] ||= :left_outer
1810
1821
  opts[:order_eager_graph] = true unless opts.include?(:order_eager_graph)
@@ -1901,6 +1912,12 @@ module Sequel
1901
1912
  Plugins.def_dataset_methods(self, [:eager, :eager_graph, :eager_graph_with_options, :association_join, :association_full_join, :association_inner_join, :association_left_join, :association_right_join])
1902
1913
 
1903
1914
  private
1915
+
1916
+ # The default value for the instance_specific option, if the association
1917
+ # could be instance specific and the :instance_specific option is not specified.
1918
+ def _association_instance_specific_default(_)
1919
+ true
1920
+ end
1904
1921
 
1905
1922
  # The module to use for the association's methods. Defaults to
1906
1923
  # the overridable_methods_module.
@@ -491,6 +491,11 @@ module Sequel
491
491
  # the module using a the camelized plugin name under Sequel::Plugins.
492
492
  def plugin(plugin, *args, &block)
493
493
  m = plugin.is_a?(Module) ? plugin : plugin_module(plugin)
494
+
495
+ if !m.respond_to?(:apply) && !m.respond_to?(:configure) && (!args.empty? || block)
496
+ Deprecation.deprecate("Plugin #{plugin} accepts no arguments or block, and passing arguments/block to it", "Remove arguments and block when loading the plugin")
497
+ end
498
+
494
499
  unless @plugins.include?(m)
495
500
  @plugins << m
496
501
  m.apply(self, *args, &block) if m.respond_to?(:apply)
@@ -500,8 +505,10 @@ module Sequel
500
505
  dataset_extend(m::DatasetMethods, :create_class_methods=>false)
501
506
  end
502
507
  end
508
+
503
509
  m.configure(self, *args, &block) if m.respond_to?(:configure)
504
510
  end
511
+ ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
505
512
 
506
513
  # Returns primary key attribute hash. If using a composite primary key
507
514
  # value such be an array with values for each primary key in the correct
@@ -632,8 +639,7 @@ module Sequel
632
639
 
633
640
  # Cache of setter methods to allow by default, in order to speed up mass assignment.
634
641
  def setter_methods
635
- return @setter_methods if @setter_methods
636
- @setter_methods = get_setter_methods
642
+ @setter_methods || (@setter_methods = get_setter_methods)
637
643
  end
638
644
 
639
645
  # Returns name of primary table for the dataset. If the table for the dataset
@@ -751,6 +757,7 @@ module Sequel
751
757
  else
752
758
  define_singleton_method(meth){|*args, &block| dataset.public_send(meth, *args, &block)}
753
759
  end
760
+ singleton_class.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
754
761
  end
755
762
 
756
763
  # Get the schema from the database, fall back on checking the columns
@@ -1988,7 +1995,6 @@ module Sequel
1988
1995
 
1989
1996
  # Get the ruby class or classes related to the given column's type.
1990
1997
  def schema_type_class(column)
1991
- # SEQUEL6: Remove
1992
1998
  if (sch = db_schema[column]) && (type = sch[:type])
1993
1999
  db.schema_type_class(type)
1994
2000
  end
@@ -31,6 +31,7 @@ module Sequel
31
31
  def self.def_dataset_methods(mod, meths)
32
32
  Array(meths).each do |meth|
33
33
  mod.class_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__)
34
+ mod.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
34
35
  end
35
36
  end
36
37
 
@@ -295,9 +295,10 @@ module Sequel
295
295
 
296
296
  if primary_key.is_a?(Array)
297
297
  if (cols = sch.values_at(*klass.primary_key)).all? && (convs = cols.map{|c| c[:type] == :integer}).all?
298
+ db = model.db
298
299
  pks.map do |cpk|
299
- cpk.zip(convs).map do |pk, conv|
300
- conv ? model.db.typecast_value(:integer, pk) : pk
300
+ cpk.map do |pk|
301
+ db.typecast_value(:integer, pk)
301
302
  end
302
303
  end
303
304
  else
@@ -99,6 +99,7 @@ module Sequel
99
99
  end
100
100
  v.public_send(meth, *args, &block)
101
101
  end
102
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
102
103
  end
103
104
 
104
105
  module ClassMethods
@@ -58,8 +58,7 @@ module Sequel
58
58
  # restricted_columns.
59
59
  def get_setter_methods
60
60
  meths = super
61
- #if !(respond_to?(:allowed_columns) && allowed_columns) && restricted_columns
62
- if (!defined?(::Sequel::Plugins::WhitelistSecurity) || !plugins.include?(::Sequel::Plugins::WhitelistSecurity) || !allowed_columns) && restricted_columns
61
+ if (!defined?(::Sequel::Plugins::WhitelistSecurity::ClassMethods) || !is_a?(::Sequel::Plugins::WhitelistSecurity::ClassMethods) || !allowed_columns) && restricted_columns
63
62
  meths -= restricted_columns.map{|x| "#{x}="}
64
63
  end
65
64
  meths
@@ -289,7 +289,7 @@ module Sequel
289
289
 
290
290
  # The name of the most recently joined table.
291
291
  def cti_table_name
292
- cti_tables ? cti_tables.last : dataset.first_source_alias
292
+ cti_tables.last
293
293
  end
294
294
 
295
295
  # The model class for the given key value.
@@ -310,7 +310,7 @@ module Sequel
310
310
  # Set table if this is a class table inheritance
311
311
  table = nil
312
312
  columns = nil
313
- if (n = subclass.name) && !n.empty?
313
+ if n = subclass.name
314
314
  if table = cti_table_map[n.to_sym]
315
315
  columns = db.schema(table).map(&:first)
316
316
  else
@@ -417,7 +417,7 @@ module Sequel
417
417
  @values[primary_key] ||= nid
418
418
  end
419
419
  end
420
- db.dataset.supports_insert_select? ? nil : @values[primary_key]
420
+ @values[primary_key]
421
421
  end
422
422
 
423
423
  # Update rows in all backing tables, using the columns in each table.
@@ -433,11 +433,6 @@ module Sequel
433
433
  end
434
434
  end
435
435
  end
436
-
437
- # Don't allow use of prepared statements.
438
- def use_prepared_statements_for?(type)
439
- false
440
- end
441
436
  end
442
437
  end
443
438
  end
@@ -82,11 +82,13 @@ module Sequel
82
82
  end
83
83
  END
84
84
  else
85
+ # :nocov:
85
86
  # :nodoc:
86
87
  def self.csv_call(*args, opts, &block)
87
88
  CSV.send(*args, opts, &block)
88
89
  end
89
90
  # :nodoc:
91
+ # :nocov:
90
92
  end
91
93
 
92
94
  module ClassMethods
@@ -41,6 +41,15 @@ module Sequel
41
41
  # artist.column_changes # => {}
42
42
  # artist.previous_changes # => {:name=>['Foo', 'Bar']}
43
43
  #
44
+ # artist.column_previously_was(:name)
45
+ # # => 'Foo'
46
+ # artist.column_previously_changed?(:name)
47
+ # # => true
48
+ # artist.column_previously_changed?(:name, from: 'Foo', to: 'Bar')
49
+ # # => true
50
+ # artist.column_previously_changed?(:name, from: 'Foo', to: 'Baz')
51
+ # # => false
52
+ #
44
53
  # There is one caveat; when used with a column that also uses the
45
54
  # serialization plugin, setting the column back to its original value
46
55
  # after changing it is not correctly detected and will leave an entry
@@ -105,6 +114,41 @@ module Sequel
105
114
  initial_values.has_key?(column)
106
115
  end
107
116
 
117
+ # Whether the column was previously changed.
118
+ # Options:
119
+ # :from :: If given, the previous initial value of the column must match this
120
+ # :to :: If given, the previous changed value of the column must match this
121
+ #
122
+ # update(name: 'Current')
123
+ # previous_changes # => {:name=>['Initial', 'Current']}
124
+ # column_previously_changed?(:name) # => true
125
+ # column_previously_changed?(:id) # => false
126
+ # column_previously_changed?(:name, from: 'Initial', to: 'Current') # => true
127
+ # column_previously_changed?(:name, from: 'Foo', to: 'Current') # => false
128
+ def column_previously_changed?(column, opts=OPTS)
129
+ return false unless (pc = @previous_changes) && (val = pc[column])
130
+
131
+ if opts.has_key?(:from)
132
+ return false unless val[0] == opts[:from]
133
+ end
134
+
135
+ if opts.has_key?(:to)
136
+ return false unless val[1] == opts[:to]
137
+ end
138
+
139
+ true
140
+ end
141
+
142
+ # The previous value of the column, which is the initial value of
143
+ # the column before the object was previously saved.
144
+ #
145
+ # initial_value(:name) # => 'Initial'
146
+ # update(name: 'Current')
147
+ # column_previously_was(:name) # => 'Initial'
148
+ def column_previously_was(column)
149
+ (pc = @previous_changes) && (val = pc[column]) && val[0]
150
+ end
151
+
108
152
  # Freeze internal data structures
109
153
  def freeze
110
154
  initial_values.freeze