sequel 3.21.0 → 3.28.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 (158) hide show
  1. data/CHANGELOG +294 -0
  2. data/README.rdoc +20 -6
  3. data/Rakefile +20 -15
  4. data/doc/association_basics.rdoc +210 -43
  5. data/doc/dataset_basics.rdoc +4 -4
  6. data/doc/mass_assignment.rdoc +54 -0
  7. data/doc/migration.rdoc +15 -538
  8. data/doc/model_hooks.rdoc +64 -27
  9. data/doc/opening_databases.rdoc +37 -10
  10. data/doc/prepared_statements.rdoc +16 -10
  11. data/doc/reflection.rdoc +8 -2
  12. data/doc/release_notes/3.22.0.txt +39 -0
  13. data/doc/release_notes/3.23.0.txt +172 -0
  14. data/doc/release_notes/3.24.0.txt +420 -0
  15. data/doc/release_notes/3.25.0.txt +88 -0
  16. data/doc/release_notes/3.26.0.txt +88 -0
  17. data/doc/release_notes/3.27.0.txt +82 -0
  18. data/doc/release_notes/3.28.0.txt +304 -0
  19. data/doc/schema_modification.rdoc +547 -0
  20. data/doc/testing.rdoc +106 -0
  21. data/doc/transactions.rdoc +97 -0
  22. data/doc/virtual_rows.rdoc +2 -2
  23. data/lib/sequel/adapters/ado.rb +12 -1
  24. data/lib/sequel/adapters/amalgalite.rb +4 -0
  25. data/lib/sequel/adapters/db2.rb +95 -58
  26. data/lib/sequel/adapters/do.rb +12 -0
  27. data/lib/sequel/adapters/firebird.rb +25 -203
  28. data/lib/sequel/adapters/ibmdb.rb +440 -0
  29. data/lib/sequel/adapters/informix.rb +4 -19
  30. data/lib/sequel/adapters/jdbc/as400.rb +0 -7
  31. data/lib/sequel/adapters/jdbc/db2.rb +49 -0
  32. data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
  33. data/lib/sequel/adapters/jdbc/h2.rb +16 -5
  34. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  35. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  36. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  37. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  38. data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
  39. data/lib/sequel/adapters/jdbc/postgresql.rb +6 -0
  40. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  41. data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
  42. data/lib/sequel/adapters/jdbc.rb +62 -29
  43. data/lib/sequel/adapters/mysql.rb +22 -139
  44. data/lib/sequel/adapters/mysql2.rb +9 -14
  45. data/lib/sequel/adapters/odbc/db2.rb +21 -0
  46. data/lib/sequel/adapters/odbc.rb +15 -3
  47. data/lib/sequel/adapters/oracle.rb +17 -1
  48. data/lib/sequel/adapters/postgres.rb +111 -16
  49. data/lib/sequel/adapters/shared/access.rb +21 -0
  50. data/lib/sequel/adapters/shared/db2.rb +290 -0
  51. data/lib/sequel/adapters/shared/firebird.rb +214 -0
  52. data/lib/sequel/adapters/shared/informix.rb +45 -0
  53. data/lib/sequel/adapters/shared/mssql.rb +85 -47
  54. data/lib/sequel/adapters/shared/mysql.rb +50 -7
  55. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +143 -0
  56. data/lib/sequel/adapters/shared/oracle.rb +0 -4
  57. data/lib/sequel/adapters/shared/postgres.rb +75 -43
  58. data/lib/sequel/adapters/shared/sqlite.rb +56 -8
  59. data/lib/sequel/adapters/sqlite.rb +12 -11
  60. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  61. data/lib/sequel/adapters/tinytds.rb +139 -7
  62. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
  63. data/lib/sequel/ast_transformer.rb +190 -0
  64. data/lib/sequel/connection_pool/threaded.rb +3 -2
  65. data/lib/sequel/connection_pool.rb +1 -1
  66. data/lib/sequel/core.rb +6 -5
  67. data/lib/sequel/database/connecting.rb +5 -5
  68. data/lib/sequel/database/dataset.rb +1 -1
  69. data/lib/sequel/database/dataset_defaults.rb +1 -1
  70. data/lib/sequel/database/logging.rb +1 -1
  71. data/lib/sequel/database/misc.rb +38 -17
  72. data/lib/sequel/database/query.rb +50 -19
  73. data/lib/sequel/database/schema_generator.rb +8 -5
  74. data/lib/sequel/database/schema_methods.rb +52 -27
  75. data/lib/sequel/dataset/actions.rb +167 -48
  76. data/lib/sequel/dataset/features.rb +57 -8
  77. data/lib/sequel/dataset/graph.rb +1 -1
  78. data/lib/sequel/dataset/misc.rb +39 -20
  79. data/lib/sequel/dataset/mutation.rb +3 -3
  80. data/lib/sequel/dataset/prepared_statements.rb +29 -14
  81. data/lib/sequel/dataset/query.rb +182 -32
  82. data/lib/sequel/dataset/sql.rb +31 -58
  83. data/lib/sequel/dataset.rb +8 -0
  84. data/lib/sequel/exceptions.rb +4 -0
  85. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  86. data/lib/sequel/extensions/migration.rb +6 -4
  87. data/lib/sequel/extensions/to_dot.rb +95 -83
  88. data/lib/sequel/model/associations.rb +893 -309
  89. data/lib/sequel/model/base.rb +302 -105
  90. data/lib/sequel/model/errors.rb +1 -1
  91. data/lib/sequel/model/exceptions.rb +5 -1
  92. data/lib/sequel/model.rb +13 -7
  93. data/lib/sequel/plugins/association_pks.rb +22 -4
  94. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  95. data/lib/sequel/plugins/identity_map.rb +113 -6
  96. data/lib/sequel/plugins/many_through_many.rb +67 -5
  97. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  98. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  99. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  100. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  101. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  102. data/lib/sequel/plugins/sharding.rb +12 -20
  103. data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
  104. data/lib/sequel/plugins/update_primary_key.rb +1 -1
  105. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  106. data/lib/sequel/sql.rb +107 -51
  107. data/lib/sequel/timezones.rb +12 -3
  108. data/lib/sequel/version.rb +1 -1
  109. data/spec/adapters/db2_spec.rb +146 -0
  110. data/spec/adapters/mssql_spec.rb +36 -0
  111. data/spec/adapters/mysql_spec.rb +36 -19
  112. data/spec/adapters/postgres_spec.rb +115 -28
  113. data/spec/adapters/spec_helper.rb +6 -0
  114. data/spec/adapters/sqlite_spec.rb +11 -0
  115. data/spec/core/connection_pool_spec.rb +62 -77
  116. data/spec/core/database_spec.rb +244 -287
  117. data/spec/core/dataset_spec.rb +383 -34
  118. data/spec/core/expression_filters_spec.rb +159 -41
  119. data/spec/core/schema_spec.rb +326 -3
  120. data/spec/core/spec_helper.rb +45 -0
  121. data/spec/extensions/association_pks_spec.rb +38 -0
  122. data/spec/extensions/columns_introspection_spec.rb +91 -0
  123. data/spec/extensions/defaults_setter_spec.rb +64 -0
  124. data/spec/extensions/identity_map_spec.rb +162 -0
  125. data/spec/extensions/many_through_many_spec.rb +195 -20
  126. data/spec/extensions/migration_spec.rb +17 -17
  127. data/spec/extensions/nested_attributes_spec.rb +1 -0
  128. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  129. data/spec/extensions/prepared_statements_safe_spec.rb +79 -0
  130. data/spec/extensions/prepared_statements_spec.rb +72 -0
  131. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  132. data/spec/extensions/schema_dumper_spec.rb +2 -2
  133. data/spec/extensions/schema_spec.rb +12 -20
  134. data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
  135. data/spec/extensions/single_table_inheritance_spec.rb +11 -0
  136. data/spec/extensions/spec_helper.rb +3 -1
  137. data/spec/extensions/to_dot_spec.rb +3 -5
  138. data/spec/extensions/xml_serializer_spec.rb +16 -4
  139. data/spec/integration/associations_test.rb +405 -15
  140. data/spec/integration/database_test.rb +4 -2
  141. data/spec/integration/dataset_test.rb +240 -20
  142. data/spec/integration/plugin_test.rb +142 -5
  143. data/spec/integration/prepared_statement_test.rb +174 -95
  144. data/spec/integration/schema_test.rb +128 -16
  145. data/spec/integration/spec_helper.rb +15 -0
  146. data/spec/integration/transaction_test.rb +40 -0
  147. data/spec/integration/type_test.rb +16 -2
  148. data/spec/model/association_reflection_spec.rb +91 -0
  149. data/spec/model/associations_spec.rb +476 -5
  150. data/spec/model/base_spec.rb +91 -1
  151. data/spec/model/eager_loading_spec.rb +519 -31
  152. data/spec/model/hooks_spec.rb +161 -0
  153. data/spec/model/model_spec.rb +89 -2
  154. data/spec/model/plugins_spec.rb +17 -0
  155. data/spec/model/record_spec.rb +184 -12
  156. data/spec/model/spec_helper.rb +5 -0
  157. data/spec/model/validations_spec.rb +11 -0
  158. metadata +85 -34
