activerecord 1.0.0 → 1.1.0

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 (47) hide show
  1. data/CHANGELOG +102 -1
  2. data/dev-utils/eval_debugger.rb +12 -7
  3. data/lib/active_record.rb +2 -0
  4. data/lib/active_record/aggregations.rb +1 -1
  5. data/lib/active_record/associations.rb +74 -53
  6. data/lib/active_record/associations.rb.orig +555 -0
  7. data/lib/active_record/associations/association_collection.rb +74 -15
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +86 -25
  9. data/lib/active_record/associations/has_many_association.rb +48 -50
  10. data/lib/active_record/base.rb +56 -24
  11. data/lib/active_record/connection_adapters/abstract_adapter.rb +46 -3
  12. data/lib/active_record/connection_adapters/mysql_adapter.rb +15 -15
  13. data/lib/active_record/connection_adapters/postgresql_adapter.rb +128 -135
  14. data/lib/active_record/connection_adapters/sqlite_adapter.rb +76 -78
  15. data/lib/active_record/deprecated_associations.rb +1 -1
  16. data/lib/active_record/fixtures.rb +137 -54
  17. data/lib/active_record/observer.rb +1 -1
  18. data/lib/active_record/support/inflector.rb +8 -0
  19. data/lib/active_record/transactions.rb +31 -14
  20. data/rakefile +13 -5
  21. data/test/abstract_unit.rb +7 -1
  22. data/test/associations_test.rb +99 -27
  23. data/test/base_test.rb +15 -1
  24. data/test/connections/native_sqlite/connection.rb +24 -14
  25. data/test/deprecated_associations_test.rb +3 -4
  26. data/test/deprecated_associations_test.rb.orig +334 -0
  27. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  28. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  29. data/test/fixtures/bad_fixtures/blank_line +3 -0
  30. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  31. data/test/fixtures/bad_fixtures/missing_value +1 -0
  32. data/test/fixtures/company_in_module.rb +15 -1
  33. data/test/fixtures/db_definitions/mysql.sql +2 -1
  34. data/test/fixtures/db_definitions/postgresql.sql +2 -1
  35. data/test/fixtures/db_definitions/sqlite.sql +2 -1
  36. data/test/fixtures/developers_projects/david_action_controller +2 -1
  37. data/test/fixtures/developers_projects/david_active_record +2 -1
  38. data/test/fixtures/fixture_database.sqlite +0 -0
  39. data/test/fixtures/fixture_database_2.sqlite +0 -0
  40. data/test/fixtures/project.rb +2 -1
  41. data/test/fixtures/projects/action_controller +1 -1
  42. data/test/fixtures/topics/second +1 -1
  43. data/test/fixtures_test.rb +63 -4
  44. data/test/inflector_test.rb +17 -0
  45. data/test/modules_test.rb +8 -0
  46. data/test/transactions_test.rb +16 -4
  47. metadata +10 -2
@@ -20,7 +20,7 @@ module ActiveRecord
20
20
 
21
21
  def deprecated_remove_association_relation(association_name)# :nodoc:
22
22
  module_eval <<-"end_eval", __FILE__, __LINE__
23
- def remove_#{association_name}(items)
23
+ def remove_#{association_name}(*items)
24
24
  #{association_name}.delete(items)
25
25
  end
26
26
  end_eval
@@ -1,4 +1,6 @@
1
1
  require 'yaml'
2
+ require 'active_record/support/class_inheritable_attributes'
3
+ require 'active_record/support/inflector'
2
4
 
3
5
  # Fixtures are a way of organizing data that you want to test against. Each fixture file is created as a row
4
6
  # in the database and created as a hash with column names as keys and data as values. All of these fixture hashes
@@ -31,6 +33,20 @@ require 'yaml'
31
33
  # assert_equal @developers["david"]["name"], Developer.find(@developers["david"]["id"]).name
32
34
  # end
33
35
  #
