activerecord 1.11.1 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (102) hide show
  1. data/CHANGELOG +198 -0
  2. data/lib/active_record.rb +19 -14
  3. data/lib/active_record/acts/list.rb +8 -6
  4. data/lib/active_record/acts/tree.rb +33 -10
  5. data/lib/active_record/aggregations.rb +1 -7
  6. data/lib/active_record/associations.rb +151 -82
  7. data/lib/active_record/associations/association_collection.rb +25 -0
  8. data/lib/active_record/associations/association_proxy.rb +9 -8
  9. data/lib/active_record/associations/belongs_to_association.rb +19 -5
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +44 -69
  11. data/lib/active_record/associations/has_many_association.rb +6 -14
  12. data/lib/active_record/associations/has_one_association.rb +5 -3
  13. data/lib/active_record/base.rb +344 -130
  14. data/lib/active_record/callbacks.rb +2 -2
  15. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +128 -0
  16. data/lib/active_record/connection_adapters/abstract/database_statements.rb +104 -0
  17. data/lib/active_record/connection_adapters/abstract/quoting.rb +51 -0
  18. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +249 -0
  19. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +245 -0
  20. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -464
  21. data/lib/active_record/connection_adapters/db2_adapter.rb +40 -10
  22. data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -60
  23. data/lib/active_record/connection_adapters/oci_adapter.rb +106 -26
  24. data/lib/active_record/connection_adapters/postgresql_adapter.rb +211 -62
  25. data/lib/active_record/connection_adapters/sqlite_adapter.rb +193 -44
  26. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +24 -15
  27. data/lib/active_record/fixtures.rb +47 -24
  28. data/lib/active_record/migration.rb +34 -5
  29. data/lib/active_record/observer.rb +32 -2
  30. data/lib/active_record/query_cache.rb +12 -11
  31. data/lib/active_record/schema.rb +58 -0
  32. data/lib/active_record/schema_dumper.rb +84 -0
  33. data/lib/active_record/transactions.rb +1 -3
  34. data/lib/active_record/validations.rb +40 -26
  35. data/lib/active_record/vendor/mysql.rb +6 -0
  36. data/lib/active_record/version.rb +9 -0
  37. data/rakefile +5 -16
  38. data/test/abstract_unit.rb +6 -11
  39. data/test/adapter_test.rb +58 -0
  40. data/test/ar_schema_test.rb +33 -0
  41. data/test/association_callbacks_test.rb +14 -0
  42. data/test/associations_go_eager_test.rb +56 -14
  43. data/test/associations_test.rb +245 -25
  44. data/test/base_test.rb +205 -34
  45. data/test/binary_test.rb +25 -42
  46. data/test/callbacks_test.rb +75 -0
  47. data/test/conditions_scoping_test.rb +136 -0
  48. data/test/connections/native_mysql/connection.rb +0 -4
  49. data/test/connections/native_sqlite3/in_memory_connection.rb +17 -0
  50. data/test/copy_table_sqlite.rb +64 -0
  51. data/test/deprecated_associations_test.rb +7 -6
  52. data/test/deprecated_finder_test.rb +3 -3
  53. data/test/finder_test.rb +33 -3
  54. data/test/fixtures/accounts.yml +5 -0
  55. data/test/fixtures/categories_ordered.yml +7 -0
  56. data/test/fixtures/category.rb +11 -1
  57. data/test/fixtures/comment.rb +22 -2
  58. data/test/fixtures/comments.yml +6 -0
  59. data/test/fixtures/companies.yml +15 -0
  60. data/test/fixtures/company.rb +24 -1
  61. data/test/fixtures/db_definitions/db2.drop.sql +5 -1
  62. data/test/fixtures/db_definitions/db2.sql +15 -1
  63. data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
  64. data/test/fixtures/db_definitions/mysql.sql +17 -2
  65. data/test/fixtures/db_definitions/oci.drop.sql +37 -5
  66. data/test/fixtures/db_definitions/oci.sql +47 -4
  67. data/test/fixtures/db_definitions/oci2.drop.sql +1 -1
  68. data/test/fixtures/db_definitions/oci2.sql +2 -2
  69. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  70. data/test/fixtures/db_definitions/postgresql.sql +33 -4
  71. data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
  72. data/test/fixtures/db_definitions/sqlite.sql +16 -2
  73. data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
  74. data/test/fixtures/db_definitions/sqlserver.sql +16 -2
  75. data/test/fixtures/developer.rb +1 -1
  76. data/test/fixtures/flowers.jpg +0 -0
  77. data/test/fixtures/keyboard.rb +3 -0
  78. data/test/fixtures/mixins.yml +11 -1
  79. data/test/fixtures/order.rb +4 -0
  80. data/test/fixtures/post.rb +4 -0
  81. data/test/fixtures/posts.yml +7 -0
  82. data/test/fixtures/project.rb +1 -0
  83. data/test/fixtures/subject.rb +4 -0
  84. data/test/fixtures/subscriber.rb +2 -4
  85. data/test/fixtures/topics.yml +2 -2
  86. data/test/fixtures_test.rb +79 -7
  87. data/test/inheritance_test.rb +2 -2
  88. data/test/lifecycle_test.rb +14 -6
  89. data/test/migration_test.rb +164 -6
  90. data/test/mixin_test.rb +78 -2
  91. data/test/pk_test.rb +25 -1
  92. data/test/readonly_test.rb +31 -0
  93. data/test/reflection_test.rb +4 -1
  94. data/test/schema_dumper_test.rb +19 -0
  95. data/test/schema_test_postgresql.rb +3 -2
  96. data/test/synonym_test_oci.rb +17 -0
  97. data/test/threaded_connections_test.rb +2 -1
  98. data/test/transactions_test.rb +109 -10
  99. data/test/validations_test.rb +70 -42
  100. metadata +25 -5
  101. data/test/fixtures/associations.png +0 -0
  102. data/test/thread_safety_test.rb +0 -36