@@ -11,7 +11,7 @@ module Sequel
11
11
  # the Model's dataset with the method of the same name with the given arguments.
12
12
  module ClassMethods
13
13
  # Which columns should be the only columns allowed in a call to a mass assignment method (e.g. set)
14
- # (default: not set, so all columns not otherwise restricted).
14
+ # (default: not set, so all columns not otherwise restricted are allowed).
15
15
  attr_reader :allowed_columns
16
16
 
17
17
  # Array of modules that extend this model's dataset. Stored
@@ -23,7 +23,7 @@ module Sequel
23
23
  # stored so when the dataset changes, methods defined with def_dataset_method
24
24
  # will be applied to the new dataset.
25
25
  attr_reader :dataset_methods
26
- #
26
+
27
27
  # Array of plugin modules loaded by this class
28
28
  #
29
29
  # Sequel::Model.plugins
@@ -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,
@@ -162,7 +182,7 @@ module Sequel
162
182
  def db
163
183
  return @db if @db
164
184
  @db = self == Model ? DATABASES.first : superclass.db
165
- raise(Error, "No database associated with #{self}") unless @db
185
+ raise(Error, "No database associated with #{self}: have you called Sequel.connect or #{self}.db= ?") unless @db
166
186
  @db
167
187
  end
168
188
 
@@ -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.
@@ -271,7 +298,8 @@ module Sequel
271
298
  db
272
299
  begin
273
300
  if self == Model || !@dataset