36
+ # == Automatic fixture setup and instance variable availability
37
+ #
38
+ # Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
39
+ #
40
+ # class FixturesTest < Test::Unit::TestCase
41
+ # fixtures :developers # you can add more with comma separation
42
+ #
43
+ # def test_developers
44
+ # assert_equal 3, @developers.size # the container for all the fixtures is automatically set
45
+ # assert_kind_of Developer, @david # works like @developers["david"].find
46
+ # assert_equal "David Heinemeier Hansson", @david.name
47
+ # end
48
+ # end
49
+ #
34
50
  # == YAML fixtures
35
51
  #
36
52
  # Additionally, fixtures supports yaml files. Like fixture files, these yaml files have a pre-defined format. The document
@@ -53,50 +69,56 @@ require 'yaml'
53
69
  # In that file, there's two records. Each record must have two parts: 'name' and 'data'. The data that you add
54
70
  # must be indented like you see above.
55
71
  #
56
- # Yaml fixtures file names must end with .yaml as in people.yaml or camel.yaml. The yaml fixtures are placed in the same
72
+ # Yaml fixtures file names must end with .yml as in people.yml or camel.yml. The yaml fixtures are placed in the same
57
73
  # directory as the normal fixtures and can happy co-exist. :)
58
- class Fixtures
74
+ class Fixtures < Hash
75
+ def self.instantiate_fixtures(object, fixtures_directory, *table_names)
76
+ [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
77
+ object.instance_variable_set "@#{table_names[idx]}", fixtures
78
+ fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
79
+ end
80
+ end
81
+
59
82
  def self.create_fixtures(fixtures_directory, *table_names)
60
83
  connection = block_given? ? yield : ActiveRecord::Base.connection
61
- ActiveRecord::Base.logger.level = Logger::ERROR
84
+ old_logger_level = ActiveRecord::Base.logger.level
62
85
 
63
- fixtures = [ table_names ].flatten.collect do |table_name|
64
- Fixtures.new(connection, table_name, "#{fixtures_directory}/#{table_name}")
86
+ begin
87
+ ActiveRecord::Base.logger.level = Logger::ERROR
88
+ fixtures = connection.transaction do
89
+ table_names.flatten.map do |table_name|
90
+ Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
91
+ end
92
+ end
93
+ return fixtures.size > 1 ? fixtures : fixtures.first
94
+ ensure
95
+ ActiveRecord::Base.logger.level = old_logger_level
65
96
  end
66
-
67
- ActiveRecord::Base.logger.level = Logger::DEBUG
68
-
69
- return fixtures.size > 1 ? fixtures : fixtures.first
70
97
  end
71
98
 
72
99
  def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yaml/)
73
100
  @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
74
- @fixtures = read_fixtures
101
+ @class_name = Inflector.classify(@table_name)
75
102
 
103
+ read_fixture_files
76
104
  delete_existing_fixtures
77
105
  insert_fixtures
78
106
  end
79
107
 
80
- # Access a fixture hash by using its file name as the key
81
- def [](key)
82
- @fixtures[key]
83
- end
84
-
85
- # Get the number of fixtures kept in this container
86
- def length
87
- @fixtures.length
88
- end
89
-
90
108
  private
91
- def read_fixtures
92
- Dir.entries(@fixture_path).inject({}) do |fixtures, file|
93
- # is this a regular fixture file?
94
- fixtures[file] = Fixture.new(@fixture_path, file) unless file =~ @file_filter
95
- # is this a *.yaml file?
96
- if file =~ /\.yaml/
97
- YamlFixture.produce( "#{@fixture_path}/#{file}" ).each { |fix| fixtures[fix.yaml_name] = fix }
109
+ def read_fixture_files
110
+ Dir.entries(@fixture_path).each do |file|
111
+ case file
112
+ when /\.ya?ml$/
113
+ path = File.join(@fixture_path, file)
114
+ YamlFixture.produce(path).each { |fixture|
115
+ self[fixture.yaml_name] = fixture
116
+ }
117
+ when @file_filter
118
+ # skip
119
+ else
120
+ self[file] = Fixture.new(@fixture_path, file, @class_name)
98
121
  end
99
- fixtures
100
122
  end
101
123
  end
102
124
 
@@ -105,20 +127,25 @@ class Fixtures
105
127
  end
106
128
 
