activerecord 1.0.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 (106) hide show
  1. data/CHANGELOG +581 -0
  2. data/README +361 -0
  3. data/RUNNING_UNIT_TESTS +36 -0
  4. data/dev-utils/eval_debugger.rb +9 -0
  5. data/examples/associations.png +0 -0
  6. data/examples/associations.rb +87 -0
  7. data/examples/shared_setup.rb +15 -0
  8. data/examples/validation.rb +88 -0
  9. data/install.rb +60 -0
  10. data/lib/active_record.rb +48 -0
  11. data/lib/active_record/aggregations.rb +165 -0
  12. data/lib/active_record/associations.rb +536 -0
  13. data/lib/active_record/associations/association_collection.rb +70 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -0
  15. data/lib/active_record/associations/has_many_association.rb +104 -0
  16. data/lib/active_record/base.rb +985 -0
  17. data/lib/active_record/callbacks.rb +337 -0
  18. data/lib/active_record/connection_adapters/abstract_adapter.rb +326 -0
  19. data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -0
  20. data/lib/active_record/connection_adapters/postgresql_adapter.rb +177 -0
  21. data/lib/active_record/connection_adapters/sqlite_adapter.rb +107 -0
  22. data/lib/active_record/deprecated_associations.rb +70 -0
  23. data/lib/active_record/fixtures.rb +172 -0
  24. data/lib/active_record/observer.rb +71 -0
  25. data/lib/active_record/reflection.rb +126 -0
  26. data/lib/active_record/support/class_attribute_accessors.rb +43 -0
  27. data/lib/active_record/support/class_inheritable_attributes.rb +37 -0
  28. data/lib/active_record/support/clean_logger.rb +10 -0
  29. data/lib/active_record/support/inflector.rb +70 -0
  30. data/lib/active_record/transactions.rb +102 -0
  31. data/lib/active_record/validations.rb +205 -0
  32. data/lib/active_record/vendor/mysql.rb +1117 -0
  33. data/lib/active_record/vendor/simple.rb +702 -0
  34. data/lib/active_record/wrappers/yaml_wrapper.rb +15 -0
  35. data/lib/active_record/wrappings.rb +59 -0
  36. data/rakefile +122 -0
  37. data/test/abstract_unit.rb +16 -0
  38. data/test/aggregations_test.rb +34 -0
  39. data/test/all.sh +8 -0
  40. data/test/associations_test.rb +477 -0
  41. data/test/base_test.rb +513 -0
  42. data/test/class_inheritable_attributes_test.rb +33 -0
  43. data/test/connections/native_mysql/connection.rb +24 -0
  44. data/test/connections/native_postgresql/connection.rb +24 -0
  45. data/test/connections/native_sqlite/connection.rb +24 -0
  46. data/test/deprecated_associations_test.rb +336 -0
  47. data/test/finder_test.rb +67 -0
  48. data/test/fixtures/accounts/signals37 +3 -0
  49. data/test/fixtures/accounts/unknown +2 -0
  50. data/test/fixtures/auto_id.rb +4 -0
  51. data/test/fixtures/column_name.rb +3 -0
  52. data/test/fixtures/companies/first_client +6 -0
  53. data/test/fixtures/companies/first_firm +4 -0
  54. data/test/fixtures/companies/second_client +6 -0
  55. data/test/fixtures/company.rb +37 -0
  56. data/test/fixtures/company_in_module.rb +33 -0
  57. data/test/fixtures/course.rb +3 -0
  58. data/test/fixtures/courses/java +2 -0
  59. data/test/fixtures/courses/ruby +2 -0
  60. data/test/fixtures/customer.rb +30 -0
  61. data/test/fixtures/customers/david +6 -0
  62. data/test/fixtures/db_definitions/mysql.sql +96 -0
  63. data/test/fixtures/db_definitions/mysql2.sql +4 -0
  64. data/test/fixtures/db_definitions/postgresql.sql +113 -0
  65. data/test/fixtures/db_definitions/postgresql2.sql +4 -0
  66. data/test/fixtures/db_definitions/sqlite.sql +85 -0
  67. data/test/fixtures/db_definitions/sqlite2.sql +4 -0
  68. data/test/fixtures/default.rb +2 -0
  69. data/test/fixtures/developer.rb +8 -0
  70. data/test/fixtures/developers/david +2 -0
  71. data/test/fixtures/developers/jamis +2 -0
  72. data/test/fixtures/developers_projects/david_action_controller +2 -0
  73. data/test/fixtures/developers_projects/david_active_record +2 -0
  74. data/test/fixtures/developers_projects/jamis_active_record +2 -0
  75. data/test/fixtures/entrant.rb +3 -0
  76. data/test/fixtures/entrants/first +3 -0
  77. data/test/fixtures/entrants/second +3 -0
  78. data/test/fixtures/entrants/third +3 -0
  79. data/test/fixtures/fixture_database.sqlite +0 -0
  80. data/test/fixtures/fixture_database_2.sqlite +0 -0
  81. data/test/fixtures/movie.rb +5 -0
  82. data/test/fixtures/movies/first +2 -0
  83. data/test/fixtures/movies/second +2 -0
  84. data/test/fixtures/project.rb +3 -0
  85. data/test/fixtures/projects/action_controller +2 -0
  86. data/test/fixtures/projects/active_record +2 -0
  87. data/test/fixtures/reply.rb +21 -0
  88. data/test/fixtures/subscriber.rb +5 -0
  89. data/test/fixtures/subscribers/first +2 -0
  90. data/test/fixtures/subscribers/second +2 -0
  91. data/test/fixtures/topic.rb +20 -0
  92. data/test/fixtures/topics/first +9 -0
  93. data/test/fixtures/topics/second +8 -0
  94. data/test/fixtures_test.rb +20 -0
  95. data/test/inflector_test.rb +104 -0
  96. data/test/inheritance_test.rb +125 -0
  97. data/test/lifecycle_test.rb +110 -0
  98. data/test/modules_test.rb +21 -0
  99. data/test/multiple_db_test.rb +46 -0
  100. data/test/pk_test.rb +57 -0
  101. data/test/reflection_test.rb +78 -0
  102. data/test/thread_safety_test.rb +33 -0
  103. data/test/transactions_test.rb +83 -0
  104. data/test/unconnected_test.rb +24 -0
  105. data/test/validations_test.rb +126 -0
  106. metadata +166 -0