274
- subclass.set_dataset(subclass.implicit_table_name) unless subclass.name.empty?
301
+ n = subclass.name
302
+ subclass.set_dataset(subclass.implicit_table_name) unless n.nil? || n.empty?
275
303
  elsif @dataset
276
304
  subclass.set_dataset(@dataset.clone, :inherited=>true)
277
305
  end
@@ -325,18 +353,13 @@ module Sequel
325
353
  unless @plugins.include?(m)
326
354
  @plugins << m
327
355
  m.apply(self, *args, &blk) if m.respond_to?(:apply)
328
- include(m::InstanceMethods) if m.const_defined?("InstanceMethods")
329
- extend(m::ClassMethods)if m.const_defined?("ClassMethods")
330
- if m.const_defined?("DatasetMethods")
331
- dataset.extend(m::DatasetMethods) if @dataset
332
- dataset_method_modules << m::DatasetMethods
333
- meths = m::DatasetMethods.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
334
- def_dataset_method(*meths) unless meths.empty?
335
- end
356
+ include(m::InstanceMethods) if plugin_module_defined?(m, :InstanceMethods)
357
+ extend(m::ClassMethods)if plugin_module_defined?(m, :ClassMethods)
358
+ dataset_extend(m::DatasetMethods) if plugin_module_defined?(m, :DatasetMethods)
336
359
  end
337
360
  m.configure(self, *args, &blk) if m.respond_to?(:configure)
338
361
  end
339
-
362
+
340
363
  # Returns primary key attribute hash. If using a composite primary key
341
364
  # value such be an array with values for each primary key in the correct
342
365
  # order. For a standard primary key, value should be an object with a
@@ -389,8 +412,8 @@ module Sequel
389
412
  # setter methods (methods that end in =) that you want to be used during
390
413
  # mass assignment, they need to be listed here as well (without the =).
391
414
  #
392
- # It may be better to use a method such as +set_only+ instead of this in places where
393
- # only certain columns may be allowed.
415
+ # It may be better to use a method such as +set_only+ or +set_fields+ that lets you specify
416
+ # the allowed fields per call.
394
417
  #
395
418
  # Artist.set_allowed_columns(:name, :hometown)
396
419
  # Artist.set(:name=>'Bob', :hometown=>'Sactown') # No Error
@@ -400,10 +423,12 @@ module Sequel
400
423
  @allowed_columns = cols
401
424
  end
402
425
 
403
- # Sets the dataset associated with the Model class. +ds+ can be a +Symbol+
404
- # (specifying a table name in the current database), or a +Dataset+.
426
+ # Sets the dataset associated with the Model class. +ds+ can be a +Symbol+,
427
+ # +LiteralString+, <tt>SQL::Identifier</tt>, <tt>SQL::QualifiedIdentifier</tt>,
428
+ # <tt>SQL::AliasedExpression</tt>
429
+ # (all specifying a table name in the current database), or a +Dataset+.
405
430
  # If a dataset is used, the model's database is changed to the database of the given
406
- # dataset. If a symbol is used, a dataset is created from the current
431
+ # dataset. If a dataset is not used, a dataset is created from the current
407
432
  # database with the table name given. Other arguments raise an +Error+.
408
433
  # Returns self.
409
434
  #
@@ -418,15 +443,15 @@ module Sequel
418
443
  def set_dataset(ds, opts={})
419
444
  inherited = opts[:inherited]
420
445
  @dataset = case ds
421
- when Symbol
446
+ when Symbol, SQL::Identifier, SQL::QualifiedIdentifier, SQL::AliasedExpression, LiteralString
422
447
  @simple_table = db.literal(ds)
423
- db[ds]
448
+ db.from(ds)
424
449
  when Dataset
425
450
  @simple_table = nil
426
451
  @db = ds.db
427
452
  ds
428
453
  else
429
- raise(Error, "Model.set_dataset takes a Symbol or a Sequel::Dataset")
454
+ raise(Error, "Model.set_dataset takes one of the following classes as an argument: Symbol, LiteralString, SQL::Identifier, SQL::QualifiedIdentifier, SQL::AliasedExpression, Dataset")
430
455
  end
431
456
  @dataset.row_proc = Proc.new{|r| load(r)}
432
457
  @require_modification = Sequel::Model.require_modification.nil? ? @dataset.provides_accurate_rows_matched? : Sequel::Model.require_modification
@@ -444,7 +469,8 @@ module Sequel
444
469
 
445
470
  # Sets the primary key for this model. You can use either a regular
446
471
  # or a composite primary key. To not use a primary key, set to nil
447
- # or use +no_primary_key+.
472
+ # or use +no_primary_key+. On most adapters, Sequel can automatically
473
+ # determine the primary key to use, so this method is not needed often.
448
474
  #
449
475
  # class Person < Sequel::Model
450
476
  # # regular key
@@ -468,10 +494,9 @@ module Sequel
468
494
  # If you have any virtual setter methods (methods that end in =) that you
469
495
  # want not to be used during mass assignment, they need to be listed here as well (without the =).
470
496
  #
471
- # It may be better to use a method such as +set_except+ instead of this in places where
472
- # certain columns are restricted. In general, it's better to have a whitelist approach
473
- # where you specify only what is allowed, as opposed to a blacklist approach that this
474
- # method uses, where everything is allowed other than what you restrict.
497
+ # It's generally a bad idea to rely on a blacklist approach for security. Using a whitelist
498
+ # approach such as set_allowed_columns or the instance level set_only or set_fields methods
499
+ # is usually a better choice. So use of this method is generally a bad idea.
475
500
  #