data/CHANGELOG CHANGED
@@ -1,3 +1,201 @@
1
+ *1.12.1* (October 19th, 2005)
2
+
3
+ * Always parenthesize :conditions options so they may be safely combined with STI and constraints.
4
+
5
+ * Correct PostgreSQL primary key sequence detection. #2507 [tmornini@infomania.com]
6
+
7
+ * Added support for using limits in eager loads that involve has_many and has_and_belongs_to_many associations
8
+
9
+
10
+ *1.12.0* (October 16th, 2005)
11
+
12
+ * Update/clean up documentation (rdoc)
13
+
14
+ * PostgreSQL sequence support. Use set_sequence_name in your model class to specify its primary key sequence. #2292 [Rick Olson <technoweenie@gmail.com>, Robby Russell <robby@planetargon.com>]
15
+
16
+ * Change default logging colors to work on both white and black backgrounds. [Sam Stephenson]
17
+
18
+ * YAML fixtures support ordered hashes for fixtures with foreign key dependencies in the same table. #1896 [purestorm@ggnore.net]
19
+
20
+ * :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 [Robby Russell <robby@planetargon.com>]
21
+
22
+ * Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around.
23
+
24
+ * Avoid memleak in dev mode when using fcgi
25
+
26
+ * Simplified .clear on active record associations by using the existing delete_records method. #1906 [Caleb <me@cpb.ca>]
27
+
28
+ * Delegate access to a customized primary key to the conventional id method. #2444. [Blair Zajac <blair@orcaware.com>]
29
+
30
+ * Fix errors caused by assigning a has-one or belongs-to property to itself
31
+
32
+ * Add ActiveRecord::Base.schema_format setting which specifies how databases should be dumped [Sam Stephenson]
33
+
34
+ * Update DB2 adapter. #2206. [contact@maik-schmidt.de]
35
+
36
+ * Corrections to SQLServer native data types. #2267. [rails.20.clarry@spamgourmet.com]
37
+
38
+ * Deprecated ActiveRecord::Base.threaded_connection in favor of ActiveRecord::Base.allow_concurrency.
39
+
40
+ * Protect id attribute from mass assigment even when the primary key is set to something else. #2438. [Blair Zajac <blair@orcaware.com>]
41
+
42
+ * Misc doc fixes (typos/grammar/etc.). #2430. [coffee2code]
43
+
44
+ * Add test coverage for content_columns. #2432. [coffee2code]
45
+
46
+ * Speed up for unthreaded environments. #2431. [skaes@web.de]
47
+
48
+ * Optimization for Mysql selects using mysql-ruby extension greater than 2.6.3. #2426. [skaes@web.de]
49
+
50
+ * Speed up the setting of table_name. #2428. [skaes@web.de]
51
+
52
+ * Optimize instantiation of STI subclass records. In partial fullfilment of #1236. [skaes@web.de]
53
+
54
+ * Fix typo of 'constrains' to 'contraints'. #2069. [Michael Schuerig <michael@schuerig.de>]
55
+
56
+ * Optimization refactoring for add_limit_offset!. In partial fullfilment of #1236. [skaes@web.de]
57
+
58
+ * Add ability to get all siblings, including the current child, with acts_as_tree. Recloses #2140. [Michael Schuerig <michael@schuerig.de>]
59
+
60
+ * Add geometric type for postgresql adapter. #2233 [akaspick@gmail.com]
61
+
62
+ * Add option (true by default) to generate reader methods for each attribute of a record to avoid the overhead of calling method missing. In partial fullfilment of #1236. [skaes@web.de]
63
+
64
+ * Add convenience predicate methods on Column class. In partial fullfilment of #1236. [skaes@web.de]
65
+
66
+ * Raise errors when invalid hash keys are passed to ActiveRecord::Base.find. #2363 [Chad Fowler <chad@chadfowler.com>, Nicholas Seckar]
67
+
68
+ * Added :force option to create_table that'll try to drop the table if it already exists before creating
69
+
70
+ * Fix transactions so that calling return while inside a transaction will not leave an open transaction on the connection. [Nicholas Seckar]
71
+
72
+ * Use foreign_key inflection uniformly. #2156 [Blair Zajac <blair@orcaware.com>]
73
+
74
+ * model.association.clear should destroy associated objects if :dependent => true instead of nullifying their foreign keys. #2221 [joergd@pobox.com, ObieFernandez <obiefernandez@gmail.com>]
75
+
76
+ * Returning false from before_destroy should cancel the action. #1829 [Jeremy Huffman]
77
+
78
+ * Recognize PostgreSQL NOW() default as equivalent to CURRENT_TIMESTAMP or CURRENT_DATE, depending on the column's type. #2256 [mat <mat@absolight.fr>]
79
+
80
+ * Extensive documentation for the abstract database adapter. #2250 [François Beausoleil <fbeausoleil@ftml.net>]
81
+
82
+ * Clean up Fixtures.reset_sequences for PostgreSQL. Handle tables with no rows and models with custom primary keys. #2174, #2183 [jay@jay.fm, Blair Zajac <blair@orcaware.com>]
83
+
84
+ * Improve error message when nil is assigned to an attr which validates_size_of within a range. #2022 [Manuel Holtgrewe <purestorm@ggnore.net>]
85
+
86
+ * Make update_attribute use the same writer method that update_attributes uses.
87
+ #2237 [trevor@protocool.com]
88
+
89
+ * Make migrations honor table name prefixes and suffixes. #2298 [Jakob S, Marcel Molina]
90
+
91
+ * Correct and optimize PostgreSQL bytea escaping. #1745, #1837 [dave@cherryville.org, ken@miriamtech.com, bellis@deepthought.org]
92
+
93
+ * Fixtures should only reset a PostgreSQL sequence if it corresponds to an integer primary key named id. #1749 [chris@chrisbrinker.com]
94
+
95
+ * Standardize the interpretation of boolean columns in the Mysql and Sqlite adapters. (Use MysqlAdapter.emulate_booleans = false to disable this behavior)
96
+
97
+ * Added new symbol-driven approach to activating observers with Base#observers= [DHH]. Example:
98
+
99
+ ActiveRecord::Base.observers = :cacher, :garbage_collector
100
+
101
+ * Added AbstractAdapter#select_value and AbstractAdapter#select_values as convenience methods for selecting single values, instead of hashes, of the first column in a SELECT #2283 [solo@gatelys.com]
102
+
103
+ * Wrap :conditions in parentheses to prevent problems with OR's #1871 [Jamis Buck]
104
+
105
+ * Allow the postgresql adapter to work with the SchemaDumper. [Jamis Buck]
106
+
107
+ * Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby file, making it easier to consolidate large migration lists and port database schemas between databases. [Jamis Buck]
108
+
109
+ * Fixed migrations for Windows when using more than 10 [David Naseby]
110
+
111
+ * Fixed that the create_x method from belongs_to wouldn't save the association properly #2042 [Florian Weber]
112
+
113
+ * Fixed saving a record with two unsaved belongs_to associations pointing to the same object #2023 [Tobias Luetke]
114
+
115
+ * Improved migrations' behavior when the schema_info table is empty. [Nicholas Seckar]
116
+
117
+ * Fixed that Observers didn't observe sub-classes #627 [Florian Weber]
118
+
119
+ * Fix eager loading error messages, allow :include to specify tables using strings or symbols. Closes #2222 [Marcel Molina]
120
+
121
+ * Added check for RAILS_CONNECTION_ADAPTERS on startup and only load the connection adapters specified within if its present (available in Rails through config.connection_adapters using the new config) #1958 [skae]
122
+
123
+ * Fixed various problems with has_and_belongs_to_many when using customer finder_sql #2094 [Florian Weber]
124
+
125
+ * Added better exception error when unknown column types are used with migrations #1814 [fbeausoleil@ftml.net]
126
+
127
+ * Fixed "connection lost" issue with the bundled Ruby/MySQL driver (would kill the app after 8 hours of inactivity) #2163, #428 [kajism@yahoo.com]
128
+
129
+ * Fixed comparison of Active Record objects so two new objects are not equal #2099 [deberg]
130
+
131
+ * Fixed that the SQL Server adapter would sometimes return DBI::Timestamp objects instead of Time #2127 [Tom Ward]
132
+
133
+ * Added the instance methods #root and #ancestors on acts_as_tree and fixed siblings to not include the current node #2142, #2140 [coffee2code]
134
+
135
+ * Fixed that Active Record would call SHOW FIELDS twice (or more) for the same model when the cached results were available #1947 [sd@notso.net]
136
+
137
+ * Added log_level and use_silence parameter to ActiveRecord::Base.benchmark. The first controls at what level the benchmark statement will be logged (now as debug, instead of info) and the second that can be passed false to include all logging statements during the benchmark block/
138
+
139
+ * Make sure the schema_info table is created before querying the current version #1903
140
+
141
+ * Fixtures ignore table name prefix and suffix #1987 [Jakob S]
142
+
143
+ * Add documentation for index_type argument to add_index method for migrations #2005 [blaine@odeo.com]
144
+
145
+ * Modify read_attribute to allow a symbol argument #2024 [Ken Kunz]
146
+
147
+ * Make destroy return self #1913 [sebastian.kanthak@muehlheim.de]
148
+
149
+ * Fix typo in validations documentation #1938 [court3nay]
150
+
151
+ * Make acts_as_list work for insert_at(1) #1966 [hensleyl@papermountain.org]
152
+
153
+ * Fix typo in count_by_sql documentation #1969 [Alexey Verkhovsky]
154
+
155
+ * Allow add_column and create_table to specify NOT NULL #1712 [emptysands@gmail.com]
156
+
157
+ * Fix create_table so that id column is implicitly added [Rick Olson]
158
+
159
+ * Default sequence names for Oracle changed to #{table_name}_seq, which is the most commonly used standard. In addition, a new method ActiveRecord::Base#set_sequence_name allows the developer to set the sequence name per model. This is a non-backwards-compatible change -- anyone using the old-style "rails_sequence" will need to either create new sequences, or set: ActiveRecord::Base.set_sequence_name = "rails_sequence" #1798
160
+
161
+ * OCIAdapter now properly handles synonyms, which are commonly used to separate out the schema owner from the application user #1798
162
+
163
+ * Fixed the handling of camelCase columns names in Oracle #1798
164
+
165
+ * Implemented for OCI the Rakefile tasks of :clone_structure_to_test, :db_structure_dump, and :purge_test_database, which enable Oracle folks to enjoy all the agile goodness of Rails for testing. Note that the current implementation is fairly limited -- only tables and sequences are cloned, not constraints or indexes. A full clone in Oracle generally requires some manual effort, and is version-specific. Post 9i, Oracle recommends the use of the DBMS_METADATA package, though that approach requires editing of the physical characteristics generated #1798
166
+
167
+ * Fixed the handling of multiple blob columns in Oracle if one or more of them are null #1798
168
+
169
+ * Added support for calling constrained class methods on has_many and has_and_belongs_to_many collections #1764 [Tobias Luetke]
170
+
171
+ class Comment < AR:B
172
+ def self.search(q)
173
+ find(:all, :conditions => ["body = ?", q])
174
+ end
175
+ end
176
+
177
+ class Post < AR:B
178
+ has_many :comments
179
+ end
180
+
181
+ Post.find(1).comments.search('hi') # => SELECT * from comments WHERE post_id = 1 AND body = 'hi'
182
+
183
+ NOTICE: This patch changes the underlying SQL generated by has_and_belongs_to_many queries. If your relying on that, such as
184
+ by explicitly referencing the old t and j aliases, you'll need to update your code. Of course, you _shouldn't_ be relying on
185
+ details like that no less than you should be diving in to touch private variables. But just in case you do, consider yourself
186
+ noticed :)
187
+
188
+ * Added migration support for SQLite (using temporary tables to simulate ALTER TABLE) #1771 [Sam Stephenson]
189
+
190
+ * Remove extra definition of supports_migrations? from abstract_adaptor.rb [Nicholas Seckar]
191
+
192
+ * Fix acts_as_list so that moving next-to-last item to the bottom does not result in duplicate item positions
193
+
194
+ * Fixed incompatibility in DB2 adapter with the new limit/offset approach #1718 [Maik Schmidt]
195
+
196
+ * Added :select option to find which can specify a different value than the default *, like find(:all, :select => "first_name, last_name"), if you either only want to select part of the columns or exclude columns otherwise included from a join #1338 [Stefan Kaes]
197
+
198
+
1
199
  *1.11.1* (11 July, 2005)