107
129
  def insert_fixtures
108
- @fixtures.values.each do |fixture|
130
+ values.each do |fixture|
109
131
  @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
110
132
  end
111
133
  end
112
-
113
- def []=(key, value)
114
- @fixtures[key] = value
115
- end
116
134
  end
117
135
 
118
136
  class Fixture #:nodoc:
119
- def initialize(fixture_path, file)
120
- @fixture_path, @file = fixture_path, file
121
- @fixture = read_fixture
137
+ include Enumerable
138
+ class FixtureError < StandardError; end
139
+ class FormatError < FixtureError; end
140
+
141
+ def initialize(fixture_path, file, class_name)
142
+ @fixture_path, @file, @class_name = fixture_path, file, class_name
143
+ @fixture = read_fixture_file
144
+ @class_name
145
+ end
146
+
147
+ def each
148
+ @fixture.each { |item| yield item }
122
149
  end
123
150
 
124
151
  def [](key)
@@ -134,14 +161,29 @@ class Fixture #:nodoc:
134
161
  end
135
162
 
136
163
  def value_list
137
- @fixture.values.map { |v| "'#{v}'" }.join(", ")
164
+ @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
138
165
  end
139
-
166
+
167
+ def find
168
+ Object.const_get(@class_name).find(self["id"])
169
+ end
170
+
140
171
  private
141
- def read_fixture
142
- IO.readlines("#{@fixture_path}/#{@file}").inject({}) do |fixture, line|
143
- key, value = line.split(/ => /)
144
- fixture[key.strip] = value.strip
172
+ def read_fixture_file
173
+ path = File.join(@fixture_path, @file)
174
+ IO.readlines(path).inject({}) do |fixture, line|
175
+ # Mercifully skip empty lines.
176
+ next if line.empty?
177
+
178
+ # Use the same regular expression for attributes as Active Record.
179
+ unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
180
+ raise FormatError, "#{path}: fixture format error at '#{line}'. Expecting 'key => value'."
181
+ end
182
+ key, value = md.captures
183
+
184
+ # Disallow duplicate keys to catch typos.
185
+ raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
186
+ fixture[key] = value.strip
145
187
  fixture
146
188
  end
147
189
  end
@@ -150,23 +192,64 @@ end
150
192
  # A YamlFixture is like a fixture, but instead of a name to use as
151
193
  # a key, it uses a yaml_name.
152
194
  class YamlFixture < Fixture #:nodoc:
153
- # yaml_name is equivalent to a normal fixture's filename
154
- attr_accessor :yaml_name
195
+ class YamlFormatError < FormatError; end
196
+
197
+ # yaml_name is analogous to a normal fixture's filename
198
+ attr_reader :yaml_name
155
199
 
156
- # constructor is passed the name & the actual instantiate fixture
200
+ # Instantiate with fixture name and data.
157
201
  def initialize(yaml_name, fixture)
158
202
  @yaml_name, @fixture = yaml_name, fixture
159
203
  end
160
204
 
161
- # given a valid yaml file name, create an array of YamlFixture objects
162
- def self.produce( yaml_file_name )
163
- results = []
164
- yaml_file = File.open( yaml_file_name )
165
- YAML::load_documents( yaml_file ) do |doc|
166
- f = YamlFixture.new( doc['name'], doc['data'] )
167
- results << f
205
+ def produce(yaml_file_name)
206
+ YamlFixture.produce(yaml_file_name)
207
+ end
208
+
209
+ # Extract an array of YamlFixtures from a yaml file.
210
+ def self.produce(yaml_file_name)
211
+ fixtures = []
212
+ File.open(yaml_file_name) do |yaml_file|
213
+ YAML::load_documents(yaml_file) do |doc|
214
+ missing = %w(name data).reject { |key| doc[key] }.join(' and ')
215
+ raise YamlFormatError, "#{path}: yaml fixture missing #{missing}: #{doc.to_yaml}" unless missing.empty?
216
+ fixtures << YamlFixture.new(doc['name'], doc['data'])
217
+ end
168
218
  end
169
- yaml_file.close
170
- results
219
+ fixtures
171
220
  end