476
501
  # Artist.set_restricted_column(:records_sold)
477
502
  # Artist.set(:name=>'Bob', :hometown=>'Sactown') # No Error
@@ -511,6 +536,9 @@ module Sequel
511
536
  # 7 days ago.
512
537
  #
513
538
  # Both the args given and the block are passed to <tt>Dataset#filter</tt>.
539
+ #
540
+ # This method creates dataset methods that do not accept arguments. To create
541
+ # dataset methods that accept arguments, you have to use def_dataset_method.
514
542
  def subset(name, *args, &block)
515
543
  def_dataset_method(name){filter(*args, &block)}
516
544
  end
@@ -526,6 +554,7 @@ module Sequel
526
554
  end
527
555
 
528
556
  # Allow the setting of the primary key(s) when using the mass assignment methods.
557
+ # Using this method can open up security issues, be very careful before using it.
529
558
  #
530
559
  # Artist.set(:id=>1) # Error
531
560
  # Artist.unrestrict_primary_key
@@ -548,6 +577,16 @@ module Sequel
548
577
  end
549
578
  end
550
579
 
580
+ # Add the module to the class's dataset_method_modules. Extend the dataset with the
581
+ # module if the model has a dataset. Add dataset methods to the class for all
582
+ # public dataset methods.
583
+ def dataset_extend(mod)
584
+ dataset.extend(mod) if @dataset
585
+ dataset_method_modules << mod
586
+ meths = mod.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
587
+ def_dataset_method(*meths) unless meths.empty?
588
+ end
589
+
551
590
  # Create a column accessor for a column with a method name that is hard to use in ruby code.
552
591
  def def_bad_column_accessor(column)
553
592
  overridable_methods_module.module_eval do
@@ -657,6 +696,15 @@ module Sequel
657
696
  Sequel::Plugins.const_get(module_name)
658
697
  end
659
698
 
699
+ # Check if the plugin module +plugin+ defines the constant named by +submod+.
700
+ def plugin_module_defined?(plugin, submod)
701
+ if RUBY_VERSION >= '1.9'
702
+ plugin.const_defined?(submod, false)
703
+ else
704
+ plugin.const_defined?(submod)
705
+ end
706
+ end
707
+
660
708
  # Find the row in the dataset that matches the primary key. Uses
661
709
  # a static SQL optimization if the table and primary key are simple.
662
710
  def primary_key_lookup(pk)
@@ -686,18 +734,19 @@ module Sequel
686
734
 
687
735
  # Sequel::Model instance methods that implement basic model functionality.
688
736
  #
689
- # * All of the methods in +HOOKS+ create instance methods that are called
737
+ # * All of the methods in +HOOKS+ and +AROUND_HOOKS+ create instance methods that are called
690
738
  # by Sequel when the appropriate action occurs. For example, when destroying
691
- # a model object, Sequel will call +before_destroy+, do the destroy,
692
- # and then call +after_destroy+.
739
+ # a model object, Sequel will call +around_destory+, which will call +before_destroy+, do
740
+ # the destroy, and then call +after_destroy+.
693
741
  # * The following instance_methods all call the class method of the same
694
- # name: columns, dataset, db, primary_key, db_schema.
742
+ # name: columns, db, primary_key, db_schema.
695
743
  # * All of the methods in +BOOLEAN_SETTINGS+ create attr_writers allowing you
696
744
  # to set values for the attribute. It also creates instnace getters returning
697
745
  # the value of the setting. If the value has not yet been set, it
698
746
  # gets the default value from the class by calling the class method of the same name.
699
747
  module InstanceMethods
700
748
  HOOKS.each{|h| class_eval("def #{h}; end", __FILE__, __LINE__)}
749
+ AROUND_HOOKS.each{|h| class_eval("def #{h}; yield end", __FILE__, __LINE__)}
701
750
 
702
751
  # Define instance method(s) that calls class method(s) of the
703
752
  # same name, caching the result in an instance variable. Define
@@ -750,7 +799,7 @@ module Sequel
750
799
  @values = {}
751
800
  @new = true
752
801
  @modified = true
753
- set(values)
802
+ initialize_set(values)
754
803
  changed_columns.clear
755
804
  yield self if block_given?
756
805
  end
@@ -766,7 +815,7 @@ module Sequel
766
815
 
767
816
  # Sets the value for the given column. If typecasting is enabled for
768
817
  # this object, typecast the value based on the column's type.
769
- # If this a a new record or the typecasted value isn't the same
818
+ # If this is a new record or the typecasted value isn't the same
770
819
  # as the current value for the column, mark the column as changed.
771
820
  #
772
821
  # a = Artist.new
@@ -874,14 +923,25 @@ module Sequel
874
923
 
875
924
  # Returns true when current instance exists, false otherwise.
876
925
  # Generally an object that isn't new will exist unless it has
877
- # been deleted.
926
+ # been deleted. Uses a database query to check for existence,
927
+ # unless the model object is new, in which case this is always
928
+ # false.
878
929
  #
879
930
  # Artist[1].exists? # SELECT 1 FROM artists WHERE (id = 1)
880
931
  # # => true
932
+ # Artist.new.exists?
933
+ # # => false
881
934
  def exists?
882
- !this.get(1).nil?
935
+ new? ? false : !this.get(1).nil?
883
936
  end
884
937
 