2
200
 
3
201
  * Added support for limit and offset with eager loading of has_one and belongs_to associations. Using the options with has_many and has_and_belongs_to_many associations will now raise an ActiveRecord::ConfigurationError #1692 [Rick Olsen]
@@ -21,15 +21,17 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ $:.unshift(File.dirname(__FILE__)) unless
25
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
24
26
 
25
- $:.unshift(File.dirname(__FILE__))
26
- $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
27
-
28
- begin
29
- require 'active_support'
30
- rescue LoadError
31
- require 'rubygems'
32
- require_gem 'activesupport'
27
+ unless defined?(ActiveSupport)
28
+ begin
29
+ $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
30
+ require 'active_support'
31
+ rescue LoadError
32
+ require 'rubygems'
33
+ require_gem 'activesupport'
34
+ end
33
35
  end
34
36
 
35
37
  require 'active_record/base'
@@ -46,11 +48,13 @@ require 'active_record/acts/tree'
46
48
  require 'active_record/acts/nested_set'
47
49
  require 'active_record/locking'
48
50
  require 'active_record/migration'
51
+ require 'active_record/schema'
49
52
 
50
53
  ActiveRecord::Base.class_eval do
51
54
  include ActiveRecord::Validations
52
55
  include ActiveRecord::Locking