@@ -0,0 +1,172 @@
1
+ require 'yaml'
2
+
3
+ # Fixtures are a way of organizing data that you want to test against. Each fixture file is created as a row
4
+ # in the database and created as a hash with column names as keys and data as values. All of these fixture hashes
5
+ # are kept in an overall hash where they can be accessed by their file name.
6
+ #
7
+ # Example:
8
+ #
9
+ # Directory with the fixture files
10
+ #
11
+ # developers/
12
+ # david
13
+ # luke
14
+ # jamis
15
+ #
16
+ # The file +david+ then contains:
17
+ #
18
+ # id => 1
19
+ # name => David Heinemeier Hansson
20
+ # birthday => 1979-10-15
21
+ # profession => Systems development
22
+ #
23
+ # Now when we call <tt>@developers = Fixtures.new(ActiveRecord::Base.connection, "developers", "developers/")</tt> all three
24
+ # developers will get inserted into the "developers" table through the active Active Record connection (that must be setup
25
+ # before-hand). And we can now query the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt>
26
+ # will return <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
27
+ #
28
+ # This can then be used for comparison in a unit test. Something like:
29
+ #
30
+ # def test_find
31
+ # assert_equal @developers["david"]["name"], Developer.find(@developers["david"]["id"]).name
32
+ # end
33
+ #
34
+ # == YAML fixtures
35
+ #
36
+ # Additionally, fixtures supports yaml files. Like fixture files, these yaml files have a pre-defined format. The document
37
+ # must be formatted like this:
38
+ #
39
+ # name: david
40
+ # data:
41
+ # id: 1
42
+ # name: David Heinemeier Hansson
43
+ # birthday: 1979-10-15
44
+ # profession: Systems development
45
+ # ---
46
+ # name: steve
47
+ # data:
48
+ # id: 2
49
+ # name: Steve Ross Kellock
50
+ # birthday: 1974-09-27
51
+ # profession: guy with keyboard
52
+ #
53
+ # In that file, there's two records. Each record must have two parts: 'name' and 'data'. The data that you add
54
+ # must be indented like you see above.
55
+ #
56
+ # Yaml fixtures file names must end with .yaml as in people.yaml or camel.yaml. The yaml fixtures are placed in the same
57
+ # directory as the normal fixtures and can happy co-exist. :)
58
+ class Fixtures
59
+ def self.create_fixtures(fixtures_directory, *table_names)
60
+ connection = block_given? ? yield : ActiveRecord::Base.connection
61
+ ActiveRecord::Base.logger.level = Logger::ERROR
62
+
63
+ fixtures = [ table_names ].flatten.collect do |table_name|
64
+ Fixtures.new(connection, table_name, "#{fixtures_directory}/#{table_name}")
65
+ end
66
+
67
+ ActiveRecord::Base.logger.level = Logger::DEBUG
68
+
69
+ return fixtures.size > 1 ? fixtures : fixtures.first
70
+ end
71
+
72
+ def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yaml/)
73
+ @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
74
+ @fixtures = read_fixtures
75
+
76
+ delete_existing_fixtures
77
+ insert_fixtures
78
+ end
79
+
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
+ 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 }
98
+ end
99
+ fixtures
100
+ end
101
+ end
102
+
103
+ def delete_existing_fixtures
104
+ @connection.delete "DELETE FROM #{@table_name}"
105
+ end
106
+
107
+ def insert_fixtures
108
+ @fixtures.values.each do |fixture|
109
+ @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
110
+ end
111
+ end
112
+
113
+ def []=(key, value)
114
+ @fixtures[key] = value
115
+ end
116
+ end
117
+
118
+ class Fixture #:nodoc:
119
+ def initialize(fixture_path, file)
120
+ @fixture_path, @file = fixture_path, file
121
+ @fixture = read_fixture
122
+ end
123
+
124
+ def [](key)
125
+ @fixture[key]
126
+ end
127
+
128
+ def to_hash
129
+ @fixture
130
+ end
131
+
132
+ def key_list
133
+ @fixture.keys.join(", ")
134
+ end
135
+
136
+ def value_list
137
+ @fixture.values.map { |v| "'#{v}'" }.join(", ")
138
+ end
139
+
140
+ 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
145
+ fixture
146
+ end
147
+ end
148
+ end
149
+
150
+ # A YamlFixture is like a fixture, but instead of a name to use as
151
+ # a key, it uses a yaml_name.
152
+ class YamlFixture < Fixture #:nodoc:
153
+ # yaml_name is equivalent to a normal fixture's filename
154
+ attr_accessor :yaml_name
155
+
156
+ # constructor is passed the name & the actual instantiate fixture
157
+ def initialize(yaml_name, fixture)
158
+ @yaml_name, @fixture = yaml_name, fixture
159
+ end
160
+
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
168
+ end
169
+ yaml_file.close
170
+ results
171
+ end
172
+ end
@@ -0,0 +1,71 @@
1
+ require 'singleton'
2
+
3
+ module ActiveRecord
4
+ # Observers can be programmed to react to lifecycle callbacks in another class to implement
5
+ # trigger-like behavior outside the original class. This is a great way to reduce the clutter that
6
+ # normally comes when the model class is burdened with excess responsibility that doesn't pertain to
7
+ # the core and nature of the class. Example:
8
+ #
9
+ # class CommentObserver < ActiveRecord::Observer
10
+ # def after_save(comment)
11
+ # NotificationServer.send_email("admin@do.com", "New comment was posted", comment)
12
+ # end
13
+ # end
14
+ #
15
+ # This Observer is triggered when a Comment#save is finished and sends a notification about it to the administrator.
16
+ #
17
+ # == Observing a class that can't be infered
18
+ #
19
+ # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
20
+ # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
21
+ # something else than the class you're interested in observing, you can implement the observed_class class method. Like this:
22
+ #
23
+ # class AuditObserver < ActiveRecord::Observer
24
+ # def self.observed_class() Account end
25
+ # def after_update(account)
26
+ # AuditTrail.new(account, "UPDATED")
27
+ # end
28
+ # end
29
+ #
30
+ # == Observing multiple classes at once
31
+ #
32
+ # If the audit observer needs to watch more than one kind of object, this can be specified in an array, like this:
33
+ #
34
+ # class AuditObserver < ActiveRecord::Observer
35
+ # def self.observed_class() [ Account, Balance ] end
36
+ # def after_update(record)
37
+ # AuditTrail.new(record, "UPDATED")
38
+ # end
39
+ # end
40
+ #
41
+ # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
42
+ #
43
+ # The observer can implement callback methods for each of the methods described in the Callbacks module.
44
+ class Observer
45
+ include Singleton
46
+
47
+ def initialize
48
+ [ observed_class ].flatten.each do |klass|
49
+ klass.add_observer(self)
50
+ klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find)
51
+ end
52
+ end
53
+
54
+ def update(callback_method, object)
55
+ send(callback_method, object) if respond_to?(callback_method)
56
+ end
57
+
58
+ private
59
+ def observed_class
60
+ if self.class.respond_to? "observed_class"
61
+ self.class.observed_class
62
+ else
63
+ Object.const_get(infer_observed_class_name)
64
+ end
65
+ end
66
+
67
+ def infer_observed_class_name
68
+ self.class.name.scan(/(.*)Observer/)[0][0]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,126 @@
1
+ module ActiveRecord
2
+ module Reflection # :nodoc:
3
+ def self.append_features(base)
4
+ super
5
+ base.extend(ClassMethods)
6
+
7
+ base.class_eval do
8
+ class << self
9
+ alias_method :composed_of_without_reflection, :composed_of
10
+
11
+ def composed_of_with_reflection(part_id, options = {})
12
+ composed_of_without_reflection(part_id, options)
13
+ write_inheritable_array "aggregations", [ AggregateReflection.new(part_id, options, self) ]
14
+ end
15
+
16
+ alias_method :composed_of, :composed_of_with_reflection
17
+ end
18
+ end
19
+
20
+ for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
21
+ base.module_eval <<-"end_eval"
22
+ class << self
23
+ alias_method :#{association_type}_without_reflection, :#{association_type}
24
+
25
+ def #{association_type}_with_reflection(association_id, options = {})
26
+ #{association_type}_without_reflection(association_id, options)
27
+ write_inheritable_array "associations", [ AssociationReflection.new(association_id, options, self) ]
28
+ end
29
+
30
+ alias_method :#{association_type}, :#{association_type}_with_reflection
31
+ end
32
+ end_eval
33
+ end
34
+ end
35
+
36
+ # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
37
+ # This information can for example be used in a form builder that took an Active Record object and created input
38
+ # fields for all of the attributes depending on their type and displayed the associations to other objects.
39
+ #
40
+ # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
41
+ module ClassMethods
42
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
43
+ def reflect_on_all_aggregations
44
+ read_inheritable_attribute "aggregations"
45
+ end
46
+
47
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
48
+ # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
49
+ def reflect_on_aggregation(aggregation)
50
+ reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
51
+ end
52
+
53
+ # Returns an array of AssociationReflection objects for all the aggregations in the class.
54
+ def reflect_on_all_associations
55
+ read_inheritable_attribute "associations"
56
+ end
57
+
58
+ # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
59
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
60
+ def reflect_on_association(association)
61
+ reflect_on_all_associations.find { |reflection| reflection.name == association } unless reflect_on_all_associations.nil?
62
+ end
63
+ end
64
+
65
+
66
+ # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
67
+ # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
68
+ class MacroReflection
69
+ attr_reader :active_record
70
+ def initialize(name, options, active_record)
71
+ @name, @options, @active_record = name, options, active_record
72
+ end
73
+
74
+ # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
75
+ # :clients for "has_many :clients".
76
+ def name
77
+ @name
78
+ end
79
+
80
+ # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
81
+ # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
82
+ def options
83
+ @options
84
+ end
85
+
86
+ # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
87
+ # "has_many :clients" would return the Client class.
88
+ def klass() end
89
+
90
+ def ==(other_aggregation)
91
+ name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
92
+ end
93
+ end
94
+
95
+
96
+ # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
97
+ class AggregateReflection < MacroReflection #:nodoc:
98
+ def klass
99
+ Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
100
+ end
101
+
102
+ private
103
+ def name_to_class_name(name)
104
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
105
+ end
106
+ end
107
+
108
+ # Holds all the meta-data about an association as it was specified in the Active Record class.
109
+ class AssociationReflection < MacroReflection #:nodoc:
110
+ def klass
111
+ active_record.send(:compute_type, (name_to_class_name(name.id2name)))
112
+ end
113
+
114
+ private
115
+ def name_to_class_name(name)
116
+ if name !~ /::/
117
+ class_name = active_record.send(
118
+ :type_name_with_module,
119
+ (options[:class_name] || active_record.class_name(active_record.table_name_prefix + name + active_record.table_name_suffix))
120
+ )
121
+ end
122
+ return class_name || name
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,43 @@
1
+ # attr_* style accessors for class-variables that can accessed both on an instance and class level.
2
+ class Class #:nodoc:
3
+ def cattr_reader(*syms)
4
+ syms.each do |sym|
5
+ class_eval <<-EOS
6
+ if ! defined? @@#{sym.id2name}
7
+ @@#{sym.id2name} = nil
8
+ end
9
+
10
+ def self.#{sym.id2name}
11
+ @@#{sym}
12
+ end
13
+
14
+ def #{sym.id2name}
15
+ self.class.#{sym.id2name}
16
+ end
17
+ EOS
18
+ end
19
+ end
20
+
21
+ def cattr_writer(*syms)
22
+ syms.each do |sym|
23
+ class_eval <<-EOS
24
+ if ! defined? @@#{sym.id2name}
25
+ @@#{sym.id2name} = nil
26
+ end
27
+
28
+ def self.#{sym.id2name}=(obj)
29
+ @@#{sym.id2name} = obj
30
+ end
31
+
32
+ def #{sym.id2name}=(obj)
33
+ self.class.#{sym.id2name}=(obj)
34
+ end
35
+ EOS
36
+ end
37
+ end
38
+
39
+ def cattr_accessor(*syms)
40
+ cattr_reader(*syms)
41
+ cattr_writer(*syms)
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ # Allows attributes to be shared within an inheritance hierarchy, but where each descentent gets a copy of
2
+ # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
3
+ # to, for example, an array without those additions being shared with either their parent, siblings, or
4
+ # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
5
+ module ClassInheritableAttributes # :nodoc:
6
+ def self.append_features(base)
7
+ super
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods # :nodoc:
12
+ @@classes ||= {}
13
+
14
+ def inheritable_attributes
15
+ @@classes[self] ||= {}
16
+ end
17
+
18
+ def write_inheritable_attribute(key, value)
19
+ inheritable_attributes[key] = value
20
+ end
21
+
22
+ def write_inheritable_array(key, elements)
23
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
24
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
25
+ end
26
+
27
+ def read_inheritable_attribute(key)
28
+ inheritable_attributes[key]
29
+ end
30
+
31
+ private
32
+ def inherited(child)
33
+ @@classes[child] = inheritable_attributes.dup
34
+ end
35
+
36
+ end
37
+ end