938
+ # Ignore the model's setter method cache when this instances extends a module, as the
939
+ # module may contain setter methods.
940
+ def extend(mod)
941
+ @singleton_setter_added = true
942
+ super
943
+ end
944
+
885
945
  # Value that should be unique for objects with the same class and pk (if pk is not nil), or
886
946
  # the same class and values (if pk is nil).
887
947
  #
@@ -890,7 +950,14 @@ module Sequel
890
950
  # Artist.new.hash == Artist.new.hash # true
891
951
  # Artist.new(:name=>'Bob').hash == Artist.new.hash # false
892
952
  def hash
893
- [model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
953
+ case primary_key
954
+ when Array
955
+ [model, !pk.all? ? @values.sort_by{|k,v| k.to_s} : pk].hash
956
+ when Symbol
957
+ [model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
958
+ else
959
+ [model, @values.sort_by{|k,v| k.to_s}].hash
960
+ end
894
961
  end
895
962
 
896
963
  # Returns value for the :id attribute, even if the primary key is
@@ -971,7 +1038,7 @@ module Sequel
971
1038
  end
972
1039
 
973
1040
  # Returns the primary key value identifying the model instance.
974
- # Raises an error if this model does not have a primary key.
1041
+ # Raises an +Error+ if this model does not have a primary key.
975
1042
  # If the model has a composite primary key, returns an array of values.
976
1043
  #
977
1044
  # Artist[1].pk # => 1
@@ -1016,26 +1083,28 @@ module Sequel
1016
1083
  #
1017
1084
  # If +save+ fails and either raise_on_save_failure or the
1018
1085
  # :raise_on_failure option is true, it raises ValidationFailed
1019
- # or BeforeHookFailed. Otherwise it returns nil.
1086
+ # or HookFailed. Otherwise it returns nil.
1020
1087
  #
1021
1088
  # If it succeeds, it returns self.
1022
1089
  #
1023
1090
  # You can provide an optional list of columns to update, in which
1024
- # case it only updates those columns.
1091
+ # case it only updates those columns, or a options hash.
1025
1092
  #
1026
1093
  # Takes the following options:
1027
1094
  #
1028
- # * :changed - save all changed columns, instead of all columns or the columns given
1029
- # * :transaction - set to true or false to override the current
1030
- # use_transactions setting
1031
- # * :validate - set to false to skip validation
1032
- # * :raise_on_failure - set to true or false to override the current
1033
- # raise_on_save_failure setting
1095
+ # :changed :: save all changed columns, instead of all columns or the columns given
1096
+ # :transaction :: set to true or false to override the current
1097
+ # +use_transactions+ setting
1098
+ # :validate :: set to false to skip validation
1099
+ # :raise_on_failure :: set to true or false to override the current
1100
+ # +raise_on_save_failure+ setting
1034
1101
  def save(*columns)
1035
1102
  opts = columns.last.is_a?(Hash) ? columns.pop : {}
1036
- if opts[:validate] != false and !valid?(opts)
1037
- raise(ValidationFailed.new(errors)) if raise_on_failure?(opts)
1038
- return
1103
+ if opts[:validate] != false
1104
+ unless checked_save_failure(opts){_valid?(true, opts)}
1105
+ raise(ValidationFailed.new(errors)) if raise_on_failure?(opts)
1106
+ return
1107
+ end
1039
1108
  end
1040
1109
  checked_save_failure(opts){checked_transaction(opts){_save(columns, opts)}}
1041
1110
  end
@@ -1075,7 +1144,8 @@ module Sequel
1075
1144
  end
1076
1145
 
1077
1146
  # Set all values using the entries in the hash, except for the keys
1078
- # given in except.
1147
+ # given in except. You should probably use +set_fields+ or +set_only+
1148
+ # instead of this method, as blacklist approaches to security are a bad idea.
1079
1149
  #
1080
1150
  # artist.set_except({:name=>'Jim'}, :hometown)
1081
1151
  # artist.name # => 'Jim'
@@ -1086,10 +1156,10 @@ module Sequel
1086
1156
  # For each of the fields in the given array +fields+, call the setter
1087
1157
  # method with the value of that +hash+ entry for the field. Returns self.
1088
1158
  #
1089
- # artist.set_fields({:name=>'Jim'}, :name)
1159
+ # artist.set_fields({:name=>'Jim'}, [:name])
1090
1160
  # artist.name # => 'Jim'
1091
1161
  #
1092
- # artist.set_fields({:hometown=>'LA'}, :name)
1162
+ # artist.set_fields({:hometown=>'LA'}, [:name])
1093
1163
  # artist.name # => nil
1094
1164
  # artist.hometown # => 'Sac'
1095
1165
  def set_fields(hash, fields)
@@ -1098,12 +1168,13 @@ module Sequel
1098
1168
  end
1099
1169
 
1100
1170
  # Set the values using the entries in the hash, only if the key
1101
- # is included in only.
1171
+ # is included in only. It may be a better idea to use +set_fields+
1172
+ # instead of this method.
1102
1173
  #
1103
1174
  # artist.set_only({:name=>'Jim'}, :name)
1104
1175
  # artist.name # => 'Jim'
1105
1176
  #
1106
- # artist.set_only({:hometown=>'LA'}, :name) # Raise error
1177
+ # artist.set_only({:hometown=>'LA'}, :name) # Raise Error
1107
1178
  def set_only(hash, *only)
1108
1179
  set_restricted(hash, only.flatten, false)
1109
1180
  end
@@ -1122,7 +1193,7 @@ module Sequel
1122
1193
  @this ||= model.dataset.filter(pk_hash).limit(1).naked
1123
1194
  end
1124
1195
 
1125
- # Runs set with the passed hash and then runs save_changes.
1196
+ # Runs #set with the passed hash and then runs save_changes.
1126
1197
  #
1127
1198
  # artist.update(:name=>'Jim') # UPDATE artists SET name = 'Jim' WHERE (id = 1)
1128
1199
  def update(hash)
@@ -1130,7 +1201,7 @@ module Sequel
1130
1201
  end
1131
1202
 
1132
1203
  # Update all values using the entries in the hash, ignoring any setting of
1133
- # allowed_columns or restricted columns in the model.
1204
+ # +allowed_columns+ or +restricted_columns+ in the model.
1134
1205
  #
1135
1206
  # Artist.set_restricted_columns(:name)
1136
1207
  # artist.update_all(:name=>'Jim') # UPDATE artists SET name = 'Jim' WHERE (id = 1)
@@ -1139,7 +1210,8 @@ module Sequel
1139
1210
  end
1140
1211
 
1141
1212
  # Update all values using the entries in the hash, except for the keys
1142
- # given in except.
1213
+ # given in except. You should probably use +update_fields+ or +update_only+
1214
+ # instead of this method, as blacklist approaches to security are a bad idea.
1143
1215
  #
1144
1216
  # artist.update_except({:name=>'Jim'}, :hometown) # UPDATE artists SET name = 'Jim' WHERE (id = 1)
1145
1217
  def update_except(hash, *except)
@@ -1149,10 +1221,10 @@ module Sequel
1149
1221
  # Update the instances values by calling +set_fields+ with the +hash+
1150
1222
  # and +fields+, then save any changes to the record. Returns self.
1151
1223
  #
1152
- # artist.update_fields({:name=>'Jim'}, :name)
1224
+ # artist.update_fields({:name=>'Jim'}, [:name])
1153
1225
  # # UPDATE artists SET name = 'Jim' WHERE (id = 1)
1154
1226
  #
1155
- # artist.update_fields({:hometown=>'LA'}, :name)
1227
+ # artist.update_fields({:hometown=>'LA'}, [:name])
1156
1228
  # # UPDATE artists SET name = NULL WHERE (id = 1)
1157
1229
  def update_fields(hash, fields)
1158
1230
  set_fields(hash, fields)
@@ -1160,7 +1232,8 @@ module Sequel
1160
1232
  end
1161
1233
 
1162
1234
  # Update the values using the entries in the hash, only if the key
1163
- # is included in only.
1235
+ # is included in only. It may be a better idea to use +update_fields+
1236
+ # instead of this method.
1164
1237
  #
1165
1238
  # artist.update_only({:name=>'Jim'}, :name)
1166
1239
  # # UPDATE artists SET name = 'Jim' WHERE (id = 1)
@@ -1185,21 +1258,15 @@ module Sequel
1185
1258
  # artist(:name=>'Invalid').valid? # => false
1186
1259
  # artist.errors.full_messages # => ['name cannot be Invalid']
1187
1260
  def valid?(opts = {})
1188
- errors.clear
1189
- if before_validation == false
1190
- raise_hook_failure(:validation) if raise_on_failure?(opts)
1191
- return false
1192
- end
1193
- validate
1194
- after_validation
1195
- errors.empty?
1261
+ _valid?(false, opts)
1196
1262
  end
1197
1263
 
1198
1264
  private
1199
1265
 
1200
- # Actually do the deletion of the object's dataset.
1266
+ # Do the deletion of the object's dataset, and check that the row
1267
+ # was actually deleted.
1201
1268
  def _delete
1202
- n = _delete_dataset.delete
1269
+ n = _delete_without_checking
1203
1270
  raise(NoExistingObject, "Attempt to delete object did not result in a single row modification (Rows Deleted: #{n}, SQL: #{_delete_dataset.delete_sql})") if require_modification && n != 1
1204
1271
  n
1205
1272
  end
@@ -1210,12 +1277,24 @@ module Sequel
1210
1277
  this
1211
1278
  end
1212
1279
 
1280
+ # Actually do the deletion of the object's dataset. Return the
1281
+ # number of rows modified.
1282
+ def _delete_without_checking
1283
+ _delete_dataset.delete
1284
+ end
1285
+
1213
1286
  # Internal destroy method, separted from destroy to
1214
1287
  # allow running inside a transaction
1215
1288
  def _destroy(opts)
1216
- raise_hook_failure(:destroy) if before_destroy == false
1217
- _destroy_delete
1218
- after_destroy
1289
+ called = false
1290
+ around_destroy do
1291
+ called = true
1292
+ raise_hook_failure(:destroy) if before_destroy == false
1293
+ _destroy_delete
1294
+ after_destroy
1295
+ true
1296
+ end
1297
+ raise_hook_failure(:destroy) unless called
1219
1298
  self
1220
1299
  end
1221
1300
 
@@ -1230,11 +1309,11 @@ module Sequel
1230
1309
  # the record should be refreshed from the database.
1231
1310
  def _insert
1232
1311
  ds = _insert_dataset
1233
- if ds.respond_to?(:insert_select) and h = ds.insert_select(@values)
1312
+ if !ds.opts[:select] and ds.supports_insert_select? and h = _insert_select_raw(ds)
1234
1313
  @values = h
1235
1314
  nil
1236
1315
  else
1237
- iid = ds.insert(@values)
1316
+ iid = _insert_raw(ds)
1238
1317
  # if we have a regular primary key and it's not set in @values,
1239
1318
  # we assume it's the last inserted id
1240
1319
  if (pk = autoincrementing_primary_key) && pk.is_a?(Symbol) && !@values[pk]
@@ -1243,52 +1322,89 @@ module Sequel
1243
1322
  pk
1244
1323
  end
1245
1324
  end
1246
-
1325
+
1247
1326
  # The dataset to use when inserting a new object. The same as the model's
1248
1327
  # dataset by default.
1249
1328
  def _insert_dataset
1250
1329
  model.dataset
1251
1330
  end
1252
1331
 
1332
+ # Insert into the given dataset and return the primary key created (if any).
1333
+ def _insert_raw(ds)
1334
+ ds.insert(@values)
1335
+ end
1336
+
1337
+ # Insert into the given dataset and return the hash of column values.
1338
+ def _insert_select_raw(ds)
1339
+ ds.insert_select(@values)
1340
+ end
1341
+
1253
1342
  # Refresh using a particular dataset, used inside save to make sure the same server
1254
1343
  # is used for reading newly inserted values from the database
1255
1344
  def _refresh(dataset)
1256
- set_values(dataset.first || raise(Error, "Record not found"))
1345
+ set_values(_refresh_get(dataset) || raise(Error, "Record not found"))
1257
1346
  changed_columns.clear
1258
1347
  self
1259
1348
  end
1349
+
1350
+ # Get the row of column data from the database.
1351
+ def _refresh_get(dataset)
1352
+ dataset.first
1353
+ end
1260
1354
 
1261
1355
  # Internal version of save, split from save to allow running inside
1262
1356
  # it's own transaction.
1263
1357
  def _save(columns, opts)
1264
- raise_hook_failure(:save) if before_save == false
1265
- if new?
1266
- raise_hook_failure(:create) if before_create == false
1267
- pk = _insert
1268
- @this = nil
1269
- @new = false
1270
- @was_new = true
1271
- after_create
1358
+ was_new = false
1359
+ pk = nil
1360
+ called_save = false
1361
+ called_cu = false
1362
+ around_save do
1363
+ called_save = true
1364
+ raise_hook_failure(:save) if before_save == false
1365
+ if new?
1366
+ was_new = true
1367
+ around_create do
1368
+ called_cu = true
1369
+ raise_hook_failure(:create) if before_create == false
1370
+ pk = _insert
1371
+ @this = nil
1372
+ @new = false
1373
+ @was_new = true
1374
+ after_create
1375
+ true
1376
+ end
1377
+ raise_hook_failure(:create) unless called_cu
1378
+ else
1379
+ around_update do
1380
+ called_cu = true
1381
+ raise_hook_failure(:update) if before_update == false
1382
+ if columns.empty?
1383
+ @columns_updated = if opts[:changed]
1384
+ @values.reject{|k,v| !changed_columns.include?(k)}
1385
+ else
1386
+ _save_update_all_columns_hash
1387
+ end
1388
+ changed_columns.clear
1389
+ else # update only the specified columns
1390
+ @columns_updated = @values.reject{|k, v| !columns.include?(k)}
1391
+ changed_columns.reject!{|c| columns.include?(c)}
1392
+ end
1393
+ _update_columns(@columns_updated)
1394
+ @this = nil
1395
+ after_update
1396
+ true
1397
+ end
1398
+ raise_hook_failure(:update) unless called_cu
1399
+ end
1272
1400
  after_save
1401
+ true
1402
+ end
1403
+ raise_hook_failure(:save) unless called_save
1404
+ if was_new
1273
1405
  @was_new = nil
1274
1406
  pk ? _save_refresh : changed_columns.clear
1275
1407
  else
1276
- raise_hook_failure(:update) if before_update == false
1277
- if columns.empty?
1278
- @columns_updated = if opts[:changed]
1279
- @values.reject{|k,v| !changed_columns.include?(k)}
1280
- else
1281
- _save_update_all_columns_hash
1282
- end
1283
- changed_columns.clear
1284
- else # update only the specified columns
1285
- @columns_updated = @values.reject{|k, v| !columns.include?(k)}
1286
- changed_columns.reject!{|c| columns.include?(c)}
1287
- end
1288
- _update_columns(@columns_updated)
1289
- @this = nil
1290
- after_update
1291
- after_save
1292
1408
  @columns_updated = nil
1293
1409
  end
1294
1410
  @modified = false
@@ -1321,9 +1437,10 @@ module Sequel
1321
1437
  _update(columns) unless columns.empty?
1322
1438
  end
1323
1439
 
1324
- # Update this instance's dataset with the supplied column hash.
1440
+ # Update this instance's dataset with the supplied column hash,
1441
+ # checking that only a single row was modified.
1325
1442
  def _update(columns)
1326
- n = _update_dataset.update(columns)
1443
+ n = _update_without_checking(columns)
1327
1444
  raise(NoExistingObject, "Attempt to update object did not result in a single row modification (SQL: #{_update_dataset.update_sql(columns)})") if require_modification && n != 1
1328
1445
  n
1329
1446
  end
@@ -1334,7 +1451,46 @@ module Sequel
1334
1451
  this
1335
1452
  end
1336
1453
 
1337
- # If not raising on failure, check for BeforeHookFailed
1454
+ # Update this instances dataset with the supplied column hash.
1455
+ def _update_without_checking(columns)
1456
+ _update_dataset.update(columns)
1457
+ end
1458
+
1459
+ # Internal validation method. If +raise_errors+ is +true+, hook
1460
+ # failures will be raised as HookFailure exceptions. If it is
1461
+ # +false+, +false+ will be returned instead.
1462
+ def _valid?(raise_errors, opts)
1463
+ errors.clear
1464
+ called = false
1465
+ error = false
1466
+ around_validation do
1467
+ called = true
1468
+ if before_validation == false
1469
+ if raise_errors
1470
+ raise_hook_failure(:validation)
1471
+ else
1472
+ error = true
1473
+ end
1474
+ false
1475
+ else
1476
+ validate
1477
+ after_validation
1478
+ errors.empty?
1479
+ end
1480
+ end
1481
+ error = true unless called
1482
+ if error
1483
+ if raise_errors
1484
+ raise_hook_failure(:validation)
1485
+ else
1486
+ false
1487
+ end
1488
+ else
1489
+ errors.empty?
1490
+ end
1491
+ end
1492
+
1493
+ # If not raising on failure, check for HookFailed
1338
1494
  # being raised by yielding and swallow it.
1339
1495
  def checked_save_failure(opts)
1340
1496
  if raise_on_failure?(opts)
@@ -1342,7 +1498,7 @@ module Sequel
1342
1498
  else
1343
1499
  begin
1344
1500
  yield
1345
- rescue BeforeHookFailed
1501
+ rescue HookFailed
1346
1502
  nil
1347
1503
  end
1348
1504
  end
@@ -1353,6 +1509,13 @@ module Sequel
1353
1509
  use_transaction?(opts) ? db.transaction(opts){yield} : yield
1354
1510
  end
1355
1511
 
1512
+ # Set the columns with the given hash. By default, the same as +set+, but
1513
+ # exists so it can be overridden. This is called only for new records, before
1514
+ # changed_columns is cleared.
1515
+ def initialize_set(h)
1516
+ set(h)
1517
+ end
1518
+
1356
1519
  # Default inspection output for the values hash, overwrite to change what #inspect displays.
1357
1520
  def inspect_values
1358
1521
  @values.inspect
@@ -1369,7 +1532,7 @@ module Sequel
1369
1532
  # Raise an error appropriate to the hook type. May be swallowed by
1370
1533
  # checked_save_failure depending on the raise_on_failure? setting.
1371
1534
  def raise_hook_failure(type)
1372
- raise BeforeHookFailed, "one of the before_#{type} hooks returned false"
1535
+ raise HookFailed, "one of the before_#{type} hooks returned false"
1373
1536
  end
1374
1537
 
1375
1538
  # Set the columns, filtered by the only and except arrays.
@@ -1385,7 +1548,16 @@ module Sequel
1385
1548
  if meths.include?(m)
1386
1549
  send(m, v)
1387
1550
  elsif strict
1388
- raise Error, "method #{m} doesn't exist or access is restricted to it"
1551
+ # Avoid using respond_to? or creating symbols from user input
1552
+ if public_methods.map{|s| s.to_s}.include?(m)
1553
+ if Array(model.primary_key).map{|s| s.to_s}.member?(k.to_s) && model.restrict_primary_key?
1554
+ raise Error, "#{k} is a restricted primary key"
1555
+ else
1556
+ raise Error, "#{k} is a restricted column"
1557
+ end
1558
+ else
1559
+ raise Error, "method #{m} doesn't exist"
1560
+ end
1389
1561
  end
1390
1562
  end
1391
1563
  self
@@ -1461,6 +1633,18 @@ module Sequel
1461
1633
  # Artist.dataset.model # => Artist
1462
1634
  attr_accessor :model
1463
1635
 
1636
+ # Assume if a single integer is given that it is a lookup by primary
1637
+ # key, and call with_pk with the argument.
1638
+ #
1639
+ # Artist.dataset[1] # SELECT * FROM artists WHERE (id = 1) LIMIT 1
1640
+ def [](*args)
1641
+ if args.length == 1 && (i = args.at(0)) && i.is_a?(Integer)
1642
+ with_pk(i)
1643
+ else
1644
+ super
1645
+ end
1646
+ end
1647
+
1464
1648
  # Destroy each row in the dataset by instantiating it and then calling
1465
1649
  # destroy on the resulting model object. This isn't as fast as deleting
1466
1650
  # the dataset, which does a single SQL call, but this runs any destroy
@@ -1491,6 +1675,19 @@ module Sequel
1491
1675
  super(pk, value_column)
1492
1676
  end
1493
1677
  end
1678
+
1679
+ # Given a primary key value, return the first record in the dataset with that primary key
1680
+ # value.
1681
+ #
1682
+ # # Single primary key
1683
+ # Artist.dataset.with_pk(1) # SELECT * FROM artists WHERE (id = 1) LIMIT 1
1684
+ #
1685
+ # # Composite primary key
1686
+ # Artist.dataset.with_pk([1, 2]) # SELECT * FROM artists
1687
+ # # WHERE ((id1 = 1) AND (id2 = 2)) LIMIT 1
1688
+ def with_pk(pk)
1689
+ first(model.qualified_primary_key_hash(pk))
1690
+ end
1494
1691
  end
1495
1692
 
1496
1693
  extend ClassMethods