53
56
  include ActiveRecord::Callbacks
57
+ include ActiveRecord::Observing
54
58
  include ActiveRecord::Timestamp
55
59
  include ActiveRecord::Associations
56
60
  include ActiveRecord::Aggregations
@@ -61,11 +65,12 @@ ActiveRecord::Base.class_eval do
61
65
  include ActiveRecord::Acts::NestedSet
62
66
  end
63
67
 
64
- require 'active_record/connection_adapters/mysql_adapter'
65
- require 'active_record/connection_adapters/postgresql_adapter'
66
- require 'active_record/connection_adapters/sqlite_adapter'
67
- require 'active_record/connection_adapters/sqlserver_adapter'
68
- require 'active_record/connection_adapters/db2_adapter'
69
- require 'active_record/connection_adapters/oci_adapter'
68
+ unless defined?(RAILS_CONNECTION_ADAPTERS)
69
+ RAILS_CONNECTION_ADAPTERS = %w(mysql postgresql sqlite sqlserver db2 oci)
70
+ end
71
+
72
+ RAILS_CONNECTION_ADAPTERS.each do |adapter|
73
+ require "active_record/connection_adapters/#{adapter}_adapter"
74
+ end
70
75
 
71
76
  require 'active_record/query_cache'
@@ -72,7 +72,7 @@ module ActiveRecord
72
72
  # the first in the list of all chapters.