172
221
  end
222
+
223
+ class Test::Unit::TestCase #:nodoc:
224
+ include ClassInheritableAttributes
225
+
226
+ cattr_accessor :fixture_path
227
+ cattr_accessor :fixture_table_names
228
+
229
+ def self.fixtures(*table_names)
230
+ write_inheritable_attribute("fixture_table_names", table_names)
231
+ end
232
+
233
+ def setup
234
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
235
+ end
236
+
237
+ def self.method_added(method_symbol)
238
+ if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
239
+ alias_method :setup_without_fixtures, :setup
240
+ define_method(:setup) do
241
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
242
+ setup_without_fixtures
243
+ end
244
+ end
245
+ end
246
+
247
+ private
248
+ def instantiate_fixtures(*table_names)
249
+ Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
250
+ end
251
+
252
+ def fixture_table_names
253
+ self.class.read_inheritable_attribute("fixture_table_names")
254
+ end
255
+ end
@@ -8,7 +8,7 @@ module ActiveRecord
8
8
  #
9
9
  # class CommentObserver < ActiveRecord::Observer
10
10
  # def after_save(comment)
11
- # NotificationServer.send_email("admin@do.com", "New comment was posted", comment)
11
+ # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
12
12
  # end
13
13
  # end
14
14
  #
@@ -31,6 +31,14 @@ module Inflector
31
31
  class_name_in_module.gsub(/^.*::/, '')
32
32
  end
33
33
 
34
+ def tableize(class_name)
35
+ pluralize(underscore(class_name))
36
+ end
37
+
38
+ def classify(table_name)
39
+ camelize(singularize(table_name))
40
+ end
41
+
34
42
  def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
35
43
  Inflector.underscore(Inflector.demodulize(class_name)) +
36
44
  (separate_class_name_and_id_with_underscore ? "_id" : "id")
@@ -18,13 +18,13 @@ module ActiveRecord
18
18
  end
19
19
  end
20
20
 
21
- # Transactions are protective blocks where SQL statements are only permanent if they can all succed as one atomic action.
21
+ # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
22
22
  # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and
23
23
  # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs.
24
24
  # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
25
25
  # not at all. Example:
26
26
  #
27
- # Account.transaction do
27
+ # transaction do
28
28
  # david.withdrawal(100)
29
29
  # mary.deposit(100)
30
30
  # end
@@ -33,6 +33,23 @@ module ActiveRecord
33
33
  # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
34
34
  # that the objects by default will _not_ have their instance data returned to their pre-transactional state.
35
35
  #
36
+ # == Transactions are not distributed across database connections
37
+ #
38
+ # A transaction acts on a single database connection. If you have
39
+ # multiple class-specific databases, the transaction will not protect
40
+ # interaction among them. One workaround is to begin a transaction
41
+ # on each class whose models you alter:
42
+ #
43
+ # Student.transaction do
44
+ # Course.transaction do
45
+ # course.enroll(student)
46
+ # student.units += course.units
47
+ # end
48
+ # end
49
+ #
50
+ # This is a poor solution, but full distributed transactions are beyond
51
+ # the scope of Active Record.
52
+ #
36
53
  # == Save and destroy are automatically wrapped in a transaction
37
54
  #
38
55
  # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
@@ -65,38 +82,38 @@ module ActiveRecord
65
82
  begin
66
83
  objects.each { |o| o.extend(Transaction::Simple) }
67
84
  objects.each { |o| o.start_transaction }
68
- connection.begin_db_transaction
69
85
 
70
- block.call
71
-
72
- connection.commit_db_transaction
86
+ result = connection.transaction(&block)
87
+
73
88
  objects.each { |o| o.commit_transaction }
74
- rescue Exception => exception
75
- connection.rollback_db_transaction
89
+ return result
90
+ rescue Exception => object_transaction_rollback
76
91
  objects.each { |o| o.abort_transaction }
77
- raise exception
92
+ raise
78
93
  ensure
79
94
  TRANSACTION_MUTEX.unlock
80
95
  end
81
96
  end
82
97
  end
83
98
 