73
73
  module InstanceMethods
74
74
  def insert_at(position = 1)
75
- position == 1 ? add_to_list_top : insert_at_position(position)
75
+ insert_at_position(position)
76
76
  end
77
77
 
78
78
  def move_lower
@@ -163,17 +163,19 @@ module ActiveRecord
163
163
  # Overwrite this method to define the scope of the list changes
164
164
  def scope_condition() "1" end
165
165
 
166
- def bottom_position_in_list
167
- item = bottom_item
166
+ def bottom_position_in_list(except = nil)
167
+ item = bottom_item(except)
168
168
  item ? item.send(position_column) : 0
169
169
  end
170
170
 
171
- def bottom_item
172
- self.class.find(:first, :conditions => scope_condition, :order => "#{position_column} DESC")
171
+ def bottom_item(except = nil)
172
+ conditions = scope_condition
173
+ conditions = "#{conditions} AND id != #{except.id}" if except
174
+ self.class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
173
175
  end
174
176
 
175
177
  def assume_bottom_position
176
- update_attribute(position_column, bottom_position_in_list.to_i + 1) unless last?
178
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
177
179
  end
178
180
 
179
181
  def assume_top_position
@@ -16,16 +16,23 @@ module ActiveRecord
16
16
  # Example :
17
17
  # root
18
18
  # \_ child1
19
- # \_ sub-child1
19
+ # \_ subchild1
20
+ # \_ subchild2
20
21
  #
21
22
  # root = Category.create("name" => "root")
22
- # child1 = root.children.create("name" => "child1")
23
- # subchild1 = child1.children.create("name" => "subchild1")
23
+ # child1 = root.children.create("name" => "child1")
24
+ # subchild1 = child1.children.create("name" => "subchild1")
24
25
  #
25
- # root.parent # => nil
26
+ # root.parent # => nil
26
27
  # child1.parent # => root
27
28
  # root.children # => [child1]
28
29
  # root.children.first.children.first # => subchild1
30
+ #
31
+ # In addition to the parent and children associations, the following instance methods are added to the class
32
+ # after specifying the act:
33
+ # * siblings: Return all the children of the parent excluding the current node ([ subchild2 ] when called from subchild1)
34
+ # * ancestors: Returns all the ancestors of the current node ([child1, root] when called from subchild2)
35
+ # * root: Returns the root of the current node (root when called from subchild2)
29
36
  module ClassMethods
30
37
  # Configuration options are:
31
38
  #
@@ -48,15 +55,31 @@ module ActiveRecord
48
55
  end
49
56
  END
50
57
 
58
+ # Returns list of ancestors, starting from parent until root.
59
+ #
60
+ # subchild1.ancestors # => [child1, root]
61
+ define_method(:ancestors) do
62
+ node, nodes = self, []
63
+ nodes << node = node.parent until not node.has_parent?
64
+ nodes
65
+ end
66
+
67
+ define_method(:root) do
68
+ node = self
69
+ node = node.parent until not node.has_parent?
70
+ node
71
+ end
72
+
51
73
  define_method(:siblings) do
52
- if parent
53
- self.class.find(:all, :conditions => [ "#{configuration[:foreign_key]} = ?", parent.id ], :order => configuration[:order])
54
- else
55
- self.class.roots
56
- end
74
+ self_and_siblings - [self]
75
+ end
76
+
77
+ define_method(:self_and_siblings) do
78
+ has_parent? ? parent.children : self.class.roots
57
79
  end
80
+
58
81
  end
59
82
  end
60
83
  end
61
84
  end
62
- end
85
+ end
@@ -120,7 +120,7 @@ module ActiveRecord
120
120
  # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
121
121
  # composed_of :gps_location
122
122
  def composed_of(part_id, options = {})
123
- validate_options([ :class_name, :mapping ], options.keys)
123
+ options.assert_valid_keys(:class_name, :mapping)
124
124
 
125
125
  name = part_id.id2name
126
126
  class_name = options[:class_name] || name_to_class_name(name)
@@ -131,12 +131,6 @@ module ActiveRecord
131
131
  end
132
132
 
133
133
  private
134
- # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
135
- def validate_options(valid_option_keys, supplied_option_keys)
136
- unknown_option_keys = supplied_option_keys - valid_option_keys
137
- raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
138
- end
139
-
140
134
  def name_to_class_name(name)
141
135
  name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
142
136
  end
@@ -167,9 +167,14 @@ module ActiveRecord
167
167
  # the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So its no
168
168
  # catch-all for performance problems, but its a great way to cut down on the number of queries in a situation as the one described above.
169
169
  #
170
- # Please note that because eager loading is fetching both models and associations in the same grab, it doesn't make sense to use the
171
- # :limit and :offset options on has_many and has_and_belongs_to_many associations and an ConfigurationError exception will be raised
172
- # if attempted. It does, however, work just fine with has_one and belongs_to associations.
170
+ # Please note that limited eager loading with has_many and has_and_belongs_to_many associations is not compatible with describing conditions
171
+ # on these eager tables. This will work:
172
+ #
173
+ # Post.find(:all, :include => :comments, :conditions => "posts.title = 'magic forest'", :limit => 2)
174
+ #
175
+ # ...but this will not (and an ArgumentError will be raised):
176
+ #
177
+ # Post.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%'", :limit => 2)
173
178
  #
174
179
  # Also have in mind that since the eager loading is pulling from multiple tables, you'll have to disambiguate any column references
175
180
  # in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that
@@ -227,7 +232,9 @@ module ActiveRecord
227
232
  # This will also destroy the objects if they're declared as belongs_to and dependent on this model.
228
233
  # * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
229
234
  # * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
230
- # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
235
+ # * <tt>collection.clear</tt> - removes every object from the collection. This destroys the associated objects if they
236
+ # are <tt>:dependent</tt>, deletes them directly from the database if they are <tt>:exclusively_dependent</tt>,
237
+ # and sets their foreign keys to NULL otherwise.
231
238
  # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
232
239
  # * <tt>collection.size</tt> - returns the number of associated objects.
233
240
  # * <tt>collection.find</tt> - finds an associated object according to the same rules as Base.find.
@@ -263,7 +270,9 @@ module ActiveRecord
263
270
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
264
271
  # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
265
272
  # as the default foreign_key.
266
- # * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object.
273
+ # * <tt>:dependent</tt> - if set to :destroy (or true) all the associated objects are destroyed
274
+ # alongside this object. Also accepts :nullify which will set the associated objects foriegn key
275
+ # field to NULL.
267
276
  # May not be set if :exclusively_dependent is also set.
268
277
  # * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
269
278
  # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
@@ -284,26 +293,39 @@ module ActiveRecord
284
293
  # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
285
294
  # 'ORDER BY p.first_name'
286
295
  def has_many(association_id, options = {})
287
- validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql,
288
- :before_add, :after_add, :before_remove, :after_remove ], options.keys)
296
+ options.assert_valid_keys(
297
+ :foreign_key, :class_name, :exclusively_dependent, :dependent,
298
+ :conditions, :order, :finder_sql, :counter_sql,
299
+ :before_add, :after_add, :before_remove, :after_remove
300
+ )
301
+
289
302
  association_name, association_class_name, association_class_primary_key_name =
290
303
  associate_identification(association_id, options[:class_name], options[:foreign_key])
291
304
 
292
305
  require_association_class(association_class_name)
293
306
 
294
- if options[:dependent] and options[:exclusively_dependent]
295
- raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
307
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' if options[:dependent] and options[:exclusively_dependent]
308
+
296
309
  # See HasManyAssociation#delete_records. Dependent associations
297
310
  # delete children, otherwise foreign key is set to NULL.
298
- elsif options[:dependent]
299
- module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
300
- elsif options[:exclusively_dependent]
311
+ case options[:dependent]
312
+ when :destroy, true
313
+ module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
314
+ when :nullify
315
+ module_eval "before_destroy { |record| #{association_class_name}.update_all(%(#{association_class_primary_key_name} = NULL), %(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
316
+ when nil, false
317
+ # pass
318
+ else
319
+ raise ArgumentError, 'The :dependent option expects either true, :destroy or :nullify'
320
+ end
321
+
322
+ if options[:exclusively_dependent]
301
323
  module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
302
324
  end
303
325
 
304
326
  add_multiple_associated_save_callbacks(association_name)
305
- add_association_callbacks(association_name, options)
306
-
327
+ add_association_callbacks(association_name, options)
328
+
307
329
  collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
308
330
 
309
331
  # deprecated api
@@ -347,7 +369,7 @@ module ActiveRecord
347
369
  # sql fragment, such as "rank = 5".
348
370
  # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
349
371
  # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
350
- # * <tt>:dependent</tt> - if set to true, the associated object is destroyed when this object is. It's also destroyed if another
372
+ # * <tt>:dependent</tt> - if set to :destroy (or true) all the associated object is destroyed when this object is. Also
351
373
  # association is assigned.
352
374
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
353
375
  # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
@@ -358,7 +380,7 @@ module ActiveRecord
358
380
  # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
359
381
  # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
360
382
  def has_one(association_id, options = {})
361
- validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
383
+ options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache)
362
384
 
363
385
  association_name, association_class_name, association_class_primary_key_name =
364
386
  associate_identification(association_id, options[:class_name], options[:foreign_key], false)
@@ -380,7 +402,16 @@ module ActiveRecord
380
402
  association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
381
403
  association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
382
404
 
383
- module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'" if options[:dependent]
405
+ case options[:dependent]
406
+ when :destroy, true
407
+ module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'"
408
+ when :nullify
409
+ module_eval "before_destroy '#{association_name}.update_attribute(\"#{association_class_primary_key_name}\", nil)'"
410
+ when nil, false
411
+ # pass
412
+ else
413
+ raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
414
+ end
384
415
 
385
416
  # deprecated api
386
417
  deprecated_has_association_method(association_name)
@@ -429,14 +460,14 @@ module ActiveRecord
429
460
  # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
430
461
  # :conditions => 'discounts > #{payments_count}'