99
+ def transaction(*objects, &block)
100
+ self.class.transaction(*objects, &block)
101
+ end
102
+
84
103
  def destroy_with_transactions #:nodoc:
85
104
  if TRANSACTION_MUTEX.locked?
86
105
  destroy_without_transactions
87
106
  else
88
- ActiveRecord::Base.transaction { destroy_without_transactions }
107
+ transaction { destroy_without_transactions }
89
108
  end
90
109
  end
91
110
 
92
111
  def save_with_transactions(perform_validation = true) #:nodoc:
93
- result = nil
94
112
  if TRANSACTION_MUTEX.locked?
95
- result = save_without_transactions(perform_validation)
113
+ save_without_transactions(perform_validation)
96
114
  else
97
- ActiveRecord::Base.transaction { result = save_without_transactions(perform_validation) }
115
+ transaction { save_without_transactions(perform_validation) }
98
116
  end
99
- return result
100
117
  end
101
118
  end
102
119
  end
data/rakefile CHANGED
@@ -6,7 +6,10 @@ require 'rake/packagetask'
6
6
  require 'rake/gempackagetask'
7
7
  require 'rake/contrib/rubyforgepublisher'
8
8
 
9
- PKG_VERSION = "1.0.0"
9
+ PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
10
+ PKG_NAME = 'activerecord'
11
+ PKG_VERSION = '1.1.0' + PKG_BUILD
12
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
10
13
 
11
14
  PKG_FILES = FileList[
12
15
  "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "rakefile"
@@ -20,7 +23,6 @@ task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_postg
20
23
 
21
24
  Rake::TestTask.new("test_ruby_mysql") { |t|
22
25
  t.libs << "test" << "test/connections/native_mysql"
23
- t.test_files = "lib/active_record/vendor/mysql.rb"
24
26
  t.pattern = 'test/*_test.rb'
25
27
  t.verbose = true
26
28
  }
@@ -43,7 +45,7 @@ Rake::TestTask.new("test_sqlite") { |t|
43
45
  t.verbose = true
44
46
  }
45
47
 
46
- # Genereate the RDoc documentation
48
+ # Generate the RDoc documentation
47
49
 
48
50
  Rake::RDocTask.new { |rdoc|
49
51
  rdoc.rdoc_dir = 'doc'
@@ -56,6 +58,12 @@ Rake::RDocTask.new { |rdoc|
56
58
  }
57
59
 
58
60
 
61
+ # Publish beta gem
62
+ desc "Publish the beta gem"
63
+ task :pgem => [:package] do
64
+ Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
65
+ end
66
+
59
67
  # Publish documentation
60
68
  desc "Publish the API documentation"
61
69
  task :pdoc => [:rdoc] do
@@ -73,7 +81,7 @@ end
73
81
  dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
74
82
 
75
83
  spec = Gem::Specification.new do |s|
76
- s.name = 'activerecord'
84
+ s.name = PKG_NAME
77
85
  s.version = PKG_VERSION
78
86
  s.summary = "Implements the ActiveRecord pattern for ORM."
79
87
  s.description = %q{Implements the ActiveRecord pattern (Fowler, PoEAA) for ORM. It ties database tables and classes together for business objects, like Customer or Subscription, that can find, save, and destroy themselves without resorting to manual SQL.}
@@ -119,4 +127,4 @@ task :lines do
119
127
  end
120
128
  }
121
129
  puts "Lines #{lines}, LOC #{codelines}"
122
- end
130
+ end
@@ -1,5 +1,9 @@
1
1
  $:.unshift(File.dirname(__FILE__) + '/../lib')
2
2
 
3
+ # Make rubygems available for testing if possible
4
+ begin require('rubygems'); rescue LoadError; end
5
+ begin require('dev-utils/debug'); rescue LoadError; end
6
+
3
7
  require 'test/unit'
4
8
  require 'active_record'
5
9
  require 'active_record/fixtures'
@@ -13,4 +17,6 @@ class Test::Unit::TestCase #:nodoc:
13
17
  Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names)
14
18
  end
15
19
  end
16
- end
20
+ end
21
+
22
+ Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"