431
462
  def belongs_to(association_id, options = {})
432
- validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
463
+ options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache)
433
464
 
434
465
  association_name, association_class_name, class_primary_key_name =
435
466
  associate_identification(association_id, options[:class_name], options[:foreign_key], false)
436
467
 
437
468
  require_association_class(association_class_name)
438
469
 
439
- association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
470
+ association_class_primary_key_name = options[:foreign_key] || association_class_name.foreign_key
440
471
 
441
472
  association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
442
473
  association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
@@ -445,11 +476,13 @@ module ActiveRecord
445
476
  module_eval do
446
477
  before_save <<-EOF
447
478
  association = instance_variable_get("@#{association_name}")
448
- if not association.nil? and association.new_record?
449
- association.save(true)
450
- self["#{association_class_primary_key_name}"] = association.id
451
- association.send(:construct_sql)
452
- end
479
+ if not association.nil?
480
+ if association.new_record?
481
+ association.save(true)
482
+ association.send(:construct_sql)
483
+ end
484
+ self["#{association_class_primary_key_name}"] = association.id if association.updated?
485
+ end
453
486
  EOF
454
487
  end
455
488
 
@@ -544,9 +577,12 @@ module ActiveRecord
544
577
  # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
545
578
  # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
546
579
  def has_and_belongs_to_many(association_id, options = {})
547
- validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
548
- :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
549
- :before_remove, :after_remove ], options.keys)
580
+ options.assert_valid_keys(
581
+ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
582
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
583
+ :before_remove, :after_remove
584
+ )
585
+
550
586
  association_name, association_class_name, association_class_primary_key_name =
551
587
  associate_identification(association_id, options[:class_name], options[:foreign_key])
552
588
 
@@ -570,12 +606,6 @@ module ActiveRecord
570
606
  end
571
607
 
572
608
  private
573
- # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
574
- def validate_options(valid_option_keys, supplied_option_keys)
575
- unknown_option_keys = supplied_option_keys - valid_option_keys
576
- raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
577
- end
578
-
579
609
  def join_table_name(first_table_name, second_table_name)
580
610
  if first_table_name < second_table_name
581
611
  join_table = "#{first_table_name}_#{second_table_name}"
@@ -594,7 +624,7 @@ module ActiveRecord
594
624
  )
595
625
  end
596
626
 
597
- primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
627
+ primary_key_name = foreign_key || name.foreign_key
598
628
 
599
629
  return association_id.id2name, association_class_name, primary_key_name
600
630
  end
@@ -681,41 +711,39 @@ module ActiveRecord
681
711
  end
682
712
 
683
713
  def add_multiple_associated_save_callbacks(association_name)
684
- module_eval do
685
- before_save <<-end_eval
686
- @new_record_before_save = new_record?
687
- association = instance_variable_get("@#{association_name}")
688
- if association.respond_to?(:loaded?)
689
- if new_record?
690
- records_to_save = association
691
- else
692
- records_to_save = association.select{ |record| record.new_record? }
693
- end
694
- records_to_save.inject(true) do |result,record|
695
- result &&= record.valid?
696
- end
714
+ method_name = "validate_associated_records_for_#{association_name}".to_sym
715
+ define_method(method_name) do
716
+ @new_record_before_save = new_record?
717
+ association = instance_variable_get("@#{association_name}")
718
+ if association.respond_to?(:loaded?)
719
+ if new_record?
720
+ association
721
+ else
722
+ association.select { |record| record.new_record? }
723
+ end.each do |record|
724
+ errors.add "#{association_name}" unless record.valid?
697
725
  end
698
- end_eval
726
+ end
699
727
  end
700
728
 
701
- module_eval do
702
- after_callback = <<-end_eval
703
- association = instance_variable_get("@#{association_name}")
704
- if association.respond_to?(:loaded?)
705
- if @new_record_before_save
706
- records_to_save = association
707
- else
708
- records_to_save = association.select{ |record| record.new_record? }
709
- end
710
- records_to_save.each{ |record| association.send(:insert_record, record) }
711
- association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
729
+ validate method_name
730
+
731
+ after_callback = <<-end_eval
732
+ association = instance_variable_get("@#{association_name}")
733
+ if association.respond_to?(:loaded?)
734
+ if @new_record_before_save
735
+ records_to_save = association
736
+ else
737
+ records_to_save = association.select { |record| record.new_record? }
712
738
  end
713
- end_eval
739
+ records_to_save.each { |record| association.send(:insert_record, record) }
740
+ association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
741
+ end
742
+ end_eval
714
743
 
715
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
716
- after_create(after_callback)
717
- after_update(after_callback)
718
- end
744
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
745
+ after_create(after_callback)
746
+ after_update(after_callback)
719
747
  end
720
748
 
721
749
  def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
@@ -743,7 +771,6 @@ module ActiveRecord
743
771
  reflections = reflect_on_included_associations(options[:include])
744
772
 
745
773
  guard_against_missing_reflections(reflections, options)
746
- guard_against_unlimitable_reflections(reflections, options)
747
774
 
748
775
  schema_abbreviations = generate_schema_abbreviations(reflections)
749
776
  primary_key_table = generate_primary_key_table(reflections, schema_abbreviations)
@@ -783,14 +810,15 @@ module ActiveRecord
783
810
 
784
811
 
785
812
  def reflect_on_included_associations(associations)
786
- [ associations ].flatten.collect { |association| reflect_on_association(association) }
813
+ [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
787
814
  end
788
815
 
789
816
  def guard_against_missing_reflections(reflections, options)
790
817
  reflections.each do |r|
791
818
  raise(
792
819
  ConfigurationError,
793
- "Association was not found; perhaps you misspelled it? You specified :include => :#{options[:include].join(', :')}"
820
+ "Association was not found; perhaps you misspelled it? " +
821
+ "You specified :include => :#{[options[:include]].flatten.join(', :')}"
794
822
  ) if r.nil?
795
823
  end
796
824
  end
@@ -843,24 +871,61 @@ module ActiveRecord
843
871
  sql = "SELECT #{column_aliases(schema_abbreviations)} FROM #{table_name} "
844
872
  sql << reflections.collect { |reflection| association_join(reflection) }.to_s
845
873
  sql << "#{options[:joins]} " if options[:joins]
874
+
846
875
  add_conditions!(sql, options[:conditions])
847
876
  add_sti_conditions!(sql, reflections)
877
+ add_limited_ids_condition!(sql, options) if !using_limitable_reflections?(reflections) && options[:limit]
878
+
848
879
  sql << "ORDER BY #{options[:order]} " if options[:order]
880
+
849
881
  add_limit!(sql, options) if using_limitable_reflections?(reflections)
882
+
883
+ return sanitize_sql(sql)
884
+ end
885
+
886
+ def add_limited_ids_condition!(sql, options)
887
+ unless (id_list = select_limited_ids_list(options)).empty?
888
+ sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) "
889
+ end
890
+ end
891
+
892
+ def select_limited_ids_list(options)
893
+ connection.select_values(
894
+ construct_finder_sql_for_association_limiting(options),
895
+ "#{name} Load IDs For Limited Eager Loading"
896
+ ).collect { |id| "'#{id}'" }.join(", ")
897
+ end
898
+
899
+ def construct_finder_sql_for_association_limiting(options)
900
+ raise(ArgumentError, "Limited eager loads and conditions on the eager tables is incompatible") if include_eager_conditions?(options)
901
+
902
+ sql = "SELECT #{primary_key} FROM #{table_name} "
903
+ add_conditions!(sql, options[:conditions])
904
+ sql << "ORDER BY #{options[:order]} " if options[:order]
905
+ add_limit!(sql, options)
850
906
  return sanitize_sql(sql)
851
907
  end
852
908
 
909
+ def include_eager_conditions?(options)
910
+ return false unless options[:conditions]
911
+
912
+ options[:conditions].scan(/ ([^.]+)\.[^.]+ /).flatten.any? do |condition_table_name|
913
+ condition_table_name != table_name
914
+ end
915
+ end
916
+
853
917
  def using_limitable_reflections?(reflections)
854
918
  reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
855
919
  end
856
920
 
857
921
  def add_sti_conditions!(sql, reflections)
858
- sti_sql = ""
859
- reflections.each do |reflection|
860
- sti_sql << " AND #{reflection.klass.send(:type_condition)}" unless reflection.klass.descends_from_active_record?
922
+ sti_conditions = reflections.collect do |reflection|
923
+ reflection.klass.send(:type_condition) unless reflection.klass.descends_from_active_record?
924
+ end.compact
925
+
926
+ unless sti_conditions.empty?
927
+ sql << condition_word(sql) + sti_conditions.join(" AND ")
861
928
  end
862
- sti_sql.sub!(/AND/, "WHERE") unless sql =~ /where/i
863
- sql << sti_sql
864
929
  end
865
930
 
866
931
  def column_aliases(schema_abbreviations)
@@ -890,16 +955,15 @@ module ActiveRecord
890
955
  end
891
956
 
892
957
  def add_association_callbacks(association_name, options)
893
- callbacks = %w(before_add after_add before_remove after_remove)
894
- callbacks.each do |callback_name|
895
- full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}"
896
- defined_callbacks = options[callback_name.to_sym]
897
- if options.has_key?(callback_name.to_sym)
898
- callback_array = defined_callbacks.kind_of?(Array) ? defined_callbacks : [defined_callbacks]
899
- class_inheritable_reader full_callback_name.to_sym
900
- write_inheritable_array(full_callback_name.to_sym, callback_array)
901
- end
902
- end
958
+ callbacks = %w(before_add after_add before_remove after_remove)
959
+ callbacks.each do |callback_name|
960
+ full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}"
961
+ defined_callbacks = options[callback_name.to_sym]
962
+ if options.has_key?(callback_name.to_sym)
963
+ class_inheritable_reader full_callback_name.to_sym
964
+ write_inheritable_array(full_callback_name.to_sym, [defined_callbacks].flatten)
965
+ end
966
+ end
903
967
  end
904
968
 
905
969
  def extract_record(schema_abbreviations, table_name, row)
@@ -910,6 +974,11 @@ module ActiveRecord
910
974
  end
911
975
  return record
912
976
  end
977
+
978
+ def condition_word(sql)
979
+ sql =~ /where/i ? " AND " : "WHERE "
980
+ end
913
981
  end
982
+
914
983
  end
915
984
  end